diff --git a/.bazelrc.common b/.bazelrc.common index 0ad0c95fdcbbd..e210b06ed2706 100644 --- a/.bazelrc.common +++ b/.bazelrc.common @@ -14,9 +14,18 @@ query --experimental_guard_against_concurrent_changes ## Cache action outputs on disk so they persist across output_base and bazel shutdown (eg. changing branches) build --disk_cache=~/.bazel-cache/disk-cache +fetch --disk_cache=~/.bazel-cache/disk-cache +query --disk_cache=~/.bazel-cache/disk-cache +sync --disk_cache=~/.bazel-cache/disk-cache +test --disk_cache=~/.bazel-cache/disk-cache ## Bazel repo cache settings build --repository_cache=~/.bazel-cache/repository-cache +fetch --repository_cache=~/.bazel-cache/repository-cache +query --repository_cache=~/.bazel-cache/repository-cache +run --repository_cache=~/.bazel-cache/repository-cache +sync --repository_cache=~/.bazel-cache/repository-cache +test --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" diff --git a/.buildkite/pipelines/es_snapshots/verify.yml b/.buildkite/pipelines/es_snapshots/verify.yml index 9cddade0b7482..7d700b1e0f489 100755 --- a/.buildkite/pipelines/es_snapshots/verify.yml +++ b/.buildkite/pipelines/es_snapshots/verify.yml @@ -27,9 +27,9 @@ steps: - command: .buildkite/scripts/steps/functional/xpack_cigroup.sh label: 'Default CI Group' - parallelism: 13 + parallelism: 27 agents: - queue: ci-group-6 + queue: n2-4 depends_on: build timeout_in_minutes: 150 key: default-cigroup @@ -41,7 +41,7 @@ steps: - command: CI_GROUP=Docker .buildkite/scripts/steps/functional/xpack_cigroup.sh label: 'Docker CI Group' agents: - queue: ci-group-6 + queue: n2-4 depends_on: build timeout_in_minutes: 120 key: default-cigroup-docker @@ -77,7 +77,7 @@ steps: - command: .buildkite/scripts/steps/test/api_integration.sh label: 'API Integration Tests' agents: - queue: jest + queue: n2-2 timeout_in_minutes: 120 key: api-integration diff --git a/.buildkite/pipelines/flaky_tests/pipeline.js b/.buildkite/pipelines/flaky_tests/pipeline.js index 208924aefe80e..bf4abb9ff4c89 100644 --- a/.buildkite/pipelines/flaky_tests/pipeline.js +++ b/.buildkite/pipelines/flaky_tests/pipeline.js @@ -8,7 +8,7 @@ const stepInput = (key, nameOfSuite) => { }; const OSS_CI_GROUPS = 12; -const XPACK_CI_GROUPS = 13; +const XPACK_CI_GROUPS = 27; const inputs = [ { @@ -23,11 +23,16 @@ for (let i = 1; i <= OSS_CI_GROUPS; i++) { inputs.push(stepInput(`oss/cigroup/${i}`, `OSS CI Group ${i}`)); } +inputs.push(stepInput(`oss/firefox`, 'OSS Firefox')); +inputs.push(stepInput(`oss/accessibility`, 'OSS Accessibility')); + for (let i = 1; i <= XPACK_CI_GROUPS; i++) { inputs.push(stepInput(`xpack/cigroup/${i}`, `Default CI Group ${i}`)); } inputs.push(stepInput(`xpack/cigroup/Docker`, 'Default CI Group Docker')); +inputs.push(stepInput(`xpack/firefox`, 'Default Firefox')); +inputs.push(stepInput(`xpack/accessibility`, 'Default Accessibility')); const pipeline = { steps: [ diff --git a/.buildkite/pipelines/flaky_tests/runner.js b/.buildkite/pipelines/flaky_tests/runner.js index bdb163504f46c..0c2db5c724f7b 100644 --- a/.buildkite/pipelines/flaky_tests/runner.js +++ b/.buildkite/pipelines/flaky_tests/runner.js @@ -65,34 +65,67 @@ for (const testSuite of testSuites) { const JOB_PARTS = TEST_SUITE.split('/'); const IS_XPACK = JOB_PARTS[0] === 'xpack'; + const TASK = JOB_PARTS[1]; const CI_GROUP = JOB_PARTS.length > 2 ? JOB_PARTS[2] : ''; if (RUN_COUNT < 1) { continue; } - if (IS_XPACK) { - steps.push({ - command: `CI_GROUP=${CI_GROUP} .buildkite/scripts/steps/functional/xpack_cigroup.sh`, - label: `Default CI Group ${CI_GROUP}`, - agents: { queue: 'ci-group-6' }, - depends_on: 'build', - parallelism: RUN_COUNT, - concurrency: concurrency, - concurrency_group: UUID, - concurrency_method: 'eager', - }); - } else { - steps.push({ - command: `CI_GROUP=${CI_GROUP} .buildkite/scripts/steps/functional/oss_cigroup.sh`, - label: `OSS CI Group ${CI_GROUP}`, - agents: { queue: 'ci-group-4d' }, - depends_on: 'build', - parallelism: RUN_COUNT, - concurrency: concurrency, - concurrency_group: UUID, - concurrency_method: 'eager', - }); + switch (TASK) { + case 'cigroup': + if (IS_XPACK) { + steps.push({ + command: `CI_GROUP=${CI_GROUP} .buildkite/scripts/steps/functional/xpack_cigroup.sh`, + label: `Default CI Group ${CI_GROUP}`, + agents: { queue: 'n2-4' }, + depends_on: 'build', + parallelism: RUN_COUNT, + concurrency: concurrency, + concurrency_group: UUID, + concurrency_method: 'eager', + }); + } else { + steps.push({ + command: `CI_GROUP=${CI_GROUP} .buildkite/scripts/steps/functional/oss_cigroup.sh`, + label: `OSS CI Group ${CI_GROUP}`, + agents: { queue: 'ci-group-4d' }, + depends_on: 'build', + parallelism: RUN_COUNT, + concurrency: concurrency, + concurrency_group: UUID, + concurrency_method: 'eager', + }); + } + break; + + case 'firefox': + steps.push({ + command: `.buildkite/scripts/steps/functional/${IS_XPACK ? 'xpack' : 'oss'}_firefox.sh`, + label: `${IS_XPACK ? 'Default' : 'OSS'} Firefox`, + agents: { queue: IS_XPACK ? 'n2-4' : 'ci-group-4d' }, + depends_on: 'build', + parallelism: RUN_COUNT, + concurrency: concurrency, + concurrency_group: UUID, + concurrency_method: 'eager', + }); + break; + + case 'accessibility': + steps.push({ + command: `.buildkite/scripts/steps/functional/${ + IS_XPACK ? 'xpack' : 'oss' + }_accessibility.sh`, + label: `${IS_XPACK ? 'Default' : 'OSS'} Accessibility`, + agents: { queue: IS_XPACK ? 'n2-4' : 'ci-group-4d' }, + depends_on: 'build', + parallelism: RUN_COUNT, + concurrency: concurrency, + concurrency_group: UUID, + concurrency_method: 'eager', + }); + break; } } diff --git a/.buildkite/pipelines/hourly.yml b/.buildkite/pipelines/hourly.yml index 4b2b17d272d17..bc9644820784d 100644 --- a/.buildkite/pipelines/hourly.yml +++ b/.buildkite/pipelines/hourly.yml @@ -17,9 +17,9 @@ steps: - command: .buildkite/scripts/steps/functional/xpack_cigroup.sh label: 'Default CI Group' - parallelism: 13 + parallelism: 27 agents: - queue: ci-group-6 + queue: n2-4 depends_on: build timeout_in_minutes: 250 key: default-cigroup @@ -31,7 +31,7 @@ steps: - command: CI_GROUP=Docker .buildkite/scripts/steps/functional/xpack_cigroup.sh label: 'Docker CI Group' agents: - queue: ci-group-6 + queue: n2-4 depends_on: build timeout_in_minutes: 120 key: default-cigroup-docker @@ -67,7 +67,7 @@ steps: - command: .buildkite/scripts/steps/functional/xpack_accessibility.sh label: 'Default Accessibility Tests' agents: - queue: ci-group-6 + queue: n2-4 depends_on: build timeout_in_minutes: 120 retry: @@ -89,7 +89,7 @@ steps: - command: .buildkite/scripts/steps/functional/xpack_firefox.sh label: 'Default Firefox Tests' agents: - queue: ci-group-6 + queue: n2-4 depends_on: build timeout_in_minutes: 120 retry: @@ -100,7 +100,7 @@ steps: - command: .buildkite/scripts/steps/functional/oss_misc.sh label: 'OSS Misc Functional Tests' agents: - queue: ci-group-6 + queue: n2-4 depends_on: build timeout_in_minutes: 120 retry: @@ -111,7 +111,7 @@ steps: - command: .buildkite/scripts/steps/functional/xpack_saved_object_field_metrics.sh label: 'Saved Object Field Metrics' agents: - queue: ci-group-6 + queue: n2-4 depends_on: build timeout_in_minutes: 120 retry: @@ -119,6 +119,14 @@ steps: - exit_status: '*' limit: 1 + - command: .buildkite/scripts/steps/test/jest.sh + label: 'Jest Tests' + parallelism: 8 + agents: + queue: n2-4 + timeout_in_minutes: 90 + key: jest + - command: .buildkite/scripts/steps/test/jest_integration.sh label: 'Jest Integration Tests' agents: @@ -133,13 +141,6 @@ steps: timeout_in_minutes: 120 key: api-integration - - command: .buildkite/scripts/steps/test/jest.sh - label: 'Jest Tests' - agents: - queue: c2-16 - timeout_in_minutes: 120 - key: jest - - command: .buildkite/scripts/steps/lint.sh label: 'Linting' agents: diff --git a/.buildkite/pipelines/pull_request/base.yml b/.buildkite/pipelines/pull_request/base.yml index 0f2a4a1026af8..b99473c23d746 100644 --- a/.buildkite/pipelines/pull_request/base.yml +++ b/.buildkite/pipelines/pull_request/base.yml @@ -15,9 +15,9 @@ steps: - command: .buildkite/scripts/steps/functional/xpack_cigroup.sh label: 'Default CI Group' - parallelism: 13 + parallelism: 27 agents: - queue: ci-group-6 + queue: n2-4 depends_on: build timeout_in_minutes: 150 key: default-cigroup @@ -29,7 +29,7 @@ steps: - command: CI_GROUP=Docker .buildkite/scripts/steps/functional/xpack_cigroup.sh label: 'Docker CI Group' agents: - queue: ci-group-6 + queue: n2-4 depends_on: build timeout_in_minutes: 120 key: default-cigroup-docker @@ -65,7 +65,7 @@ steps: - command: .buildkite/scripts/steps/functional/xpack_accessibility.sh label: 'Default Accessibility Tests' agents: - queue: ci-group-6 + queue: n2-4 depends_on: build timeout_in_minutes: 120 retry: @@ -87,7 +87,7 @@ steps: - command: .buildkite/scripts/steps/functional/xpack_firefox.sh label: 'Default Firefox Tests' agents: - queue: ci-group-6 + queue: n2-4 depends_on: build timeout_in_minutes: 120 retry: @@ -98,7 +98,7 @@ steps: - command: .buildkite/scripts/steps/functional/oss_misc.sh label: 'OSS Misc Functional Tests' agents: - queue: ci-group-6 + queue: n2-4 depends_on: build timeout_in_minutes: 120 retry: @@ -109,7 +109,7 @@ steps: - command: .buildkite/scripts/steps/functional/xpack_saved_object_field_metrics.sh label: 'Saved Object Field Metrics' agents: - queue: ci-group-6 + queue: n2-4 depends_on: build timeout_in_minutes: 120 retry: @@ -117,6 +117,14 @@ steps: - exit_status: '*' limit: 1 + - command: .buildkite/scripts/steps/test/jest.sh + label: 'Jest Tests' + parallelism: 8 + agents: + queue: n2-4 + timeout_in_minutes: 90 + key: jest + - command: .buildkite/scripts/steps/test/jest_integration.sh label: 'Jest Integration Tests' agents: @@ -131,13 +139,6 @@ steps: timeout_in_minutes: 120 key: api-integration - - command: .buildkite/scripts/steps/test/jest.sh - label: 'Jest Tests' - agents: - queue: c2-16 - timeout_in_minutes: 120 - key: jest - - command: .buildkite/scripts/steps/lint.sh label: 'Linting' agents: @@ -155,7 +156,7 @@ steps: - command: .buildkite/scripts/steps/checks.sh label: 'Checks' agents: - queue: c2-4 + queue: c2-8 key: checks timeout_in_minutes: 120 diff --git a/.buildkite/scripts/bootstrap.sh b/.buildkite/scripts/bootstrap.sh index df38c105d2fd3..272cd0a086170 100755 --- a/.buildkite/scripts/bootstrap.sh +++ b/.buildkite/scripts/bootstrap.sh @@ -6,7 +6,17 @@ source .buildkite/scripts/common/util.sh source .buildkite/scripts/common/setup_bazel.sh echo "--- yarn install and bootstrap" -retry 2 15 yarn kbn bootstrap +if ! yarn kbn bootstrap; then + echo "bootstrap failed, trying again in 15 seconds" + sleep 15 + + # Most bootstrap failures will result in a problem inside node_modules that does not get fixed on the next bootstrap + # So, we should just delete node_modules in between attempts + rm -rf node_modules + + echo "--- yarn install and bootstrap, attempt 2" + yarn kbn bootstrap +fi ### ### upload ts-refs-cache artifacts as quickly as possible so they are available for download diff --git a/.buildkite/scripts/build_kibana.sh b/.buildkite/scripts/build_kibana.sh index e26d7790215f3..84d66a30ea213 100755 --- a/.buildkite/scripts/build_kibana.sh +++ b/.buildkite/scripts/build_kibana.sh @@ -11,6 +11,19 @@ else node scripts/build fi +if [[ "${GITHUB_PR_LABELS:-}" == *"ci:deploy-cloud"* ]]; then + echo "--- Build Kibana Cloud Distribution" + node scripts/build \ + --skip-initialize \ + --skip-generic-folders \ + --skip-platform-folders \ + --skip-archives \ + --docker-images \ + --skip-docker-ubi \ + --skip-docker-centos \ + --skip-docker-contexts +fi + echo "--- Archive Kibana Distribution" linuxBuild="$(find "$KIBANA_DIR/target" -name 'kibana-*-linux-x86_64.tar.gz')" installDir="$KIBANA_DIR/install/kibana" diff --git a/.buildkite/scripts/common/env.sh b/.buildkite/scripts/common/env.sh index cd33cdc714cbe..b5acfe140df24 100755 --- a/.buildkite/scripts/common/env.sh +++ b/.buildkite/scripts/common/env.sh @@ -38,11 +38,17 @@ export ELASTIC_APM_TRANSACTION_SAMPLE_RATE=0.1 if is_pr; then if [[ "${GITHUB_PR_LABELS:-}" == *"ci:collect-apm"* ]]; then export ELASTIC_APM_ACTIVE=true + export ELASTIC_APM_CONTEXT_PROPAGATION_ONLY=false else - export ELASTIC_APM_ACTIVE=false + export ELASTIC_APM_ACTIVE=true + export ELASTIC_APM_CONTEXT_PROPAGATION_ONLY=true fi - export CHECKS_REPORTER_ACTIVE=true + if [[ "${GITHUB_STEP_COMMIT_STATUS_ENABLED:-}" != "true" ]]; then + export CHECKS_REPORTER_ACTIVE=true + else + export CHECKS_REPORTER_ACTIVE=false + fi # These can be removed once we're not supporting Jenkins and Buildkite at the same time # These are primarily used by github checks reporter and can be configured via /github_checks_api.json @@ -57,6 +63,7 @@ if is_pr; then export PR_TARGET_BRANCH="$GITHUB_PR_TARGET_BRANCH" else export ELASTIC_APM_ACTIVE=true + export ELASTIC_APM_CONTEXT_PROPAGATION_ONLY=false export CHECKS_REPORTER_ACTIVE=false fi diff --git a/.buildkite/scripts/lifecycle/post_build.sh b/.buildkite/scripts/lifecycle/post_build.sh index 35e5a6006ee24..5a181e8fa5489 100755 --- a/.buildkite/scripts/lifecycle/post_build.sh +++ b/.buildkite/scripts/lifecycle/post_build.sh @@ -5,7 +5,9 @@ set -euo pipefail BUILD_SUCCESSFUL=$(node "$(dirname "${0}")/build_status.js") export BUILD_SUCCESSFUL -"$(dirname "${0}")/commit_status_complete.sh" +if [[ "${GITHUB_BUILD_COMMIT_STATUS_ENABLED:-}" != "true" ]]; then + "$(dirname "${0}")/commit_status_complete.sh" +fi node "$(dirname "${0}")/ci_stats_complete.js" diff --git a/.buildkite/scripts/lifecycle/pre_build.sh b/.buildkite/scripts/lifecycle/pre_build.sh index d91597a00a080..d901594e36ce4 100755 --- a/.buildkite/scripts/lifecycle/pre_build.sh +++ b/.buildkite/scripts/lifecycle/pre_build.sh @@ -4,7 +4,9 @@ set -euo pipefail source .buildkite/scripts/common/util.sh -"$(dirname "${0}")/commit_status_start.sh" +if [[ "${GITHUB_BUILD_COMMIT_STATUS_ENABLED:-}" != "true" ]]; then + "$(dirname "${0}")/commit_status_start.sh" +fi export CI_STATS_TOKEN="$(retry 5 5 vault read -field=api_token secret/kibana-issues/dev/kibana_ci_stats)" export CI_STATS_HOST="$(retry 5 5 vault read -field=api_host secret/kibana-issues/dev/kibana_ci_stats)" diff --git a/.buildkite/scripts/steps/checks/type_check_plugin_public_api_docs.sh b/.buildkite/scripts/steps/checks/type_check_plugin_public_api_docs.sh index 1d73d1748ddf7..5827fd5eb2284 100755 --- a/.buildkite/scripts/steps/checks/type_check_plugin_public_api_docs.sh +++ b/.buildkite/scripts/steps/checks/type_check_plugin_public_api_docs.sh @@ -11,9 +11,27 @@ checks-reporter-with-killswitch "Build TS Refs" \ --no-cache \ --force -echo --- Check Types checks-reporter-with-killswitch "Check Types" \ - node scripts/type_check + node scripts/type_check &> target/check_types.log & +check_types_pid=$! + +node --max-old-space-size=12000 scripts/build_api_docs &> target/build_api_docs.log & +api_docs_pid=$! + +wait $check_types_pid +check_types_exit=$? + +wait $api_docs_pid +api_docs_exit=$? + +echo --- Check Types +cat target/check_types.log +if [[ "$check_types_exit" != "0" ]]; then echo "^^^ +++"; fi echo --- Building api docs -node --max-old-space-size=12000 scripts/build_api_docs +cat target/build_api_docs.log +if [[ "$api_docs_exit" != "0" ]]; then echo "^^^ +++"; fi + +if [[ "${api_docs_exit}${check_types_exit}" != "00" ]]; then + exit 1 +fi diff --git a/.buildkite/scripts/steps/test/jest.sh b/.buildkite/scripts/steps/test/jest.sh index 2c4e3fe21902d..d2d1ed10043d6 100755 --- a/.buildkite/scripts/steps/test/jest.sh +++ b/.buildkite/scripts/steps/test/jest.sh @@ -9,5 +9,5 @@ is_test_execution_step .buildkite/scripts/bootstrap.sh echo '--- Jest' -checks-reporter-with-killswitch "Jest Unit Tests" \ - node scripts/jest --ci --verbose --maxWorkers=10 +checks-reporter-with-killswitch "Jest Unit Tests $((BUILDKITE_PARALLEL_JOB+1))" \ + .buildkite/scripts/steps/test/jest_parallel.sh diff --git a/.buildkite/scripts/steps/test/jest_parallel.sh b/.buildkite/scripts/steps/test/jest_parallel.sh new file mode 100755 index 0000000000000..c9e0e1aff5cf2 --- /dev/null +++ b/.buildkite/scripts/steps/test/jest_parallel.sh @@ -0,0 +1,30 @@ +#!/bin/bash + +set -uo pipefail + +JOB=$BUILDKITE_PARALLEL_JOB +JOB_COUNT=$BUILDKITE_PARALLEL_JOB_COUNT + +# a jest failure will result in the script returning an exit code of 10 + +i=0 +exitCode=0 + +while read -r config; do + if [ "$((i % JOB_COUNT))" -eq "$JOB" ]; then + echo "--- $ node scripts/jest --config $config" + node --max-old-space-size=14336 ./node_modules/.bin/jest --config="$config" --runInBand --coverage=false + lastCode=$? + + if [ $lastCode -ne 0 ]; then + exitCode=10 + echo "Jest exited with code $lastCode" + echo "^^^ +++" + fi + fi + + ((i=i+1)) +# uses heredoc to avoid the while loop being in a sub-shell thus unable to overwrite exitCode +done <<< "$(find src x-pack packages -name jest.config.js -not -path "*/__fixtures__/*" | sort)" + +exit $exitCode \ No newline at end of file diff --git a/.ci/ci_groups.yml b/.ci/ci_groups.yml index 9c3a039f51166..1be6e8c196a2d 100644 --- a/.ci/ci_groups.yml +++ b/.ci/ci_groups.yml @@ -25,4 +25,18 @@ xpack: - ciGroup11 - ciGroup12 - ciGroup13 + - ciGroup14 + - ciGroup15 + - ciGroup16 + - ciGroup17 + - ciGroup18 + - ciGroup19 + - ciGroup20 + - ciGroup21 + - ciGroup22 + - ciGroup23 + - ciGroup24 + - ciGroup25 + - ciGroup26 + - ciGroup27 - ciGroupDocker diff --git a/.eslintrc.js b/.eslintrc.js index 60f3ae1528fbc..b303a9fefb691 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -226,6 +226,10 @@ const RESTRICTED_IMPORTS = [ name: 'react-use', message: 'Please use react-use/lib/{method} instead.', }, + { + name: '@kbn/io-ts-utils', + message: `Import directly from @kbn/io-ts-utils/{method} submodules`, + }, ]; module.exports = { @@ -700,6 +704,7 @@ module.exports = { 'packages/kbn-eslint-plugin-eslint/**/*', 'x-pack/gulpfile.js', 'x-pack/scripts/*.js', + '**/jest.config.js', ], excludedFiles: ['**/integration_tests/**/*'], rules: { @@ -902,17 +907,6 @@ module.exports = { }, }, - /** - * Cases overrides - */ - { - files: ['x-pack/plugins/cases/**/*.{js,mjs,ts,tsx}'], - rules: { - 'no-duplicate-imports': 'off', - '@typescript-eslint/no-duplicate-imports': ['error'], - }, - }, - /** * Security Solution overrides. These rules below are maintained and owned by * the people within the security-solution-platform team. Please see ping them @@ -928,6 +922,8 @@ module.exports = { 'x-pack/plugins/security_solution/common/**/*.{js,mjs,ts,tsx}', 'x-pack/plugins/timelines/public/**/*.{js,mjs,ts,tsx}', 'x-pack/plugins/timelines/common/**/*.{js,mjs,ts,tsx}', + 'x-pack/plugins/cases/public/**/*.{js,mjs,ts,tsx}', + 'x-pack/plugins/cases/common/**/*.{js,mjs,ts,tsx}', ], rules: { 'import/no-nodejs-modules': 'error', @@ -949,10 +945,12 @@ module.exports = { files: [ 'x-pack/plugins/security_solution/**/*.{ts,tsx}', 'x-pack/plugins/timelines/**/*.{ts,tsx}', + 'x-pack/plugins/cases/**/*.{ts,tsx}', ], excludedFiles: [ 'x-pack/plugins/security_solution/**/*.{test,mock,test_helper}.{ts,tsx}', 'x-pack/plugins/timelines/**/*.{test,mock,test_helper}.{ts,tsx}', + 'x-pack/plugins/cases/**/*.{test,mock,test_helper}.{ts,tsx}', ], rules: { '@typescript-eslint/no-non-null-assertion': 'error', @@ -963,6 +961,7 @@ module.exports = { files: [ 'x-pack/plugins/security_solution/**/*.{ts,tsx}', 'x-pack/plugins/timelines/**/*.{ts,tsx}', + 'x-pack/plugins/cases/**/*.{ts,tsx}', ], rules: { '@typescript-eslint/no-this-alias': 'error', @@ -985,6 +984,7 @@ module.exports = { files: [ 'x-pack/plugins/security_solution/**/*.{js,mjs,ts,tsx}', 'x-pack/plugins/timelines/**/*.{js,mjs,ts,tsx}', + 'x-pack/plugins/cases/**/*.{js,mjs,ts,tsx}', ], plugins: ['eslint-plugin-node', 'react'], env: { diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index e807885e17294..d5c569ac9d552 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -59,8 +59,10 @@ /examples/url_generators_explorer/ @elastic/kibana-app-services /examples/field_formats_example/ @elastic/kibana-app-services /examples/partial_results_example/ @elastic/kibana-app-services +/examples/search_examples/ @elastic/kibana-app-services /packages/elastic-datemath/ @elastic/kibana-app-services /packages/kbn-interpreter/ @elastic/kibana-app-services +/packages/kbn-react-field/ @elastic/kibana-app-services /src/plugins/bfetch/ @elastic/kibana-app-services /src/plugins/data/ @elastic/kibana-app-services /src/plugins/data_views/ @elastic/kibana-app-services @@ -77,24 +79,33 @@ /src/plugins/ui_actions/ @elastic/kibana-app-services /src/plugins/index_pattern_field_editor @elastic/kibana-app-services /src/plugins/screenshot_mode @elastic/kibana-app-services +/src/plugins/bfetch/ @elastic/kibana-app-services +/src/plugins/index_pattern_management/ @elastic/kibana-app-services +/src/plugins/inspector/ @elastic/kibana-app-services /x-pack/examples/ui_actions_enhanced_examples/ @elastic/kibana-app-services /x-pack/plugins/data_enhanced/ @elastic/kibana-app-services /x-pack/plugins/embeddable_enhanced/ @elastic/kibana-app-services /x-pack/plugins/ui_actions_enhanced/ @elastic/kibana-app-services /x-pack/plugins/runtime_fields @elastic/kibana-app-services /x-pack/test/search_sessions_integration/ @elastic/kibana-app-services -#CC# /src/plugins/bfetch/ @elastic/kibana-app-services -#CC# /src/plugins/index_pattern_management/ @elastic/kibana-app-services -#CC# /src/plugins/inspector/ @elastic/kibana-app-services -#CC# /src/plugins/share/ @elastic/kibana-app-services -#CC# /x-pack/plugins/drilldowns/ @elastic/kibana-app-services -#CC# /packages/kbn-interpreter/ @elastic/kibana-app-services ### Observability Plugins # Observability Shared /x-pack/plugins/observability/ @elastic/observability-ui +# Unified Observability +/x-pack/plugins/observability/public/pages/overview @elastic/unified-observability +/x-pack/plugins/observability/public/pages/home @elastic/unified-observability +/x-pack/plugins/observability/public/pages/landing @elastic/unified-observability +/x-pack/plugins/observability/public/context @elastic/unified-observability + +# Actionable Observability +/x-pack/plugins/observability/common/rules @elastic/actionable-observability +/x-pack/plugins/observability/public/rules @elastic/actionable-observability +/x-pack/plugins/observability/public/pages/alerts @elastic/actionable-observability +/x-pack/plugins/observability/public/pages/cases @elastic/actionable-observability + # Infra Monitoring /x-pack/plugins/infra/ @elastic/infra-monitoring-ui /x-pack/test/functional/apps/infra @elastic/infra-monitoring-ui @@ -404,6 +415,12 @@ /x-pack/plugins/security_solution/scripts/endpoint/trusted_apps/ @elastic/security-onboarding-and-lifecycle-mgt /x-pack/test/security_solution_endpoint/apps/endpoint/ @elastic/security-onboarding-and-lifecycle-mgt +## Security Solution sub teams - security-engineering-productivity +x-pack/plugins/security_solution/cypress/ccs_integration +x-pack/plugins/security_solution/cypress/upgrade_integration +x-pack/plugins/security_solution/cypress/README.md +x-pack/test/security_solution_cypress + # Security Intelligence And Analytics /x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules @elastic/security-intelligence-analytics diff --git a/api_docs/alerting.json b/api_docs/alerting.json index 2b82778439707..b0d37c25a10aa 100644 --- a/api_docs/alerting.json +++ b/api_docs/alerting.json @@ -1793,7 +1793,7 @@ "section": "def-server.RulesClient", "text": "RulesClient" }, - ", \"create\" | \"delete\" | \"find\" | \"get\" | \"resolve\" | \"update\" | \"aggregate\" | \"enable\" | \"disable\" | \"muteAll\" | \"getAlertState\" | \"getAlertInstanceSummary\" | \"updateApiKey\" | \"unmuteAll\" | \"muteInstance\" | \"unmuteInstance\" | \"listAlertTypes\" | \"getSpaceId\">" + ", \"create\" | \"delete\" | \"find\" | \"get\" | \"resolve\" | \"update\" | \"aggregate\" | \"enable\" | \"disable\" | \"muteAll\" | \"getAlertState\" | \"getAlertSummary\" | \"updateApiKey\" | \"unmuteAll\" | \"muteInstance\" | \"unmuteInstance\" | \"listAlertTypes\" | \"getSpaceId\">" ], "path": "x-pack/plugins/alerting/server/plugin.ts", "deprecated": false, @@ -2223,15 +2223,15 @@ "AggregateOptions", " | undefined; }) => Promise<", "AggregateResult", - ">; enable: ({ id }: { id: string; }) => Promise; disable: ({ id }: { id: string; }) => Promise; muteAll: ({ id }: { id: string; }) => Promise; getAlertState: ({ id }: { id: string; }) => Promise; getAlertInstanceSummary: ({ id, dateStart, }: ", - "GetAlertInstanceSummaryParams", + ">; enable: ({ id }: { id: string; }) => Promise; disable: ({ id }: { id: string; }) => Promise; muteAll: ({ id }: { id: string; }) => Promise; getAlertState: ({ id }: { id: string; }) => Promise; getAlertSummary: ({ id, dateStart }: ", + "GetAlertSummaryParams", ") => Promise<", { "pluginId": "alerting", "scope": "common", "docId": "kibAlertingPluginApi", - "section": "def-common.AlertInstanceSummary", - "text": "AlertInstanceSummary" + "section": "def-common.AlertSummary", + "text": "AlertSummary" }, ">; updateApiKey: ({ id }: { id: string; }) => Promise; unmuteAll: ({ id }: { id: string; }) => Promise; muteInstance: ({ alertId, alertInstanceId }: ", "MuteOptions", @@ -3157,17 +3157,125 @@ }, { "parentPluginId": "alerting", - "id": "def-common.AlertInstanceStatus", + "id": "def-common.AlertsHealth", "type": "Interface", "tags": [], - "label": "AlertInstanceStatus", + "label": "AlertsHealth", "description": [], - "path": "x-pack/plugins/alerting/common/alert_instance_summary.ts", + "path": "x-pack/plugins/alerting/common/alert.ts", "deprecated": false, "children": [ { "parentPluginId": "alerting", - "id": "def-common.AlertInstanceStatus.status", + "id": "def-common.AlertsHealth.decryptionHealth", + "type": "Object", + "tags": [], + "label": "decryptionHealth", + "description": [], + "signature": [ + "{ status: ", + { + "pluginId": "alerting", + "scope": "common", + "docId": "kibAlertingPluginApi", + "section": "def-common.HealthStatus", + "text": "HealthStatus" + }, + "; timestamp: string; }" + ], + "path": "x-pack/plugins/alerting/common/alert.ts", + "deprecated": false + }, + { + "parentPluginId": "alerting", + "id": "def-common.AlertsHealth.executionHealth", + "type": "Object", + "tags": [], + "label": "executionHealth", + "description": [], + "signature": [ + "{ status: ", + { + "pluginId": "alerting", + "scope": "common", + "docId": "kibAlertingPluginApi", + "section": "def-common.HealthStatus", + "text": "HealthStatus" + }, + "; timestamp: string; }" + ], + "path": "x-pack/plugins/alerting/common/alert.ts", + "deprecated": false + }, + { + "parentPluginId": "alerting", + "id": "def-common.AlertsHealth.readHealth", + "type": "Object", + "tags": [], + "label": "readHealth", + "description": [], + "signature": [ + "{ status: ", + { + "pluginId": "alerting", + "scope": "common", + "docId": "kibAlertingPluginApi", + "section": "def-common.HealthStatus", + "text": "HealthStatus" + }, + "; timestamp: string; }" + ], + "path": "x-pack/plugins/alerting/common/alert.ts", + "deprecated": false + } + ], + "initialIsOpen": false + }, + { + "parentPluginId": "alerting", + "id": "def-common.AlertStateNavigation", + "type": "Interface", + "tags": [], + "label": "AlertStateNavigation", + "description": [], + "path": "x-pack/plugins/alerting/common/alert_navigation.ts", + "deprecated": false, + "children": [ + { + "parentPluginId": "alerting", + "id": "def-common.AlertStateNavigation.state", + "type": "Object", + "tags": [], + "label": "state", + "description": [], + "signature": [ + { + "pluginId": "@kbn/utility-types", + "scope": "server", + "docId": "kibKbnUtilityTypesPluginApi", + "section": "def-server.JsonObject", + "text": "JsonObject" + } + ], + "path": "x-pack/plugins/alerting/common/alert_navigation.ts", + "deprecated": false + } + ], + "initialIsOpen": false + }, + { + "parentPluginId": "alerting", + "id": "def-common.AlertStatus", + "type": "Interface", + "tags": [], + "label": "AlertStatus", + "description": [], + "path": "x-pack/plugins/alerting/common/alert_summary.ts", + "deprecated": false, + "children": [ + { + "parentPluginId": "alerting", + "id": "def-common.AlertStatus.status", "type": "CompoundType", "tags": [], "label": "status", @@ -3175,22 +3283,22 @@ "signature": [ "\"OK\" | \"Active\"" ], - "path": "x-pack/plugins/alerting/common/alert_instance_summary.ts", + "path": "x-pack/plugins/alerting/common/alert_summary.ts", "deprecated": false }, { "parentPluginId": "alerting", - "id": "def-common.AlertInstanceStatus.muted", + "id": "def-common.AlertStatus.muted", "type": "boolean", "tags": [], "label": "muted", "description": [], - "path": "x-pack/plugins/alerting/common/alert_instance_summary.ts", + "path": "x-pack/plugins/alerting/common/alert_summary.ts", "deprecated": false }, { "parentPluginId": "alerting", - "id": "def-common.AlertInstanceStatus.actionGroupId", + "id": "def-common.AlertStatus.actionGroupId", "type": "string", "tags": [], "label": "actionGroupId", @@ -3198,12 +3306,12 @@ "signature": [ "string | undefined" ], - "path": "x-pack/plugins/alerting/common/alert_instance_summary.ts", + "path": "x-pack/plugins/alerting/common/alert_summary.ts", "deprecated": false }, { "parentPluginId": "alerting", - "id": "def-common.AlertInstanceStatus.actionSubgroup", + "id": "def-common.AlertStatus.actionSubgroup", "type": "string", "tags": [], "label": "actionSubgroup", @@ -3211,12 +3319,12 @@ "signature": [ "string | undefined" ], - "path": "x-pack/plugins/alerting/common/alert_instance_summary.ts", + "path": "x-pack/plugins/alerting/common/alert_summary.ts", "deprecated": false }, { "parentPluginId": "alerting", - "id": "def-common.AlertInstanceStatus.activeStartDate", + "id": "def-common.AlertStatus.activeStartDate", "type": "string", "tags": [], "label": "activeStartDate", @@ -3224,7 +3332,7 @@ "signature": [ "string | undefined" ], - "path": "x-pack/plugins/alerting/common/alert_instance_summary.ts", + "path": "x-pack/plugins/alerting/common/alert_summary.ts", "deprecated": false } ], @@ -3232,37 +3340,37 @@ }, { "parentPluginId": "alerting", - "id": "def-common.AlertInstanceSummary", + "id": "def-common.AlertSummary", "type": "Interface", "tags": [], - "label": "AlertInstanceSummary", + "label": "AlertSummary", "description": [], - "path": "x-pack/plugins/alerting/common/alert_instance_summary.ts", + "path": "x-pack/plugins/alerting/common/alert_summary.ts", "deprecated": false, "children": [ { "parentPluginId": "alerting", - "id": "def-common.AlertInstanceSummary.id", + "id": "def-common.AlertSummary.id", "type": "string", "tags": [], "label": "id", "description": [], - "path": "x-pack/plugins/alerting/common/alert_instance_summary.ts", + "path": "x-pack/plugins/alerting/common/alert_summary.ts", "deprecated": false }, { "parentPluginId": "alerting", - "id": "def-common.AlertInstanceSummary.name", + "id": "def-common.AlertSummary.name", "type": "string", "tags": [], "label": "name", "description": [], - "path": "x-pack/plugins/alerting/common/alert_instance_summary.ts", + "path": "x-pack/plugins/alerting/common/alert_summary.ts", "deprecated": false }, { "parentPluginId": "alerting", - "id": "def-common.AlertInstanceSummary.tags", + "id": "def-common.AlertSummary.tags", "type": "Array", "tags": [], "label": "tags", @@ -3270,42 +3378,42 @@ "signature": [ "string[]" ], - "path": "x-pack/plugins/alerting/common/alert_instance_summary.ts", + "path": "x-pack/plugins/alerting/common/alert_summary.ts", "deprecated": false }, { "parentPluginId": "alerting", - "id": "def-common.AlertInstanceSummary.alertTypeId", + "id": "def-common.AlertSummary.ruleTypeId", "type": "string", "tags": [], - "label": "alertTypeId", + "label": "ruleTypeId", "description": [], - "path": "x-pack/plugins/alerting/common/alert_instance_summary.ts", + "path": "x-pack/plugins/alerting/common/alert_summary.ts", "deprecated": false }, { "parentPluginId": "alerting", - "id": "def-common.AlertInstanceSummary.consumer", + "id": "def-common.AlertSummary.consumer", "type": "string", "tags": [], "label": "consumer", "description": [], - "path": "x-pack/plugins/alerting/common/alert_instance_summary.ts", + "path": "x-pack/plugins/alerting/common/alert_summary.ts", "deprecated": false }, { "parentPluginId": "alerting", - "id": "def-common.AlertInstanceSummary.muteAll", + "id": "def-common.AlertSummary.muteAll", "type": "boolean", "tags": [], "label": "muteAll", "description": [], - "path": "x-pack/plugins/alerting/common/alert_instance_summary.ts", + "path": "x-pack/plugins/alerting/common/alert_summary.ts", "deprecated": false }, { "parentPluginId": "alerting", - "id": "def-common.AlertInstanceSummary.throttle", + "id": "def-common.AlertSummary.throttle", "type": "CompoundType", "tags": [], "label": "throttle", @@ -3313,42 +3421,42 @@ "signature": [ "string | null" ], - "path": "x-pack/plugins/alerting/common/alert_instance_summary.ts", + "path": "x-pack/plugins/alerting/common/alert_summary.ts", "deprecated": false }, { "parentPluginId": "alerting", - "id": "def-common.AlertInstanceSummary.enabled", + "id": "def-common.AlertSummary.enabled", "type": "boolean", "tags": [], "label": "enabled", "description": [], - "path": "x-pack/plugins/alerting/common/alert_instance_summary.ts", + "path": "x-pack/plugins/alerting/common/alert_summary.ts", "deprecated": false }, { "parentPluginId": "alerting", - "id": "def-common.AlertInstanceSummary.statusStartDate", + "id": "def-common.AlertSummary.statusStartDate", "type": "string", "tags": [], "label": "statusStartDate", "description": [], - "path": "x-pack/plugins/alerting/common/alert_instance_summary.ts", + "path": "x-pack/plugins/alerting/common/alert_summary.ts", "deprecated": false }, { "parentPluginId": "alerting", - "id": "def-common.AlertInstanceSummary.statusEndDate", + "id": "def-common.AlertSummary.statusEndDate", "type": "string", "tags": [], "label": "statusEndDate", "description": [], - "path": "x-pack/plugins/alerting/common/alert_instance_summary.ts", + "path": "x-pack/plugins/alerting/common/alert_summary.ts", "deprecated": false }, { "parentPluginId": "alerting", - "id": "def-common.AlertInstanceSummary.status", + "id": "def-common.AlertSummary.status", "type": "CompoundType", "tags": [], "label": "status", @@ -3356,12 +3464,12 @@ "signature": [ "\"OK\" | \"Active\" | \"Error\"" ], - "path": "x-pack/plugins/alerting/common/alert_instance_summary.ts", + "path": "x-pack/plugins/alerting/common/alert_summary.ts", "deprecated": false }, { "parentPluginId": "alerting", - "id": "def-common.AlertInstanceSummary.lastRun", + "id": "def-common.AlertSummary.lastRun", "type": "string", "tags": [], "label": "lastRun", @@ -3369,12 +3477,12 @@ "signature": [ "string | undefined" ], - "path": "x-pack/plugins/alerting/common/alert_instance_summary.ts", + "path": "x-pack/plugins/alerting/common/alert_summary.ts", "deprecated": false }, { "parentPluginId": "alerting", - "id": "def-common.AlertInstanceSummary.errorMessages", + "id": "def-common.AlertSummary.errorMessages", "type": "Array", "tags": [], "label": "errorMessages", @@ -3382,15 +3490,15 @@ "signature": [ "{ date: string; message: string; }[]" ], - "path": "x-pack/plugins/alerting/common/alert_instance_summary.ts", + "path": "x-pack/plugins/alerting/common/alert_summary.ts", "deprecated": false }, { "parentPluginId": "alerting", - "id": "def-common.AlertInstanceSummary.instances", + "id": "def-common.AlertSummary.alerts", "type": "Object", "tags": [], - "label": "instances", + "label": "alerts", "description": [], "signature": [ "{ [x: string]: ", @@ -3398,17 +3506,17 @@ "pluginId": "alerting", "scope": "common", "docId": "kibAlertingPluginApi", - "section": "def-common.AlertInstanceStatus", - "text": "AlertInstanceStatus" + "section": "def-common.AlertStatus", + "text": "AlertStatus" }, "; }" ], - "path": "x-pack/plugins/alerting/common/alert_instance_summary.ts", + "path": "x-pack/plugins/alerting/common/alert_summary.ts", "deprecated": false }, { "parentPluginId": "alerting", - "id": "def-common.AlertInstanceSummary.executionDuration", + "id": "def-common.AlertSummary.executionDuration", "type": "Object", "tags": [], "label": "executionDuration", @@ -3416,115 +3524,7 @@ "signature": [ "{ average: number; values: number[]; }" ], - "path": "x-pack/plugins/alerting/common/alert_instance_summary.ts", - "deprecated": false - } - ], - "initialIsOpen": false - }, - { - "parentPluginId": "alerting", - "id": "def-common.AlertsHealth", - "type": "Interface", - "tags": [], - "label": "AlertsHealth", - "description": [], - "path": "x-pack/plugins/alerting/common/alert.ts", - "deprecated": false, - "children": [ - { - "parentPluginId": "alerting", - "id": "def-common.AlertsHealth.decryptionHealth", - "type": "Object", - "tags": [], - "label": "decryptionHealth", - "description": [], - "signature": [ - "{ status: ", - { - "pluginId": "alerting", - "scope": "common", - "docId": "kibAlertingPluginApi", - "section": "def-common.HealthStatus", - "text": "HealthStatus" - }, - "; timestamp: string; }" - ], - "path": "x-pack/plugins/alerting/common/alert.ts", - "deprecated": false - }, - { - "parentPluginId": "alerting", - "id": "def-common.AlertsHealth.executionHealth", - "type": "Object", - "tags": [], - "label": "executionHealth", - "description": [], - "signature": [ - "{ status: ", - { - "pluginId": "alerting", - "scope": "common", - "docId": "kibAlertingPluginApi", - "section": "def-common.HealthStatus", - "text": "HealthStatus" - }, - "; timestamp: string; }" - ], - "path": "x-pack/plugins/alerting/common/alert.ts", - "deprecated": false - }, - { - "parentPluginId": "alerting", - "id": "def-common.AlertsHealth.readHealth", - "type": "Object", - "tags": [], - "label": "readHealth", - "description": [], - "signature": [ - "{ status: ", - { - "pluginId": "alerting", - "scope": "common", - "docId": "kibAlertingPluginApi", - "section": "def-common.HealthStatus", - "text": "HealthStatus" - }, - "; timestamp: string; }" - ], - "path": "x-pack/plugins/alerting/common/alert.ts", - "deprecated": false - } - ], - "initialIsOpen": false - }, - { - "parentPluginId": "alerting", - "id": "def-common.AlertStateNavigation", - "type": "Interface", - "tags": [], - "label": "AlertStateNavigation", - "description": [], - "path": "x-pack/plugins/alerting/common/alert_navigation.ts", - "deprecated": false, - "children": [ - { - "parentPluginId": "alerting", - "id": "def-common.AlertStateNavigation.state", - "type": "Object", - "tags": [], - "label": "state", - "description": [], - "signature": [ - { - "pluginId": "@kbn/utility-types", - "scope": "server", - "docId": "kibKbnUtilityTypesPluginApi", - "section": "def-server.JsonObject", - "text": "JsonObject" - } - ], - "path": "x-pack/plugins/alerting/common/alert_navigation.ts", + "path": "x-pack/plugins/alerting/common/alert_summary.ts", "deprecated": false } ], @@ -3912,20 +3912,6 @@ "deprecated": false, "initialIsOpen": false }, - { - "parentPluginId": "alerting", - "id": "def-common.AlertInstanceStatusValues", - "type": "Type", - "tags": [], - "label": "AlertInstanceStatusValues", - "description": [], - "signature": [ - "\"OK\" | \"Active\"" - ], - "path": "x-pack/plugins/alerting/common/alert_instance_summary.ts", - "deprecated": false, - "initialIsOpen": false - }, { "parentPluginId": "alerting", "id": "def-common.AlertNavigation", @@ -3990,9 +3976,9 @@ "label": "AlertStatusValues", "description": [], "signature": [ - "\"OK\" | \"Active\" | \"Error\"" + "\"OK\" | \"Active\"" ], - "path": "x-pack/plugins/alerting/common/alert_instance_summary.ts", + "path": "x-pack/plugins/alerting/common/alert_summary.ts", "deprecated": false, "initialIsOpen": false }, @@ -4180,6 +4166,20 @@ "deprecated": false, "initialIsOpen": false }, + { + "parentPluginId": "alerting", + "id": "def-common.RuleStatusValues", + "type": "Type", + "tags": [], + "label": "RuleStatusValues", + "description": [], + "signature": [ + "\"OK\" | \"Active\" | \"Error\"" + ], + "path": "x-pack/plugins/alerting/common/alert_summary.ts", + "deprecated": false, + "initialIsOpen": false + }, { "parentPluginId": "alerting", "id": "def-common.SanitizedAlert", diff --git a/api_docs/apm.json b/api_docs/apm.json index a97e5428b2b8e..2fd290d8b9c35 100644 --- a/api_docs/apm.json +++ b/api_docs/apm.json @@ -737,7 +737,7 @@ "label": "APIEndpoint", "description": [], "signature": [ - "\"POST /internal/apm/data_view/static\" | \"GET /internal/apm/data_view/dynamic\" | \"GET /internal/apm/environments\" | \"GET /internal/apm/services/{serviceName}/errors\" | \"GET /internal/apm/services/{serviceName}/errors/{groupId}\" | \"GET /internal/apm/services/{serviceName}/errors/distribution\" | \"POST /internal/apm/latency/overall_distribution\" | \"GET /internal/apm/services/{serviceName}/metrics/charts\" | \"GET /internal/apm/observability_overview\" | \"GET /internal/apm/observability_overview/has_data\" | \"GET /api/apm/rum/client-metrics\" | \"GET /api/apm/rum-client/page-load-distribution\" | \"GET /api/apm/rum-client/page-load-distribution/breakdown\" | \"GET /api/apm/rum-client/page-view-trends\" | \"GET /api/apm/rum-client/services\" | \"GET /api/apm/rum-client/visitor-breakdown\" | \"GET /api/apm/rum-client/web-core-vitals\" | \"GET /api/apm/rum-client/long-task-metrics\" | \"GET /api/apm/rum-client/url-search\" | \"GET /api/apm/rum-client/js-errors\" | \"GET /api/apm/observability_overview/has_rum_data\" | \"GET /internal/apm/service-map\" | \"GET /internal/apm/service-map/service/{serviceName}\" | \"GET /internal/apm/service-map/backend\" | \"GET /internal/apm/services/{serviceName}/serviceNodes\" | \"GET /internal/apm/services\" | \"GET /internal/apm/services/detailed_statistics\" | \"GET /internal/apm/services/{serviceName}/metadata/details\" | \"GET /internal/apm/services/{serviceName}/metadata/icons\" | \"GET /internal/apm/services/{serviceName}/agent\" | \"GET /internal/apm/services/{serviceName}/transaction_types\" | \"GET /internal/apm/services/{serviceName}/node/{serviceNodeName}/metadata\" | \"GET /api/apm/services/{serviceName}/annotation/search\" | \"POST /api/apm/services/{serviceName}/annotation\" | \"GET /internal/apm/services/{serviceName}/error_groups/main_statistics\" | \"GET /internal/apm/services/{serviceName}/error_groups/detailed_statistics\" | \"GET /internal/apm/services/{serviceName}/service_overview_instances/details/{serviceNodeName}\" | \"GET /internal/apm/services/{serviceName}/throughput\" | \"GET /internal/apm/services/{serviceName}/service_overview_instances/main_statistics\" | \"GET /internal/apm/services/{serviceName}/service_overview_instances/detailed_statistics\" | \"GET /internal/apm/services/{serviceName}/dependencies\" | \"GET /internal/apm/services/{serviceName}/dependencies/breakdown\" | \"GET /internal/apm/services/{serviceName}/profiling/timeline\" | \"GET /internal/apm/services/{serviceName}/profiling/statistics\" | \"GET /internal/apm/services/{serviceName}/alerts\" | \"GET /internal/apm/services/{serviceName}/infrastructure\" | \"GET /internal/apm/suggestions\" | \"GET /internal/apm/traces/{traceId}\" | \"GET /internal/apm/traces\" | \"GET /internal/apm/traces/{traceId}/root_transaction\" | \"GET /internal/apm/transactions/{transactionId}\" | \"GET /internal/apm/services/{serviceName}/transactions/groups/main_statistics\" | \"GET /internal/apm/services/{serviceName}/transactions/groups/detailed_statistics\" | \"GET /internal/apm/services/{serviceName}/transactions/charts/latency\" | \"GET /internal/apm/services/{serviceName}/transactions/traces/samples\" | \"GET /internal/apm/services/{serviceName}/transaction/charts/breakdown\" | \"GET /internal/apm/services/{serviceName}/transactions/charts/error_rate\" | \"GET /internal/apm/alerts/chart_preview/transaction_error_rate\" | \"GET /internal/apm/alerts/chart_preview/transaction_duration\" | \"GET /internal/apm/alerts/chart_preview/transaction_error_count\" | \"GET /api/apm/settings/agent-configuration\" | \"GET /api/apm/settings/agent-configuration/view\" | \"DELETE /api/apm/settings/agent-configuration\" | \"PUT /api/apm/settings/agent-configuration\" | \"POST /api/apm/settings/agent-configuration/search\" | \"GET /api/apm/settings/agent-configuration/services\" | \"GET /api/apm/settings/agent-configuration/environments\" | \"GET /api/apm/settings/agent-configuration/agent_name\" | \"GET /internal/apm/settings/anomaly-detection/jobs\" | \"POST /internal/apm/settings/anomaly-detection/jobs\" | \"GET /internal/apm/settings/anomaly-detection/environments\" | \"GET /internal/apm/settings/apm-index-settings\" | \"GET /internal/apm/settings/apm-indices\" | \"POST /internal/apm/settings/apm-indices/save\" | \"GET /internal/apm/settings/custom_links/transaction\" | \"GET /internal/apm/settings/custom_links\" | \"POST /internal/apm/settings/custom_links\" | \"PUT /internal/apm/settings/custom_links/{id}\" | \"DELETE /internal/apm/settings/custom_links/{id}\" | \"GET /api/apm/sourcemaps\" | \"POST /api/apm/sourcemaps\" | \"DELETE /api/apm/sourcemaps/{id}\" | \"GET /internal/apm/fleet/has_data\" | \"GET /internal/apm/fleet/agents\" | \"POST /api/apm/fleet/apm_server_schema\" | \"GET /internal/apm/fleet/apm_server_schema/unsupported\" | \"GET /internal/apm/fleet/migration_check\" | \"POST /internal/apm/fleet/cloud_apm_package_policy\" | \"GET /internal/apm/backends/top_backends\" | \"GET /internal/apm/backends/upstream_services\" | \"GET /internal/apm/backends/metadata\" | \"GET /internal/apm/backends/charts/latency\" | \"GET /internal/apm/backends/charts/throughput\" | \"GET /internal/apm/backends/charts/error_rate\" | \"GET /internal/apm/fallback_to_transactions\" | \"GET /internal/apm/has_data\" | \"GET /internal/apm/event_metadata/{processorEvent}/{id}\"" + "\"POST /internal/apm/data_view/static\" | \"GET /internal/apm/data_view/dynamic\" | \"GET /internal/apm/environments\" | \"GET /internal/apm/services/{serviceName}/errors\" | \"GET /internal/apm/services/{serviceName}/errors/{groupId}\" | \"GET /internal/apm/services/{serviceName}/errors/distribution\" | \"POST /internal/apm/latency/overall_distribution\" | \"GET /internal/apm/services/{serviceName}/metrics/charts\" | \"GET /internal/apm/observability_overview\" | \"GET /internal/apm/observability_overview/has_data\" | \"GET /internal/apm/ux/client-metrics\" | \"GET /internal/apm/ux/page-load-distribution\" | \"GET /internal/apm/ux/page-load-distribution/breakdown\" | \"GET /internal/apm/ux/page-view-trends\" | \"GET /internal/apm/ux/services\" | \"GET /internal/apm/ux/visitor-breakdown\" | \"GET /internal/apm/ux/web-core-vitals\" | \"GET /internal/apm/ux/long-task-metrics\" | \"GET /internal/apm/ux/url-search\" | \"GET /internal/apm/ux/js-errors\" | \"GET /api/apm/observability_overview/has_rum_data\" | \"GET /internal/apm/service-map\" | \"GET /internal/apm/service-map/service/{serviceName}\" | \"GET /internal/apm/service-map/backend\" | \"GET /internal/apm/services/{serviceName}/serviceNodes\" | \"GET /internal/apm/services\" | \"GET /internal/apm/services/detailed_statistics\" | \"GET /internal/apm/services/{serviceName}/metadata/details\" | \"GET /internal/apm/services/{serviceName}/metadata/icons\" | \"GET /internal/apm/services/{serviceName}/agent\" | \"GET /internal/apm/services/{serviceName}/transaction_types\" | \"GET /internal/apm/services/{serviceName}/node/{serviceNodeName}/metadata\" | \"GET /api/apm/services/{serviceName}/annotation/search\" | \"POST /api/apm/services/{serviceName}/annotation\" | \"GET /internal/apm/services/{serviceName}/error_groups/main_statistics\" | \"GET /internal/apm/services/{serviceName}/error_groups/detailed_statistics\" | \"GET /internal/apm/services/{serviceName}/service_overview_instances/details/{serviceNodeName}\" | \"GET /internal/apm/services/{serviceName}/throughput\" | \"GET /internal/apm/services/{serviceName}/service_overview_instances/main_statistics\" | \"GET /internal/apm/services/{serviceName}/service_overview_instances/detailed_statistics\" | \"GET /internal/apm/services/{serviceName}/dependencies\" | \"GET /internal/apm/services/{serviceName}/dependencies/breakdown\" | \"GET /internal/apm/services/{serviceName}/profiling/timeline\" | \"GET /internal/apm/services/{serviceName}/profiling/statistics\" | \"GET /internal/apm/services/{serviceName}/alerts\" | \"GET /internal/apm/services/{serviceName}/infrastructure\" | \"GET /internal/apm/suggestions\" | \"GET /internal/apm/traces/{traceId}\" | \"GET /internal/apm/traces\" | \"GET /internal/apm/traces/{traceId}/root_transaction\" | \"GET /internal/apm/transactions/{transactionId}\" | \"GET /internal/apm/services/{serviceName}/transactions/groups/main_statistics\" | \"GET /internal/apm/services/{serviceName}/transactions/groups/detailed_statistics\" | \"GET /internal/apm/services/{serviceName}/transactions/charts/latency\" | \"GET /internal/apm/services/{serviceName}/transactions/traces/samples\" | \"GET /internal/apm/services/{serviceName}/transaction/charts/breakdown\" | \"GET /internal/apm/services/{serviceName}/transactions/charts/error_rate\" | \"GET /internal/apm/alerts/chart_preview/transaction_error_rate\" | \"GET /internal/apm/alerts/chart_preview/transaction_duration\" | \"GET /internal/apm/alerts/chart_preview/transaction_error_count\" | \"GET /api/apm/settings/agent-configuration\" | \"GET /api/apm/settings/agent-configuration/view\" | \"DELETE /api/apm/settings/agent-configuration\" | \"PUT /api/apm/settings/agent-configuration\" | \"POST /api/apm/settings/agent-configuration/search\" | \"GET /api/apm/settings/agent-configuration/services\" | \"GET /api/apm/settings/agent-configuration/environments\" | \"GET /api/apm/settings/agent-configuration/agent_name\" | \"GET /internal/apm/settings/anomaly-detection/jobs\" | \"POST /internal/apm/settings/anomaly-detection/jobs\" | \"GET /internal/apm/settings/anomaly-detection/environments\" | \"GET /internal/apm/settings/apm-index-settings\" | \"GET /internal/apm/settings/apm-indices\" | \"POST /internal/apm/settings/apm-indices/save\" | \"GET /internal/apm/settings/custom_links/transaction\" | \"GET /internal/apm/settings/custom_links\" | \"POST /internal/apm/settings/custom_links\" | \"PUT /internal/apm/settings/custom_links/{id}\" | \"DELETE /internal/apm/settings/custom_links/{id}\" | \"GET /api/apm/sourcemaps\" | \"POST /api/apm/sourcemaps\" | \"DELETE /api/apm/sourcemaps/{id}\" | \"GET /internal/apm/fleet/has_data\" | \"GET /internal/apm/fleet/agents\" | \"POST /api/apm/fleet/apm_server_schema\" | \"GET /internal/apm/fleet/apm_server_schema/unsupported\" | \"GET /internal/apm/fleet/migration_check\" | \"POST /internal/apm/fleet/cloud_apm_package_policy\" | \"GET /internal/apm/backends/top_backends\" | \"GET /internal/apm/backends/upstream_services\" | \"GET /internal/apm/backends/metadata\" | \"GET /internal/apm/backends/charts/latency\" | \"GET /internal/apm/backends/charts/throughput\" | \"GET /internal/apm/backends/charts/error_rate\" | \"GET /internal/apm/fallback_to_transactions\" | \"GET /internal/apm/has_data\" | \"GET /internal/apm/event_metadata/{processorEvent}/{id}\"" ], "path": "x-pack/plugins/apm/server/routes/get_global_apm_server_route_repository.ts", "deprecated": false, @@ -1222,7 +1222,7 @@ "section": "def-server.APMRouteHandlerResources", "text": "APMRouteHandlerResources" }, - ", { serviceCount: number; transactionPerMinute: { value: undefined; timeseries: never[]; } | { value: number; timeseries: { x: number; y: number; }[]; }; }, ", + ", { serviceCount: number; transactionPerMinute: { value: undefined; timeseries: never[]; } | { value: number; timeseries: { x: number; y: number | null; }[]; }; }, ", "APMRouteCreateOptions", ">; } & { \"GET /internal/apm/observability_overview/has_data\": ", { @@ -1244,7 +1244,7 @@ "ApmIndicesConfig", "; }, ", "APMRouteCreateOptions", - ">; } & { \"GET /api/apm/rum/client-metrics\": ", + ">; } & { \"GET /internal/apm/ux/client-metrics\": ", { "pluginId": "@kbn/server-route-repository", "scope": "server", @@ -1252,7 +1252,7 @@ "section": "def-server.ServerRoute", "text": "ServerRoute" }, - "<\"GET /api/apm/rum/client-metrics\", ", + "<\"GET /internal/apm/ux/client-metrics\", ", "TypeC", "<{ query: ", "IntersectionC", @@ -1282,7 +1282,7 @@ }, ", { pageViews: { value: number; }; totalPageLoadDuration: { value: number; }; backEnd: { value: number; }; frontEnd: { value: number; }; }, ", "APMRouteCreateOptions", - ">; } & { \"GET /api/apm/rum-client/page-load-distribution\": ", + ">; } & { \"GET /internal/apm/ux/page-load-distribution\": ", { "pluginId": "@kbn/server-route-repository", "scope": "server", @@ -1290,7 +1290,7 @@ "section": "def-server.ServerRoute", "text": "ServerRoute" }, - "<\"GET /api/apm/rum-client/page-load-distribution\", ", + "<\"GET /internal/apm/ux/page-load-distribution\", ", "TypeC", "<{ query: ", "IntersectionC", @@ -1328,7 +1328,7 @@ }, ", { pageLoadDistribution: { pageLoadDistribution: { x: number; y: number; }[]; percentiles: Record | undefined; minDuration: number; maxDuration: number; } | null; }, ", "APMRouteCreateOptions", - ">; } & { \"GET /api/apm/rum-client/page-load-distribution/breakdown\": ", + ">; } & { \"GET /internal/apm/ux/page-load-distribution/breakdown\": ", { "pluginId": "@kbn/server-route-repository", "scope": "server", @@ -1336,7 +1336,7 @@ "section": "def-server.ServerRoute", "text": "ServerRoute" }, - "<\"GET /api/apm/rum-client/page-load-distribution/breakdown\", ", + "<\"GET /internal/apm/ux/page-load-distribution/breakdown\", ", "TypeC", "<{ query: ", "IntersectionC", @@ -1378,7 +1378,7 @@ }, ", { pageLoadDistBreakdown: { name: string; data: { x: number; y: number; }[]; }[] | undefined; }, ", "APMRouteCreateOptions", - ">; } & { \"GET /api/apm/rum-client/page-view-trends\": ", + ">; } & { \"GET /internal/apm/ux/page-view-trends\": ", { "pluginId": "@kbn/server-route-repository", "scope": "server", @@ -1386,7 +1386,7 @@ "section": "def-server.ServerRoute", "text": "ServerRoute" }, - "<\"GET /api/apm/rum-client/page-view-trends\", ", + "<\"GET /internal/apm/ux/page-view-trends\", ", "TypeC", "<{ query: ", "IntersectionC", @@ -1422,7 +1422,7 @@ }, ", { topItems: string[]; items: Record[]; }, ", "APMRouteCreateOptions", - ">; } & { \"GET /api/apm/rum-client/services\": ", + ">; } & { \"GET /internal/apm/ux/services\": ", { "pluginId": "@kbn/server-route-repository", "scope": "server", @@ -1430,7 +1430,7 @@ "section": "def-server.ServerRoute", "text": "ServerRoute" }, - "<\"GET /api/apm/rum-client/services\", ", + "<\"GET /internal/apm/ux/services\", ", "TypeC", "<{ query: ", "IntersectionC", @@ -1454,7 +1454,7 @@ }, ", { rumServices: string[]; }, ", "APMRouteCreateOptions", - ">; } & { \"GET /api/apm/rum-client/visitor-breakdown\": ", + ">; } & { \"GET /internal/apm/ux/visitor-breakdown\": ", { "pluginId": "@kbn/server-route-repository", "scope": "server", @@ -1462,7 +1462,7 @@ "section": "def-server.ServerRoute", "text": "ServerRoute" }, - "<\"GET /api/apm/rum-client/visitor-breakdown\", ", + "<\"GET /internal/apm/ux/visitor-breakdown\", ", "TypeC", "<{ query: ", "IntersectionC", @@ -1492,7 +1492,7 @@ }, ", { os: { count: number; name: string; }[]; browsers: { count: number; name: string; }[]; }, ", "APMRouteCreateOptions", - ">; } & { \"GET /api/apm/rum-client/web-core-vitals\": ", + ">; } & { \"GET /internal/apm/ux/web-core-vitals\": ", { "pluginId": "@kbn/server-route-repository", "scope": "server", @@ -1500,7 +1500,7 @@ "section": "def-server.ServerRoute", "text": "ServerRoute" }, - "<\"GET /api/apm/rum-client/web-core-vitals\", ", + "<\"GET /internal/apm/ux/web-core-vitals\", ", "TypeC", "<{ query: ", "IntersectionC", @@ -1530,7 +1530,7 @@ }, ", { coreVitalPages: number; cls: number | null; fid: number | null | undefined; lcp: number | null | undefined; tbt: number; fcp: number | null | undefined; lcpRanks: number[]; fidRanks: number[]; clsRanks: number[]; }, ", "APMRouteCreateOptions", - ">; } & { \"GET /api/apm/rum-client/long-task-metrics\": ", + ">; } & { \"GET /internal/apm/ux/long-task-metrics\": ", { "pluginId": "@kbn/server-route-repository", "scope": "server", @@ -1538,7 +1538,7 @@ "section": "def-server.ServerRoute", "text": "ServerRoute" }, - "<\"GET /api/apm/rum-client/long-task-metrics\", ", + "<\"GET /internal/apm/ux/long-task-metrics\", ", "TypeC", "<{ query: ", "IntersectionC", @@ -1568,7 +1568,7 @@ }, ", { noOfLongTasks: number; sumOfLongTasks: number; longestLongTask: number; }, ", "APMRouteCreateOptions", - ">; } & { \"GET /api/apm/rum-client/url-search\": ", + ">; } & { \"GET /internal/apm/ux/url-search\": ", { "pluginId": "@kbn/server-route-repository", "scope": "server", @@ -1576,7 +1576,7 @@ "section": "def-server.ServerRoute", "text": "ServerRoute" }, - "<\"GET /api/apm/rum-client/url-search\", ", + "<\"GET /internal/apm/ux/url-search\", ", "TypeC", "<{ query: ", "IntersectionC", @@ -1606,7 +1606,7 @@ }, ", { total: number; items: { url: string; count: number; pld: number; }[]; }, ", "APMRouteCreateOptions", - ">; } & { \"GET /api/apm/rum-client/js-errors\": ", + ">; } & { \"GET /internal/apm/ux/js-errors\": ", { "pluginId": "@kbn/server-route-repository", "scope": "server", @@ -1614,7 +1614,7 @@ "section": "def-server.ServerRoute", "text": "ServerRoute" }, - "<\"GET /api/apm/rum-client/js-errors\", ", + "<\"GET /internal/apm/ux/js-errors\", ", "TypeC", "<{ query: ", "IntersectionC", @@ -2478,7 +2478,7 @@ "section": "def-server.APMRouteHandlerResources", "text": "APMRouteHandlerResources" }, - ", { currentPeriod: { x: number; y: number; }[]; previousPeriod: { x: number; y: number | null | undefined; }[]; }, ", + ", { currentPeriod: { x: number; y: number | null; }[]; previousPeriod: { x: number; y: number | null | undefined; }[]; }, ", "APMRouteCreateOptions", ">; } & { \"GET /internal/apm/services/{serviceName}/service_overview_instances/main_statistics\": ", { @@ -3456,7 +3456,7 @@ "section": "def-server.APMRouteHandlerResources", "text": "APMRouteHandlerResources" }, - ", { noHits: boolean; traceSamples: { transactionId: string; traceId: string; }[]; }, ", + ", { traceSamples: { transactionId: string; traceId: string; }[]; }, ", "APMRouteCreateOptions", ">; } & { \"GET /internal/apm/services/{serviceName}/transaction/charts/breakdown\": ", { @@ -3580,9 +3580,9 @@ "section": "def-server.APMRouteHandlerResources", "text": "APMRouteHandlerResources" }, - ", { currentPeriod: { noHits: boolean; transactionErrorRate: ", + ", { currentPeriod: { timeseries: ", "Coordinate", - "[]; average: number | null; }; previousPeriod: { transactionErrorRate: { x: number; y: number | null | undefined; }[]; noHits: boolean; average: number | null; }; }, ", + "[]; average: number | null; }; previousPeriod: { timeseries: { x: number; y: number | null | undefined; }[]; average: number | null; }; }, ", "APMRouteCreateOptions", ">; } & { \"GET /internal/apm/alerts/chart_preview/transaction_error_rate\": ", { @@ -4832,7 +4832,7 @@ "section": "def-server.APMRouteHandlerResources", "text": "APMRouteHandlerResources" }, - ", { currentTimeseries: { x: number; y: number; }[]; comparisonTimeseries: { x: number; y: number; }[] | null; }, ", + ", { currentTimeseries: { x: number; y: number | null; }[]; comparisonTimeseries: { x: number; y: number | null; }[] | null; }, ", "APMRouteCreateOptions", ">; } & { \"GET /internal/apm/backends/charts/error_rate\": ", { diff --git a/config/kibana.yml b/config/kibana.yml index eeb7c84df4318..f6f85f057172c 100644 --- a/config/kibana.yml +++ b/config/kibana.yml @@ -1,3 +1,7 @@ +# For more configuration options see the configuration guide for Kibana in +# https://www.elastic.co/guide/index.html + +# =================== System: Kibana Server =================== # Kibana is served by a back end server. This setting specifies the port to use. #server.port: 5601 @@ -14,8 +18,7 @@ # Specifies whether Kibana should rewrite requests that are prefixed with # `server.basePath` or require that they are rewritten by your reverse proxy. -# This setting was effectively always `false` before Kibana 6.3 and will -# default to `true` starting in Kibana 7.0. +# Defaults to `false`. #server.rewriteBasePath: false # Specifies the public URL at which Kibana is available for end users. If @@ -25,9 +28,17 @@ # The maximum payload size in bytes for incoming server requests. #server.maxPayload: 1048576 -# The Kibana server's name. This is used for display purposes. +# The Kibana server's name. This is used for display purposes. #server.name: "your-hostname" +# =================== System: Kibana Server (Optional) =================== +# Enables SSL and paths to the PEM-format SSL certificate and SSL key files, respectively. +# These settings enable SSL for outgoing requests from the Kibana server to the browser. +#server.ssl.enabled: false +#server.ssl.certificate: /path/to/your/server.crt +#server.ssl.key: /path/to/your/server.key + +# =================== System: Elasticsearch =================== # The URLs of the Elasticsearch instances to use for all your queries. #elasticsearch.hosts: ["http://localhost:9200"] @@ -39,28 +50,10 @@ #elasticsearch.password: "pass" # Kibana can also authenticate to Elasticsearch via "service account tokens". -# If may use this token instead of a username/password. +# Service account tokens are Bearer style tokens that replace the traditional username/password based configuration. +# Use this token instead of a username/password. # elasticsearch.serviceAccountToken: "my_token" -# Enables SSL and paths to the PEM-format SSL certificate and SSL key files, respectively. -# These settings enable SSL for outgoing requests from the Kibana server to the browser. -#server.ssl.enabled: false -#server.ssl.certificate: /path/to/your/server.crt -#server.ssl.key: /path/to/your/server.key - -# Optional settings that provide the paths to the PEM-format SSL certificate and key files. -# These files are used to verify the identity of Kibana to Elasticsearch and are required when -# xpack.security.http.ssl.client_authentication in Elasticsearch is set to required. -#elasticsearch.ssl.certificate: /path/to/your/client.crt -#elasticsearch.ssl.key: /path/to/your/client.key - -# Optional setting that enables you to specify a path to the PEM file for the certificate -# authority for your Elasticsearch instance. -#elasticsearch.ssl.certificateAuthorities: [ "/path/to/your/CA.pem" ] - -# To disregard the validity of SSL certificates, change this setting's value to 'none'. -#elasticsearch.ssl.verificationMode: full - # Time in milliseconds to wait for Elasticsearch to respond to pings. Defaults to the value of # the elasticsearch.requestTimeout setting. #elasticsearch.pingTimeout: 1500 @@ -80,10 +73,21 @@ # Time in milliseconds for Elasticsearch to wait for responses from shards. Set to 0 to disable. #elasticsearch.shardTimeout: 30000 -# Specifies the path where Kibana creates the process ID file. -#pid.file: /run/kibana/kibana.pid +# =================== System: Elasticsearch (Optional) =================== +# These files are used to verify the identity of Kibana to Elasticsearch and are required when +# xpack.security.http.ssl.client_authentication in Elasticsearch is set to required. +#elasticsearch.ssl.certificate: /path/to/your/client.crt +#elasticsearch.ssl.key: /path/to/your/client.key + +# Enables you to specify a path to the PEM file for the certificate +# authority for your Elasticsearch instance. +#elasticsearch.ssl.certificateAuthorities: [ "/path/to/your/CA.pem" ] + +# To disregard the validity of SSL certificates, change this setting's value to 'none'. +#elasticsearch.ssl.verificationMode: full -# Set the value of this setting to off to suppress all logging output, or to debug to log everything. +# =================== System: Logging =================== +# Set the value of this setting to off to suppress all logging output, or to debug to log everything. Defaults to 'info' #logging.root.level: debug # Enables you to specify a file where Kibana stores log output. @@ -108,10 +112,47 @@ # - name: metrics.ops # level: debug +# =================== System: Other =================== +# The path where Kibana stores persistent data not saved in Elasticsearch. Defaults to data +#path.data: data + +# Specifies the path where Kibana creates the process ID file. +#pid.file: /run/kibana/kibana.pid + # Set the interval in milliseconds to sample system and process performance -# metrics. Minimum is 100ms. Defaults to 5000. +# metrics. Minimum is 100ms. Defaults to 5000ms. #ops.interval: 5000 # Specifies locale to be used for all localizable strings, dates and number formats. # Supported languages are the following: English - en , by default , Chinese - zh-CN . #i18n.locale: "en" + +# =================== Frequently used (Optional)=================== + +# =================== Saved Objects: Migrations =================== +# Saved object migrations run at startup. If you run into migration-related issues, you might need to adjust these settings. + +# The number of documents migrated at a time. +# If Kibana can't start up or upgrade due to an Elasticsearch `circuit_breaking_exception`, +# use a smaller batchSize value to reduce the memory pressure. Defaults to 1000 objects per batch. +#migrations.batchSize: 1000 + +# The maximum payload size for indexing batches of upgraded saved objects. +# To avoid migrations failing due to a 413 Request Entity Too Large response from Elasticsearch. +# This value should be lower than or equal to your Elasticsearch cluster’s `http.max_content_length` +# configuration option. Default: 100mb +#migrations.maxBatchSizeBytes: 100mb + +# The number of times to retry temporary migration failures. Increase the setting +# if migrations fail frequently with a message such as `Unable to complete the [...] step after +# 15 attempts, terminating`. Defaults to 15 +#migrations.retryAttempts: 15 + +# =================== Search Autocomplete =================== +# Time in milliseconds to wait for autocomplete suggestions from Elasticsearch. +# This value must be a whole number greater than zero. Defaults to 1000ms +#data.autocomplete.valueSuggestions.timeout: 1000 + +# Maximum number of documents loaded by each shard to generate autocomplete suggestions. +# This value must be a whole number greater than zero. Defaults to 100_000 +#data.autocomplete.valueSuggestions.terminateAfter: 100000 diff --git a/dev_docs/getting_started/development_windows.mdx b/dev_docs/getting_started/development_windows.mdx new file mode 100644 index 0000000000000..4300c307a7b11 --- /dev/null +++ b/dev_docs/getting_started/development_windows.mdx @@ -0,0 +1,45 @@ +--- +id: kibDevTutorialSetupDevWindows +slug: /kibana-dev-docs/tutorial/setup-dev-windows +title: Development on Windows +summary: Learn how to setup a development environment on Windows +date: 2021-08-11 +tags: ['kibana', 'onboarding', 'dev', 'windows', 'setup'] +--- + + +# Overview + +Development on Windows is recommended through WSL2. WSL lets users run a Linux environment on Windows, providing a supported development environment for Kibana. + +## Install WSL + +The latest setup instructions can be found at https://docs.microsoft.com/en-us/windows/wsl/install-win10 + +1) Open Powershell as an administrator +1) Enable WSL + ``` + dism.exe /online /enable-feature /featurename:Microsoft-Windows-Subsystem-Linux /all /norestart + ``` +1) Enable Virtual Machine Platform + ``` + dism.exe /online /enable-feature /featurename:VirtualMachinePlatform /all /norestart + ``` +1) Download and install the [Linux kernel update package](https://wslstorestorage.blob.core.windows.net/wslblob/wsl_update_x64.msi) +1) Set WSL 2 as the default version + ``` + wsl --set-default-version 2 + ``` +1) Open the Micrsoft Store application and install a Linux distribution + +## Setup Kibana + +1. + +## Install VS Code + +Remote development is supported with an extension. [Reference](https://code.visualstudio.com/docs/remote/wsl). + +1) Install VS Code on Windows +1) Check the "Add to PATH" option during setup +1) Install the [Remote Development](https://aka.ms/vscode-remote/download/extension) package diff --git a/dev_docs/tutorials/testing_plugins.mdx b/dev_docs/tutorials/testing_plugins.mdx index b8f8029e7b16c..044d610aa3489 100644 --- a/dev_docs/tutorials/testing_plugins.mdx +++ b/dev_docs/tutorials/testing_plugins.mdx @@ -458,7 +458,7 @@ describe('Plugin', () => { const [coreStartMock, startDepsMock] = await coreSetup.getStartServices(); const unmountMock = jest.fn(); renderAppMock.mockReturnValue(unmountMock); - const params = coreMock.createAppMountParamters('/fake/base/path'); + const params = coreMock.createAppMountParameters('/fake/base/path'); new Plugin(coreMock.createPluginInitializerContext()).setup(coreSetup); // Grab registered mount function @@ -528,7 +528,7 @@ import { renderApp } from './application'; describe('renderApp', () => { it('mounts and unmounts UI', () => { - const params = coreMock.createAppMountParamters('/fake/base/path'); + const params = coreMock.createAppMountParameters('/fake/base/path'); const core = coreMock.createStart(); // Verify some expected DOM element is rendered into the element @@ -540,7 +540,7 @@ describe('renderApp', () => { }); it('unsubscribes from uiSettings', () => { - const params = coreMock.createAppMountParamters('/fake/base/path'); + const params = coreMock.createAppMountParameters('/fake/base/path'); const core = coreMock.createStart(); // Create a fake Subject you can use to monitor observers const settings$ = new Subject(); @@ -555,7 +555,7 @@ describe('renderApp', () => { }); it('resets chrome visibility', () => { - const params = coreMock.createAppMountParamters('/fake/base/path'); + const params = coreMock.createAppMountParameters('/fake/base/path'); const core = coreMock.createStart(); // Verify stateful Core API was called on mount diff --git a/docs/CHANGELOG.asciidoc b/docs/CHANGELOG.asciidoc index 8111172893795..31cdcbca9d1f9 100644 --- a/docs/CHANGELOG.asciidoc +++ b/docs/CHANGELOG.asciidoc @@ -18,8 +18,6 @@ Review important information about the {kib} 8.0.0 releases. [[release-notes-8.0.0-beta1]] == {kib} 8.0.0-beta1 -coming::[8.0.0-beta1] - Review the {kib} 8.0.0-beta1 changes, then use the <> to complete the upgrade. [float] diff --git a/docs/api/dashboard-api.asciidoc b/docs/api/dashboard-api.asciidoc index 94511c3154fe0..e6f54dd9156ec 100644 --- a/docs/api/dashboard-api.asciidoc +++ b/docs/api/dashboard-api.asciidoc @@ -1,7 +1,7 @@ [[dashboard-api]] == Import and export dashboard APIs -deprecated::[7.15.0,These experimental APIs have been deprecated in favor of <> and <>.] +deprecated::[7.15.0,Both of these APIs have been deprecated in favor of <> and <>.] Import and export dashboards with the corresponding saved objects, such as visualizations, saved searches, and index patterns. diff --git a/docs/api/dashboard/export-dashboard.asciidoc b/docs/api/dashboard/export-dashboard.asciidoc index 098ec976569bd..3a20eff0a54d2 100644 --- a/docs/api/dashboard/export-dashboard.asciidoc +++ b/docs/api/dashboard/export-dashboard.asciidoc @@ -6,7 +6,7 @@ deprecated::[7.15.0,Use <> instead.] -experimental[] Export dashboards and corresponding saved objects. +Export dashboards and corresponding saved objects. [[dashboard-api-export-request]] ==== Request diff --git a/docs/api/dashboard/import-dashboard.asciidoc b/docs/api/dashboard/import-dashboard.asciidoc index 41eb47500c8d7..e4817d6cb7ee9 100644 --- a/docs/api/dashboard/import-dashboard.asciidoc +++ b/docs/api/dashboard/import-dashboard.asciidoc @@ -6,7 +6,7 @@ deprecated::[7.15.0,Use <> instead.] -experimental[] Import dashboards and corresponding saved objects. +Import dashboards and corresponding saved objects. [[dashboard-api-import-request]] ==== Request diff --git a/docs/api/upgrade-assistant/batch_reindexing.asciidoc b/docs/api/upgrade-assistant/batch_reindexing.asciidoc index db3e080d09185..6b355185de5ce 100644 --- a/docs/api/upgrade-assistant/batch_reindexing.asciidoc +++ b/docs/api/upgrade-assistant/batch_reindexing.asciidoc @@ -6,7 +6,7 @@ experimental["The underlying Upgrade Assistant concepts are stable, but the APIs for managing Upgrade Assistant are experimental."] -Start or resume multiple reindexing tasks in one request. Additionally, reindexing tasks started or resumed +Start or resume multiple <> tasks in one request. Additionally, reindexing tasks started or resumed via the batch endpoint will be placed on a queue and executed one-by-one, which ensures that minimal cluster resources are consumed over time. @@ -76,7 +76,7 @@ Similar to the <>, the API retur } -------------------------------------------------- -<1> A list of reindex operations created, the order in the array indicates the order in which tasks will be executed. +<1> A list of reindex tasks created, the order in the array indicates the order in which tasks will be executed. <2> Presence of this key indicates that the reindex job will occur in the batch. <3> A Unix timestamp of when the reindex task was placed in the queue. <4> A list of errors that may have occurred preventing the reindex task from being created. diff --git a/docs/api/upgrade-assistant/cancel_reindex.asciidoc b/docs/api/upgrade-assistant/cancel_reindex.asciidoc index 04ab3bdde35fc..93e4c6fda6b40 100644 --- a/docs/api/upgrade-assistant/cancel_reindex.asciidoc +++ b/docs/api/upgrade-assistant/cancel_reindex.asciidoc @@ -4,7 +4,7 @@ Cancel reindex ++++ -experimental[] Cancel reindexes that are waiting for the {es} reindex task to complete. For example, `lastCompletedStep` set to `40`. +experimental["The underlying Upgrade Assistant concepts are stable, but the APIs for managing Upgrade Assistant are experimental."] Cancel reindexes that are waiting for the Elasticsearch reindex task to complete. For example, `lastCompletedStep` set to `40`. diff --git a/docs/api/upgrade-assistant/check_reindex_status.asciidoc b/docs/api/upgrade-assistant/check_reindex_status.asciidoc index 75aac7b3699f5..934fd92312b04 100644 --- a/docs/api/upgrade-assistant/check_reindex_status.asciidoc +++ b/docs/api/upgrade-assistant/check_reindex_status.asciidoc @@ -4,7 +4,9 @@ Check reindex status ++++ -experimental[] Check the status of the reindex operation. +experimental["The underlying Upgrade Assistant concepts are stable, but the APIs for managing Upgrade Assistant are experimental."] + +Check the status of the reindex task. [[check-reindex-status-request]] ==== Request @@ -43,7 +45,7 @@ The API returns the following: <2> Current status of the reindex. For details, see <>. <3> Last successfully completed step of the reindex. For details, see <> table. <4> Task ID of the reindex task in Elasticsearch. Only present if reindexing has started. -<5> Percentage of how far the reindexing task in Elasticsearch has progressed, in decimal from from 0 to 1. +<5> Percentage of how far the reindexing task in Elasticsearch has progressed, in decimal form from 0 to 1. <6> Error that caused the reindex to fail, if it failed. <7> An array of any warning codes explaining what changes are required for this reindex. For details, see <>. <8> Specifies if the user has sufficient privileges to reindex this index. When security is unavailable or disables, returns `true`. @@ -73,7 +75,7 @@ To resume the reindex, you must submit a new POST request to the `/api/upgrade_a ==== Step codes `0`:: - The reindex operation has been created in Kibana. + The reindex task has been created in Kibana. `10`:: The index group services stopped. Only applies to some system indices. diff --git a/docs/api/upgrade-assistant/reindexing.asciidoc b/docs/api/upgrade-assistant/reindexing.asciidoc index ce5670822e5ad..ccb9433ac24b1 100644 --- a/docs/api/upgrade-assistant/reindexing.asciidoc +++ b/docs/api/upgrade-assistant/reindexing.asciidoc @@ -4,9 +4,18 @@ Start or resume reindex ++++ -experimental[] Start a new reindex or resume a paused reindex. +experimental["The underlying Upgrade Assistant concepts are stable, but the APIs for managing Upgrade Assistant are experimental."] + +Start a new reindex or resume a paused reindex. Following steps are performed during +a reindex task: + +. Setting the index to read-only +. Creating a new index +. {ref}/docs-reindex.html[Reindexing] documents into the new index +. Creating an index alias for the new index +. Deleting the old index + -Start a new reindex or resume a paused reindex. [[start-resume-reindex-request]] ==== Request @@ -40,6 +49,6 @@ The API returns the following: <1> The name of the new index. <2> The reindex status. For more information, refer to <>. <3> The last successfully completed step of the reindex. For more information, refer to <>. -<4> The task ID of the reindex task in {es}. Appears when the reindexing starts. -<5> The progress of the reindexing task in {es}. Appears in decimal form, from 0 to 1. +<4> The task ID of the {ref}/docs-reindex.html[reindex] task in {es}. Appears when the reindexing starts. +<5> The progress of the {ref}/docs-reindex.html[reindexing] task in {es}. Appears in decimal form, from 0 to 1. <6> The error that caused the reindex to fail, if it failed. diff --git a/docs/api/upgrade-assistant/status.asciidoc b/docs/api/upgrade-assistant/status.asciidoc index 42030061c4289..b0c11939ca784 100644 --- a/docs/api/upgrade-assistant/status.asciidoc +++ b/docs/api/upgrade-assistant/status.asciidoc @@ -4,7 +4,7 @@ Upgrade readiness status ++++ -experimental[] Check the status of your cluster. +experimental["The underlying Upgrade Assistant concepts are stable, but the APIs for managing Upgrade Assistant are experimental."] Check the status of your cluster. diff --git a/docs/developer/contributing/development-functional-tests.asciidoc b/docs/developer/contributing/development-functional-tests.asciidoc index cb614c5149f95..4695a499ca6b6 100644 --- a/docs/developer/contributing/development-functional-tests.asciidoc +++ b/docs/developer/contributing/development-functional-tests.asciidoc @@ -490,7 +490,7 @@ From the command line run: ["source","shell"] ----------- -node --debug-brk --inspect scripts/functional_test_runner +node --inspect-brk scripts/functional_test_runner ----------- This prints out a URL that you can visit in Chrome and debug your functional tests in the browser. diff --git a/docs/developer/contributing/development-unit-tests.asciidoc b/docs/developer/contributing/development-unit-tests.asciidoc index 9f0896f8a673f..0a21dbbb449cc 100644 --- a/docs/developer/contributing/development-unit-tests.asciidoc +++ b/docs/developer/contributing/development-unit-tests.asciidoc @@ -75,7 +75,7 @@ In order to ease the pain specialized tasks provide alternate methods for running the tests. You could also add the `--debug` option so that `node` is run using -the `--debug-brk` flag. You’ll need to connect a remote debugger such +the `--inspect-brk` flag. You’ll need to connect a remote debugger such as https://github.com/node-inspector/node-inspector[`node-inspector`] to proceed in this mode. diff --git a/docs/developer/plugin-list.asciidoc b/docs/developer/plugin-list.asciidoc index de679692e7a84..1429ad29be5fd 100644 --- a/docs/developer/plugin-list.asciidoc +++ b/docs/developer/plugin-list.asciidoc @@ -600,8 +600,7 @@ As a developer you can reuse and extend built-in alerts and actions UI functiona |{kib-repo}blob/{branch}/x-pack/plugins/upgrade_assistant/README.md[upgradeAssistant] -|Upgrade Assistant helps users prepare their Stack for being upgraded to the next major. Its primary -purposes are to: +|Upgrade Assistant helps users prepare their Stack for being upgraded to the next major. It will only be enabled on the last minor before the next major release. This is controlled via the config: xpack.upgrade_assistant.readonly (#101296). |{kib-repo}blob/{branch}/x-pack/plugins/uptime/README.md[uptime] diff --git a/docs/development/core/public/kibana-plugin-core-public.app.deeplinks.md b/docs/development/core/public/kibana-plugin-core-public.app.deeplinks.md index 8186996b63fe5..d9f76fb38a55d 100644 --- a/docs/development/core/public/kibana-plugin-core-public.app.deeplinks.md +++ b/docs/development/core/public/kibana-plugin-core-public.app.deeplinks.md @@ -44,6 +44,5 @@ core.application.register({ ], mount: () => { ... } }) - ``` diff --git a/docs/development/core/public/kibana-plugin-core-public.app.exactroute.md b/docs/development/core/public/kibana-plugin-core-public.app.exactroute.md index eb050b62c7d43..71f6352ec006c 100644 --- a/docs/development/core/public/kibana-plugin-core-public.app.exactroute.md +++ b/docs/development/core/public/kibana-plugin-core-public.app.exactroute.md @@ -25,6 +25,5 @@ core.application.register({ // '[basePath]/app/my_app' will be matched // '[basePath]/app/my_app/some/path' will not be matched - ``` diff --git a/docs/development/core/public/kibana-plugin-core-public.app.md b/docs/development/core/public/kibana-plugin-core-public.app.md index d79a12a83367d..7af32efcb9c12 100644 --- a/docs/development/core/public/kibana-plugin-core-public.app.md +++ b/docs/development/core/public/kibana-plugin-core-public.app.md @@ -10,24 +10,25 @@ ```typescript export interface App extends AppNavOptions ``` +Extends: AppNavOptions ## Properties | Property | Type | Description | | --- | --- | --- | -| [appRoute](./kibana-plugin-core-public.app.approute.md) | string | Override the application's routing path from /app/${id}. Must be unique across registered applications. Should not include the base path from HTTP. | -| [capabilities](./kibana-plugin-core-public.app.capabilities.md) | Partial<Capabilities> | Custom capabilities defined by the app. | -| [category](./kibana-plugin-core-public.app.category.md) | AppCategory | The category definition of the product See [AppCategory](./kibana-plugin-core-public.appcategory.md) See DEFAULT\_APP\_CATEGORIES for more reference | -| [chromeless](./kibana-plugin-core-public.app.chromeless.md) | boolean | Hide the UI chrome when the application is mounted. Defaults to false. Takes precedence over chrome service visibility settings. | -| [deepLinks](./kibana-plugin-core-public.app.deeplinks.md) | AppDeepLink[] | Input type for registering secondary in-app locations for an application.Deep links must include at least one of path or deepLinks. A deep link that does not have a path represents a topological level in the application's hierarchy, but does not have a destination URL that is user-accessible. | -| [defaultPath](./kibana-plugin-core-public.app.defaultpath.md) | string | Allow to define the default path a user should be directed to when navigating to the app. When defined, this value will be used as a default for the path option when calling [navigateToApp](./kibana-plugin-core-public.applicationstart.navigatetoapp.md)\`, and will also be appended to the [application navLink](./kibana-plugin-core-public.chromenavlink.md) in the navigation bar. | -| [exactRoute](./kibana-plugin-core-public.app.exactroute.md) | boolean | If set to true, the application's route will only be checked against an exact match. Defaults to false. | -| [id](./kibana-plugin-core-public.app.id.md) | string | The unique identifier of the application | -| [keywords](./kibana-plugin-core-public.app.keywords.md) | string[] | Optional keywords to match with in deep links search. Omit if this part of the hierarchy does not have a page URL. | -| [mount](./kibana-plugin-core-public.app.mount.md) | AppMount<HistoryLocationState> | A mount function called when the user navigates to this app's route. | -| [navLinkStatus](./kibana-plugin-core-public.app.navlinkstatus.md) | AppNavLinkStatus | The initial status of the application's navLink. Defaulting to visible if status is accessible and hidden if status is inaccessible See [AppNavLinkStatus](./kibana-plugin-core-public.appnavlinkstatus.md) | -| [searchable](./kibana-plugin-core-public.app.searchable.md) | boolean | The initial flag to determine if the application is searchable in the global search. Defaulting to true if navLinkStatus is visible or omitted. | -| [status](./kibana-plugin-core-public.app.status.md) | AppStatus | The initial status of the application. Defaulting to accessible | -| [title](./kibana-plugin-core-public.app.title.md) | string | The title of the application. | -| [updater$](./kibana-plugin-core-public.app.updater_.md) | Observable<AppUpdater> | An [AppUpdater](./kibana-plugin-core-public.appupdater.md) observable that can be used to update the application [AppUpdatableFields](./kibana-plugin-core-public.appupdatablefields.md) at runtime. | +| [appRoute?](./kibana-plugin-core-public.app.approute.md) | string | (Optional) Override the application's routing path from /app/${id}. Must be unique across registered applications. Should not include the base path from HTTP. | +| [capabilities?](./kibana-plugin-core-public.app.capabilities.md) | Partial<Capabilities> | (Optional) Custom capabilities defined by the app. | +| [category?](./kibana-plugin-core-public.app.category.md) | AppCategory | (Optional) The category definition of the product See [AppCategory](./kibana-plugin-core-public.appcategory.md) See DEFAULT\_APP\_CATEGORIES for more reference | +| [chromeless?](./kibana-plugin-core-public.app.chromeless.md) | boolean | (Optional) Hide the UI chrome when the application is mounted. Defaults to false. Takes precedence over chrome service visibility settings. | +| [deepLinks?](./kibana-plugin-core-public.app.deeplinks.md) | AppDeepLink\[\] | (Optional) Input type for registering secondary in-app locations for an application.Deep links must include at least one of path or deepLinks. A deep link that does not have a path represents a topological level in the application's hierarchy, but does not have a destination URL that is user-accessible. | +| [defaultPath?](./kibana-plugin-core-public.app.defaultpath.md) | string | (Optional) Allow to define the default path a user should be directed to when navigating to the app. When defined, this value will be used as a default for the path option when calling [navigateToApp](./kibana-plugin-core-public.applicationstart.navigatetoapp.md)\`, and will also be appended to the [application navLink](./kibana-plugin-core-public.chromenavlink.md) in the navigation bar. | +| [exactRoute?](./kibana-plugin-core-public.app.exactroute.md) | boolean | (Optional) If set to true, the application's route will only be checked against an exact match. Defaults to false. | +| [id](./kibana-plugin-core-public.app.id.md) | string | The unique identifier of the application | +| [keywords?](./kibana-plugin-core-public.app.keywords.md) | string\[\] | (Optional) Optional keywords to match with in deep links search. Omit if this part of the hierarchy does not have a page URL. | +| [mount](./kibana-plugin-core-public.app.mount.md) | AppMount<HistoryLocationState> | A mount function called when the user navigates to this app's route. | +| [navLinkStatus?](./kibana-plugin-core-public.app.navlinkstatus.md) | AppNavLinkStatus | (Optional) The initial status of the application's navLink. Defaulting to visible if status is accessible and hidden if status is inaccessible See [AppNavLinkStatus](./kibana-plugin-core-public.appnavlinkstatus.md) | +| [searchable?](./kibana-plugin-core-public.app.searchable.md) | boolean | (Optional) The initial flag to determine if the application is searchable in the global search. Defaulting to true if navLinkStatus is visible or omitted. | +| [status?](./kibana-plugin-core-public.app.status.md) | AppStatus | (Optional) The initial status of the application. Defaulting to accessible | +| [title](./kibana-plugin-core-public.app.title.md) | string | The title of the application. | +| [updater$?](./kibana-plugin-core-public.app.updater_.md) | Observable<AppUpdater> | (Optional) An [AppUpdater](./kibana-plugin-core-public.appupdater.md) observable that can be used to update the application [AppUpdatableFields](./kibana-plugin-core-public.appupdatablefields.md) at runtime. | diff --git a/docs/development/core/public/kibana-plugin-core-public.app.updater_.md b/docs/development/core/public/kibana-plugin-core-public.app.updater_.md index 67acccbd02965..e6789a38f12f7 100644 --- a/docs/development/core/public/kibana-plugin-core-public.app.updater_.md +++ b/docs/development/core/public/kibana-plugin-core-public.app.updater_.md @@ -39,6 +39,5 @@ export class MyPlugin implements Plugin { navLinkStatus: AppNavLinkStatus.disabled, }) } - ``` diff --git a/docs/development/core/public/kibana-plugin-core-public.appcategory.md b/docs/development/core/public/kibana-plugin-core-public.appcategory.md index b0ec377e165b6..40c714b51b8bd 100644 --- a/docs/development/core/public/kibana-plugin-core-public.appcategory.md +++ b/docs/development/core/public/kibana-plugin-core-public.appcategory.md @@ -16,9 +16,9 @@ export interface AppCategory | Property | Type | Description | | --- | --- | --- | -| [ariaLabel](./kibana-plugin-core-public.appcategory.arialabel.md) | string | If the visual label isn't appropriate for screen readers, can override it here | -| [euiIconType](./kibana-plugin-core-public.appcategory.euiicontype.md) | string | Define an icon to be used for the category If the category is only 1 item, and no icon is defined, will default to the product icon Defaults to initials if no icon is defined | -| [id](./kibana-plugin-core-public.appcategory.id.md) | string | Unique identifier for the categories | -| [label](./kibana-plugin-core-public.appcategory.label.md) | string | Label used for category name. Also used as aria-label if one isn't set. | -| [order](./kibana-plugin-core-public.appcategory.order.md) | number | The order that categories will be sorted in Prefer large steps between categories to allow for further editing (Default categories are in steps of 1000) | +| [ariaLabel?](./kibana-plugin-core-public.appcategory.arialabel.md) | string | (Optional) If the visual label isn't appropriate for screen readers, can override it here | +| [euiIconType?](./kibana-plugin-core-public.appcategory.euiicontype.md) | string | (Optional) Define an icon to be used for the category If the category is only 1 item, and no icon is defined, will default to the product icon Defaults to initials if no icon is defined | +| [id](./kibana-plugin-core-public.appcategory.id.md) | string | Unique identifier for the categories | +| [label](./kibana-plugin-core-public.appcategory.label.md) | string | Label used for category name. Also used as aria-label if one isn't set. | +| [order?](./kibana-plugin-core-public.appcategory.order.md) | number | (Optional) The order that categories will be sorted in Prefer large steps between categories to allow for further editing (Default categories are in steps of 1000) | diff --git a/docs/development/core/public/kibana-plugin-core-public.appleaveconfirmaction.md b/docs/development/core/public/kibana-plugin-core-public.appleaveconfirmaction.md index 8650cd9868940..e44fe49c27c8c 100644 --- a/docs/development/core/public/kibana-plugin-core-public.appleaveconfirmaction.md +++ b/docs/development/core/public/kibana-plugin-core-public.appleaveconfirmaction.md @@ -18,8 +18,8 @@ export interface AppLeaveConfirmAction | Property | Type | Description | | --- | --- | --- | -| [callback](./kibana-plugin-core-public.appleaveconfirmaction.callback.md) | () => void | | -| [text](./kibana-plugin-core-public.appleaveconfirmaction.text.md) | string | | -| [title](./kibana-plugin-core-public.appleaveconfirmaction.title.md) | string | | -| [type](./kibana-plugin-core-public.appleaveconfirmaction.type.md) | AppLeaveActionType.confirm | | +| [callback?](./kibana-plugin-core-public.appleaveconfirmaction.callback.md) | () => void | (Optional) | +| [text](./kibana-plugin-core-public.appleaveconfirmaction.text.md) | string | | +| [title?](./kibana-plugin-core-public.appleaveconfirmaction.title.md) | string | (Optional) | +| [type](./kibana-plugin-core-public.appleaveconfirmaction.type.md) | AppLeaveActionType.confirm | | diff --git a/docs/development/core/public/kibana-plugin-core-public.appleavedefaultaction.md b/docs/development/core/public/kibana-plugin-core-public.appleavedefaultaction.md index f6df1c0516bd4..5d0e0d2a216e1 100644 --- a/docs/development/core/public/kibana-plugin-core-public.appleavedefaultaction.md +++ b/docs/development/core/public/kibana-plugin-core-public.appleavedefaultaction.md @@ -18,5 +18,5 @@ export interface AppLeaveDefaultAction | Property | Type | Description | | --- | --- | --- | -| [type](./kibana-plugin-core-public.appleavedefaultaction.type.md) | AppLeaveActionType.default | | +| [type](./kibana-plugin-core-public.appleavedefaultaction.type.md) | AppLeaveActionType.default | | diff --git a/docs/development/core/public/kibana-plugin-core-public.applicationsetup.register.md b/docs/development/core/public/kibana-plugin-core-public.applicationsetup.register.md index 6f4ecdc855df8..e53b28e88d6ea 100644 --- a/docs/development/core/public/kibana-plugin-core-public.applicationsetup.register.md +++ b/docs/development/core/public/kibana-plugin-core-public.applicationsetup.register.md @@ -16,9 +16,9 @@ register(app: App): void; | Parameter | Type | Description | | --- | --- | --- | -| app | App<HistoryLocationState> | an [App](./kibana-plugin-core-public.app.md) | +| app | App<HistoryLocationState> | an [App](./kibana-plugin-core-public.app.md) | Returns: -`void` +void diff --git a/docs/development/core/public/kibana-plugin-core-public.applicationsetup.registerappupdater.md b/docs/development/core/public/kibana-plugin-core-public.applicationsetup.registerappupdater.md index 88800913364fa..6e8203fd68197 100644 --- a/docs/development/core/public/kibana-plugin-core-public.applicationsetup.registerappupdater.md +++ b/docs/development/core/public/kibana-plugin-core-public.applicationsetup.registerappupdater.md @@ -18,11 +18,11 @@ registerAppUpdater(appUpdater$: Observable): void; | Parameter | Type | Description | | --- | --- | --- | -| appUpdater$ | Observable<AppUpdater> | | +| appUpdater$ | Observable<AppUpdater> | | Returns: -`void` +void ## Example @@ -42,6 +42,5 @@ export class MyPlugin implements Plugin { ); } } - ``` diff --git a/docs/development/core/public/kibana-plugin-core-public.applicationstart.geturlforapp.md b/docs/development/core/public/kibana-plugin-core-public.applicationstart.geturlforapp.md index 6229aeb9238e8..8bc89f617e157 100644 --- a/docs/development/core/public/kibana-plugin-core-public.applicationstart.geturlforapp.md +++ b/docs/development/core/public/kibana-plugin-core-public.applicationstart.geturlforapp.md @@ -24,10 +24,10 @@ getUrlForApp(appId: string, options?: { | Parameter | Type | Description | | --- | --- | --- | -| appId | string | | -| options | {
path?: string;
absolute?: boolean;
deepLinkId?: string;
} | | +| appId | string | | +| options | { path?: string; absolute?: boolean; deepLinkId?: string; } | | Returns: -`string` +string diff --git a/docs/development/core/public/kibana-plugin-core-public.applicationstart.md b/docs/development/core/public/kibana-plugin-core-public.applicationstart.md index 993234d4c6e09..cadf0f91b01d6 100644 --- a/docs/development/core/public/kibana-plugin-core-public.applicationstart.md +++ b/docs/development/core/public/kibana-plugin-core-public.applicationstart.md @@ -15,9 +15,9 @@ export interface ApplicationStart | Property | Type | Description | | --- | --- | --- | -| [applications$](./kibana-plugin-core-public.applicationstart.applications_.md) | Observable<ReadonlyMap<string, PublicAppInfo>> | Observable emitting the list of currently registered apps and their associated status. | -| [capabilities](./kibana-plugin-core-public.applicationstart.capabilities.md) | RecursiveReadonly<Capabilities> | Gets the read-only capabilities. | -| [currentAppId$](./kibana-plugin-core-public.applicationstart.currentappid_.md) | Observable<string | undefined> | An observable that emits the current application id and each subsequent id update. | +| [applications$](./kibana-plugin-core-public.applicationstart.applications_.md) | Observable<ReadonlyMap<string, PublicAppInfo>> | Observable emitting the list of currently registered apps and their associated status. | +| [capabilities](./kibana-plugin-core-public.applicationstart.capabilities.md) | RecursiveReadonly<Capabilities> | Gets the read-only capabilities. | +| [currentAppId$](./kibana-plugin-core-public.applicationstart.currentappid_.md) | Observable<string \| undefined> | An observable that emits the current application id and each subsequent id update. | ## Methods diff --git a/docs/development/core/public/kibana-plugin-core-public.applicationstart.navigatetoapp.md b/docs/development/core/public/kibana-plugin-core-public.applicationstart.navigatetoapp.md index e1f08c7b38133..a6f87209148fd 100644 --- a/docs/development/core/public/kibana-plugin-core-public.applicationstart.navigatetoapp.md +++ b/docs/development/core/public/kibana-plugin-core-public.applicationstart.navigatetoapp.md @@ -16,10 +16,10 @@ navigateToApp(appId: string, options?: NavigateToAppOptions): Promise; | Parameter | Type | Description | | --- | --- | --- | -| appId | string | | -| options | NavigateToAppOptions | navigation options | +| appId | string | | +| options | NavigateToAppOptions | navigation options | Returns: -`Promise` +Promise<void> diff --git a/docs/development/core/public/kibana-plugin-core-public.applicationstart.navigatetourl.md b/docs/development/core/public/kibana-plugin-core-public.applicationstart.navigatetourl.md index 8639394cbc421..9e6644e2b1ca7 100644 --- a/docs/development/core/public/kibana-plugin-core-public.applicationstart.navigatetourl.md +++ b/docs/development/core/public/kibana-plugin-core-public.applicationstart.navigatetourl.md @@ -22,11 +22,11 @@ navigateToUrl(url: string): Promise; | Parameter | Type | Description | | --- | --- | --- | -| url | string | an absolute URL, an absolute path or a relative path, to navigate to. | +| url | string | an absolute URL, an absolute path or a relative path, to navigate to. | Returns: -`Promise` +Promise<void> ## Example @@ -45,6 +45,5 @@ application.navigateToUrl('/app/discover/some-path') // does not include the cur application.navigateToUrl('/base-path/s/my-space/app/unknown-app/some-path') // unknown application application.navigateToUrl('../discover') // resolve to `/base-path/s/my-space/discover` which is not a path of a known app. application.navigateToUrl('../../other-space/discover') // resolve to `/base-path/s/other-space/discover` which is not within the current basePath. - ``` diff --git a/docs/development/core/public/kibana-plugin-core-public.appmountparameters.appbasepath.md b/docs/development/core/public/kibana-plugin-core-public.appmountparameters.appbasepath.md index b9ebcec6fa8e4..fd16c78d4cbbf 100644 --- a/docs/development/core/public/kibana-plugin-core-public.appmountparameters.appbasepath.md +++ b/docs/development/core/public/kibana-plugin-core-public.appmountparameters.appbasepath.md @@ -35,7 +35,6 @@ export class MyPlugin implements Plugin { }); } } - ``` ```ts @@ -58,6 +57,5 @@ export renderApp = ({ appBasePath, element }: AppMountParameters) => { return () => ReactDOM.unmountComponentAtNode(element); } - ``` diff --git a/docs/development/core/public/kibana-plugin-core-public.appmountparameters.history.md b/docs/development/core/public/kibana-plugin-core-public.appmountparameters.history.md index 84f2c2564bfd9..c22267eadbe28 100644 --- a/docs/development/core/public/kibana-plugin-core-public.appmountparameters.history.md +++ b/docs/development/core/public/kibana-plugin-core-public.appmountparameters.history.md @@ -30,7 +30,6 @@ export class MyPlugin implements Plugin { }); } } - ``` ```ts @@ -52,6 +51,5 @@ export renderApp = ({ element, history }: AppMountParameters) => { return () => ReactDOM.unmountComponentAtNode(element); } - ``` diff --git a/docs/development/core/public/kibana-plugin-core-public.appmountparameters.md b/docs/development/core/public/kibana-plugin-core-public.appmountparameters.md index f6c57603bedde..d32faa55a5f86 100644 --- a/docs/development/core/public/kibana-plugin-core-public.appmountparameters.md +++ b/docs/development/core/public/kibana-plugin-core-public.appmountparameters.md @@ -15,9 +15,9 @@ export interface AppMountParameters | Property | Type | Description | | --- | --- | --- | -| [appBasePath](./kibana-plugin-core-public.appmountparameters.appbasepath.md) | string | The route path for configuring navigation to the application. This string should not include the base path from HTTP. | -| [element](./kibana-plugin-core-public.appmountparameters.element.md) | HTMLElement | The container element to render the application into. | -| [history](./kibana-plugin-core-public.appmountparameters.history.md) | ScopedHistory<HistoryLocationState> | A scoped history instance for your application. Should be used to wire up your applications Router. | -| [onAppLeave](./kibana-plugin-core-public.appmountparameters.onappleave.md) | (handler: AppLeaveHandler) => void | A function that can be used to register a handler that will be called when the user is leaving the current application, allowing to prompt a confirmation message before actually changing the page.This will be called either when the user goes to another application, or when trying to close the tab or manually changing the url. | -| [setHeaderActionMenu](./kibana-plugin-core-public.appmountparameters.setheaderactionmenu.md) | (menuMount: MountPoint | undefined) => void | A function that can be used to set the mount point used to populate the application action container in the chrome header.Calling the handler multiple time will erase the current content of the action menu with the mount from the latest call. Calling the handler with undefined will unmount the current mount point. Calling the handler after the application has been unmounted will have no effect. | +| [appBasePath](./kibana-plugin-core-public.appmountparameters.appbasepath.md) | string | The route path for configuring navigation to the application. This string should not include the base path from HTTP. | +| [element](./kibana-plugin-core-public.appmountparameters.element.md) | HTMLElement | The container element to render the application into. | +| [history](./kibana-plugin-core-public.appmountparameters.history.md) | ScopedHistory<HistoryLocationState> | A scoped history instance for your application. Should be used to wire up your applications Router. | +| [onAppLeave](./kibana-plugin-core-public.appmountparameters.onappleave.md) | (handler: AppLeaveHandler) => void | A function that can be used to register a handler that will be called when the user is leaving the current application, allowing to prompt a confirmation message before actually changing the page.This will be called either when the user goes to another application, or when trying to close the tab or manually changing the url. | +| [setHeaderActionMenu](./kibana-plugin-core-public.appmountparameters.setheaderactionmenu.md) | (menuMount: MountPoint \| undefined) => void | A function that can be used to set the mount point used to populate the application action container in the chrome header.Calling the handler multiple time will erase the current content of the action menu with the mount from the latest call. Calling the handler with undefined will unmount the current mount point. Calling the handler after the application has been unmounted will have no effect. | diff --git a/docs/development/core/public/kibana-plugin-core-public.appmountparameters.onappleave.md b/docs/development/core/public/kibana-plugin-core-public.appmountparameters.onappleave.md index e64e40a49e44e..fa75e3e4084a6 100644 --- a/docs/development/core/public/kibana-plugin-core-public.appmountparameters.onappleave.md +++ b/docs/development/core/public/kibana-plugin-core-public.appmountparameters.onappleave.md @@ -41,6 +41,5 @@ export renderApp = ({ element, history, onAppLeave }: AppMountParameters) => { }); return renderApp({ element, history }); } - ``` diff --git a/docs/development/core/public/kibana-plugin-core-public.appmountparameters.setheaderactionmenu.md b/docs/development/core/public/kibana-plugin-core-public.appmountparameters.setheaderactionmenu.md index ca9cee64bb1f9..715e1ba4bf291 100644 --- a/docs/development/core/public/kibana-plugin-core-public.appmountparameters.setheaderactionmenu.md +++ b/docs/development/core/public/kibana-plugin-core-public.appmountparameters.setheaderactionmenu.md @@ -34,6 +34,5 @@ export renderApp = ({ element, history, setHeaderActionMenu }: AppMountParameter }) return renderApp({ element, history }); } - ``` diff --git a/docs/development/core/public/kibana-plugin-core-public.appnavoptions.md b/docs/development/core/public/kibana-plugin-core-public.appnavoptions.md index cb5ae936988dc..c6c583b7a9098 100644 --- a/docs/development/core/public/kibana-plugin-core-public.appnavoptions.md +++ b/docs/development/core/public/kibana-plugin-core-public.appnavoptions.md @@ -16,8 +16,8 @@ export interface AppNavOptions | Property | Type | Description | | --- | --- | --- | -| [euiIconType](./kibana-plugin-core-public.appnavoptions.euiicontype.md) | string | A EUI iconType that will be used for the app's icon. This icon takes precedence over the icon property. | -| [icon](./kibana-plugin-core-public.appnavoptions.icon.md) | string | A URL to an image file used as an icon. Used as a fallback if euiIconType is not provided. | -| [order](./kibana-plugin-core-public.appnavoptions.order.md) | number | An ordinal used to sort nav links relative to one another for display. | -| [tooltip](./kibana-plugin-core-public.appnavoptions.tooltip.md) | string | A tooltip shown when hovering over app link. | +| [euiIconType?](./kibana-plugin-core-public.appnavoptions.euiicontype.md) | string | (Optional) A EUI iconType that will be used for the app's icon. This icon takes precedence over the icon property. | +| [icon?](./kibana-plugin-core-public.appnavoptions.icon.md) | string | (Optional) A URL to an image file used as an icon. Used as a fallback if euiIconType is not provided. | +| [order?](./kibana-plugin-core-public.appnavoptions.order.md) | number | (Optional) An ordinal used to sort nav links relative to one another for display. | +| [tooltip?](./kibana-plugin-core-public.appnavoptions.tooltip.md) | string | (Optional) A tooltip shown when hovering over app link. | diff --git a/docs/development/core/public/kibana-plugin-core-public.asyncplugin.md b/docs/development/core/public/kibana-plugin-core-public.asyncplugin.md index cf315e1fd337e..cb9559dddc684 100644 --- a/docs/development/core/public/kibana-plugin-core-public.asyncplugin.md +++ b/docs/development/core/public/kibana-plugin-core-public.asyncplugin.md @@ -6,7 +6,7 @@ > Warning: This API is now obsolete. > -> Asynchronous lifecycles are deprecated, and should be migrated to sync [plugin](./kibana-plugin-core-public.plugin.md) +> Asynchronous lifecycles are deprecated, and should be migrated to sync > A plugin with asynchronous lifecycle methods. @@ -23,5 +23,5 @@ export interface AsyncPlugin(Optional) | diff --git a/docs/development/core/public/kibana-plugin-core-public.asyncplugin.setup.md b/docs/development/core/public/kibana-plugin-core-public.asyncplugin.setup.md index 54507b44cdd72..67a5dad22a0a2 100644 --- a/docs/development/core/public/kibana-plugin-core-public.asyncplugin.setup.md +++ b/docs/development/core/public/kibana-plugin-core-public.asyncplugin.setup.md @@ -14,10 +14,10 @@ setup(core: CoreSetup, plugins: TPluginsSetup): TSetup | | Parameter | Type | Description | | --- | --- | --- | -| core | CoreSetup<TPluginsStart, TStart> | | -| plugins | TPluginsSetup | | +| core | CoreSetup<TPluginsStart, TStart> | | +| plugins | TPluginsSetup | | Returns: -`TSetup | Promise` +TSetup \| Promise<TSetup> diff --git a/docs/development/core/public/kibana-plugin-core-public.asyncplugin.start.md b/docs/development/core/public/kibana-plugin-core-public.asyncplugin.start.md index f16d3c46bf849..89554a1afaf1a 100644 --- a/docs/development/core/public/kibana-plugin-core-public.asyncplugin.start.md +++ b/docs/development/core/public/kibana-plugin-core-public.asyncplugin.start.md @@ -14,10 +14,10 @@ start(core: CoreStart, plugins: TPluginsStart): TStart | Promise; | Parameter | Type | Description | | --- | --- | --- | -| core | CoreStart | | -| plugins | TPluginsStart | | +| core | CoreStart | | +| plugins | TPluginsStart | | Returns: -`TStart | Promise` +TStart \| Promise<TStart> diff --git a/docs/development/core/public/kibana-plugin-core-public.asyncplugin.stop.md b/docs/development/core/public/kibana-plugin-core-public.asyncplugin.stop.md index f809f75783c26..3fb7504879cf6 100644 --- a/docs/development/core/public/kibana-plugin-core-public.asyncplugin.stop.md +++ b/docs/development/core/public/kibana-plugin-core-public.asyncplugin.stop.md @@ -11,5 +11,5 @@ stop?(): void; ``` Returns: -`void` +void diff --git a/docs/development/core/public/kibana-plugin-core-public.capabilities.md b/docs/development/core/public/kibana-plugin-core-public.capabilities.md index 077899a4847d5..e908bd554d88d 100644 --- a/docs/development/core/public/kibana-plugin-core-public.capabilities.md +++ b/docs/development/core/public/kibana-plugin-core-public.capabilities.md @@ -16,7 +16,7 @@ export interface Capabilities | Property | Type | Description | | --- | --- | --- | -| [catalogue](./kibana-plugin-core-public.capabilities.catalogue.md) | Record<string, boolean> | Catalogue capabilities. Catalogue entries drive the visibility of the Kibana homepage options. | -| [management](./kibana-plugin-core-public.capabilities.management.md) | {
[sectionId: string]: Record<string, boolean>;
} | Management section capabilities. | -| [navLinks](./kibana-plugin-core-public.capabilities.navlinks.md) | Record<string, boolean> | Navigation link capabilities. | +| [catalogue](./kibana-plugin-core-public.capabilities.catalogue.md) | Record<string, boolean> | Catalogue capabilities. Catalogue entries drive the visibility of the Kibana homepage options. | +| [management](./kibana-plugin-core-public.capabilities.management.md) | { \[sectionId: string\]: Record<string, boolean>; } | Management section capabilities. | +| [navLinks](./kibana-plugin-core-public.capabilities.navlinks.md) | Record<string, boolean> | Navigation link capabilities. | diff --git a/docs/development/core/public/kibana-plugin-core-public.chromebadge.md b/docs/development/core/public/kibana-plugin-core-public.chromebadge.md index 0af3e5f367556..e2e4d1910fdd5 100644 --- a/docs/development/core/public/kibana-plugin-core-public.chromebadge.md +++ b/docs/development/core/public/kibana-plugin-core-public.chromebadge.md @@ -15,7 +15,7 @@ export interface ChromeBadge | Property | Type | Description | | --- | --- | --- | -| [iconType](./kibana-plugin-core-public.chromebadge.icontype.md) | IconType | | -| [text](./kibana-plugin-core-public.chromebadge.text.md) | string | | -| [tooltip](./kibana-plugin-core-public.chromebadge.tooltip.md) | string | | +| [iconType?](./kibana-plugin-core-public.chromebadge.icontype.md) | IconType | (Optional) | +| [text](./kibana-plugin-core-public.chromebadge.text.md) | string | | +| [tooltip](./kibana-plugin-core-public.chromebadge.tooltip.md) | string | | diff --git a/docs/development/core/public/kibana-plugin-core-public.chromedoctitle.change.md b/docs/development/core/public/kibana-plugin-core-public.chromedoctitle.change.md index aa44f38df15a9..cf31d16cae0e0 100644 --- a/docs/development/core/public/kibana-plugin-core-public.chromedoctitle.change.md +++ b/docs/development/core/public/kibana-plugin-core-public.chromedoctitle.change.md @@ -16,11 +16,11 @@ change(newTitle: string | string[]): void; | Parameter | Type | Description | | --- | --- | --- | -| newTitle | string | string[] | | +| newTitle | string \| string\[\] | The new title to set, either a string or string array | Returns: -`void` +void ## Example @@ -29,6 +29,5 @@ How to change the title of the document ```ts chrome.docTitle.change('My application title') chrome.docTitle.change(['My application', 'My section']) - ``` diff --git a/docs/development/core/public/kibana-plugin-core-public.chromedoctitle.md b/docs/development/core/public/kibana-plugin-core-public.chromedoctitle.md index 5a6ab40d52d7a..48e04b648e8d8 100644 --- a/docs/development/core/public/kibana-plugin-core-public.chromedoctitle.md +++ b/docs/development/core/public/kibana-plugin-core-public.chromedoctitle.md @@ -18,7 +18,6 @@ How to change the title of the document ```ts chrome.docTitle.change('My application') - ``` ## Example 2 @@ -27,7 +26,6 @@ How to reset the title of the document to it's initial value ```ts chrome.docTitle.reset() - ``` ## Methods diff --git a/docs/development/core/public/kibana-plugin-core-public.chromedoctitle.reset.md b/docs/development/core/public/kibana-plugin-core-public.chromedoctitle.reset.md index ac38db8d28935..e11635fd6d3f8 100644 --- a/docs/development/core/public/kibana-plugin-core-public.chromedoctitle.reset.md +++ b/docs/development/core/public/kibana-plugin-core-public.chromedoctitle.reset.md @@ -13,5 +13,5 @@ reset(): void; ``` Returns: -`void` +void diff --git a/docs/development/core/public/kibana-plugin-core-public.chromehelpextension.md b/docs/development/core/public/kibana-plugin-core-public.chromehelpextension.md index d90a9bf70486f..07fda8d926a29 100644 --- a/docs/development/core/public/kibana-plugin-core-public.chromehelpextension.md +++ b/docs/development/core/public/kibana-plugin-core-public.chromehelpextension.md @@ -15,7 +15,7 @@ export interface ChromeHelpExtension | Property | Type | Description | | --- | --- | --- | -| [appName](./kibana-plugin-core-public.chromehelpextension.appname.md) | string | Provide your plugin's name to create a header for separation | -| [content](./kibana-plugin-core-public.chromehelpextension.content.md) | (element: HTMLDivElement) => () => void | Custom content to occur below the list of links | -| [links](./kibana-plugin-core-public.chromehelpextension.links.md) | ChromeHelpExtensionMenuLink[] | Creates unified links for sending users to documentation, GitHub, Discuss, or a custom link/button | +| [appName](./kibana-plugin-core-public.chromehelpextension.appname.md) | string | Provide your plugin's name to create a header for separation | +| [content?](./kibana-plugin-core-public.chromehelpextension.content.md) | (element: HTMLDivElement) => () => void | (Optional) Custom content to occur below the list of links | +| [links?](./kibana-plugin-core-public.chromehelpextension.links.md) | ChromeHelpExtensionMenuLink\[\] | (Optional) Creates unified links for sending users to documentation, GitHub, Discuss, or a custom link/button | diff --git a/docs/development/core/public/kibana-plugin-core-public.chromehelpextensionmenucustomlink.md b/docs/development/core/public/kibana-plugin-core-public.chromehelpextensionmenucustomlink.md index ff4978e69df62..daf724c72c23e 100644 --- a/docs/development/core/public/kibana-plugin-core-public.chromehelpextensionmenucustomlink.md +++ b/docs/development/core/public/kibana-plugin-core-public.chromehelpextensionmenucustomlink.md @@ -10,12 +10,13 @@ ```typescript export interface ChromeHelpExtensionMenuCustomLink extends ChromeHelpExtensionLinkBase ``` +Extends: ChromeHelpExtensionLinkBase ## Properties | Property | Type | Description | | --- | --- | --- | -| [content](./kibana-plugin-core-public.chromehelpextensionmenucustomlink.content.md) | React.ReactNode | Content of the button (in lieu of children) | -| [href](./kibana-plugin-core-public.chromehelpextensionmenucustomlink.href.md) | string | URL of the link | -| [linkType](./kibana-plugin-core-public.chromehelpextensionmenucustomlink.linktype.md) | 'custom' | Extend EuiButtonEmpty to provide extra functionality | +| [content](./kibana-plugin-core-public.chromehelpextensionmenucustomlink.content.md) | React.ReactNode | Content of the button (in lieu of children) | +| [href](./kibana-plugin-core-public.chromehelpextensionmenucustomlink.href.md) | string | URL of the link | +| [linkType](./kibana-plugin-core-public.chromehelpextensionmenucustomlink.linktype.md) | 'custom' | Extend EuiButtonEmpty to provide extra functionality | diff --git a/docs/development/core/public/kibana-plugin-core-public.chromehelpextensionmenudiscusslink.md b/docs/development/core/public/kibana-plugin-core-public.chromehelpextensionmenudiscusslink.md index a73f6daad28c2..3dc32fcb6d87f 100644 --- a/docs/development/core/public/kibana-plugin-core-public.chromehelpextensionmenudiscusslink.md +++ b/docs/development/core/public/kibana-plugin-core-public.chromehelpextensionmenudiscusslink.md @@ -10,11 +10,12 @@ ```typescript export interface ChromeHelpExtensionMenuDiscussLink extends ChromeHelpExtensionLinkBase ``` +Extends: ChromeHelpExtensionLinkBase ## Properties | Property | Type | Description | | --- | --- | --- | -| [href](./kibana-plugin-core-public.chromehelpextensionmenudiscusslink.href.md) | string | URL to discuss page. i.e. https://discuss.elastic.co/c/${appName} | -| [linkType](./kibana-plugin-core-public.chromehelpextensionmenudiscusslink.linktype.md) | 'discuss' | Creates a generic give feedback link with comment icon | +| [href](./kibana-plugin-core-public.chromehelpextensionmenudiscusslink.href.md) | string | URL to discuss page. i.e. https://discuss.elastic.co/c/${appName} | +| [linkType](./kibana-plugin-core-public.chromehelpextensionmenudiscusslink.linktype.md) | 'discuss' | Creates a generic give feedback link with comment icon | diff --git a/docs/development/core/public/kibana-plugin-core-public.chromehelpextensionmenudocumentationlink.md b/docs/development/core/public/kibana-plugin-core-public.chromehelpextensionmenudocumentationlink.md index fab49d06d4774..d20b513b1c550 100644 --- a/docs/development/core/public/kibana-plugin-core-public.chromehelpextensionmenudocumentationlink.md +++ b/docs/development/core/public/kibana-plugin-core-public.chromehelpextensionmenudocumentationlink.md @@ -10,11 +10,12 @@ ```typescript export interface ChromeHelpExtensionMenuDocumentationLink extends ChromeHelpExtensionLinkBase ``` +Extends: ChromeHelpExtensionLinkBase ## Properties | Property | Type | Description | | --- | --- | --- | -| [href](./kibana-plugin-core-public.chromehelpextensionmenudocumentationlink.href.md) | string | URL to documentation page. i.e. ${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/${appName}.html, | -| [linkType](./kibana-plugin-core-public.chromehelpextensionmenudocumentationlink.linktype.md) | 'documentation' | Creates a deep-link to app-specific documentation | +| [href](./kibana-plugin-core-public.chromehelpextensionmenudocumentationlink.href.md) | string | URL to documentation page. i.e. ${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/${appName}.html, | +| [linkType](./kibana-plugin-core-public.chromehelpextensionmenudocumentationlink.linktype.md) | 'documentation' | Creates a deep-link to app-specific documentation | diff --git a/docs/development/core/public/kibana-plugin-core-public.chromehelpextensionmenugithublink.md b/docs/development/core/public/kibana-plugin-core-public.chromehelpextensionmenugithublink.md index ca9ceecffa6f1..56eee43d26902 100644 --- a/docs/development/core/public/kibana-plugin-core-public.chromehelpextensionmenugithublink.md +++ b/docs/development/core/public/kibana-plugin-core-public.chromehelpextensionmenugithublink.md @@ -10,12 +10,13 @@ ```typescript export interface ChromeHelpExtensionMenuGitHubLink extends ChromeHelpExtensionLinkBase ``` +Extends: ChromeHelpExtensionLinkBase ## Properties | Property | Type | Description | | --- | --- | --- | -| [labels](./kibana-plugin-core-public.chromehelpextensionmenugithublink.labels.md) | string[] | Include at least one app-specific label to be applied to the new github issue | -| [linkType](./kibana-plugin-core-public.chromehelpextensionmenugithublink.linktype.md) | 'github' | Creates a link to a new github issue in the Kibana repo | -| [title](./kibana-plugin-core-public.chromehelpextensionmenugithublink.title.md) | string | Provides initial text for the title of the issue | +| [labels](./kibana-plugin-core-public.chromehelpextensionmenugithublink.labels.md) | string\[\] | Include at least one app-specific label to be applied to the new github issue | +| [linkType](./kibana-plugin-core-public.chromehelpextensionmenugithublink.linktype.md) | 'github' | Creates a link to a new github issue in the Kibana repo | +| [title?](./kibana-plugin-core-public.chromehelpextensionmenugithublink.title.md) | string | (Optional) Provides initial text for the title of the issue | diff --git a/docs/development/core/public/kibana-plugin-core-public.chromenavcontrol.md b/docs/development/core/public/kibana-plugin-core-public.chromenavcontrol.md index 0fad08fdbce81..c0371078c28a4 100644 --- a/docs/development/core/public/kibana-plugin-core-public.chromenavcontrol.md +++ b/docs/development/core/public/kibana-plugin-core-public.chromenavcontrol.md @@ -15,6 +15,6 @@ export interface ChromeNavControl | Property | Type | Description | | --- | --- | --- | -| [mount](./kibana-plugin-core-public.chromenavcontrol.mount.md) | MountPoint | | -| [order](./kibana-plugin-core-public.chromenavcontrol.order.md) | number | | +| [mount](./kibana-plugin-core-public.chromenavcontrol.mount.md) | MountPoint | | +| [order?](./kibana-plugin-core-public.chromenavcontrol.order.md) | number | (Optional) | diff --git a/docs/development/core/public/kibana-plugin-core-public.chromenavcontrols.md b/docs/development/core/public/kibana-plugin-core-public.chromenavcontrols.md index 47365782599ed..72018d46428a4 100644 --- a/docs/development/core/public/kibana-plugin-core-public.chromenavcontrols.md +++ b/docs/development/core/public/kibana-plugin-core-public.chromenavcontrols.md @@ -23,7 +23,6 @@ chrome.navControls.registerLeft({ return () => ReactDOM.unmountComponentAtNode(targetDomElement); } }) - ``` ## Methods diff --git a/docs/development/core/public/kibana-plugin-core-public.chromenavcontrols.registercenter.md b/docs/development/core/public/kibana-plugin-core-public.chromenavcontrols.registercenter.md index 2f921050e58dd..68b243bf47f85 100644 --- a/docs/development/core/public/kibana-plugin-core-public.chromenavcontrols.registercenter.md +++ b/docs/development/core/public/kibana-plugin-core-public.chromenavcontrols.registercenter.md @@ -16,9 +16,9 @@ registerCenter(navControl: ChromeNavControl): void; | Parameter | Type | Description | | --- | --- | --- | -| navControl | ChromeNavControl | | +| navControl | ChromeNavControl | | Returns: -`void` +void diff --git a/docs/development/core/public/kibana-plugin-core-public.chromenavcontrols.registerleft.md b/docs/development/core/public/kibana-plugin-core-public.chromenavcontrols.registerleft.md index 514c44bd9d710..ee0789c285f0b 100644 --- a/docs/development/core/public/kibana-plugin-core-public.chromenavcontrols.registerleft.md +++ b/docs/development/core/public/kibana-plugin-core-public.chromenavcontrols.registerleft.md @@ -16,9 +16,9 @@ registerLeft(navControl: ChromeNavControl): void; | Parameter | Type | Description | | --- | --- | --- | -| navControl | ChromeNavControl | | +| navControl | ChromeNavControl | | Returns: -`void` +void diff --git a/docs/development/core/public/kibana-plugin-core-public.chromenavcontrols.registerright.md b/docs/development/core/public/kibana-plugin-core-public.chromenavcontrols.registerright.md index eb56e0e38c6c9..9091736c62eeb 100644 --- a/docs/development/core/public/kibana-plugin-core-public.chromenavcontrols.registerright.md +++ b/docs/development/core/public/kibana-plugin-core-public.chromenavcontrols.registerright.md @@ -16,9 +16,9 @@ registerRight(navControl: ChromeNavControl): void; | Parameter | Type | Description | | --- | --- | --- | -| navControl | ChromeNavControl | | +| navControl | ChromeNavControl | | Returns: -`void` +void diff --git a/docs/development/core/public/kibana-plugin-core-public.chromenavlink.md b/docs/development/core/public/kibana-plugin-core-public.chromenavlink.md index c7dd461617e34..964f4d4b86aaa 100644 --- a/docs/development/core/public/kibana-plugin-core-public.chromenavlink.md +++ b/docs/development/core/public/kibana-plugin-core-public.chromenavlink.md @@ -15,16 +15,16 @@ export interface ChromeNavLink | Property | Type | Description | | --- | --- | --- | -| [baseUrl](./kibana-plugin-core-public.chromenavlink.baseurl.md) | string | The base route used to open the root of an application. | -| [category](./kibana-plugin-core-public.chromenavlink.category.md) | AppCategory | The category the app lives in | -| [disabled](./kibana-plugin-core-public.chromenavlink.disabled.md) | boolean | Disables a link from being clickable. | -| [euiIconType](./kibana-plugin-core-public.chromenavlink.euiicontype.md) | string | A EUI iconType that will be used for the app's icon. This icon takes precedence over the icon property. | -| [hidden](./kibana-plugin-core-public.chromenavlink.hidden.md) | boolean | Hides a link from the navigation. | -| [href](./kibana-plugin-core-public.chromenavlink.href.md) | string | Settled state between url, baseUrl, and active | -| [icon](./kibana-plugin-core-public.chromenavlink.icon.md) | string | A URL to an image file used as an icon. Used as a fallback if euiIconType is not provided. | -| [id](./kibana-plugin-core-public.chromenavlink.id.md) | string | A unique identifier for looking up links. | -| [order](./kibana-plugin-core-public.chromenavlink.order.md) | number | An ordinal used to sort nav links relative to one another for display. | -| [title](./kibana-plugin-core-public.chromenavlink.title.md) | string | The title of the application. | -| [tooltip](./kibana-plugin-core-public.chromenavlink.tooltip.md) | string | A tooltip shown when hovering over an app link. | -| [url](./kibana-plugin-core-public.chromenavlink.url.md) | string | The route used to open the default path and the deep links of an application. | +| [baseUrl](./kibana-plugin-core-public.chromenavlink.baseurl.md) | string | The base route used to open the root of an application. | +| [category?](./kibana-plugin-core-public.chromenavlink.category.md) | AppCategory | (Optional) The category the app lives in | +| [disabled?](./kibana-plugin-core-public.chromenavlink.disabled.md) | boolean | (Optional) Disables a link from being clickable. | +| [euiIconType?](./kibana-plugin-core-public.chromenavlink.euiicontype.md) | string | (Optional) A EUI iconType that will be used for the app's icon. This icon takes precedence over the icon property. | +| [hidden?](./kibana-plugin-core-public.chromenavlink.hidden.md) | boolean | (Optional) Hides a link from the navigation. | +| [href](./kibana-plugin-core-public.chromenavlink.href.md) | string | Settled state between url, baseUrl, and active | +| [icon?](./kibana-plugin-core-public.chromenavlink.icon.md) | string | (Optional) A URL to an image file used as an icon. Used as a fallback if euiIconType is not provided. | +| [id](./kibana-plugin-core-public.chromenavlink.id.md) | string | A unique identifier for looking up links. | +| [order?](./kibana-plugin-core-public.chromenavlink.order.md) | number | (Optional) An ordinal used to sort nav links relative to one another for display. | +| [title](./kibana-plugin-core-public.chromenavlink.title.md) | string | The title of the application. | +| [tooltip?](./kibana-plugin-core-public.chromenavlink.tooltip.md) | string | (Optional) A tooltip shown when hovering over an app link. | +| [url](./kibana-plugin-core-public.chromenavlink.url.md) | string | The route used to open the default path and the deep links of an application. | diff --git a/docs/development/core/public/kibana-plugin-core-public.chromenavlinks.enableforcedappswitchernavigation.md b/docs/development/core/public/kibana-plugin-core-public.chromenavlinks.enableforcedappswitchernavigation.md index b65ad2b17c1e1..4f9b6aaada5db 100644 --- a/docs/development/core/public/kibana-plugin-core-public.chromenavlinks.enableforcedappswitchernavigation.md +++ b/docs/development/core/public/kibana-plugin-core-public.chromenavlinks.enableforcedappswitchernavigation.md @@ -13,7 +13,7 @@ enableForcedAppSwitcherNavigation(): void; ``` Returns: -`void` +void ## Remarks diff --git a/docs/development/core/public/kibana-plugin-core-public.chromenavlinks.get.md b/docs/development/core/public/kibana-plugin-core-public.chromenavlinks.get.md index f616f99f639ee..796d99b9b0e0c 100644 --- a/docs/development/core/public/kibana-plugin-core-public.chromenavlinks.get.md +++ b/docs/development/core/public/kibana-plugin-core-public.chromenavlinks.get.md @@ -16,9 +16,9 @@ get(id: string): ChromeNavLink | undefined; | Parameter | Type | Description | | --- | --- | --- | -| id | string | | +| id | string | | Returns: -`ChromeNavLink | undefined` +ChromeNavLink \| undefined diff --git a/docs/development/core/public/kibana-plugin-core-public.chromenavlinks.getall.md b/docs/development/core/public/kibana-plugin-core-public.chromenavlinks.getall.md index 94a7b25160af7..08d5707fe3251 100644 --- a/docs/development/core/public/kibana-plugin-core-public.chromenavlinks.getall.md +++ b/docs/development/core/public/kibana-plugin-core-public.chromenavlinks.getall.md @@ -13,5 +13,5 @@ getAll(): Array>; ``` Returns: -`Array>` +Array<Readonly<ChromeNavLink>> diff --git a/docs/development/core/public/kibana-plugin-core-public.chromenavlinks.getforceappswitchernavigation_.md b/docs/development/core/public/kibana-plugin-core-public.chromenavlinks.getforceappswitchernavigation_.md index ded2c8c08ba49..3b87790c37297 100644 --- a/docs/development/core/public/kibana-plugin-core-public.chromenavlinks.getforceappswitchernavigation_.md +++ b/docs/development/core/public/kibana-plugin-core-public.chromenavlinks.getforceappswitchernavigation_.md @@ -13,5 +13,5 @@ getForceAppSwitcherNavigation$(): Observable; ``` Returns: -`Observable` +Observable<boolean> diff --git a/docs/development/core/public/kibana-plugin-core-public.chromenavlinks.getnavlinks_.md b/docs/development/core/public/kibana-plugin-core-public.chromenavlinks.getnavlinks_.md index d93b4381271e0..8ee5c0fb83081 100644 --- a/docs/development/core/public/kibana-plugin-core-public.chromenavlinks.getnavlinks_.md +++ b/docs/development/core/public/kibana-plugin-core-public.chromenavlinks.getnavlinks_.md @@ -13,5 +13,5 @@ getNavLinks$(): Observable>>; ``` Returns: -`Observable>>` +Observable<Array<Readonly<ChromeNavLink>>> diff --git a/docs/development/core/public/kibana-plugin-core-public.chromenavlinks.has.md b/docs/development/core/public/kibana-plugin-core-public.chromenavlinks.has.md index abef76582cef1..dfaae86a9d891 100644 --- a/docs/development/core/public/kibana-plugin-core-public.chromenavlinks.has.md +++ b/docs/development/core/public/kibana-plugin-core-public.chromenavlinks.has.md @@ -16,9 +16,9 @@ has(id: string): boolean; | Parameter | Type | Description | | --- | --- | --- | -| id | string | | +| id | string | | Returns: -`boolean` +boolean diff --git a/docs/development/core/public/kibana-plugin-core-public.chromerecentlyaccessed.add.md b/docs/development/core/public/kibana-plugin-core-public.chromerecentlyaccessed.add.md index 329105394e41c..5c99c6bf7fbcb 100644 --- a/docs/development/core/public/kibana-plugin-core-public.chromerecentlyaccessed.add.md +++ b/docs/development/core/public/kibana-plugin-core-public.chromerecentlyaccessed.add.md @@ -16,19 +16,18 @@ add(link: string, label: string, id: string): void; | Parameter | Type | Description | | --- | --- | --- | -| link | string | | -| label | string | | -| id | string | | +| link | string | a relative URL to the resource (not including the ) | +| label | string | the label to display in the UI | +| id | string | a unique string used to de-duplicate the recently accessed list. | Returns: -`void` +void ## Example ```js chrome.recentlyAccessed.add('/app/map/1234', 'Map 1234', '1234'); - ``` diff --git a/docs/development/core/public/kibana-plugin-core-public.chromerecentlyaccessed.get.md b/docs/development/core/public/kibana-plugin-core-public.chromerecentlyaccessed.get.md index b0d66e25d1fe0..da696737b3bb7 100644 --- a/docs/development/core/public/kibana-plugin-core-public.chromerecentlyaccessed.get.md +++ b/docs/development/core/public/kibana-plugin-core-public.chromerecentlyaccessed.get.md @@ -13,13 +13,12 @@ get(): ChromeRecentlyAccessedHistoryItem[]; ``` Returns: -`ChromeRecentlyAccessedHistoryItem[]` +ChromeRecentlyAccessedHistoryItem\[\] ## Example ```js chrome.recentlyAccessed.get().forEach(console.log); - ``` diff --git a/docs/development/core/public/kibana-plugin-core-public.chromerecentlyaccessed.get_.md b/docs/development/core/public/kibana-plugin-core-public.chromerecentlyaccessed.get_.md index da53c6535b25d..4655289642f99 100644 --- a/docs/development/core/public/kibana-plugin-core-public.chromerecentlyaccessed.get_.md +++ b/docs/development/core/public/kibana-plugin-core-public.chromerecentlyaccessed.get_.md @@ -13,13 +13,12 @@ get$(): Observable; ``` Returns: -`Observable` +Observable<ChromeRecentlyAccessedHistoryItem\[\]> ## Example ```js chrome.recentlyAccessed.get$().subscribe(console.log); - ``` diff --git a/docs/development/core/public/kibana-plugin-core-public.chromerecentlyaccessedhistoryitem.md b/docs/development/core/public/kibana-plugin-core-public.chromerecentlyaccessedhistoryitem.md index e07492f883e53..3b67b41d37a35 100644 --- a/docs/development/core/public/kibana-plugin-core-public.chromerecentlyaccessedhistoryitem.md +++ b/docs/development/core/public/kibana-plugin-core-public.chromerecentlyaccessedhistoryitem.md @@ -15,7 +15,7 @@ export interface ChromeRecentlyAccessedHistoryItem | Property | Type | Description | | --- | --- | --- | -| [id](./kibana-plugin-core-public.chromerecentlyaccessedhistoryitem.id.md) | string | | -| [label](./kibana-plugin-core-public.chromerecentlyaccessedhistoryitem.label.md) | string | | -| [link](./kibana-plugin-core-public.chromerecentlyaccessedhistoryitem.link.md) | string | | +| [id](./kibana-plugin-core-public.chromerecentlyaccessedhistoryitem.id.md) | string | | +| [label](./kibana-plugin-core-public.chromerecentlyaccessedhistoryitem.label.md) | string | | +| [link](./kibana-plugin-core-public.chromerecentlyaccessedhistoryitem.link.md) | string | | diff --git a/docs/development/core/public/kibana-plugin-core-public.chromestart.getbadge_.md b/docs/development/core/public/kibana-plugin-core-public.chromestart.getbadge_.md index 586a61a9f214a..d3dc459bae9de 100644 --- a/docs/development/core/public/kibana-plugin-core-public.chromestart.getbadge_.md +++ b/docs/development/core/public/kibana-plugin-core-public.chromestart.getbadge_.md @@ -13,5 +13,5 @@ getBadge$(): Observable; ``` Returns: -`Observable` +Observable<ChromeBadge \| undefined> diff --git a/docs/development/core/public/kibana-plugin-core-public.chromestart.getbreadcrumbs_.md b/docs/development/core/public/kibana-plugin-core-public.chromestart.getbreadcrumbs_.md index 155f3423d69e4..c4d3751549b16 100644 --- a/docs/development/core/public/kibana-plugin-core-public.chromestart.getbreadcrumbs_.md +++ b/docs/development/core/public/kibana-plugin-core-public.chromestart.getbreadcrumbs_.md @@ -13,5 +13,5 @@ getBreadcrumbs$(): Observable; ``` Returns: -`Observable` +Observable<ChromeBreadcrumb\[\]> diff --git a/docs/development/core/public/kibana-plugin-core-public.chromestart.getbreadcrumbsappendextension_.md b/docs/development/core/public/kibana-plugin-core-public.chromestart.getbreadcrumbsappendextension_.md index dfe25c5c9e42d..21c12514debec 100644 --- a/docs/development/core/public/kibana-plugin-core-public.chromestart.getbreadcrumbsappendextension_.md +++ b/docs/development/core/public/kibana-plugin-core-public.chromestart.getbreadcrumbsappendextension_.md @@ -13,5 +13,5 @@ getBreadcrumbsAppendExtension$(): ObservableReturns: -`Observable` +Observable<ChromeBreadcrumbsAppendExtension \| undefined> diff --git a/docs/development/core/public/kibana-plugin-core-public.chromestart.getcustomnavlink_.md b/docs/development/core/public/kibana-plugin-core-public.chromestart.getcustomnavlink_.md index 64805eefbfea1..59346a409562e 100644 --- a/docs/development/core/public/kibana-plugin-core-public.chromestart.getcustomnavlink_.md +++ b/docs/development/core/public/kibana-plugin-core-public.chromestart.getcustomnavlink_.md @@ -13,5 +13,5 @@ getCustomNavLink$(): Observable | undefined>; ``` Returns: -`Observable | undefined>` +Observable<Partial<ChromeNavLink> \| undefined> diff --git a/docs/development/core/public/kibana-plugin-core-public.chromestart.gethelpextension_.md b/docs/development/core/public/kibana-plugin-core-public.chromestart.gethelpextension_.md index 90c42a98bd60a..052bbe2630f70 100644 --- a/docs/development/core/public/kibana-plugin-core-public.chromestart.gethelpextension_.md +++ b/docs/development/core/public/kibana-plugin-core-public.chromestart.gethelpextension_.md @@ -13,5 +13,5 @@ getHelpExtension$(): Observable; ``` Returns: -`Observable` +Observable<ChromeHelpExtension \| undefined> diff --git a/docs/development/core/public/kibana-plugin-core-public.chromestart.getisnavdrawerlocked_.md b/docs/development/core/public/kibana-plugin-core-public.chromestart.getisnavdrawerlocked_.md index 78a4442a651e6..12aa71366aaac 100644 --- a/docs/development/core/public/kibana-plugin-core-public.chromestart.getisnavdrawerlocked_.md +++ b/docs/development/core/public/kibana-plugin-core-public.chromestart.getisnavdrawerlocked_.md @@ -13,5 +13,5 @@ getIsNavDrawerLocked$(): Observable; ``` Returns: -`Observable` +Observable<boolean> diff --git a/docs/development/core/public/kibana-plugin-core-public.chromestart.getisvisible_.md b/docs/development/core/public/kibana-plugin-core-public.chromestart.getisvisible_.md index b6204a1913909..70a9c832926e1 100644 --- a/docs/development/core/public/kibana-plugin-core-public.chromestart.getisvisible_.md +++ b/docs/development/core/public/kibana-plugin-core-public.chromestart.getisvisible_.md @@ -13,5 +13,5 @@ getIsVisible$(): Observable; ``` Returns: -`Observable` +Observable<boolean> diff --git a/docs/development/core/public/kibana-plugin-core-public.chromestart.hasheaderbanner_.md b/docs/development/core/public/kibana-plugin-core-public.chromestart.hasheaderbanner_.md index 6ce0671eb5230..66dd1e2562f50 100644 --- a/docs/development/core/public/kibana-plugin-core-public.chromestart.hasheaderbanner_.md +++ b/docs/development/core/public/kibana-plugin-core-public.chromestart.hasheaderbanner_.md @@ -13,5 +13,5 @@ hasHeaderBanner$(): Observable; ``` Returns: -`Observable` +Observable<boolean> diff --git a/docs/development/core/public/kibana-plugin-core-public.chromestart.md b/docs/development/core/public/kibana-plugin-core-public.chromestart.md index ffc77dd653c0f..3e672fbc14d75 100644 --- a/docs/development/core/public/kibana-plugin-core-public.chromestart.md +++ b/docs/development/core/public/kibana-plugin-core-public.chromestart.md @@ -22,7 +22,6 @@ How to add a recently accessed item to the sidebar: ```ts core.chrome.recentlyAccessed.add('/app/map/1234', 'Map 1234', '1234'); - ``` ## Example 2 @@ -34,17 +33,16 @@ core.chrome.setHelpExtension(elem => { ReactDOM.render(, elem); return () => ReactDOM.unmountComponentAtNode(elem); }); - ``` ## Properties | Property | Type | Description | | --- | --- | --- | -| [docTitle](./kibana-plugin-core-public.chromestart.doctitle.md) | ChromeDocTitle | APIs for accessing and updating the document title. | -| [navControls](./kibana-plugin-core-public.chromestart.navcontrols.md) | ChromeNavControls | [APIs](./kibana-plugin-core-public.chromenavcontrols.md) for registering new controls to be displayed in the navigation bar. | -| [navLinks](./kibana-plugin-core-public.chromestart.navlinks.md) | ChromeNavLinks | [APIs](./kibana-plugin-core-public.chromenavlinks.md) for manipulating nav links. | -| [recentlyAccessed](./kibana-plugin-core-public.chromestart.recentlyaccessed.md) | ChromeRecentlyAccessed | [APIs](./kibana-plugin-core-public.chromerecentlyaccessed.md) for recently accessed history. | +| [docTitle](./kibana-plugin-core-public.chromestart.doctitle.md) | ChromeDocTitle | APIs for accessing and updating the document title. | +| [navControls](./kibana-plugin-core-public.chromestart.navcontrols.md) | ChromeNavControls | [APIs](./kibana-plugin-core-public.chromenavcontrols.md) for registering new controls to be displayed in the navigation bar. | +| [navLinks](./kibana-plugin-core-public.chromestart.navlinks.md) | ChromeNavLinks | [APIs](./kibana-plugin-core-public.chromenavlinks.md) for manipulating nav links. | +| [recentlyAccessed](./kibana-plugin-core-public.chromestart.recentlyaccessed.md) | ChromeRecentlyAccessed | [APIs](./kibana-plugin-core-public.chromerecentlyaccessed.md) for recently accessed history. | ## Methods diff --git a/docs/development/core/public/kibana-plugin-core-public.chromestart.setbadge.md b/docs/development/core/public/kibana-plugin-core-public.chromestart.setbadge.md index 52e807658d238..7e974b139d141 100644 --- a/docs/development/core/public/kibana-plugin-core-public.chromestart.setbadge.md +++ b/docs/development/core/public/kibana-plugin-core-public.chromestart.setbadge.md @@ -16,9 +16,9 @@ setBadge(badge?: ChromeBadge): void; | Parameter | Type | Description | | --- | --- | --- | -| badge | ChromeBadge | | +| badge | ChromeBadge | | Returns: -`void` +void diff --git a/docs/development/core/public/kibana-plugin-core-public.chromestart.setbreadcrumbs.md b/docs/development/core/public/kibana-plugin-core-public.chromestart.setbreadcrumbs.md index 80a1514ef7652..f44e3e6cfd562 100644 --- a/docs/development/core/public/kibana-plugin-core-public.chromestart.setbreadcrumbs.md +++ b/docs/development/core/public/kibana-plugin-core-public.chromestart.setbreadcrumbs.md @@ -16,9 +16,9 @@ setBreadcrumbs(newBreadcrumbs: ChromeBreadcrumb[]): void; | Parameter | Type | Description | | --- | --- | --- | -| newBreadcrumbs | ChromeBreadcrumb[] | | +| newBreadcrumbs | ChromeBreadcrumb\[\] | | Returns: -`void` +void diff --git a/docs/development/core/public/kibana-plugin-core-public.chromestart.setbreadcrumbsappendextension.md b/docs/development/core/public/kibana-plugin-core-public.chromestart.setbreadcrumbsappendextension.md index 02adb9b4d325d..b8fa965f2726e 100644 --- a/docs/development/core/public/kibana-plugin-core-public.chromestart.setbreadcrumbsappendextension.md +++ b/docs/development/core/public/kibana-plugin-core-public.chromestart.setbreadcrumbsappendextension.md @@ -16,9 +16,9 @@ setBreadcrumbsAppendExtension(breadcrumbsAppendExtension?: ChromeBreadcrumbsAppe | Parameter | Type | Description | | --- | --- | --- | -| breadcrumbsAppendExtension | ChromeBreadcrumbsAppendExtension | | +| breadcrumbsAppendExtension | ChromeBreadcrumbsAppendExtension | | Returns: -`void` +void diff --git a/docs/development/core/public/kibana-plugin-core-public.chromestart.setcustomnavlink.md b/docs/development/core/public/kibana-plugin-core-public.chromestart.setcustomnavlink.md index adfb57f9c5ff2..7b100a25a4b2b 100644 --- a/docs/development/core/public/kibana-plugin-core-public.chromestart.setcustomnavlink.md +++ b/docs/development/core/public/kibana-plugin-core-public.chromestart.setcustomnavlink.md @@ -16,9 +16,9 @@ setCustomNavLink(newCustomNavLink?: Partial): void; | Parameter | Type | Description | | --- | --- | --- | -| newCustomNavLink | Partial<ChromeNavLink> | | +| newCustomNavLink | Partial<ChromeNavLink> | | Returns: -`void` +void diff --git a/docs/development/core/public/kibana-plugin-core-public.chromestart.setheaderbanner.md b/docs/development/core/public/kibana-plugin-core-public.chromestart.setheaderbanner.md index 02a2fa65ed478..75f711c0bf10b 100644 --- a/docs/development/core/public/kibana-plugin-core-public.chromestart.setheaderbanner.md +++ b/docs/development/core/public/kibana-plugin-core-public.chromestart.setheaderbanner.md @@ -16,11 +16,11 @@ setHeaderBanner(headerBanner?: ChromeUserBanner): void; | Parameter | Type | Description | | --- | --- | --- | -| headerBanner | ChromeUserBanner | | +| headerBanner | ChromeUserBanner | | Returns: -`void` +void ## Remarks diff --git a/docs/development/core/public/kibana-plugin-core-public.chromestart.sethelpextension.md b/docs/development/core/public/kibana-plugin-core-public.chromestart.sethelpextension.md index c03cf2e9203bc..c2bc691349f3c 100644 --- a/docs/development/core/public/kibana-plugin-core-public.chromestart.sethelpextension.md +++ b/docs/development/core/public/kibana-plugin-core-public.chromestart.sethelpextension.md @@ -16,9 +16,9 @@ setHelpExtension(helpExtension?: ChromeHelpExtension): void; | Parameter | Type | Description | | --- | --- | --- | -| helpExtension | ChromeHelpExtension | | +| helpExtension | ChromeHelpExtension | | Returns: -`void` +void diff --git a/docs/development/core/public/kibana-plugin-core-public.chromestart.sethelpsupporturl.md b/docs/development/core/public/kibana-plugin-core-public.chromestart.sethelpsupporturl.md index a08c54c1f37c7..baeb37a89ca44 100644 --- a/docs/development/core/public/kibana-plugin-core-public.chromestart.sethelpsupporturl.md +++ b/docs/development/core/public/kibana-plugin-core-public.chromestart.sethelpsupporturl.md @@ -16,9 +16,9 @@ setHelpSupportUrl(url: string): void; | Parameter | Type | Description | | --- | --- | --- | -| url | string | | +| url | string | The updated support URL | Returns: -`void` +void diff --git a/docs/development/core/public/kibana-plugin-core-public.chromestart.setisvisible.md b/docs/development/core/public/kibana-plugin-core-public.chromestart.setisvisible.md index 76e2dc666fc82..9c8cc737bea4f 100644 --- a/docs/development/core/public/kibana-plugin-core-public.chromestart.setisvisible.md +++ b/docs/development/core/public/kibana-plugin-core-public.chromestart.setisvisible.md @@ -16,9 +16,9 @@ setIsVisible(isVisible: boolean): void; | Parameter | Type | Description | | --- | --- | --- | -| isVisible | boolean | | +| isVisible | boolean | | Returns: -`void` +void diff --git a/docs/development/core/public/kibana-plugin-core-public.chromeuserbanner.md b/docs/development/core/public/kibana-plugin-core-public.chromeuserbanner.md index 8617c5c4d2b12..0417197ab55f3 100644 --- a/docs/development/core/public/kibana-plugin-core-public.chromeuserbanner.md +++ b/docs/development/core/public/kibana-plugin-core-public.chromeuserbanner.md @@ -15,5 +15,5 @@ export interface ChromeUserBanner | Property | Type | Description | | --- | --- | --- | -| [content](./kibana-plugin-core-public.chromeuserbanner.content.md) | MountPoint<HTMLDivElement> | | +| [content](./kibana-plugin-core-public.chromeuserbanner.content.md) | MountPoint<HTMLDivElement> | | diff --git a/docs/development/core/public/kibana-plugin-core-public.coresetup.md b/docs/development/core/public/kibana-plugin-core-public.coresetup.md index 5d2288120da05..18af0c7ea5855 100644 --- a/docs/development/core/public/kibana-plugin-core-public.coresetup.md +++ b/docs/development/core/public/kibana-plugin-core-public.coresetup.md @@ -16,11 +16,11 @@ export interface CoreSetupApplicationSetup | [ApplicationSetup](./kibana-plugin-core-public.applicationsetup.md) | -| [fatalErrors](./kibana-plugin-core-public.coresetup.fatalerrors.md) | FatalErrorsSetup | [FatalErrorsSetup](./kibana-plugin-core-public.fatalerrorssetup.md) | -| [getStartServices](./kibana-plugin-core-public.coresetup.getstartservices.md) | StartServicesAccessor<TPluginsStart, TStart> | [StartServicesAccessor](./kibana-plugin-core-public.startservicesaccessor.md) | -| [http](./kibana-plugin-core-public.coresetup.http.md) | HttpSetup | [HttpSetup](./kibana-plugin-core-public.httpsetup.md) | -| [injectedMetadata](./kibana-plugin-core-public.coresetup.injectedmetadata.md) | {
getInjectedVar: (name: string, defaultValue?: any) => unknown;
} | 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 in the new platform. | -| [notifications](./kibana-plugin-core-public.coresetup.notifications.md) | NotificationsSetup | [NotificationsSetup](./kibana-plugin-core-public.notificationssetup.md) | -| [uiSettings](./kibana-plugin-core-public.coresetup.uisettings.md) | IUiSettingsClient | [IUiSettingsClient](./kibana-plugin-core-public.iuisettingsclient.md) | +| [application](./kibana-plugin-core-public.coresetup.application.md) | ApplicationSetup | [ApplicationSetup](./kibana-plugin-core-public.applicationsetup.md) | +| [fatalErrors](./kibana-plugin-core-public.coresetup.fatalerrors.md) | FatalErrorsSetup | [FatalErrorsSetup](./kibana-plugin-core-public.fatalerrorssetup.md) | +| [getStartServices](./kibana-plugin-core-public.coresetup.getstartservices.md) | StartServicesAccessor<TPluginsStart, TStart> | [StartServicesAccessor](./kibana-plugin-core-public.startservicesaccessor.md) | +| [http](./kibana-plugin-core-public.coresetup.http.md) | HttpSetup | [HttpSetup](./kibana-plugin-core-public.httpsetup.md) | +| [injectedMetadata](./kibana-plugin-core-public.coresetup.injectedmetadata.md) | { getInjectedVar: (name: string, defaultValue?: any) => unknown; } | 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 in the new platform. | +| [notifications](./kibana-plugin-core-public.coresetup.notifications.md) | NotificationsSetup | [NotificationsSetup](./kibana-plugin-core-public.notificationssetup.md) | +| [uiSettings](./kibana-plugin-core-public.coresetup.uisettings.md) | IUiSettingsClient | [IUiSettingsClient](./kibana-plugin-core-public.iuisettingsclient.md) | 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 6ad9adca53ef5..e0f6a68782410 100644 --- a/docs/development/core/public/kibana-plugin-core-public.corestart.md +++ b/docs/development/core/public/kibana-plugin-core-public.corestart.md @@ -16,16 +16,16 @@ export interface CoreStart | Property | Type | Description | | --- | --- | --- | -| [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) | -| [i18n](./kibana-plugin-core-public.corestart.i18n.md) | I18nStart | [I18nStart](./kibana-plugin-core-public.i18nstart.md) | -| [injectedMetadata](./kibana-plugin-core-public.corestart.injectedmetadata.md) | {
getInjectedVar: (name: string, defaultValue?: any) => unknown;
} | 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 in the new platform. | -| [notifications](./kibana-plugin-core-public.corestart.notifications.md) | NotificationsStart | [NotificationsStart](./kibana-plugin-core-public.notificationsstart.md) | -| [overlays](./kibana-plugin-core-public.corestart.overlays.md) | OverlayStart | [OverlayStart](./kibana-plugin-core-public.overlaystart.md) | -| [savedObjects](./kibana-plugin-core-public.corestart.savedobjects.md) | SavedObjectsStart | [SavedObjectsStart](./kibana-plugin-core-public.savedobjectsstart.md) | -| [uiSettings](./kibana-plugin-core-public.corestart.uisettings.md) | IUiSettingsClient | [IUiSettingsClient](./kibana-plugin-core-public.iuisettingsclient.md) | +| [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) | +| [i18n](./kibana-plugin-core-public.corestart.i18n.md) | I18nStart | [I18nStart](./kibana-plugin-core-public.i18nstart.md) | +| [injectedMetadata](./kibana-plugin-core-public.corestart.injectedmetadata.md) | { getInjectedVar: (name: string, defaultValue?: any) => unknown; } | 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 in the new platform. | +| [notifications](./kibana-plugin-core-public.corestart.notifications.md) | NotificationsStart | [NotificationsStart](./kibana-plugin-core-public.notificationsstart.md) | +| [overlays](./kibana-plugin-core-public.corestart.overlays.md) | OverlayStart | [OverlayStart](./kibana-plugin-core-public.overlaystart.md) | +| [savedObjects](./kibana-plugin-core-public.corestart.savedobjects.md) | SavedObjectsStart | [SavedObjectsStart](./kibana-plugin-core-public.savedobjectsstart.md) | +| [uiSettings](./kibana-plugin-core-public.corestart.uisettings.md) | IUiSettingsClient | [IUiSettingsClient](./kibana-plugin-core-public.iuisettingsclient.md) | diff --git a/docs/development/core/public/kibana-plugin-core-public.deprecationsservicestart.md b/docs/development/core/public/kibana-plugin-core-public.deprecationsservicestart.md index 0d2c963ec5547..bfc1d78f4d045 100644 --- a/docs/development/core/public/kibana-plugin-core-public.deprecationsservicestart.md +++ b/docs/development/core/public/kibana-plugin-core-public.deprecationsservicestart.md @@ -16,8 +16,8 @@ export interface DeprecationsServiceStart | 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. | +| [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.doclinksstart.links.md b/docs/development/core/public/kibana-plugin-core-public.doclinksstart.links.md index 4e44df9d4e183..37524daa39c51 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 @@ -10,6 +10,9 @@ readonly links: { readonly settings: string; readonly elasticStackGetStarted: string; + readonly upgrade: { + readonly upgradingElasticStack: string; + }; readonly apm: { readonly kibanaSettings: string; readonly supportedServiceMaps: string; @@ -86,6 +89,7 @@ readonly links: { readonly range: string; readonly significant_terms: string; readonly terms: string; + readonly terms_doc_count_error: string; readonly avg: string; readonly avg_bucket: string; readonly max_bucket: string; @@ -133,7 +137,11 @@ readonly links: { }; readonly addData: string; readonly kibana: string; - readonly upgradeAssistant: string; + readonly upgradeAssistant: { + readonly overview: string; + readonly batchReindex: string; + readonly remoteReindex: string; + }; readonly rollupJobs: string; readonly elasticsearch: Record; readonly siem: { @@ -236,6 +244,7 @@ readonly links: { fleetServerAddFleetServer: string; settings: string; settingsFleetServerHostSettings: string; + settingsFleetServerProxySettings: string; troubleshooting: string; elasticAgent: string; datastreams: string; @@ -245,6 +254,7 @@ readonly links: { upgradeElasticAgent712lower: string; learnMoreBlog: string; apiKeysLearnMore: string; + onPremRegistry: string; }>; readonly ecs: { readonly guide: string; @@ -261,5 +271,8 @@ readonly links: { readonly rubyOverview: string; readonly rustGuide: string; }; + readonly endpoints: { + readonly troubleshooting: string; + }; }; ``` 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 5871a84c5402e..a1363b7a519d1 100644 --- a/docs/development/core/public/kibana-plugin-core-public.doclinksstart.md +++ b/docs/development/core/public/kibana-plugin-core-public.doclinksstart.md @@ -15,7 +15,7 @@ export interface DocLinksStart | Property | Type | Description | | --- | --- | --- | -| [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 settings: string;
readonly elasticStackGetStarted: string;
readonly apm: {
readonly kibanaSettings: string;
readonly supportedServiceMaps: string;
readonly customLinks: string;
readonly droppedTransactionSpans: string;
readonly upgrading: string;
readonly metaData: string;
};
readonly canvas: {
readonly guide: string;
};
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 suricataModule: string;
readonly zeekModule: string;
};
readonly auditbeat: {
readonly base: string;
readonly auditdModule: string;
readonly systemModule: 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 libbeat: {
readonly getStarted: 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 overview: string;
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 search: {
readonly sessions: string;
readonly sessionLimits: string;
};
readonly indexPatterns: {
readonly introduction: string;
readonly fieldFormattersNumber: string;
readonly fieldFormattersString: string;
readonly runtimeFields: string;
};
readonly addData: string;
readonly kibana: string;
readonly upgradeAssistant: string;
readonly rollupJobs: string;
readonly elasticsearch: Record<string, string>;
readonly siem: {
readonly privileges: string;
readonly guide: string;
readonly gettingStarted: string;
readonly ml: string;
readonly ruleChangeLog: string;
readonly detectionsReq: string;
readonly networkMap: string;
readonly troubleshootGaps: string;
};
readonly securitySolution: {
readonly trustedApps: string;
};
readonly query: {
readonly eql: string;
readonly kueryQuerySyntax: string;
readonly luceneQuerySyntax: string;
readonly percolate: string;
readonly queryDsl: string;
readonly autocompleteChanges: 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<{
bulkIndexAlias: string;
byteSizeUnits: string;
createAutoFollowPattern: string;
createFollower: string;
createIndex: string;
createSnapshotLifecyclePolicy: string;
createRoleMapping: string;
createRoleMappingTemplates: string;
createRollupJobsRequest: 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;
putIndexTemplateV1: string;
putWatch: string;
simulatePipeline: string;
timeUnits: string;
updateTransform: string;
}>;
readonly observability: Readonly<{
guide: string;
infrastructureThreshold: string;
logsThreshold: string;
metricsThreshold: string;
monitorStatus: string;
monitorUptime: string;
tlsCertificate: string;
uptimeDurationAnomaly: 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;
elasticsearchEnableApiKeys: string;
indicesPrivileges: string;
kibanaTLS: string;
kibanaPrivileges: string;
mappingRoles: string;
mappingRolesFieldRules: string;
runAsPrivilege: string;
}>;
readonly spaces: Readonly<{
kibanaLegacyUrlAliases: string;
kibanaDisableLegacyUrlAliasesApi: 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>;
readonly fleet: Readonly<{
datastreamsILM: string;
beatsAgentComparison: string;
guide: string;
fleetServer: string;
fleetServerAddFleetServer: string;
settings: string;
settingsFleetServerHostSettings: string;
troubleshooting: string;
elasticAgent: string;
datastreams: string;
datastreamsNamingScheme: string;
installElasticAgent: string;
upgradeElasticAgent: string;
upgradeElasticAgent712lower: string;
learnMoreBlog: string;
apiKeysLearnMore: string;
}>;
readonly ecs: {
readonly guide: string;
};
readonly clients: {
readonly guide: string;
readonly goOverview: string;
readonly javaIndex: string;
readonly jsIntro: string;
readonly netGuide: string;
readonly perlGuide: string;
readonly phpGuide: string;
readonly pythonGuide: string;
readonly rubyOverview: string;
readonly rustGuide: string;
};
} | | +| [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 settings: string; readonly elasticStackGetStarted: string; readonly upgrade: { readonly upgradingElasticStack: string; }; readonly apm: { readonly kibanaSettings: string; readonly supportedServiceMaps: string; readonly customLinks: string; readonly droppedTransactionSpans: string; readonly upgrading: string; readonly metaData: string; }; readonly canvas: { readonly guide: string; }; 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 suricataModule: string; readonly zeekModule: string; }; readonly auditbeat: { readonly base: string; readonly auditdModule: string; readonly systemModule: 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 libbeat: { readonly getStarted: 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 terms\_doc\_count\_error: 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 overview: string; 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 search: { readonly sessions: string; readonly sessionLimits: string; }; readonly indexPatterns: { readonly introduction: string; readonly fieldFormattersNumber: string; readonly fieldFormattersString: string; readonly runtimeFields: string; }; readonly addData: string; readonly kibana: string; readonly upgradeAssistant: { readonly overview: string; readonly batchReindex: string; readonly remoteReindex: string; }; readonly rollupJobs: string; readonly elasticsearch: Record<string, string>; readonly siem: { readonly privileges: string; readonly guide: string; readonly gettingStarted: string; readonly ml: string; readonly ruleChangeLog: string; readonly detectionsReq: string; readonly networkMap: string; readonly troubleshootGaps: string; }; readonly securitySolution: { readonly trustedApps: string; }; readonly query: { readonly eql: string; readonly kueryQuerySyntax: string; readonly luceneQuerySyntax: string; readonly percolate: string; readonly queryDsl: string; readonly autocompleteChanges: 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<{ bulkIndexAlias: string; byteSizeUnits: string; createAutoFollowPattern: string; createFollower: string; createIndex: string; createSnapshotLifecyclePolicy: string; createRoleMapping: string; createRoleMappingTemplates: string; createRollupJobsRequest: 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; putIndexTemplateV1: string; putWatch: string; simulatePipeline: string; timeUnits: string; updateTransform: string; }>; readonly observability: Readonly<{ guide: string; infrastructureThreshold: string; logsThreshold: string; metricsThreshold: string; monitorStatus: string; monitorUptime: string; tlsCertificate: string; uptimeDurationAnomaly: 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; elasticsearchEnableApiKeys: string; indicesPrivileges: string; kibanaTLS: string; kibanaPrivileges: string; mappingRoles: string; mappingRolesFieldRules: string; runAsPrivilege: string; }>; readonly spaces: Readonly<{ kibanaLegacyUrlAliases: string; kibanaDisableLegacyUrlAliasesApi: 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>; readonly fleet: Readonly<{ datastreamsILM: string; beatsAgentComparison: string; guide: string; fleetServer: string; fleetServerAddFleetServer: string; settings: string; settingsFleetServerHostSettings: string; settingsFleetServerProxySettings: string; troubleshooting: string; elasticAgent: string; datastreams: string; datastreamsNamingScheme: string; installElasticAgent: string; upgradeElasticAgent: string; upgradeElasticAgent712lower: string; learnMoreBlog: string; apiKeysLearnMore: string; onPremRegistry: string; }>; readonly ecs: { readonly guide: string; }; readonly clients: { readonly guide: string; readonly goOverview: string; readonly javaIndex: string; readonly jsIntro: string; readonly netGuide: string; readonly perlGuide: string; readonly phpGuide: string; readonly pythonGuide: string; readonly rubyOverview: string; readonly rustGuide: string; }; readonly endpoints: { readonly troubleshooting: string; }; } | | diff --git a/docs/development/core/public/kibana-plugin-core-public.errortoastoptions.md b/docs/development/core/public/kibana-plugin-core-public.errortoastoptions.md index dc256e6f5bc06..c2bddc58d9c3b 100644 --- a/docs/development/core/public/kibana-plugin-core-public.errortoastoptions.md +++ b/docs/development/core/public/kibana-plugin-core-public.errortoastoptions.md @@ -11,11 +11,12 @@ Options available for [IToasts](./kibana-plugin-core-public.itoasts.md) error AP ```typescript export interface ErrorToastOptions extends ToastOptions ``` +Extends: ToastOptions ## Properties | Property | Type | Description | | --- | --- | --- | -| [title](./kibana-plugin-core-public.errortoastoptions.title.md) | string | The title of the toast and the dialog when expanding the message. | -| [toastMessage](./kibana-plugin-core-public.errortoastoptions.toastmessage.md) | string | The message to be shown in the toast. If this is not specified the error's message will be shown in the toast instead. Overwriting that message can be used to provide more user-friendly toasts. If you specify this, the error message will still be shown in the detailed error modal. | +| [title](./kibana-plugin-core-public.errortoastoptions.title.md) | string | The title of the toast and the dialog when expanding the message. | +| [toastMessage?](./kibana-plugin-core-public.errortoastoptions.toastmessage.md) | string | (Optional) The message to be shown in the toast. If this is not specified the error's message will be shown in the toast instead. Overwriting that message can be used to provide more user-friendly toasts. If you specify this, the error message will still be shown in the detailed error modal. | diff --git a/docs/development/core/public/kibana-plugin-core-public.fatalerrorinfo.md b/docs/development/core/public/kibana-plugin-core-public.fatalerrorinfo.md index 51facf549bd01..9b2803e4f12ea 100644 --- a/docs/development/core/public/kibana-plugin-core-public.fatalerrorinfo.md +++ b/docs/development/core/public/kibana-plugin-core-public.fatalerrorinfo.md @@ -16,6 +16,6 @@ export interface FatalErrorInfo | Property | Type | Description | | --- | --- | --- | -| [message](./kibana-plugin-core-public.fatalerrorinfo.message.md) | string | | -| [stack](./kibana-plugin-core-public.fatalerrorinfo.stack.md) | string | undefined | | +| [message](./kibana-plugin-core-public.fatalerrorinfo.message.md) | string | | +| [stack](./kibana-plugin-core-public.fatalerrorinfo.stack.md) | string \| undefined | | diff --git a/docs/development/core/public/kibana-plugin-core-public.fatalerrorssetup.md b/docs/development/core/public/kibana-plugin-core-public.fatalerrorssetup.md index 31abcf13b820e..1f27fd52b7e32 100644 --- a/docs/development/core/public/kibana-plugin-core-public.fatalerrorssetup.md +++ b/docs/development/core/public/kibana-plugin-core-public.fatalerrorssetup.md @@ -16,6 +16,6 @@ export interface FatalErrorsSetup | Property | Type | Description | | --- | --- | --- | -| [add](./kibana-plugin-core-public.fatalerrorssetup.add.md) | (error: string | Error, source?: string) => never | Add a new fatal error. This will stop the Kibana Public Core and display a fatal error screen with details about the Kibana build and the error. | -| [get$](./kibana-plugin-core-public.fatalerrorssetup.get_.md) | () => Rx.Observable<FatalErrorInfo> | An Observable that will emit whenever a fatal error is added with add() | +| [add](./kibana-plugin-core-public.fatalerrorssetup.add.md) | (error: string \| Error, source?: string) => never | Add a new fatal error. This will stop the Kibana Public Core and display a fatal error screen with details about the Kibana build and the error. | +| [get$](./kibana-plugin-core-public.fatalerrorssetup.get_.md) | () => Rx.Observable<FatalErrorInfo> | An Observable that will emit whenever a fatal error is added with add() | diff --git a/docs/development/core/public/kibana-plugin-core-public.httpfetchoptions.md b/docs/development/core/public/kibana-plugin-core-public.httpfetchoptions.md index 45a48372b4512..9a7f05ab9cd3e 100644 --- a/docs/development/core/public/kibana-plugin-core-public.httpfetchoptions.md +++ b/docs/development/core/public/kibana-plugin-core-public.httpfetchoptions.md @@ -11,15 +11,16 @@ All options that may be used with a [HttpHandler](./kibana-plugin-core-public.ht ```typescript export interface HttpFetchOptions extends HttpRequestInit ``` +Extends: HttpRequestInit ## Properties | Property | Type | Description | | --- | --- | --- | -| [asResponse](./kibana-plugin-core-public.httpfetchoptions.asresponse.md) | boolean | When true the return type of [HttpHandler](./kibana-plugin-core-public.httphandler.md) will be an [HttpResponse](./kibana-plugin-core-public.httpresponse.md) with detailed request and response information. When false, the return type will just be the parsed response body. Defaults to false. | -| [asSystemRequest](./kibana-plugin-core-public.httpfetchoptions.assystemrequest.md) | boolean | Whether or not the request should include the "system request" header to differentiate an end user request from Kibana internal request. Can be read on the server-side using KibanaRequest\#isSystemRequest. Defaults to false. | -| [context](./kibana-plugin-core-public.httpfetchoptions.context.md) | KibanaExecutionContext | | -| [headers](./kibana-plugin-core-public.httpfetchoptions.headers.md) | HttpHeadersInit | Headers to send with the request. See [HttpHeadersInit](./kibana-plugin-core-public.httpheadersinit.md). | -| [prependBasePath](./kibana-plugin-core-public.httpfetchoptions.prependbasepath.md) | boolean | Whether or not the request should automatically prepend the basePath. Defaults to true. | -| [query](./kibana-plugin-core-public.httpfetchoptions.query.md) | HttpFetchQuery | The query string for an HTTP request. See [HttpFetchQuery](./kibana-plugin-core-public.httpfetchquery.md). | +| [asResponse?](./kibana-plugin-core-public.httpfetchoptions.asresponse.md) | boolean | (Optional) When true the return type of [HttpHandler](./kibana-plugin-core-public.httphandler.md) will be an [HttpResponse](./kibana-plugin-core-public.httpresponse.md) with detailed request and response information. When false, the return type will just be the parsed response body. Defaults to false. | +| [asSystemRequest?](./kibana-plugin-core-public.httpfetchoptions.assystemrequest.md) | boolean | (Optional) Whether or not the request should include the "system request" header to differentiate an end user request from Kibana internal request. Can be read on the server-side using KibanaRequest\#isSystemRequest. Defaults to false. | +| [context?](./kibana-plugin-core-public.httpfetchoptions.context.md) | KibanaExecutionContext | (Optional) | +| [headers?](./kibana-plugin-core-public.httpfetchoptions.headers.md) | HttpHeadersInit | (Optional) Headers to send with the request. See [HttpHeadersInit](./kibana-plugin-core-public.httpheadersinit.md). | +| [prependBasePath?](./kibana-plugin-core-public.httpfetchoptions.prependbasepath.md) | boolean | (Optional) Whether or not the request should automatically prepend the basePath. Defaults to true. | +| [query?](./kibana-plugin-core-public.httpfetchoptions.query.md) | HttpFetchQuery | (Optional) The query string for an HTTP request. See [HttpFetchQuery](./kibana-plugin-core-public.httpfetchquery.md). | diff --git a/docs/development/core/public/kibana-plugin-core-public.httpfetchoptionswithpath.md b/docs/development/core/public/kibana-plugin-core-public.httpfetchoptionswithpath.md index 37ea559605d3c..78155adaf627e 100644 --- a/docs/development/core/public/kibana-plugin-core-public.httpfetchoptionswithpath.md +++ b/docs/development/core/public/kibana-plugin-core-public.httpfetchoptionswithpath.md @@ -11,10 +11,11 @@ Similar to [HttpFetchOptions](./kibana-plugin-core-public.httpfetchoptions.md) b ```typescript export interface HttpFetchOptionsWithPath extends HttpFetchOptions ``` +Extends: HttpFetchOptions ## Properties | Property | Type | Description | | --- | --- | --- | -| [path](./kibana-plugin-core-public.httpfetchoptionswithpath.path.md) | string | | +| [path](./kibana-plugin-core-public.httpfetchoptionswithpath.path.md) | string | | diff --git a/docs/development/core/public/kibana-plugin-core-public.httpinterceptor.md b/docs/development/core/public/kibana-plugin-core-public.httpinterceptor.md index 84dd88eff9e4c..e1843b1a52988 100644 --- a/docs/development/core/public/kibana-plugin-core-public.httpinterceptor.md +++ b/docs/development/core/public/kibana-plugin-core-public.httpinterceptor.md @@ -16,8 +16,8 @@ export interface HttpInterceptor | Method | Description | | --- | --- | -| [request(fetchOptions, controller)](./kibana-plugin-core-public.httpinterceptor.request.md) | Define an interceptor to be executed before a request is sent. | -| [requestError(httpErrorRequest, controller)](./kibana-plugin-core-public.httpinterceptor.requesterror.md) | Define an interceptor to be executed if a request interceptor throws an error or returns a rejected Promise. | -| [response(httpResponse, controller)](./kibana-plugin-core-public.httpinterceptor.response.md) | Define an interceptor to be executed after a response is received. | -| [responseError(httpErrorResponse, controller)](./kibana-plugin-core-public.httpinterceptor.responseerror.md) | Define an interceptor to be executed if a response interceptor throws an error or returns a rejected Promise. | +| [request(fetchOptions, controller)?](./kibana-plugin-core-public.httpinterceptor.request.md) | (Optional) Define an interceptor to be executed before a request is sent. | +| [requestError(httpErrorRequest, controller)?](./kibana-plugin-core-public.httpinterceptor.requesterror.md) | (Optional) Define an interceptor to be executed if a request interceptor throws an error or returns a rejected Promise. | +| [response(httpResponse, controller)?](./kibana-plugin-core-public.httpinterceptor.response.md) | (Optional) Define an interceptor to be executed after a response is received. | +| [responseError(httpErrorResponse, controller)?](./kibana-plugin-core-public.httpinterceptor.responseerror.md) | (Optional) Define an interceptor to be executed if a response interceptor throws an error or returns a rejected Promise. | diff --git a/docs/development/core/public/kibana-plugin-core-public.httpinterceptor.request.md b/docs/development/core/public/kibana-plugin-core-public.httpinterceptor.request.md index d9051c5f8d72c..95181e6d509f1 100644 --- a/docs/development/core/public/kibana-plugin-core-public.httpinterceptor.request.md +++ b/docs/development/core/public/kibana-plugin-core-public.httpinterceptor.request.md @@ -16,10 +16,10 @@ request?(fetchOptions: Readonly, controller: IHttpInte | Parameter | Type | Description | | --- | --- | --- | -| fetchOptions | Readonly<HttpFetchOptionsWithPath> | | -| controller | IHttpInterceptController | | +| fetchOptions | Readonly<HttpFetchOptionsWithPath> | | +| controller | IHttpInterceptController | [IHttpInterceptController](./kibana-plugin-core-public.ihttpinterceptcontroller.md) | Returns: -`MaybePromise> | void` +MaybePromise<Partial<HttpFetchOptionsWithPath>> \| void diff --git a/docs/development/core/public/kibana-plugin-core-public.httpinterceptor.requesterror.md b/docs/development/core/public/kibana-plugin-core-public.httpinterceptor.requesterror.md index 16980d67fd81e..c2bd14a6d1ead 100644 --- a/docs/development/core/public/kibana-plugin-core-public.httpinterceptor.requesterror.md +++ b/docs/development/core/public/kibana-plugin-core-public.httpinterceptor.requesterror.md @@ -16,10 +16,10 @@ requestError?(httpErrorRequest: HttpInterceptorRequestError, controller: IHttpIn | Parameter | Type | Description | | --- | --- | --- | -| httpErrorRequest | HttpInterceptorRequestError | | -| controller | IHttpInterceptController | | +| httpErrorRequest | HttpInterceptorRequestError | [HttpInterceptorRequestError](./kibana-plugin-core-public.httpinterceptorrequesterror.md) | +| controller | IHttpInterceptController | [IHttpInterceptController](./kibana-plugin-core-public.ihttpinterceptcontroller.md) | Returns: -`MaybePromise> | void` +MaybePromise<Partial<HttpFetchOptionsWithPath>> \| void diff --git a/docs/development/core/public/kibana-plugin-core-public.httpinterceptor.response.md b/docs/development/core/public/kibana-plugin-core-public.httpinterceptor.response.md index 374c6bfe09a95..40cfeffacc0ca 100644 --- a/docs/development/core/public/kibana-plugin-core-public.httpinterceptor.response.md +++ b/docs/development/core/public/kibana-plugin-core-public.httpinterceptor.response.md @@ -16,10 +16,10 @@ response?(httpResponse: HttpResponse, controller: IHttpInterceptController): May | Parameter | Type | Description | | --- | --- | --- | -| httpResponse | HttpResponse | | -| controller | IHttpInterceptController | | +| httpResponse | HttpResponse | [HttpResponse](./kibana-plugin-core-public.httpresponse.md) | +| controller | IHttpInterceptController | [IHttpInterceptController](./kibana-plugin-core-public.ihttpinterceptcontroller.md) | Returns: -`MaybePromise | void` +MaybePromise<IHttpResponseInterceptorOverrides> \| void diff --git a/docs/development/core/public/kibana-plugin-core-public.httpinterceptor.responseerror.md b/docs/development/core/public/kibana-plugin-core-public.httpinterceptor.responseerror.md index fa0acd323fd72..d9be2e87761fc 100644 --- a/docs/development/core/public/kibana-plugin-core-public.httpinterceptor.responseerror.md +++ b/docs/development/core/public/kibana-plugin-core-public.httpinterceptor.responseerror.md @@ -16,10 +16,10 @@ responseError?(httpErrorResponse: HttpInterceptorResponseError, controller: IHtt | Parameter | Type | Description | | --- | --- | --- | -| httpErrorResponse | HttpInterceptorResponseError | | -| controller | IHttpInterceptController | | +| httpErrorResponse | HttpInterceptorResponseError | [HttpInterceptorResponseError](./kibana-plugin-core-public.httpinterceptorresponseerror.md) | +| controller | IHttpInterceptController | [IHttpInterceptController](./kibana-plugin-core-public.ihttpinterceptcontroller.md) | Returns: -`MaybePromise | void` +MaybePromise<IHttpResponseInterceptorOverrides> \| void diff --git a/docs/development/core/public/kibana-plugin-core-public.httpinterceptorrequesterror.md b/docs/development/core/public/kibana-plugin-core-public.httpinterceptorrequesterror.md index 69eadf43cb87b..499bc61ce68af 100644 --- a/docs/development/core/public/kibana-plugin-core-public.httpinterceptorrequesterror.md +++ b/docs/development/core/public/kibana-plugin-core-public.httpinterceptorrequesterror.md @@ -15,6 +15,6 @@ export interface HttpInterceptorRequestError | Property | Type | Description | | --- | --- | --- | -| [error](./kibana-plugin-core-public.httpinterceptorrequesterror.error.md) | Error | | -| [fetchOptions](./kibana-plugin-core-public.httpinterceptorrequesterror.fetchoptions.md) | Readonly<HttpFetchOptionsWithPath> | | +| [error](./kibana-plugin-core-public.httpinterceptorrequesterror.error.md) | Error | | +| [fetchOptions](./kibana-plugin-core-public.httpinterceptorrequesterror.fetchoptions.md) | Readonly<HttpFetchOptionsWithPath> | | diff --git a/docs/development/core/public/kibana-plugin-core-public.httpinterceptorresponseerror.md b/docs/development/core/public/kibana-plugin-core-public.httpinterceptorresponseerror.md index 6d2b8c6ec9965..014cebeb3ec4d 100644 --- a/docs/development/core/public/kibana-plugin-core-public.httpinterceptorresponseerror.md +++ b/docs/development/core/public/kibana-plugin-core-public.httpinterceptorresponseerror.md @@ -10,11 +10,12 @@ ```typescript export interface HttpInterceptorResponseError extends HttpResponse ``` +Extends: HttpResponse ## Properties | Property | Type | Description | | --- | --- | --- | -| [error](./kibana-plugin-core-public.httpinterceptorresponseerror.error.md) | Error | IHttpFetchError | | -| [request](./kibana-plugin-core-public.httpinterceptorresponseerror.request.md) | Readonly<Request> | | +| [error](./kibana-plugin-core-public.httpinterceptorresponseerror.error.md) | Error \| IHttpFetchError | | +| [request](./kibana-plugin-core-public.httpinterceptorresponseerror.request.md) | Readonly<Request> | | diff --git a/docs/development/core/public/kibana-plugin-core-public.httprequestinit.md b/docs/development/core/public/kibana-plugin-core-public.httprequestinit.md index 496fba97491ed..6b0e054ff1eb3 100644 --- a/docs/development/core/public/kibana-plugin-core-public.httprequestinit.md +++ b/docs/development/core/public/kibana-plugin-core-public.httprequestinit.md @@ -16,17 +16,17 @@ export interface HttpRequestInit | Property | Type | Description | | --- | --- | --- | -| [body](./kibana-plugin-core-public.httprequestinit.body.md) | BodyInit | null | A BodyInit object or null to set request's body. | -| [cache](./kibana-plugin-core-public.httprequestinit.cache.md) | RequestCache | The cache mode associated with request, which is a string indicating how the request will interact with the browser's cache when fetching. | -| [credentials](./kibana-plugin-core-public.httprequestinit.credentials.md) | RequestCredentials | The credentials mode associated with request, which is a string indicating whether credentials will be sent with the request always, never, or only when sent to a same-origin URL. | -| [headers](./kibana-plugin-core-public.httprequestinit.headers.md) | HttpHeadersInit | [HttpHeadersInit](./kibana-plugin-core-public.httpheadersinit.md) | -| [integrity](./kibana-plugin-core-public.httprequestinit.integrity.md) | string | Subresource integrity metadata, which is a cryptographic hash of the resource being fetched. Its value consists of multiple hashes separated by whitespace. | -| [keepalive](./kibana-plugin-core-public.httprequestinit.keepalive.md) | boolean | Whether or not request can outlive the global in which it was created. | -| [method](./kibana-plugin-core-public.httprequestinit.method.md) | string | HTTP method, which is "GET" by default. | -| [mode](./kibana-plugin-core-public.httprequestinit.mode.md) | RequestMode | The mode associated with request, which is a string indicating whether the request will use CORS, or will be restricted to same-origin URLs. | -| [redirect](./kibana-plugin-core-public.httprequestinit.redirect.md) | RequestRedirect | The redirect mode associated with request, which is a string indicating how redirects for the request will be handled during fetching. A request will follow redirects by default. | -| [referrer](./kibana-plugin-core-public.httprequestinit.referrer.md) | string | The referrer of request. Its value can be a same-origin URL if explicitly set in init, the empty string to indicate no referrer, and "about:client" when defaulting to the global's default. This is used during fetching to determine the value of the Referer header of the request being made. | -| [referrerPolicy](./kibana-plugin-core-public.httprequestinit.referrerpolicy.md) | ReferrerPolicy | The referrer policy associated with request. This is used during fetching to compute the value of the request's referrer. | -| [signal](./kibana-plugin-core-public.httprequestinit.signal.md) | AbortSignal | null | Returns the signal associated with request, which is an AbortSignal object indicating whether or not request has been aborted, and its abort event handler. | -| [window](./kibana-plugin-core-public.httprequestinit.window.md) | null | Can only be null. Used to disassociate request from any Window. | +| [body?](./kibana-plugin-core-public.httprequestinit.body.md) | BodyInit \| null | (Optional) A BodyInit object or null to set request's body. | +| [cache?](./kibana-plugin-core-public.httprequestinit.cache.md) | RequestCache | (Optional) The cache mode associated with request, which is a string indicating how the request will interact with the browser's cache when fetching. | +| [credentials?](./kibana-plugin-core-public.httprequestinit.credentials.md) | RequestCredentials | (Optional) The credentials mode associated with request, which is a string indicating whether credentials will be sent with the request always, never, or only when sent to a same-origin URL. | +| [headers?](./kibana-plugin-core-public.httprequestinit.headers.md) | HttpHeadersInit | (Optional) [HttpHeadersInit](./kibana-plugin-core-public.httpheadersinit.md) | +| [integrity?](./kibana-plugin-core-public.httprequestinit.integrity.md) | string | (Optional) Subresource integrity metadata, which is a cryptographic hash of the resource being fetched. Its value consists of multiple hashes separated by whitespace. | +| [keepalive?](./kibana-plugin-core-public.httprequestinit.keepalive.md) | boolean | (Optional) Whether or not request can outlive the global in which it was created. | +| [method?](./kibana-plugin-core-public.httprequestinit.method.md) | string | (Optional) HTTP method, which is "GET" by default. | +| [mode?](./kibana-plugin-core-public.httprequestinit.mode.md) | RequestMode | (Optional) The mode associated with request, which is a string indicating whether the request will use CORS, or will be restricted to same-origin URLs. | +| [redirect?](./kibana-plugin-core-public.httprequestinit.redirect.md) | RequestRedirect | (Optional) The redirect mode associated with request, which is a string indicating how redirects for the request will be handled during fetching. A request will follow redirects by default. | +| [referrer?](./kibana-plugin-core-public.httprequestinit.referrer.md) | string | (Optional) The referrer of request. Its value can be a same-origin URL if explicitly set in init, the empty string to indicate no referrer, and "about:client" when defaulting to the global's default. This is used during fetching to determine the value of the Referer header of the request being made. | +| [referrerPolicy?](./kibana-plugin-core-public.httprequestinit.referrerpolicy.md) | ReferrerPolicy | (Optional) The referrer policy associated with request. This is used during fetching to compute the value of the request's referrer. | +| [signal?](./kibana-plugin-core-public.httprequestinit.signal.md) | AbortSignal \| null | (Optional) Returns the signal associated with request, which is an AbortSignal object indicating whether or not request has been aborted, and its abort event handler. | +| [window?](./kibana-plugin-core-public.httprequestinit.window.md) | null | (Optional) Can only be null. Used to disassociate request from any Window. | diff --git a/docs/development/core/public/kibana-plugin-core-public.httpresponse.md b/docs/development/core/public/kibana-plugin-core-public.httpresponse.md index dcbfa556da65d..c0a3644ecaf2f 100644 --- a/docs/development/core/public/kibana-plugin-core-public.httpresponse.md +++ b/docs/development/core/public/kibana-plugin-core-public.httpresponse.md @@ -15,8 +15,8 @@ export interface HttpResponse | Property | Type | Description | | --- | --- | --- | -| [body](./kibana-plugin-core-public.httpresponse.body.md) | TResponseBody | Parsed body received, may be undefined if there was an error. | -| [fetchOptions](./kibana-plugin-core-public.httpresponse.fetchoptions.md) | Readonly<HttpFetchOptionsWithPath> | The original [HttpFetchOptionsWithPath](./kibana-plugin-core-public.httpfetchoptionswithpath.md) used to send this request. | -| [request](./kibana-plugin-core-public.httpresponse.request.md) | Readonly<Request> | Raw request sent to Kibana server. | -| [response](./kibana-plugin-core-public.httpresponse.response.md) | Readonly<Response> | Raw response received, may be undefined if there was an error. | +| [body?](./kibana-plugin-core-public.httpresponse.body.md) | TResponseBody | (Optional) Parsed body received, may be undefined if there was an error. | +| [fetchOptions](./kibana-plugin-core-public.httpresponse.fetchoptions.md) | Readonly<HttpFetchOptionsWithPath> | The original [HttpFetchOptionsWithPath](./kibana-plugin-core-public.httpfetchoptionswithpath.md) used to send this request. | +| [request](./kibana-plugin-core-public.httpresponse.request.md) | Readonly<Request> | Raw request sent to Kibana server. | +| [response?](./kibana-plugin-core-public.httpresponse.response.md) | Readonly<Response> | (Optional) Raw response received, may be undefined if there was an error. | diff --git a/docs/development/core/public/kibana-plugin-core-public.httpsetup.addloadingcountsource.md b/docs/development/core/public/kibana-plugin-core-public.httpsetup.addloadingcountsource.md index 71746b7b1b73f..7962772dbaa5c 100644 --- a/docs/development/core/public/kibana-plugin-core-public.httpsetup.addloadingcountsource.md +++ b/docs/development/core/public/kibana-plugin-core-public.httpsetup.addloadingcountsource.md @@ -16,9 +16,9 @@ addLoadingCountSource(countSource$: Observable): void; | Parameter | Type | Description | | --- | --- | --- | -| countSource$ | Observable<number> | | +| countSource$ | Observable<number> | an Observable to subscribe to for loading count updates. | Returns: -`void` +void diff --git a/docs/development/core/public/kibana-plugin-core-public.httpsetup.getloadingcount_.md b/docs/development/core/public/kibana-plugin-core-public.httpsetup.getloadingcount_.md index d60826f3ce5fa..e10278470f542 100644 --- a/docs/development/core/public/kibana-plugin-core-public.httpsetup.getloadingcount_.md +++ b/docs/development/core/public/kibana-plugin-core-public.httpsetup.getloadingcount_.md @@ -13,5 +13,5 @@ getLoadingCount$(): Observable; ``` Returns: -`Observable` +Observable<number> diff --git a/docs/development/core/public/kibana-plugin-core-public.httpsetup.intercept.md b/docs/development/core/public/kibana-plugin-core-public.httpsetup.intercept.md index d774d9896a92b..27962d3c3867b 100644 --- a/docs/development/core/public/kibana-plugin-core-public.httpsetup.intercept.md +++ b/docs/development/core/public/kibana-plugin-core-public.httpsetup.intercept.md @@ -16,11 +16,11 @@ intercept(interceptor: HttpInterceptor): () => void; | Parameter | Type | Description | | --- | --- | --- | -| interceptor | HttpInterceptor | | +| interceptor | HttpInterceptor | a [HttpInterceptor](./kibana-plugin-core-public.httpinterceptor.md) | Returns: -`() => void` +() => void a function for removing the attached interceptor. diff --git a/docs/development/core/public/kibana-plugin-core-public.httpsetup.md b/docs/development/core/public/kibana-plugin-core-public.httpsetup.md index a921110018c70..2d8116b0eeba6 100644 --- a/docs/development/core/public/kibana-plugin-core-public.httpsetup.md +++ b/docs/development/core/public/kibana-plugin-core-public.httpsetup.md @@ -15,17 +15,17 @@ export interface HttpSetup | Property | Type | Description | | --- | --- | --- | -| [anonymousPaths](./kibana-plugin-core-public.httpsetup.anonymouspaths.md) | IAnonymousPaths | APIs for denoting certain paths for not requiring authentication | -| [basePath](./kibana-plugin-core-public.httpsetup.basepath.md) | IBasePath | APIs for manipulating the basePath on URL segments. See [IBasePath](./kibana-plugin-core-public.ibasepath.md) | -| [delete](./kibana-plugin-core-public.httpsetup.delete.md) | HttpHandler | Makes an HTTP request with the DELETE method. See [HttpHandler](./kibana-plugin-core-public.httphandler.md) for options. | -| [externalUrl](./kibana-plugin-core-public.httpsetup.externalurl.md) | IExternalUrl | | -| [fetch](./kibana-plugin-core-public.httpsetup.fetch.md) | HttpHandler | Makes an HTTP request. Defaults to a GET request unless overridden. See [HttpHandler](./kibana-plugin-core-public.httphandler.md) for options. | -| [get](./kibana-plugin-core-public.httpsetup.get.md) | HttpHandler | Makes an HTTP request with the GET method. See [HttpHandler](./kibana-plugin-core-public.httphandler.md) for options. | -| [head](./kibana-plugin-core-public.httpsetup.head.md) | HttpHandler | Makes an HTTP request with the HEAD method. See [HttpHandler](./kibana-plugin-core-public.httphandler.md) for options. | -| [options](./kibana-plugin-core-public.httpsetup.options.md) | HttpHandler | Makes an HTTP request with the OPTIONS method. See [HttpHandler](./kibana-plugin-core-public.httphandler.md) for options. | -| [patch](./kibana-plugin-core-public.httpsetup.patch.md) | HttpHandler | Makes an HTTP request with the PATCH method. See [HttpHandler](./kibana-plugin-core-public.httphandler.md) for options. | -| [post](./kibana-plugin-core-public.httpsetup.post.md) | HttpHandler | Makes an HTTP request with the POST method. See [HttpHandler](./kibana-plugin-core-public.httphandler.md) for options. | -| [put](./kibana-plugin-core-public.httpsetup.put.md) | HttpHandler | Makes an HTTP request with the PUT method. See [HttpHandler](./kibana-plugin-core-public.httphandler.md) for options. | +| [anonymousPaths](./kibana-plugin-core-public.httpsetup.anonymouspaths.md) | IAnonymousPaths | APIs for denoting certain paths for not requiring authentication | +| [basePath](./kibana-plugin-core-public.httpsetup.basepath.md) | IBasePath | APIs for manipulating the basePath on URL segments. See [IBasePath](./kibana-plugin-core-public.ibasepath.md) | +| [delete](./kibana-plugin-core-public.httpsetup.delete.md) | HttpHandler | Makes an HTTP request with the DELETE method. See [HttpHandler](./kibana-plugin-core-public.httphandler.md) for options. | +| [externalUrl](./kibana-plugin-core-public.httpsetup.externalurl.md) | IExternalUrl | | +| [fetch](./kibana-plugin-core-public.httpsetup.fetch.md) | HttpHandler | Makes an HTTP request. Defaults to a GET request unless overridden. See [HttpHandler](./kibana-plugin-core-public.httphandler.md) for options. | +| [get](./kibana-plugin-core-public.httpsetup.get.md) | HttpHandler | Makes an HTTP request with the GET method. See [HttpHandler](./kibana-plugin-core-public.httphandler.md) for options. | +| [head](./kibana-plugin-core-public.httpsetup.head.md) | HttpHandler | Makes an HTTP request with the HEAD method. See [HttpHandler](./kibana-plugin-core-public.httphandler.md) for options. | +| [options](./kibana-plugin-core-public.httpsetup.options.md) | HttpHandler | Makes an HTTP request with the OPTIONS method. See [HttpHandler](./kibana-plugin-core-public.httphandler.md) for options. | +| [patch](./kibana-plugin-core-public.httpsetup.patch.md) | HttpHandler | Makes an HTTP request with the PATCH method. See [HttpHandler](./kibana-plugin-core-public.httphandler.md) for options. | +| [post](./kibana-plugin-core-public.httpsetup.post.md) | HttpHandler | Makes an HTTP request with the POST method. See [HttpHandler](./kibana-plugin-core-public.httphandler.md) for options. | +| [put](./kibana-plugin-core-public.httpsetup.put.md) | HttpHandler | Makes an HTTP request with the PUT method. See [HttpHandler](./kibana-plugin-core-public.httphandler.md) for options. | ## Methods diff --git a/docs/development/core/public/kibana-plugin-core-public.i18nstart.md b/docs/development/core/public/kibana-plugin-core-public.i18nstart.md index 5aff9d69c4590..586f5797abe6c 100644 --- a/docs/development/core/public/kibana-plugin-core-public.i18nstart.md +++ b/docs/development/core/public/kibana-plugin-core-public.i18nstart.md @@ -16,5 +16,5 @@ export interface I18nStart | Property | Type | Description | | --- | --- | --- | -| [Context](./kibana-plugin-core-public.i18nstart.context.md) | ({ children }: {
children: React.ReactNode;
}) => JSX.Element | React Context provider required as the topmost component for any i18n-compatible React tree. | +| [Context](./kibana-plugin-core-public.i18nstart.context.md) | ({ children }: { children: React.ReactNode; }) => JSX.Element | React Context provider required as the topmost component for any i18n-compatible React tree. | diff --git a/docs/development/core/public/kibana-plugin-core-public.ianonymouspaths.isanonymous.md b/docs/development/core/public/kibana-plugin-core-public.ianonymouspaths.isanonymous.md index 179b4e0e8663e..115285c84ea78 100644 --- a/docs/development/core/public/kibana-plugin-core-public.ianonymouspaths.isanonymous.md +++ b/docs/development/core/public/kibana-plugin-core-public.ianonymouspaths.isanonymous.md @@ -16,9 +16,9 @@ isAnonymous(path: string): boolean; | Parameter | Type | Description | | --- | --- | --- | -| path | string | | +| path | string | | Returns: -`boolean` +boolean diff --git a/docs/development/core/public/kibana-plugin-core-public.ianonymouspaths.register.md b/docs/development/core/public/kibana-plugin-core-public.ianonymouspaths.register.md index 5188ffd24f7f1..bfcc0f6decd5d 100644 --- a/docs/development/core/public/kibana-plugin-core-public.ianonymouspaths.register.md +++ b/docs/development/core/public/kibana-plugin-core-public.ianonymouspaths.register.md @@ -16,9 +16,9 @@ register(path: string): void; | Parameter | Type | Description | | --- | --- | --- | -| path | string | | +| path | string | | Returns: -`void` +void diff --git a/docs/development/core/public/kibana-plugin-core-public.ibasepath.md b/docs/development/core/public/kibana-plugin-core-public.ibasepath.md index 3afce9fee2a7c..72a863f7d515c 100644 --- a/docs/development/core/public/kibana-plugin-core-public.ibasepath.md +++ b/docs/development/core/public/kibana-plugin-core-public.ibasepath.md @@ -16,9 +16,9 @@ export interface IBasePath | Property | Type | Description | | --- | --- | --- | -| [get](./kibana-plugin-core-public.ibasepath.get.md) | () => string | Gets the basePath string. | -| [prepend](./kibana-plugin-core-public.ibasepath.prepend.md) | (url: string) => string | Prepends path with the basePath. | -| [publicBaseUrl](./kibana-plugin-core-public.ibasepath.publicbaseurl.md) | string | The server's publicly exposed base URL, if configured. Includes protocol, host, port (optional) and the [IBasePath.serverBasePath](./kibana-plugin-core-public.ibasepath.serverbasepath.md). | -| [remove](./kibana-plugin-core-public.ibasepath.remove.md) | (url: string) => string | Removes the prepended basePath from the path. | -| [serverBasePath](./kibana-plugin-core-public.ibasepath.serverbasepath.md) | string | Returns the server's root basePath as configured, without any namespace prefix.See for getting the basePath value for a specific request | +| [get](./kibana-plugin-core-public.ibasepath.get.md) | () => string | Gets the basePath string. | +| [prepend](./kibana-plugin-core-public.ibasepath.prepend.md) | (url: string) => string | Prepends path with the basePath. | +| [publicBaseUrl?](./kibana-plugin-core-public.ibasepath.publicbaseurl.md) | string | (Optional) The server's publicly exposed base URL, if configured. Includes protocol, host, port (optional) and the [IBasePath.serverBasePath](./kibana-plugin-core-public.ibasepath.serverbasepath.md). | +| [remove](./kibana-plugin-core-public.ibasepath.remove.md) | (url: string) => string | Removes the prepended basePath from the path. | +| [serverBasePath](./kibana-plugin-core-public.ibasepath.serverbasepath.md) | string | Returns the server's root basePath as configured, without any namespace prefix.See for getting the basePath value for a specific request | diff --git a/docs/development/core/public/kibana-plugin-core-public.iexternalurl.validateurl.md b/docs/development/core/public/kibana-plugin-core-public.iexternalurl.validateurl.md index 466d7cfebf547..24140effc45d9 100644 --- a/docs/development/core/public/kibana-plugin-core-public.iexternalurl.validateurl.md +++ b/docs/development/core/public/kibana-plugin-core-public.iexternalurl.validateurl.md @@ -18,9 +18,9 @@ validateUrl(relativeOrAbsoluteUrl: string): URL | null; | Parameter | Type | Description | | --- | --- | --- | -| relativeOrAbsoluteUrl | string | | +| relativeOrAbsoluteUrl | string | | Returns: -`URL | null` +URL \| null diff --git a/docs/development/core/public/kibana-plugin-core-public.iexternalurlpolicy.host.md b/docs/development/core/public/kibana-plugin-core-public.iexternalurlpolicy.host.md index 842d86db45d73..1d3c9fc9bbaf1 100644 --- a/docs/development/core/public/kibana-plugin-core-public.iexternalurlpolicy.host.md +++ b/docs/development/core/public/kibana-plugin-core-public.iexternalurlpolicy.host.md @@ -19,6 +19,5 @@ host?: string; // allows access to all of google.com, using any protocol. allow: true, host: 'google.com' - ``` diff --git a/docs/development/core/public/kibana-plugin-core-public.iexternalurlpolicy.md b/docs/development/core/public/kibana-plugin-core-public.iexternalurlpolicy.md index 3a1e571460974..6623fec18d976 100644 --- a/docs/development/core/public/kibana-plugin-core-public.iexternalurlpolicy.md +++ b/docs/development/core/public/kibana-plugin-core-public.iexternalurlpolicy.md @@ -16,7 +16,7 @@ export interface IExternalUrlPolicy | Property | Type | Description | | --- | --- | --- | -| [allow](./kibana-plugin-core-public.iexternalurlpolicy.allow.md) | boolean | Indicates if this policy allows or denies access to the described destination. | -| [host](./kibana-plugin-core-public.iexternalurlpolicy.host.md) | string | Optional host describing the external destination. May be combined with protocol. | -| [protocol](./kibana-plugin-core-public.iexternalurlpolicy.protocol.md) | string | Optional protocol describing the external destination. May be combined with host. | +| [allow](./kibana-plugin-core-public.iexternalurlpolicy.allow.md) | boolean | Indicates if this policy allows or denies access to the described destination. | +| [host?](./kibana-plugin-core-public.iexternalurlpolicy.host.md) | string | (Optional) Optional host describing the external destination. May be combined with protocol. | +| [protocol?](./kibana-plugin-core-public.iexternalurlpolicy.protocol.md) | string | (Optional) Optional protocol describing the external destination. May be combined with host. | diff --git a/docs/development/core/public/kibana-plugin-core-public.iexternalurlpolicy.protocol.md b/docs/development/core/public/kibana-plugin-core-public.iexternalurlpolicy.protocol.md index ac73412b6e143..6b6f8b9638bb8 100644 --- a/docs/development/core/public/kibana-plugin-core-public.iexternalurlpolicy.protocol.md +++ b/docs/development/core/public/kibana-plugin-core-public.iexternalurlpolicy.protocol.md @@ -19,6 +19,5 @@ protocol?: string; // allows access to all destinations over the `https` protocol. allow: true, protocol: 'https' - ``` diff --git a/docs/development/core/public/kibana-plugin-core-public.ihttpfetcherror.md b/docs/development/core/public/kibana-plugin-core-public.ihttpfetcherror.md index 8c21d1636711f..9aaae1be72028 100644 --- a/docs/development/core/public/kibana-plugin-core-public.ihttpfetcherror.md +++ b/docs/development/core/public/kibana-plugin-core-public.ihttpfetcherror.md @@ -10,15 +10,16 @@ ```typescript export interface IHttpFetchError extends Error ``` +Extends: Error ## Properties | Property | Type | Description | | --- | --- | --- | -| [body](./kibana-plugin-core-public.ihttpfetcherror.body.md) | TResponseBody | | -| [name](./kibana-plugin-core-public.ihttpfetcherror.name.md) | string | | -| [req](./kibana-plugin-core-public.ihttpfetcherror.req.md) | Request | | -| [request](./kibana-plugin-core-public.ihttpfetcherror.request.md) | Request | | -| [res](./kibana-plugin-core-public.ihttpfetcherror.res.md) | Response | | -| [response](./kibana-plugin-core-public.ihttpfetcherror.response.md) | Response | | +| [body?](./kibana-plugin-core-public.ihttpfetcherror.body.md) | TResponseBody | (Optional) | +| [name](./kibana-plugin-core-public.ihttpfetcherror.name.md) | string | | +| [req](./kibana-plugin-core-public.ihttpfetcherror.req.md) | Request | | +| [request](./kibana-plugin-core-public.ihttpfetcherror.request.md) | Request | | +| [res?](./kibana-plugin-core-public.ihttpfetcherror.res.md) | Response | (Optional) | +| [response?](./kibana-plugin-core-public.ihttpfetcherror.response.md) | Response | (Optional) | diff --git a/docs/development/core/public/kibana-plugin-core-public.ihttpinterceptcontroller.halt.md b/docs/development/core/public/kibana-plugin-core-public.ihttpinterceptcontroller.halt.md index 012805d22ba4e..b982e4dbac8a6 100644 --- a/docs/development/core/public/kibana-plugin-core-public.ihttpinterceptcontroller.halt.md +++ b/docs/development/core/public/kibana-plugin-core-public.ihttpinterceptcontroller.halt.md @@ -13,5 +13,5 @@ halt(): void; ``` Returns: -`void` +void diff --git a/docs/development/core/public/kibana-plugin-core-public.ihttpinterceptcontroller.md b/docs/development/core/public/kibana-plugin-core-public.ihttpinterceptcontroller.md index 5b720fda34f4b..15a66ef569e7d 100644 --- a/docs/development/core/public/kibana-plugin-core-public.ihttpinterceptcontroller.md +++ b/docs/development/core/public/kibana-plugin-core-public.ihttpinterceptcontroller.md @@ -16,7 +16,7 @@ export interface IHttpInterceptController | Property | Type | Description | | --- | --- | --- | -| [halted](./kibana-plugin-core-public.ihttpinterceptcontroller.halted.md) | boolean | Whether or not this chain has been halted. | +| [halted](./kibana-plugin-core-public.ihttpinterceptcontroller.halted.md) | boolean | Whether or not this chain has been halted. | ## Methods diff --git a/docs/development/core/public/kibana-plugin-core-public.ihttpresponseinterceptoroverrides.md b/docs/development/core/public/kibana-plugin-core-public.ihttpresponseinterceptoroverrides.md index 4b55cec8f3a2f..57a4555cd6da5 100644 --- a/docs/development/core/public/kibana-plugin-core-public.ihttpresponseinterceptoroverrides.md +++ b/docs/development/core/public/kibana-plugin-core-public.ihttpresponseinterceptoroverrides.md @@ -16,6 +16,6 @@ export interface IHttpResponseInterceptorOverrides | Property | Type | Description | | --- | --- | --- | -| [body](./kibana-plugin-core-public.ihttpresponseinterceptoroverrides.body.md) | TResponseBody | Parsed body received, may be undefined if there was an error. | -| [response](./kibana-plugin-core-public.ihttpresponseinterceptoroverrides.response.md) | Readonly<Response> | Raw response received, may be undefined if there was an error. | +| [body?](./kibana-plugin-core-public.ihttpresponseinterceptoroverrides.body.md) | TResponseBody | (Optional) Parsed body received, may be undefined if there was an error. | +| [response?](./kibana-plugin-core-public.ihttpresponseinterceptoroverrides.response.md) | Readonly<Response> | (Optional) Raw response received, may be undefined if there was an error. | diff --git a/docs/development/core/public/kibana-plugin-core-public.iuisettingsclient.md b/docs/development/core/public/kibana-plugin-core-public.iuisettingsclient.md index d6f3b3186b542..5d2429a799fe6 100644 --- a/docs/development/core/public/kibana-plugin-core-public.iuisettingsclient.md +++ b/docs/development/core/public/kibana-plugin-core-public.iuisettingsclient.md @@ -16,15 +16,15 @@ export interface IUiSettingsClient | Property | Type | Description | | --- | --- | --- | -| [get](./kibana-plugin-core-public.iuisettingsclient.get.md) | <T = any>(key: string, defaultOverride?: T) => T | Gets the value for a specific uiSetting. If this setting has no user-defined value then the defaultOverride parameter is returned (and parsed if setting is of type "json" or "number). If the parameter is not defined and the key is not registered by any plugin then an error is thrown, otherwise reads the default value defined by a plugin. | -| [get$](./kibana-plugin-core-public.iuisettingsclient.get_.md) | <T = any>(key: string, defaultOverride?: T) => Observable<T> | Gets an observable of the current value for a config key, and all updates to that config key in the future. Providing a defaultOverride argument behaves the same as it does in \#get() | -| [getAll](./kibana-plugin-core-public.iuisettingsclient.getall.md) | () => Readonly<Record<string, PublicUiSettingsParams & UserProvidedValues>> | Gets the metadata about all uiSettings, including the type, default value, and user value for each key. | -| [getUpdate$](./kibana-plugin-core-public.iuisettingsclient.getupdate_.md) | <T = any>() => Observable<{
key: string;
newValue: T;
oldValue: T;
}> | Returns an Observable that notifies subscribers of each update to the uiSettings, including the key, newValue, and oldValue of the setting that changed. | -| [getUpdateErrors$](./kibana-plugin-core-public.iuisettingsclient.getupdateerrors_.md) | () => Observable<Error> | Returns an Observable that notifies subscribers of each error while trying to update the settings, containing the actual Error class. | -| [isCustom](./kibana-plugin-core-public.iuisettingsclient.iscustom.md) | (key: string) => boolean | Returns true if the setting wasn't registered by any plugin, but was either added directly via set(), or is an unknown setting found in the uiSettings saved object | -| [isDeclared](./kibana-plugin-core-public.iuisettingsclient.isdeclared.md) | (key: string) => boolean | Returns true if the key is a "known" uiSetting, meaning it is either registered by any plugin or was previously added as a custom setting via the set() method. | -| [isDefault](./kibana-plugin-core-public.iuisettingsclient.isdefault.md) | (key: string) => boolean | Returns true if the setting has no user-defined value or is unknown | -| [isOverridden](./kibana-plugin-core-public.iuisettingsclient.isoverridden.md) | (key: string) => boolean | Shows whether the uiSettings value set by the user. | -| [remove](./kibana-plugin-core-public.iuisettingsclient.remove.md) | (key: string) => Promise<boolean> | Removes the user-defined value for a setting, causing it to revert to the default. This method behaves the same as calling set(key, null), including the synchronization, custom setting, and error behavior of that method. | -| [set](./kibana-plugin-core-public.iuisettingsclient.set.md) | (key: string, value: any) => Promise<boolean> | Sets the value for a uiSetting. If the setting is not registered by any plugin it will be stored as a custom setting. The new value will be synchronously available via the get() method and sent to the server in the background. If the request to the server fails then a updateErrors$ will be notified and the setting will be reverted to its value before set() was called. | +| [get](./kibana-plugin-core-public.iuisettingsclient.get.md) | <T = any>(key: string, defaultOverride?: T) => T | Gets the value for a specific uiSetting. If this setting has no user-defined value then the defaultOverride parameter is returned (and parsed if setting is of type "json" or "number). If the parameter is not defined and the key is not registered by any plugin then an error is thrown, otherwise reads the default value defined by a plugin. | +| [get$](./kibana-plugin-core-public.iuisettingsclient.get_.md) | <T = any>(key: string, defaultOverride?: T) => Observable<T> | Gets an observable of the current value for a config key, and all updates to that config key in the future. Providing a defaultOverride argument behaves the same as it does in \#get() | +| [getAll](./kibana-plugin-core-public.iuisettingsclient.getall.md) | () => Readonly<Record<string, PublicUiSettingsParams & UserProvidedValues>> | Gets the metadata about all uiSettings, including the type, default value, and user value for each key. | +| [getUpdate$](./kibana-plugin-core-public.iuisettingsclient.getupdate_.md) | <T = any>() => Observable<{ key: string; newValue: T; oldValue: T; }> | Returns an Observable that notifies subscribers of each update to the uiSettings, including the key, newValue, and oldValue of the setting that changed. | +| [getUpdateErrors$](./kibana-plugin-core-public.iuisettingsclient.getupdateerrors_.md) | () => Observable<Error> | Returns an Observable that notifies subscribers of each error while trying to update the settings, containing the actual Error class. | +| [isCustom](./kibana-plugin-core-public.iuisettingsclient.iscustom.md) | (key: string) => boolean | Returns true if the setting wasn't registered by any plugin, but was either added directly via set(), or is an unknown setting found in the uiSettings saved object | +| [isDeclared](./kibana-plugin-core-public.iuisettingsclient.isdeclared.md) | (key: string) => boolean | Returns true if the key is a "known" uiSetting, meaning it is either registered by any plugin or was previously added as a custom setting via the set() method. | +| [isDefault](./kibana-plugin-core-public.iuisettingsclient.isdefault.md) | (key: string) => boolean | Returns true if the setting has no user-defined value or is unknown | +| [isOverridden](./kibana-plugin-core-public.iuisettingsclient.isoverridden.md) | (key: string) => boolean | Shows whether the uiSettings value set by the user. | +| [remove](./kibana-plugin-core-public.iuisettingsclient.remove.md) | (key: string) => Promise<boolean> | Removes the user-defined value for a setting, causing it to revert to the default. This method behaves the same as calling set(key, null), including the synchronization, custom setting, and error behavior of that method. | +| [set](./kibana-plugin-core-public.iuisettingsclient.set.md) | (key: string, value: any) => Promise<boolean> | Sets the value for a uiSetting. If the setting is not registered by any plugin it will be stored as a custom setting. The new value will be synchronously available via the get() method and sent to the server in the background. If the request to the server fails then a updateErrors$ will be notified and the setting will be reverted to its value before set() was called. | diff --git a/docs/development/core/public/kibana-plugin-core-public.md b/docs/development/core/public/kibana-plugin-core-public.md index a32dceafd74a9..dee77e8994155 100644 --- a/docs/development/core/public/kibana-plugin-core-public.md +++ b/docs/development/core/public/kibana-plugin-core-public.md @@ -6,7 +6,7 @@ The Kibana Core APIs for client-side plugins. -A plugin's `public/index` file must contain a named import, `plugin`, that implements [PluginInitializer](./kibana-plugin-core-public.plugininitializer.md) which returns an object that implements [Plugin](./kibana-plugin-core-public.plugin.md). +A plugin's `public/index` file must contain a named import, `plugin`, that implements [PluginInitializer](./kibana-plugin-core-public.plugininitializer.md) which returns an object that implements . The plugin integrates with the core system via lifecycle events: `setup`, `start`, and `stop`. In each lifecycle method, the plugin will receive the corresponding core services available (either [CoreSetup](./kibana-plugin-core-public.coresetup.md) or [CoreStart](./kibana-plugin-core-public.corestart.md)) and any interfaces returned by dependency plugins' lifecycle method. Anything returned by the plugin's lifecycle method will be exposed to downstream dependencies when their corresponding lifecycle methods are invoked. @@ -94,7 +94,7 @@ The plugin integrates with the core system via lifecycle events: `setup` | [OverlayModalStart](./kibana-plugin-core-public.overlaymodalstart.md) | APIs to open and manage modal dialogs. | | [OverlayRef](./kibana-plugin-core-public.overlayref.md) | Returned by [OverlayStart](./kibana-plugin-core-public.overlaystart.md) methods for closing a mounted overlay. | | [OverlayStart](./kibana-plugin-core-public.overlaystart.md) | | -| [Plugin](./kibana-plugin-core-public.plugin.md) | The interface that should be returned by a PluginInitializer. | +| [Plugin\_2](./kibana-plugin-core-public.plugin_2.md) | The interface that should be returned by a PluginInitializer. | | [PluginInitializerContext](./kibana-plugin-core-public.plugininitializercontext.md) | The available core services passed to a PluginInitializer | | [ResolvedSimpleSavedObject](./kibana-plugin-core-public.resolvedsimplesavedobject.md) | This interface is a very simple wrapper for SavedObjects resolved from the server with the [SavedObjectsClient](./kibana-plugin-core-public.savedobjectsclient.md). | | [ResponseErrorBody](./kibana-plugin-core-public.responseerrorbody.md) | | diff --git a/docs/development/core/public/kibana-plugin-core-public.navigatetoappoptions.md b/docs/development/core/public/kibana-plugin-core-public.navigatetoappoptions.md index 7b01bab056d84..c8ec5bdaf8c0d 100644 --- a/docs/development/core/public/kibana-plugin-core-public.navigatetoappoptions.md +++ b/docs/development/core/public/kibana-plugin-core-public.navigatetoappoptions.md @@ -16,9 +16,9 @@ export interface NavigateToAppOptions | Property | Type | Description | | --- | --- | --- | -| [deepLinkId](./kibana-plugin-core-public.navigatetoappoptions.deeplinkid.md) | string | optional [deep link](./kibana-plugin-core-public.app.deeplinks.md) id inside the application to navigate to. If an additional [path](./kibana-plugin-core-public.navigatetoappoptions.path.md) is defined it will be appended to the deep link path. | -| [openInNewTab](./kibana-plugin-core-public.navigatetoappoptions.openinnewtab.md) | boolean | if true, will open the app in new tab, will share session information via window.open if base | -| [path](./kibana-plugin-core-public.navigatetoappoptions.path.md) | string | optional path inside application to deep link to. If undefined, will use [the app's default path](./kibana-plugin-core-public.app.defaultpath.md) as default. | -| [replace](./kibana-plugin-core-public.navigatetoappoptions.replace.md) | boolean | if true, will not create a new history entry when navigating (using replace instead of push) | -| [state](./kibana-plugin-core-public.navigatetoappoptions.state.md) | unknown | optional state to forward to the application | +| [deepLinkId?](./kibana-plugin-core-public.navigatetoappoptions.deeplinkid.md) | string | (Optional) optional [deep link](./kibana-plugin-core-public.app.deeplinks.md) id inside the application to navigate to. If an additional [path](./kibana-plugin-core-public.navigatetoappoptions.path.md) is defined it will be appended to the deep link path. | +| [openInNewTab?](./kibana-plugin-core-public.navigatetoappoptions.openinnewtab.md) | boolean | (Optional) if true, will open the app in new tab, will share session information via window.open if base | +| [path?](./kibana-plugin-core-public.navigatetoappoptions.path.md) | string | (Optional) optional path inside application to deep link to. If undefined, will use [the app's default path](./kibana-plugin-core-public.app.defaultpath.md) as default. | +| [replace?](./kibana-plugin-core-public.navigatetoappoptions.replace.md) | boolean | (Optional) if true, will not create a new history entry when navigating (using replace instead of push) | +| [state?](./kibana-plugin-core-public.navigatetoappoptions.state.md) | unknown | (Optional) optional state to forward to the application | diff --git a/docs/development/core/public/kibana-plugin-core-public.notificationssetup.md b/docs/development/core/public/kibana-plugin-core-public.notificationssetup.md index fb78fb055a79d..efaeafa1afb1a 100644 --- a/docs/development/core/public/kibana-plugin-core-public.notificationssetup.md +++ b/docs/development/core/public/kibana-plugin-core-public.notificationssetup.md @@ -15,5 +15,5 @@ export interface NotificationsSetup | Property | Type | Description | | --- | --- | --- | -| [toasts](./kibana-plugin-core-public.notificationssetup.toasts.md) | ToastsSetup | [ToastsSetup](./kibana-plugin-core-public.toastssetup.md) | +| [toasts](./kibana-plugin-core-public.notificationssetup.toasts.md) | ToastsSetup | [ToastsSetup](./kibana-plugin-core-public.toastssetup.md) | diff --git a/docs/development/core/public/kibana-plugin-core-public.notificationsstart.md b/docs/development/core/public/kibana-plugin-core-public.notificationsstart.md index 9b1f6e62400f0..0e77badd51235 100644 --- a/docs/development/core/public/kibana-plugin-core-public.notificationsstart.md +++ b/docs/development/core/public/kibana-plugin-core-public.notificationsstart.md @@ -15,5 +15,5 @@ export interface NotificationsStart | Property | Type | Description | | --- | --- | --- | -| [toasts](./kibana-plugin-core-public.notificationsstart.toasts.md) | ToastsStart | [ToastsStart](./kibana-plugin-core-public.toastsstart.md) | +| [toasts](./kibana-plugin-core-public.notificationsstart.toasts.md) | ToastsStart | [ToastsStart](./kibana-plugin-core-public.toastsstart.md) | diff --git a/docs/development/core/public/kibana-plugin-core-public.overlaybannersstart.add.md b/docs/development/core/public/kibana-plugin-core-public.overlaybannersstart.add.md index 4cedda4e8092a..fd3ce0b3a4292 100644 --- a/docs/development/core/public/kibana-plugin-core-public.overlaybannersstart.add.md +++ b/docs/development/core/public/kibana-plugin-core-public.overlaybannersstart.add.md @@ -16,12 +16,12 @@ add(mount: MountPoint, priority?: number): string; | Parameter | Type | Description | | --- | --- | --- | -| mount | MountPoint | | -| priority | number | | +| mount | MountPoint | [MountPoint](./kibana-plugin-core-public.mountpoint.md) | +| priority | number | optional priority order to display this banner. Higher priority values are shown first. | Returns: -`string` +string a unique identifier for the given banner to be used with [OverlayBannersStart.remove()](./kibana-plugin-core-public.overlaybannersstart.remove.md) and [OverlayBannersStart.replace()](./kibana-plugin-core-public.overlaybannersstart.replace.md) diff --git a/docs/development/core/public/kibana-plugin-core-public.overlaybannersstart.getcomponent.md b/docs/development/core/public/kibana-plugin-core-public.overlaybannersstart.getcomponent.md index dc167f4f8fb8d..b5f0ab1d01299 100644 --- a/docs/development/core/public/kibana-plugin-core-public.overlaybannersstart.getcomponent.md +++ b/docs/development/core/public/kibana-plugin-core-public.overlaybannersstart.getcomponent.md @@ -11,5 +11,5 @@ getComponent(): JSX.Element; ``` Returns: -`JSX.Element` +JSX.Element diff --git a/docs/development/core/public/kibana-plugin-core-public.overlaybannersstart.remove.md b/docs/development/core/public/kibana-plugin-core-public.overlaybannersstart.remove.md index 2c69506afb612..ce1e3ee08bd51 100644 --- a/docs/development/core/public/kibana-plugin-core-public.overlaybannersstart.remove.md +++ b/docs/development/core/public/kibana-plugin-core-public.overlaybannersstart.remove.md @@ -16,11 +16,11 @@ remove(id: string): boolean; | Parameter | Type | Description | | --- | --- | --- | -| id | string | | +| id | string | the unique identifier for the banner returned by [OverlayBannersStart.add()](./kibana-plugin-core-public.overlaybannersstart.add.md) | Returns: -`boolean` +boolean if the banner was found or not diff --git a/docs/development/core/public/kibana-plugin-core-public.overlaybannersstart.replace.md b/docs/development/core/public/kibana-plugin-core-public.overlaybannersstart.replace.md index 1112d781bae4f..ea16c739cc847 100644 --- a/docs/development/core/public/kibana-plugin-core-public.overlaybannersstart.replace.md +++ b/docs/development/core/public/kibana-plugin-core-public.overlaybannersstart.replace.md @@ -16,13 +16,13 @@ replace(id: string | undefined, mount: MountPoint, priority?: number): string; | Parameter | Type | Description | | --- | --- | --- | -| id | string | undefined | | -| mount | MountPoint | | -| priority | number | | +| id | string \| undefined | the unique identifier for the banner returned by [OverlayBannersStart.add()](./kibana-plugin-core-public.overlaybannersstart.add.md) | +| mount | MountPoint | [MountPoint](./kibana-plugin-core-public.mountpoint.md) | +| priority | number | optional priority order to display this banner. Higher priority values are shown first. | Returns: -`string` +string a new identifier for the given banner to be used with [OverlayBannersStart.remove()](./kibana-plugin-core-public.overlaybannersstart.remove.md) and [OverlayBannersStart.replace()](./kibana-plugin-core-public.overlaybannersstart.replace.md) diff --git a/docs/development/core/public/kibana-plugin-core-public.overlayflyoutopenoptions.maskprops.md b/docs/development/core/public/kibana-plugin-core-public.overlayflyoutopenoptions.maskprops.md new file mode 100644 index 0000000000000..3cb3e0b4902a9 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-core-public.overlayflyoutopenoptions.maskprops.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [OverlayFlyoutOpenOptions](./kibana-plugin-core-public.overlayflyoutopenoptions.md) > [maskProps](./kibana-plugin-core-public.overlayflyoutopenoptions.maskprops.md) + +## OverlayFlyoutOpenOptions.maskProps property + +Signature: + +```typescript +maskProps?: EuiOverlayMaskProps; +``` diff --git a/docs/development/core/public/kibana-plugin-core-public.overlayflyoutopenoptions.md b/docs/development/core/public/kibana-plugin-core-public.overlayflyoutopenoptions.md index dcecdeb840869..defbf79b0ffe2 100644 --- a/docs/development/core/public/kibana-plugin-core-public.overlayflyoutopenoptions.md +++ b/docs/development/core/public/kibana-plugin-core-public.overlayflyoutopenoptions.md @@ -15,13 +15,14 @@ export interface OverlayFlyoutOpenOptions | Property | Type | Description | | --- | --- | --- | -| ["aria-label"](./kibana-plugin-core-public.overlayflyoutopenoptions._aria-label_.md) | string | | -| ["data-test-subj"](./kibana-plugin-core-public.overlayflyoutopenoptions._data-test-subj_.md) | string | | -| [className](./kibana-plugin-core-public.overlayflyoutopenoptions.classname.md) | string | | -| [closeButtonAriaLabel](./kibana-plugin-core-public.overlayflyoutopenoptions.closebuttonarialabel.md) | string | | -| [hideCloseButton](./kibana-plugin-core-public.overlayflyoutopenoptions.hideclosebutton.md) | boolean | | -| [maxWidth](./kibana-plugin-core-public.overlayflyoutopenoptions.maxwidth.md) | boolean | number | string | | -| [onClose](./kibana-plugin-core-public.overlayflyoutopenoptions.onclose.md) | (flyout: OverlayRef) => void | EuiFlyout onClose handler. If provided the consumer is responsible for calling flyout.close() to close the flyout; | -| [ownFocus](./kibana-plugin-core-public.overlayflyoutopenoptions.ownfocus.md) | boolean | | -| [size](./kibana-plugin-core-public.overlayflyoutopenoptions.size.md) | EuiFlyoutSize | | +| ["aria-label"?](./kibana-plugin-core-public.overlayflyoutopenoptions._aria-label_.md) | string | (Optional) | +| ["data-test-subj"?](./kibana-plugin-core-public.overlayflyoutopenoptions._data-test-subj_.md) | string | (Optional) | +| [className?](./kibana-plugin-core-public.overlayflyoutopenoptions.classname.md) | string | (Optional) | +| [closeButtonAriaLabel?](./kibana-plugin-core-public.overlayflyoutopenoptions.closebuttonarialabel.md) | string | (Optional) | +| [hideCloseButton?](./kibana-plugin-core-public.overlayflyoutopenoptions.hideclosebutton.md) | boolean | (Optional) | +| [maskProps?](./kibana-plugin-core-public.overlayflyoutopenoptions.maskprops.md) | EuiOverlayMaskProps | (Optional) | +| [maxWidth?](./kibana-plugin-core-public.overlayflyoutopenoptions.maxwidth.md) | boolean \| number \| string | (Optional) | +| [onClose?](./kibana-plugin-core-public.overlayflyoutopenoptions.onclose.md) | (flyout: OverlayRef) => void | (Optional) EuiFlyout onClose handler. If provided the consumer is responsible for calling flyout.close() to close the flyout; | +| [ownFocus?](./kibana-plugin-core-public.overlayflyoutopenoptions.ownfocus.md) | boolean | (Optional) | +| [size?](./kibana-plugin-core-public.overlayflyoutopenoptions.size.md) | EuiFlyoutSize | (Optional) | diff --git a/docs/development/core/public/kibana-plugin-core-public.overlayflyoutstart.open.md b/docs/development/core/public/kibana-plugin-core-public.overlayflyoutstart.open.md index 1f740410ca282..94290eb91f942 100644 --- a/docs/development/core/public/kibana-plugin-core-public.overlayflyoutstart.open.md +++ b/docs/development/core/public/kibana-plugin-core-public.overlayflyoutstart.open.md @@ -16,10 +16,10 @@ open(mount: MountPoint, options?: OverlayFlyoutOpenOptions): OverlayRef; | Parameter | Type | Description | | --- | --- | --- | -| mount | MountPoint | | -| options | OverlayFlyoutOpenOptions | | +| mount | MountPoint | [MountPoint](./kibana-plugin-core-public.mountpoint.md) - Mounts the children inside a flyout panel | +| options | OverlayFlyoutOpenOptions | [OverlayFlyoutOpenOptions](./kibana-plugin-core-public.overlayflyoutopenoptions.md) - options for the flyout [OverlayRef](./kibana-plugin-core-public.overlayref.md) A reference to the opened flyout panel. | Returns: -`OverlayRef` +OverlayRef diff --git a/docs/development/core/public/kibana-plugin-core-public.overlaymodalconfirmoptions.md b/docs/development/core/public/kibana-plugin-core-public.overlaymodalconfirmoptions.md index 83405a151a372..2f672e551ba51 100644 --- a/docs/development/core/public/kibana-plugin-core-public.overlaymodalconfirmoptions.md +++ b/docs/development/core/public/kibana-plugin-core-public.overlaymodalconfirmoptions.md @@ -15,13 +15,13 @@ export interface OverlayModalConfirmOptions | Property | Type | Description | | --- | --- | --- | -| ["data-test-subj"](./kibana-plugin-core-public.overlaymodalconfirmoptions._data-test-subj_.md) | string | | -| [buttonColor](./kibana-plugin-core-public.overlaymodalconfirmoptions.buttoncolor.md) | EuiConfirmModalProps['buttonColor'] | | -| [cancelButtonText](./kibana-plugin-core-public.overlaymodalconfirmoptions.cancelbuttontext.md) | string | | -| [className](./kibana-plugin-core-public.overlaymodalconfirmoptions.classname.md) | string | | -| [closeButtonAriaLabel](./kibana-plugin-core-public.overlaymodalconfirmoptions.closebuttonarialabel.md) | string | | -| [confirmButtonText](./kibana-plugin-core-public.overlaymodalconfirmoptions.confirmbuttontext.md) | string | | -| [defaultFocusedButton](./kibana-plugin-core-public.overlaymodalconfirmoptions.defaultfocusedbutton.md) | EuiConfirmModalProps['defaultFocusedButton'] | | -| [maxWidth](./kibana-plugin-core-public.overlaymodalconfirmoptions.maxwidth.md) | boolean | number | string | Sets the max-width of the modal. Set to true to use the default (euiBreakpoints 'm'), set to false to not restrict the width, set to a number for a custom width in px, set to a string for a custom width in custom measurement. | -| [title](./kibana-plugin-core-public.overlaymodalconfirmoptions.title.md) | string | | +| ["data-test-subj"?](./kibana-plugin-core-public.overlaymodalconfirmoptions._data-test-subj_.md) | string | (Optional) | +| [buttonColor?](./kibana-plugin-core-public.overlaymodalconfirmoptions.buttoncolor.md) | EuiConfirmModalProps\['buttonColor'\] | (Optional) | +| [cancelButtonText?](./kibana-plugin-core-public.overlaymodalconfirmoptions.cancelbuttontext.md) | string | (Optional) | +| [className?](./kibana-plugin-core-public.overlaymodalconfirmoptions.classname.md) | string | (Optional) | +| [closeButtonAriaLabel?](./kibana-plugin-core-public.overlaymodalconfirmoptions.closebuttonarialabel.md) | string | (Optional) | +| [confirmButtonText?](./kibana-plugin-core-public.overlaymodalconfirmoptions.confirmbuttontext.md) | string | (Optional) | +| [defaultFocusedButton?](./kibana-plugin-core-public.overlaymodalconfirmoptions.defaultfocusedbutton.md) | EuiConfirmModalProps\['defaultFocusedButton'\] | (Optional) | +| [maxWidth?](./kibana-plugin-core-public.overlaymodalconfirmoptions.maxwidth.md) | boolean \| number \| string | (Optional) Sets the max-width of the modal. Set to true to use the default (euiBreakpoints 'm'), set to false to not restrict the width, set to a number for a custom width in px, set to a string for a custom width in custom measurement. | +| [title?](./kibana-plugin-core-public.overlaymodalconfirmoptions.title.md) | string | (Optional) | diff --git a/docs/development/core/public/kibana-plugin-core-public.overlaymodalopenoptions.md b/docs/development/core/public/kibana-plugin-core-public.overlaymodalopenoptions.md index 5307a8357a814..5fc978ea26262 100644 --- a/docs/development/core/public/kibana-plugin-core-public.overlaymodalopenoptions.md +++ b/docs/development/core/public/kibana-plugin-core-public.overlaymodalopenoptions.md @@ -15,8 +15,8 @@ export interface OverlayModalOpenOptions | Property | Type | Description | | --- | --- | --- | -| ["data-test-subj"](./kibana-plugin-core-public.overlaymodalopenoptions._data-test-subj_.md) | string | | -| [className](./kibana-plugin-core-public.overlaymodalopenoptions.classname.md) | string | | -| [closeButtonAriaLabel](./kibana-plugin-core-public.overlaymodalopenoptions.closebuttonarialabel.md) | string | | -| [maxWidth](./kibana-plugin-core-public.overlaymodalopenoptions.maxwidth.md) | boolean | number | string | | +| ["data-test-subj"?](./kibana-plugin-core-public.overlaymodalopenoptions._data-test-subj_.md) | string | (Optional) | +| [className?](./kibana-plugin-core-public.overlaymodalopenoptions.classname.md) | string | (Optional) | +| [closeButtonAriaLabel?](./kibana-plugin-core-public.overlaymodalopenoptions.closebuttonarialabel.md) | string | (Optional) | +| [maxWidth?](./kibana-plugin-core-public.overlaymodalopenoptions.maxwidth.md) | boolean \| number \| string | (Optional) | diff --git a/docs/development/core/public/kibana-plugin-core-public.overlaymodalstart.open.md b/docs/development/core/public/kibana-plugin-core-public.overlaymodalstart.open.md index 1c6b82e37a624..35bfa406b4d17 100644 --- a/docs/development/core/public/kibana-plugin-core-public.overlaymodalstart.open.md +++ b/docs/development/core/public/kibana-plugin-core-public.overlaymodalstart.open.md @@ -16,10 +16,10 @@ open(mount: MountPoint, options?: OverlayModalOpenOptions): OverlayRef; | Parameter | Type | Description | | --- | --- | --- | -| mount | MountPoint | | -| options | OverlayModalOpenOptions | | +| mount | MountPoint | [MountPoint](./kibana-plugin-core-public.mountpoint.md) - Mounts the children inside the modal | +| options | OverlayModalOpenOptions | [OverlayModalOpenOptions](./kibana-plugin-core-public.overlaymodalopenoptions.md) - options for the modal [OverlayRef](./kibana-plugin-core-public.overlayref.md) A reference to the opened modal. | Returns: -`OverlayRef` +OverlayRef diff --git a/docs/development/core/public/kibana-plugin-core-public.overlaymodalstart.openconfirm.md b/docs/development/core/public/kibana-plugin-core-public.overlaymodalstart.openconfirm.md index b0052c0f6460e..056f512de87bf 100644 --- a/docs/development/core/public/kibana-plugin-core-public.overlaymodalstart.openconfirm.md +++ b/docs/development/core/public/kibana-plugin-core-public.overlaymodalstart.openconfirm.md @@ -16,10 +16,10 @@ openConfirm(message: MountPoint | string, options?: OverlayModalConfirmOptions): | Parameter | Type | Description | | --- | --- | --- | -| message | MountPoint | string | | -| options | OverlayModalConfirmOptions | | +| message | MountPoint \| string | [MountPoint](./kibana-plugin-core-public.mountpoint.md) - string or mountpoint to be used a the confirm message body | +| options | OverlayModalConfirmOptions | [OverlayModalConfirmOptions](./kibana-plugin-core-public.overlaymodalconfirmoptions.md) - options for the confirm modal | Returns: -`Promise` +Promise<boolean> diff --git a/docs/development/core/public/kibana-plugin-core-public.overlayref.close.md b/docs/development/core/public/kibana-plugin-core-public.overlayref.close.md index 656afa64e5490..454723f6ffd09 100644 --- a/docs/development/core/public/kibana-plugin-core-public.overlayref.close.md +++ b/docs/development/core/public/kibana-plugin-core-public.overlayref.close.md @@ -13,5 +13,5 @@ close(): Promise; ``` Returns: -`Promise` +Promise<void> diff --git a/docs/development/core/public/kibana-plugin-core-public.overlayref.md b/docs/development/core/public/kibana-plugin-core-public.overlayref.md index 0fc76057d0390..da11e284f285d 100644 --- a/docs/development/core/public/kibana-plugin-core-public.overlayref.md +++ b/docs/development/core/public/kibana-plugin-core-public.overlayref.md @@ -16,7 +16,7 @@ export interface OverlayRef | Property | Type | Description | | --- | --- | --- | -| [onClose](./kibana-plugin-core-public.overlayref.onclose.md) | Promise<void> | A Promise that will resolve once this overlay is closed.Overlays can close from user interaction, calling close() on the overlay reference or another overlay replacing yours via openModal or openFlyout. | +| [onClose](./kibana-plugin-core-public.overlayref.onclose.md) | Promise<void> | A Promise that will resolve once this overlay is closed.Overlays can close from user interaction, calling close() on the overlay reference or another overlay replacing yours via openModal or openFlyout. | ## Methods diff --git a/docs/development/core/public/kibana-plugin-core-public.overlaystart.md b/docs/development/core/public/kibana-plugin-core-public.overlaystart.md index 2cc4d89dda643..3bbdd4ab9b918 100644 --- a/docs/development/core/public/kibana-plugin-core-public.overlaystart.md +++ b/docs/development/core/public/kibana-plugin-core-public.overlaystart.md @@ -15,8 +15,8 @@ export interface OverlayStart | Property | Type | Description | | --- | --- | --- | -| [banners](./kibana-plugin-core-public.overlaystart.banners.md) | OverlayBannersStart | [OverlayBannersStart](./kibana-plugin-core-public.overlaybannersstart.md) | -| [openConfirm](./kibana-plugin-core-public.overlaystart.openconfirm.md) | OverlayModalStart['openConfirm'] | | -| [openFlyout](./kibana-plugin-core-public.overlaystart.openflyout.md) | OverlayFlyoutStart['open'] | | -| [openModal](./kibana-plugin-core-public.overlaystart.openmodal.md) | OverlayModalStart['open'] | | +| [banners](./kibana-plugin-core-public.overlaystart.banners.md) | OverlayBannersStart | [OverlayBannersStart](./kibana-plugin-core-public.overlaybannersstart.md) | +| [openConfirm](./kibana-plugin-core-public.overlaystart.openconfirm.md) | OverlayModalStart\['openConfirm'\] | | +| [openFlyout](./kibana-plugin-core-public.overlaystart.openflyout.md) | OverlayFlyoutStart\['open'\] | | +| [openModal](./kibana-plugin-core-public.overlaystart.openmodal.md) | OverlayModalStart\['open'\] | | diff --git a/docs/development/core/public/kibana-plugin-core-public.plugin.md b/docs/development/core/public/kibana-plugin-core-public.plugin.md deleted file mode 100644 index 4de46ae55797c..0000000000000 --- a/docs/development/core/public/kibana-plugin-core-public.plugin.md +++ /dev/null @@ -1,22 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [Plugin](./kibana-plugin-core-public.plugin.md) - -## Plugin interface - -The interface that should be returned by a `PluginInitializer`. - -Signature: - -```typescript -export interface Plugin -``` - -## Methods - -| Method | Description | -| --- | --- | -| [setup(core, plugins)](./kibana-plugin-core-public.plugin.setup.md) | | -| [start(core, plugins)](./kibana-plugin-core-public.plugin.start.md) | | -| [stop()](./kibana-plugin-core-public.plugin.stop.md) | | - diff --git a/docs/development/core/public/kibana-plugin-core-public.plugin.setup.md b/docs/development/core/public/kibana-plugin-core-public.plugin.setup.md deleted file mode 100644 index 232851cd342ce..0000000000000 --- a/docs/development/core/public/kibana-plugin-core-public.plugin.setup.md +++ /dev/null @@ -1,23 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [Plugin](./kibana-plugin-core-public.plugin.md) > [setup](./kibana-plugin-core-public.plugin.setup.md) - -## Plugin.setup() method - -Signature: - -```typescript -setup(core: CoreSetup, plugins: TPluginsSetup): TSetup; -``` - -## Parameters - -| Parameter | Type | Description | -| --- | --- | --- | -| core | CoreSetup<TPluginsStart, TStart> | | -| plugins | TPluginsSetup | | - -Returns: - -`TSetup` - diff --git a/docs/development/core/public/kibana-plugin-core-public.plugin.start.md b/docs/development/core/public/kibana-plugin-core-public.plugin.start.md deleted file mode 100644 index ec5ed211a9d2b..0000000000000 --- a/docs/development/core/public/kibana-plugin-core-public.plugin.start.md +++ /dev/null @@ -1,23 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [Plugin](./kibana-plugin-core-public.plugin.md) > [start](./kibana-plugin-core-public.plugin.start.md) - -## Plugin.start() method - -Signature: - -```typescript -start(core: CoreStart, plugins: TPluginsStart): TStart; -``` - -## Parameters - -| Parameter | Type | Description | -| --- | --- | --- | -| core | CoreStart | | -| plugins | TPluginsStart | | - -Returns: - -`TStart` - diff --git a/docs/development/core/public/kibana-plugin-core-public.plugin.stop.md b/docs/development/core/public/kibana-plugin-core-public.plugin.stop.md deleted file mode 100644 index b509d1ae25340..0000000000000 --- a/docs/development/core/public/kibana-plugin-core-public.plugin.stop.md +++ /dev/null @@ -1,15 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [Plugin](./kibana-plugin-core-public.plugin.md) > [stop](./kibana-plugin-core-public.plugin.stop.md) - -## Plugin.stop() method - -Signature: - -```typescript -stop?(): void; -``` -Returns: - -`void` - diff --git a/docs/development/core/public/kibana-plugin-core-public.plugin_2.md b/docs/development/core/public/kibana-plugin-core-public.plugin_2.md new file mode 100644 index 0000000000000..da8cef9b83cc7 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-core-public.plugin_2.md @@ -0,0 +1,22 @@ + + +[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [Plugin\_2](./kibana-plugin-core-public.plugin_2.md) + +## Plugin\_2 interface + +The interface that should be returned by a `PluginInitializer`. + +Signature: + +```typescript +export interface Plugin +``` + +## Methods + +| Method | Description | +| --- | --- | +| [setup(core, plugins)](./kibana-plugin-core-public.plugin_2.setup.md) | | +| [start(core, plugins)](./kibana-plugin-core-public.plugin_2.start.md) | | +| [stop()?](./kibana-plugin-core-public.plugin_2.stop.md) | (Optional) | + diff --git a/docs/development/core/public/kibana-plugin-core-public.plugin_2.setup.md b/docs/development/core/public/kibana-plugin-core-public.plugin_2.setup.md new file mode 100644 index 0000000000000..5ebb5a1a74811 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-core-public.plugin_2.setup.md @@ -0,0 +1,23 @@ + + +[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [Plugin\_2](./kibana-plugin-core-public.plugin_2.md) > [setup](./kibana-plugin-core-public.plugin_2.setup.md) + +## Plugin\_2.setup() method + +Signature: + +```typescript +setup(core: CoreSetup, plugins: TPluginsSetup): TSetup; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| core | CoreSetup<TPluginsStart, TStart> | | +| plugins | TPluginsSetup | | + +Returns: + +TSetup + diff --git a/docs/development/core/public/kibana-plugin-core-public.plugin_2.start.md b/docs/development/core/public/kibana-plugin-core-public.plugin_2.start.md new file mode 100644 index 0000000000000..f4979ee033aac --- /dev/null +++ b/docs/development/core/public/kibana-plugin-core-public.plugin_2.start.md @@ -0,0 +1,23 @@ + + +[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [Plugin\_2](./kibana-plugin-core-public.plugin_2.md) > [start](./kibana-plugin-core-public.plugin_2.start.md) + +## Plugin\_2.start() method + +Signature: + +```typescript +start(core: CoreStart, plugins: TPluginsStart): TStart; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| core | CoreStart | | +| plugins | TPluginsStart | | + +Returns: + +TStart + diff --git a/docs/development/core/public/kibana-plugin-core-public.plugin_2.stop.md b/docs/development/core/public/kibana-plugin-core-public.plugin_2.stop.md new file mode 100644 index 0000000000000..69532f2d0e09d --- /dev/null +++ b/docs/development/core/public/kibana-plugin-core-public.plugin_2.stop.md @@ -0,0 +1,15 @@ + + +[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [Plugin\_2](./kibana-plugin-core-public.plugin_2.md) > [stop](./kibana-plugin-core-public.plugin_2.stop.md) + +## Plugin\_2.stop() method + +Signature: + +```typescript +stop?(): void; +``` +Returns: + +void + diff --git a/docs/development/core/public/kibana-plugin-core-public.plugininitializercontext.md b/docs/development/core/public/kibana-plugin-core-public.plugininitializercontext.md index 422bf5a71cddc..3304b4bf3def4 100644 --- a/docs/development/core/public/kibana-plugin-core-public.plugininitializercontext.md +++ b/docs/development/core/public/kibana-plugin-core-public.plugininitializercontext.md @@ -16,7 +16,7 @@ export interface PluginInitializerContext | Property | Type | Description | | --- | --- | --- | -| [config](./kibana-plugin-core-public.plugininitializercontext.config.md) | {
get: <T extends object = ConfigSchema>() => T;
} | | -| [env](./kibana-plugin-core-public.plugininitializercontext.env.md) | {
mode: Readonly<EnvironmentMode>;
packageInfo: Readonly<PackageInfo>;
} | | -| [opaqueId](./kibana-plugin-core-public.plugininitializercontext.opaqueid.md) | PluginOpaqueId | A symbol used to identify this plugin in the system. Needed when registering handlers or context providers. | +| [config](./kibana-plugin-core-public.plugininitializercontext.config.md) | { get: <T extends object = ConfigSchema>() => T; } | | +| [env](./kibana-plugin-core-public.plugininitializercontext.env.md) | { mode: Readonly<EnvironmentMode>; packageInfo: Readonly<PackageInfo>; } | | +| [opaqueId](./kibana-plugin-core-public.plugininitializercontext.opaqueid.md) | PluginOpaqueId | A symbol used to identify this plugin in the system. Needed when registering handlers or context providers. | diff --git a/docs/development/core/public/kibana-plugin-core-public.resolvedsimplesavedobject.md b/docs/development/core/public/kibana-plugin-core-public.resolvedsimplesavedobject.md index 4936598c58799..2844bd97db7f2 100644 --- a/docs/development/core/public/kibana-plugin-core-public.resolvedsimplesavedobject.md +++ b/docs/development/core/public/kibana-plugin-core-public.resolvedsimplesavedobject.md @@ -16,7 +16,7 @@ export interface ResolvedSimpleSavedObject | Property | Type | Description | | --- | --- | --- | -| [alias\_target\_id](./kibana-plugin-core-public.resolvedsimplesavedobject.alias_target_id.md) | SavedObjectsResolveResponse['alias_target_id'] | The ID of the object that the legacy URL alias points to. This is only defined when the outcome is 'aliasMatch' or 'conflict'. | -| [outcome](./kibana-plugin-core-public.resolvedsimplesavedobject.outcome.md) | SavedObjectsResolveResponse['outcome'] | The outcome for a successful resolve call is one of the following values:\* 'exactMatch' -- One document exactly matched the given ID. \* 'aliasMatch' -- One document with a legacy URL alias matched the given ID; in this case the saved_object.id field is different than the given ID. \* 'conflict' -- Two documents matched the given ID, one was an exact match and another with a legacy URL alias; in this case the saved_object object is the exact match, and the saved_object.id field is the same as the given ID. | -| [saved\_object](./kibana-plugin-core-public.resolvedsimplesavedobject.saved_object.md) | SimpleSavedObject<T> | The saved object that was found. | +| [alias\_target\_id?](./kibana-plugin-core-public.resolvedsimplesavedobject.alias_target_id.md) | SavedObjectsResolveResponse\['alias\_target\_id'\] | (Optional) The ID of the object that the legacy URL alias points to. This is only defined when the outcome is 'aliasMatch' or 'conflict'. | +| [outcome](./kibana-plugin-core-public.resolvedsimplesavedobject.outcome.md) | SavedObjectsResolveResponse\['outcome'\] | The outcome for a successful resolve call is one of the following values:\* 'exactMatch' -- One document exactly matched the given ID. \* 'aliasMatch' -- One document with a legacy URL alias matched the given ID; in this case the saved_object.id field is different than the given ID. \* 'conflict' -- Two documents matched the given ID, one was an exact match and another with a legacy URL alias; in this case the saved_object object is the exact match, and the saved_object.id field is the same as the given ID. | +| [saved\_object](./kibana-plugin-core-public.resolvedsimplesavedobject.saved_object.md) | SimpleSavedObject<T> | The saved object that was found. | diff --git a/docs/development/core/public/kibana-plugin-core-public.responseerrorbody.md b/docs/development/core/public/kibana-plugin-core-public.responseerrorbody.md index 8a990909fac3e..5bc9103691014 100644 --- a/docs/development/core/public/kibana-plugin-core-public.responseerrorbody.md +++ b/docs/development/core/public/kibana-plugin-core-public.responseerrorbody.md @@ -15,7 +15,7 @@ export interface ResponseErrorBody | Property | Type | Description | | --- | --- | --- | -| [attributes](./kibana-plugin-core-public.responseerrorbody.attributes.md) | Record<string, unknown> | | -| [message](./kibana-plugin-core-public.responseerrorbody.message.md) | string | | -| [statusCode](./kibana-plugin-core-public.responseerrorbody.statuscode.md) | number | | +| [attributes?](./kibana-plugin-core-public.responseerrorbody.attributes.md) | Record<string, unknown> | (Optional) | +| [message](./kibana-plugin-core-public.responseerrorbody.message.md) | string | | +| [statusCode](./kibana-plugin-core-public.responseerrorbody.statuscode.md) | number | | diff --git a/docs/development/core/public/kibana-plugin-core-public.savedobject.md b/docs/development/core/public/kibana-plugin-core-public.savedobject.md index 26f472b741268..283a960013305 100644 --- a/docs/development/core/public/kibana-plugin-core-public.savedobject.md +++ b/docs/development/core/public/kibana-plugin-core-public.savedobject.md @@ -14,15 +14,15 @@ export interface SavedObject | Property | Type | Description | | --- | --- | --- | -| [attributes](./kibana-plugin-core-public.savedobject.attributes.md) | T | The data for a Saved Object is stored as an object in the attributes property. | -| [coreMigrationVersion](./kibana-plugin-core-public.savedobject.coremigrationversion.md) | string | A semver value that is used when upgrading objects between Kibana versions. | -| [error](./kibana-plugin-core-public.savedobject.error.md) | SavedObjectError | | -| [id](./kibana-plugin-core-public.savedobject.id.md) | string | The ID of this Saved Object, guaranteed to be unique for all objects of the same type | -| [migrationVersion](./kibana-plugin-core-public.savedobject.migrationversion.md) | SavedObjectsMigrationVersion | Information about the migrations that have been applied to this SavedObject. When Kibana starts up, KibanaMigrator detects outdated documents and migrates them based on this value. For each migration that has been applied, the plugin's name is used as a key and the latest migration version as the value. | -| [namespaces](./kibana-plugin-core-public.savedobject.namespaces.md) | string[] | Space(s) that this saved object exists in. This attribute is not used for "global" saved object types which are registered with namespaceType: 'agnostic'. | -| [originId](./kibana-plugin-core-public.savedobject.originid.md) | string | The ID of the saved object this originated from. This is set if this object's id was regenerated; that can happen during migration from a legacy single-namespace type, or during import. It is only set during migration or create operations. This is used during import to ensure that ID regeneration is deterministic, so saved objects will be overwritten if they are imported multiple times into a given space. | -| [references](./kibana-plugin-core-public.savedobject.references.md) | SavedObjectReference[] | A reference to another saved object. | -| [type](./kibana-plugin-core-public.savedobject.type.md) | string | The type of Saved Object. Each plugin can define it's own custom Saved Object types. | -| [updated\_at](./kibana-plugin-core-public.savedobject.updated_at.md) | string | Timestamp of the last time this document had been updated. | -| [version](./kibana-plugin-core-public.savedobject.version.md) | string | An opaque version number which changes on each successful write operation. Can be used for implementing optimistic concurrency control. | +| [attributes](./kibana-plugin-core-public.savedobject.attributes.md) | T | The data for a Saved Object is stored as an object in the attributes property. | +| [coreMigrationVersion?](./kibana-plugin-core-public.savedobject.coremigrationversion.md) | string | (Optional) A semver value that is used when upgrading objects between Kibana versions. | +| [error?](./kibana-plugin-core-public.savedobject.error.md) | SavedObjectError | (Optional) | +| [id](./kibana-plugin-core-public.savedobject.id.md) | string | The ID of this Saved Object, guaranteed to be unique for all objects of the same type | +| [migrationVersion?](./kibana-plugin-core-public.savedobject.migrationversion.md) | SavedObjectsMigrationVersion | (Optional) Information about the migrations that have been applied to this SavedObject. When Kibana starts up, KibanaMigrator detects outdated documents and migrates them based on this value. For each migration that has been applied, the plugin's name is used as a key and the latest migration version as the value. | +| [namespaces?](./kibana-plugin-core-public.savedobject.namespaces.md) | string\[\] | (Optional) Space(s) that this saved object exists in. This attribute is not used for "global" saved object types which are registered with namespaceType: 'agnostic'. | +| [originId?](./kibana-plugin-core-public.savedobject.originid.md) | string | (Optional) The ID of the saved object this originated from. This is set if this object's id was regenerated; that can happen during migration from a legacy single-namespace type, or during import. It is only set during migration or create operations. This is used during import to ensure that ID regeneration is deterministic, so saved objects will be overwritten if they are imported multiple times into a given space. | +| [references](./kibana-plugin-core-public.savedobject.references.md) | SavedObjectReference\[\] | A reference to another saved object. | +| [type](./kibana-plugin-core-public.savedobject.type.md) | string | The type of Saved Object. Each plugin can define it's own custom Saved Object types. | +| [updated\_at?](./kibana-plugin-core-public.savedobject.updated_at.md) | string | (Optional) Timestamp of the last time this document had been updated. | +| [version?](./kibana-plugin-core-public.savedobject.version.md) | string | (Optional) An opaque version number which changes on each successful write operation. Can be used for implementing optimistic concurrency control. | diff --git a/docs/development/core/public/kibana-plugin-core-public.savedobjecterror.md b/docs/development/core/public/kibana-plugin-core-public.savedobjecterror.md index 2117cea433b5c..f6e8874b212b0 100644 --- a/docs/development/core/public/kibana-plugin-core-public.savedobjecterror.md +++ b/docs/development/core/public/kibana-plugin-core-public.savedobjecterror.md @@ -14,8 +14,8 @@ export interface SavedObjectError | Property | Type | Description | | --- | --- | --- | -| [error](./kibana-plugin-core-public.savedobjecterror.error.md) | string | | -| [message](./kibana-plugin-core-public.savedobjecterror.message.md) | string | | -| [metadata](./kibana-plugin-core-public.savedobjecterror.metadata.md) | Record<string, unknown> | | -| [statusCode](./kibana-plugin-core-public.savedobjecterror.statuscode.md) | number | | +| [error](./kibana-plugin-core-public.savedobjecterror.error.md) | string | | +| [message](./kibana-plugin-core-public.savedobjecterror.message.md) | string | | +| [metadata?](./kibana-plugin-core-public.savedobjecterror.metadata.md) | Record<string, unknown> | (Optional) | +| [statusCode](./kibana-plugin-core-public.savedobjecterror.statuscode.md) | number | | diff --git a/docs/development/core/public/kibana-plugin-core-public.savedobjectreference.md b/docs/development/core/public/kibana-plugin-core-public.savedobjectreference.md index 410ab23f0b604..e63ba254602db 100644 --- a/docs/development/core/public/kibana-plugin-core-public.savedobjectreference.md +++ b/docs/development/core/public/kibana-plugin-core-public.savedobjectreference.md @@ -16,7 +16,7 @@ export interface SavedObjectReference | Property | Type | Description | | --- | --- | --- | -| [id](./kibana-plugin-core-public.savedobjectreference.id.md) | string | | -| [name](./kibana-plugin-core-public.savedobjectreference.name.md) | string | | -| [type](./kibana-plugin-core-public.savedobjectreference.type.md) | string | | +| [id](./kibana-plugin-core-public.savedobjectreference.id.md) | string | | +| [name](./kibana-plugin-core-public.savedobjectreference.name.md) | string | | +| [type](./kibana-plugin-core-public.savedobjectreference.type.md) | string | | diff --git a/docs/development/core/public/kibana-plugin-core-public.savedobjectreferencewithcontext.md b/docs/development/core/public/kibana-plugin-core-public.savedobjectreferencewithcontext.md index a79fa96695e36..39e14607d861f 100644 --- a/docs/development/core/public/kibana-plugin-core-public.savedobjectreferencewithcontext.md +++ b/docs/development/core/public/kibana-plugin-core-public.savedobjectreferencewithcontext.md @@ -16,10 +16,10 @@ export interface SavedObjectReferenceWithContext | Property | Type | Description | | --- | --- | --- | -| [id](./kibana-plugin-core-public.savedobjectreferencewithcontext.id.md) | string | The ID of the referenced object | -| [inboundReferences](./kibana-plugin-core-public.savedobjectreferencewithcontext.inboundreferences.md) | Array<{
type: string;
id: string;
name: string;
}> | References to this object; note that this does not contain \_all inbound references everywhere for this object\_, it only contains inbound references for the scope of this operation | -| [isMissing](./kibana-plugin-core-public.savedobjectreferencewithcontext.ismissing.md) | boolean | Whether or not this object or reference is missing | -| [spaces](./kibana-plugin-core-public.savedobjectreferencewithcontext.spaces.md) | string[] | The space(s) that the referenced object exists in | -| [spacesWithMatchingAliases](./kibana-plugin-core-public.savedobjectreferencewithcontext.spaceswithmatchingaliases.md) | string[] | The space(s) that legacy URL aliases matching this type/id exist in | -| [type](./kibana-plugin-core-public.savedobjectreferencewithcontext.type.md) | string | The type of the referenced object | +| [id](./kibana-plugin-core-public.savedobjectreferencewithcontext.id.md) | string | The ID of the referenced object | +| [inboundReferences](./kibana-plugin-core-public.savedobjectreferencewithcontext.inboundreferences.md) | Array<{ type: string; id: string; name: string; }> | References to this object; note that this does not contain \_all inbound references everywhere for this object\_, it only contains inbound references for the scope of this operation | +| [isMissing?](./kibana-plugin-core-public.savedobjectreferencewithcontext.ismissing.md) | boolean | (Optional) Whether or not this object or reference is missing | +| [spaces](./kibana-plugin-core-public.savedobjectreferencewithcontext.spaces.md) | string\[\] | The space(s) that the referenced object exists in | +| [spacesWithMatchingAliases?](./kibana-plugin-core-public.savedobjectreferencewithcontext.spaceswithmatchingaliases.md) | string\[\] | (Optional) The space(s) that legacy URL aliases matching this type/id exist in | +| [type](./kibana-plugin-core-public.savedobjectreferencewithcontext.type.md) | string | The type of the referenced object | diff --git a/docs/development/core/public/kibana-plugin-core-public.savedobjectsbaseoptions.md b/docs/development/core/public/kibana-plugin-core-public.savedobjectsbaseoptions.md index 838d8fb1979a8..f86d3b5afc04e 100644 --- a/docs/development/core/public/kibana-plugin-core-public.savedobjectsbaseoptions.md +++ b/docs/development/core/public/kibana-plugin-core-public.savedobjectsbaseoptions.md @@ -15,5 +15,5 @@ export interface SavedObjectsBaseOptions | Property | Type | Description | | --- | --- | --- | -| [namespace](./kibana-plugin-core-public.savedobjectsbaseoptions.namespace.md) | string | Specify the namespace for this operation | +| [namespace?](./kibana-plugin-core-public.savedobjectsbaseoptions.namespace.md) | string | (Optional) Specify the namespace for this operation | diff --git a/docs/development/core/public/kibana-plugin-core-public.savedobjectsbatchresponse.md b/docs/development/core/public/kibana-plugin-core-public.savedobjectsbatchresponse.md index 1551836008700..3926231db17b5 100644 --- a/docs/development/core/public/kibana-plugin-core-public.savedobjectsbatchresponse.md +++ b/docs/development/core/public/kibana-plugin-core-public.savedobjectsbatchresponse.md @@ -15,5 +15,5 @@ export interface SavedObjectsBatchResponse | Property | Type | Description | | --- | --- | --- | -| [savedObjects](./kibana-plugin-core-public.savedobjectsbatchresponse.savedobjects.md) | Array<SimpleSavedObject<T>> | | +| [savedObjects](./kibana-plugin-core-public.savedobjectsbatchresponse.savedobjects.md) | Array<SimpleSavedObject<T>> | | diff --git a/docs/development/core/public/kibana-plugin-core-public.savedobjectsbulkcreateobject.md b/docs/development/core/public/kibana-plugin-core-public.savedobjectsbulkcreateobject.md index 20d137819a90e..f9ff61859b1a8 100644 --- a/docs/development/core/public/kibana-plugin-core-public.savedobjectsbulkcreateobject.md +++ b/docs/development/core/public/kibana-plugin-core-public.savedobjectsbulkcreateobject.md @@ -9,11 +9,12 @@ ```typescript export interface SavedObjectsBulkCreateObject extends SavedObjectsCreateOptions ``` +Extends: SavedObjectsCreateOptions ## Properties | Property | Type | Description | | --- | --- | --- | -| [attributes](./kibana-plugin-core-public.savedobjectsbulkcreateobject.attributes.md) | T | | -| [type](./kibana-plugin-core-public.savedobjectsbulkcreateobject.type.md) | string | | +| [attributes](./kibana-plugin-core-public.savedobjectsbulkcreateobject.attributes.md) | T | | +| [type](./kibana-plugin-core-public.savedobjectsbulkcreateobject.type.md) | string | | diff --git a/docs/development/core/public/kibana-plugin-core-public.savedobjectsbulkcreateoptions.md b/docs/development/core/public/kibana-plugin-core-public.savedobjectsbulkcreateoptions.md index 02e659bd858f7..ada12c064e0a1 100644 --- a/docs/development/core/public/kibana-plugin-core-public.savedobjectsbulkcreateoptions.md +++ b/docs/development/core/public/kibana-plugin-core-public.savedobjectsbulkcreateoptions.md @@ -15,5 +15,5 @@ export interface SavedObjectsBulkCreateOptions | Property | Type | Description | | --- | --- | --- | -| [overwrite](./kibana-plugin-core-public.savedobjectsbulkcreateoptions.overwrite.md) | boolean | If a document with the given id already exists, overwrite it's contents (default=false). | +| [overwrite?](./kibana-plugin-core-public.savedobjectsbulkcreateoptions.overwrite.md) | boolean | (Optional) If a document with the given id already exists, overwrite it's contents (default=false). | diff --git a/docs/development/core/public/kibana-plugin-core-public.savedobjectsbulkresolveobject.md b/docs/development/core/public/kibana-plugin-core-public.savedobjectsbulkresolveobject.md index 8ca5da9d7db4f..fbff3d3bd8f25 100644 --- a/docs/development/core/public/kibana-plugin-core-public.savedobjectsbulkresolveobject.md +++ b/docs/development/core/public/kibana-plugin-core-public.savedobjectsbulkresolveobject.md @@ -15,6 +15,6 @@ export interface SavedObjectsBulkResolveObject | Property | Type | Description | | --- | --- | --- | -| [id](./kibana-plugin-core-public.savedobjectsbulkresolveobject.id.md) | string | | -| [type](./kibana-plugin-core-public.savedobjectsbulkresolveobject.type.md) | string | | +| [id](./kibana-plugin-core-public.savedobjectsbulkresolveobject.id.md) | string | | +| [type](./kibana-plugin-core-public.savedobjectsbulkresolveobject.type.md) | string | | diff --git a/docs/development/core/public/kibana-plugin-core-public.savedobjectsbulkresolveresponse.md b/docs/development/core/public/kibana-plugin-core-public.savedobjectsbulkresolveresponse.md index 36a92d02b8aaa..e34bf6fe32618 100644 --- a/docs/development/core/public/kibana-plugin-core-public.savedobjectsbulkresolveresponse.md +++ b/docs/development/core/public/kibana-plugin-core-public.savedobjectsbulkresolveresponse.md @@ -15,5 +15,5 @@ export interface SavedObjectsBulkResolveResponse | Property | Type | Description | | --- | --- | --- | -| [resolved\_objects](./kibana-plugin-core-public.savedobjectsbulkresolveresponse.resolved_objects.md) | Array<SavedObjectsResolveResponse<T>> | | +| [resolved\_objects](./kibana-plugin-core-public.savedobjectsbulkresolveresponse.resolved_objects.md) | Array<SavedObjectsResolveResponse<T>> | | diff --git a/docs/development/core/public/kibana-plugin-core-public.savedobjectsbulkupdateobject.md b/docs/development/core/public/kibana-plugin-core-public.savedobjectsbulkupdateobject.md index fd6572f2c0cbe..f28d99cb110c6 100644 --- a/docs/development/core/public/kibana-plugin-core-public.savedobjectsbulkupdateobject.md +++ b/docs/development/core/public/kibana-plugin-core-public.savedobjectsbulkupdateobject.md @@ -15,9 +15,9 @@ export interface SavedObjectsBulkUpdateObject | Property | Type | Description | | --- | --- | --- | -| [attributes](./kibana-plugin-core-public.savedobjectsbulkupdateobject.attributes.md) | T | | -| [id](./kibana-plugin-core-public.savedobjectsbulkupdateobject.id.md) | string | | -| [references](./kibana-plugin-core-public.savedobjectsbulkupdateobject.references.md) | SavedObjectReference[] | | -| [type](./kibana-plugin-core-public.savedobjectsbulkupdateobject.type.md) | string | | -| [version](./kibana-plugin-core-public.savedobjectsbulkupdateobject.version.md) | string | | +| [attributes](./kibana-plugin-core-public.savedobjectsbulkupdateobject.attributes.md) | T | | +| [id](./kibana-plugin-core-public.savedobjectsbulkupdateobject.id.md) | string | | +| [references?](./kibana-plugin-core-public.savedobjectsbulkupdateobject.references.md) | SavedObjectReference\[\] | (Optional) | +| [type](./kibana-plugin-core-public.savedobjectsbulkupdateobject.type.md) | string | | +| [version?](./kibana-plugin-core-public.savedobjectsbulkupdateobject.version.md) | string | (Optional) | diff --git a/docs/development/core/public/kibana-plugin-core-public.savedobjectsbulkupdateoptions.md b/docs/development/core/public/kibana-plugin-core-public.savedobjectsbulkupdateoptions.md index 35cc72baa0ef6..a2cdd3eb801e6 100644 --- a/docs/development/core/public/kibana-plugin-core-public.savedobjectsbulkupdateoptions.md +++ b/docs/development/core/public/kibana-plugin-core-public.savedobjectsbulkupdateoptions.md @@ -15,5 +15,5 @@ export interface SavedObjectsBulkUpdateOptions | Property | Type | Description | | --- | --- | --- | -| [namespace](./kibana-plugin-core-public.savedobjectsbulkupdateoptions.namespace.md) | string | | +| [namespace?](./kibana-plugin-core-public.savedobjectsbulkupdateoptions.namespace.md) | string | (Optional) | diff --git a/docs/development/core/public/kibana-plugin-core-public.savedobjectsclient.bulkupdate.md b/docs/development/core/public/kibana-plugin-core-public.savedobjectsclient.bulkupdate.md index 05c84d9c27192..0e3bfb2bd896b 100644 --- a/docs/development/core/public/kibana-plugin-core-public.savedobjectsclient.bulkupdate.md +++ b/docs/development/core/public/kibana-plugin-core-public.savedobjectsclient.bulkupdate.md @@ -16,11 +16,11 @@ bulkUpdate(objects?: SavedObjectsBulkUpdateObject[]): PromiseSavedObjectsBulkUpdateObject[] | | +| objects | SavedObjectsBulkUpdateObject\[\] | \[{ type, id, attributes, options: { version, references } }\] | Returns: -`Promise>` +Promise<SavedObjectsBatchResponse<unknown>> The result of the update operation containing both failed and updated saved objects. diff --git a/docs/development/core/public/kibana-plugin-core-public.savedobjectsclient.md b/docs/development/core/public/kibana-plugin-core-public.savedobjectsclient.md index 1a630ebe8c9ae..d18d680feffd5 100644 --- a/docs/development/core/public/kibana-plugin-core-public.savedobjectsclient.md +++ b/docs/development/core/public/kibana-plugin-core-public.savedobjectsclient.md @@ -20,14 +20,14 @@ The constructor for this class is marked as internal. Third-party code should no | Property | Modifiers | Type | Description | | --- | --- | --- | --- | -| [bulkCreate](./kibana-plugin-core-public.savedobjectsclient.bulkcreate.md) | | (objects?: SavedObjectsBulkCreateObject[], options?: SavedObjectsBulkCreateOptions) => Promise<SavedObjectsBatchResponse<unknown>> | Creates multiple documents at once | -| [bulkGet](./kibana-plugin-core-public.savedobjectsclient.bulkget.md) | | (objects?: Array<{
id: string;
type: string;
}>) => Promise<SavedObjectsBatchResponse<unknown>> | Returns an array of objects by id | -| [bulkResolve](./kibana-plugin-core-public.savedobjectsclient.bulkresolve.md) | | <T = unknown>(objects?: Array<{
id: string;
type: string;
}>) => Promise<{
resolved_objects: ResolvedSimpleSavedObject<T>[];
}> | Resolves an array of objects by id, using any legacy URL aliases if they exist | -| [create](./kibana-plugin-core-public.savedobjectsclient.create.md) | | <T = unknown>(type: string, attributes: T, options?: SavedObjectsCreateOptions) => Promise<SimpleSavedObject<T>> | Persists an object | -| [delete](./kibana-plugin-core-public.savedobjectsclient.delete.md) | | (type: string, id: string, options?: SavedObjectsDeleteOptions | undefined) => ReturnType<SavedObjectsApi['delete']> | Deletes an object | -| [find](./kibana-plugin-core-public.savedobjectsclient.find.md) | | <T = unknown, A = unknown>(options: SavedObjectsFindOptions) => Promise<SavedObjectsFindResponsePublic<T, unknown>> | Search for objects | -| [get](./kibana-plugin-core-public.savedobjectsclient.get.md) | | <T = unknown>(type: string, id: string) => Promise<SimpleSavedObject<T>> | Fetches a single object | -| [resolve](./kibana-plugin-core-public.savedobjectsclient.resolve.md) | | <T = unknown>(type: string, id: string) => Promise<ResolvedSimpleSavedObject<T>> | Resolves a single object | +| [bulkCreate](./kibana-plugin-core-public.savedobjectsclient.bulkcreate.md) | | (objects?: SavedObjectsBulkCreateObject\[\], options?: SavedObjectsBulkCreateOptions) => Promise<SavedObjectsBatchResponse<unknown>> | Creates multiple documents at once | +| [bulkGet](./kibana-plugin-core-public.savedobjectsclient.bulkget.md) | | (objects?: Array<{ id: string; type: string; }>) => Promise<SavedObjectsBatchResponse<unknown>> | Returns an array of objects by id | +| [bulkResolve](./kibana-plugin-core-public.savedobjectsclient.bulkresolve.md) | | <T = unknown>(objects?: Array<{ id: string; type: string; }>) => Promise<{ resolved\_objects: ResolvedSimpleSavedObject<T>\[\]; }> | Resolves an array of objects by id, using any legacy URL aliases if they exist | +| [create](./kibana-plugin-core-public.savedobjectsclient.create.md) | | <T = unknown>(type: string, attributes: T, options?: SavedObjectsCreateOptions) => Promise<SimpleSavedObject<T>> | Persists an object | +| [delete](./kibana-plugin-core-public.savedobjectsclient.delete.md) | | (type: string, id: string, options?: SavedObjectsDeleteOptions \| undefined) => ReturnType<SavedObjectsApi\['delete'\]> | Deletes an object | +| [find](./kibana-plugin-core-public.savedobjectsclient.find.md) | | <T = unknown, A = unknown>(options: SavedObjectsFindOptions) => Promise<SavedObjectsFindResponsePublic<T, unknown>> | Search for objects | +| [get](./kibana-plugin-core-public.savedobjectsclient.get.md) | | <T = unknown>(type: string, id: string) => Promise<SimpleSavedObject<T>> | Fetches a single object | +| [resolve](./kibana-plugin-core-public.savedobjectsclient.resolve.md) | | <T = unknown>(type: string, id: string) => Promise<ResolvedSimpleSavedObject<T>> | Resolves a single object | ## Methods diff --git a/docs/development/core/public/kibana-plugin-core-public.savedobjectsclient.update.md b/docs/development/core/public/kibana-plugin-core-public.savedobjectsclient.update.md index a5847d6a26198..dec84fb58bf5e 100644 --- a/docs/development/core/public/kibana-plugin-core-public.savedobjectsclient.update.md +++ b/docs/development/core/public/kibana-plugin-core-public.savedobjectsclient.update.md @@ -16,13 +16,13 @@ update(type: string, id: string, attributes: T, { version, referenc | Parameter | Type | Description | | --- | --- | --- | -| type | string | | -| id | string | | -| attributes | T | | -| { version, references, upsert } | SavedObjectsUpdateOptions | | +| type | string | | +| id | string | | +| attributes | T | | +| { version, references, upsert } | SavedObjectsUpdateOptions | | Returns: -`Promise>` +Promise<SimpleSavedObject<T>> diff --git a/docs/development/core/public/kibana-plugin-core-public.savedobjectscollectmultinamespacereferencesresponse.md b/docs/development/core/public/kibana-plugin-core-public.savedobjectscollectmultinamespacereferencesresponse.md index a6e0a274008a6..a356850fa1ad4 100644 --- a/docs/development/core/public/kibana-plugin-core-public.savedobjectscollectmultinamespacereferencesresponse.md +++ b/docs/development/core/public/kibana-plugin-core-public.savedobjectscollectmultinamespacereferencesresponse.md @@ -16,5 +16,5 @@ export interface SavedObjectsCollectMultiNamespaceReferencesResponse | Property | Type | Description | | --- | --- | --- | -| [objects](./kibana-plugin-core-public.savedobjectscollectmultinamespacereferencesresponse.objects.md) | SavedObjectReferenceWithContext[] | | +| [objects](./kibana-plugin-core-public.savedobjectscollectmultinamespacereferencesresponse.objects.md) | SavedObjectReferenceWithContext\[\] | | diff --git a/docs/development/core/public/kibana-plugin-core-public.savedobjectscreateoptions.md b/docs/development/core/public/kibana-plugin-core-public.savedobjectscreateoptions.md index a039b9f5b4fe4..835a9e87a1dba 100644 --- a/docs/development/core/public/kibana-plugin-core-public.savedobjectscreateoptions.md +++ b/docs/development/core/public/kibana-plugin-core-public.savedobjectscreateoptions.md @@ -15,9 +15,9 @@ export interface SavedObjectsCreateOptions | Property | Type | Description | | --- | --- | --- | -| [coreMigrationVersion](./kibana-plugin-core-public.savedobjectscreateoptions.coremigrationversion.md) | string | A semver value that is used when upgrading objects between Kibana versions. | -| [id](./kibana-plugin-core-public.savedobjectscreateoptions.id.md) | string | (Not recommended) Specify an id instead of having the saved objects service generate one for you. | -| [migrationVersion](./kibana-plugin-core-public.savedobjectscreateoptions.migrationversion.md) | SavedObjectsMigrationVersion | Information about the migrations that have been applied to this SavedObject. When Kibana starts up, KibanaMigrator detects outdated documents and migrates them based on this value. For each migration that has been applied, the plugin's name is used as a key and the latest migration version as the value. | -| [overwrite](./kibana-plugin-core-public.savedobjectscreateoptions.overwrite.md) | boolean | If a document with the given id already exists, overwrite it's contents (default=false). | -| [references](./kibana-plugin-core-public.savedobjectscreateoptions.references.md) | SavedObjectReference[] | | +| [coreMigrationVersion?](./kibana-plugin-core-public.savedobjectscreateoptions.coremigrationversion.md) | string | (Optional) A semver value that is used when upgrading objects between Kibana versions. | +| [id?](./kibana-plugin-core-public.savedobjectscreateoptions.id.md) | string | (Optional) (Not recommended) Specify an id instead of having the saved objects service generate one for you. | +| [migrationVersion?](./kibana-plugin-core-public.savedobjectscreateoptions.migrationversion.md) | SavedObjectsMigrationVersion | (Optional) Information about the migrations that have been applied to this SavedObject. When Kibana starts up, KibanaMigrator detects outdated documents and migrates them based on this value. For each migration that has been applied, the plugin's name is used as a key and the latest migration version as the value. | +| [overwrite?](./kibana-plugin-core-public.savedobjectscreateoptions.overwrite.md) | boolean | (Optional) If a document with the given id already exists, overwrite it's contents (default=false). | +| [references?](./kibana-plugin-core-public.savedobjectscreateoptions.references.md) | SavedObjectReference\[\] | (Optional) | diff --git a/docs/development/core/public/kibana-plugin-core-public.savedobjectsfindoptions.md b/docs/development/core/public/kibana-plugin-core-public.savedobjectsfindoptions.md index 706408f81f02a..f429911476307 100644 --- a/docs/development/core/public/kibana-plugin-core-public.savedobjectsfindoptions.md +++ b/docs/development/core/public/kibana-plugin-core-public.savedobjectsfindoptions.md @@ -15,22 +15,22 @@ export interface SavedObjectsFindOptions | Property | Type | Description | | --- | --- | --- | -| [defaultSearchOperator](./kibana-plugin-core-public.savedobjectsfindoptions.defaultsearchoperator.md) | 'AND' | 'OR' | The search operator to use with the provided filter. Defaults to OR | -| [fields](./kibana-plugin-core-public.savedobjectsfindoptions.fields.md) | string[] | An array of fields to include in the results | -| [filter](./kibana-plugin-core-public.savedobjectsfindoptions.filter.md) | string | KueryNode | | -| [hasReference](./kibana-plugin-core-public.savedobjectsfindoptions.hasreference.md) | SavedObjectsFindOptionsReference | SavedObjectsFindOptionsReference[] | Search for documents having a reference to the specified objects. Use hasReferenceOperator to specify the operator to use when searching for multiple references. | -| [hasReferenceOperator](./kibana-plugin-core-public.savedobjectsfindoptions.hasreferenceoperator.md) | 'AND' | 'OR' | The operator to use when searching by multiple references using the hasReference option. Defaults to OR | -| [namespaces](./kibana-plugin-core-public.savedobjectsfindoptions.namespaces.md) | string[] | | -| [page](./kibana-plugin-core-public.savedobjectsfindoptions.page.md) | number | | -| [perPage](./kibana-plugin-core-public.savedobjectsfindoptions.perpage.md) | number | | -| [pit](./kibana-plugin-core-public.savedobjectsfindoptions.pit.md) | SavedObjectsPitParams | Search against a specific Point In Time (PIT) that you've opened with . | -| [preference](./kibana-plugin-core-public.savedobjectsfindoptions.preference.md) | string | An optional ES preference value to be used for the query \* | -| [rootSearchFields](./kibana-plugin-core-public.savedobjectsfindoptions.rootsearchfields.md) | string[] | The fields to perform the parsed query against. Unlike the searchFields argument, these are expected to be root fields and will not be modified. If used in conjunction with searchFields, both are concatenated together. | -| [search](./kibana-plugin-core-public.savedobjectsfindoptions.search.md) | string | Search documents using the Elasticsearch Simple Query String syntax. See Elasticsearch Simple Query String query argument for more information | -| [searchAfter](./kibana-plugin-core-public.savedobjectsfindoptions.searchafter.md) | estypes.Id[] | Use the sort values from the previous page to retrieve the next page of results. | -| [searchFields](./kibana-plugin-core-public.savedobjectsfindoptions.searchfields.md) | string[] | The fields to perform the parsed query against. See Elasticsearch Simple Query String fields argument for more information | -| [sortField](./kibana-plugin-core-public.savedobjectsfindoptions.sortfield.md) | string | | -| [sortOrder](./kibana-plugin-core-public.savedobjectsfindoptions.sortorder.md) | estypes.SearchSortOrder | | -| [type](./kibana-plugin-core-public.savedobjectsfindoptions.type.md) | string | string[] | | -| [typeToNamespacesMap](./kibana-plugin-core-public.savedobjectsfindoptions.typetonamespacesmap.md) | Map<string, string[] | undefined> | This map defines each type to search for, and the namespace(s) to search for the type in; this is only intended to be used by a saved object client wrapper. If this is defined, it supersedes the type and namespaces fields when building the Elasticsearch query. Any types that are not included in this map will be excluded entirely. If a type is included but its value is undefined, the operation will search for that type in the Default namespace. | +| [defaultSearchOperator?](./kibana-plugin-core-public.savedobjectsfindoptions.defaultsearchoperator.md) | 'AND' \| 'OR' | (Optional) The search operator to use with the provided filter. Defaults to OR | +| [fields?](./kibana-plugin-core-public.savedobjectsfindoptions.fields.md) | string\[\] | (Optional) An array of fields to include in the results | +| [filter?](./kibana-plugin-core-public.savedobjectsfindoptions.filter.md) | string \| KueryNode | (Optional) | +| [hasReference?](./kibana-plugin-core-public.savedobjectsfindoptions.hasreference.md) | SavedObjectsFindOptionsReference \| SavedObjectsFindOptionsReference\[\] | (Optional) Search for documents having a reference to the specified objects. Use hasReferenceOperator to specify the operator to use when searching for multiple references. | +| [hasReferenceOperator?](./kibana-plugin-core-public.savedobjectsfindoptions.hasreferenceoperator.md) | 'AND' \| 'OR' | (Optional) The operator to use when searching by multiple references using the hasReference option. Defaults to OR | +| [namespaces?](./kibana-plugin-core-public.savedobjectsfindoptions.namespaces.md) | string\[\] | (Optional) | +| [page?](./kibana-plugin-core-public.savedobjectsfindoptions.page.md) | number | (Optional) | +| [perPage?](./kibana-plugin-core-public.savedobjectsfindoptions.perpage.md) | number | (Optional) | +| [pit?](./kibana-plugin-core-public.savedobjectsfindoptions.pit.md) | SavedObjectsPitParams | (Optional) Search against a specific Point In Time (PIT) that you've opened with . | +| [preference?](./kibana-plugin-core-public.savedobjectsfindoptions.preference.md) | string | (Optional) An optional ES preference value to be used for the query \* | +| [rootSearchFields?](./kibana-plugin-core-public.savedobjectsfindoptions.rootsearchfields.md) | string\[\] | (Optional) The fields to perform the parsed query against. Unlike the searchFields argument, these are expected to be root fields and will not be modified. If used in conjunction with searchFields, both are concatenated together. | +| [search?](./kibana-plugin-core-public.savedobjectsfindoptions.search.md) | string | (Optional) Search documents using the Elasticsearch Simple Query String syntax. See Elasticsearch Simple Query String query argument for more information | +| [searchAfter?](./kibana-plugin-core-public.savedobjectsfindoptions.searchafter.md) | estypes.Id\[\] | (Optional) Use the sort values from the previous page to retrieve the next page of results. | +| [searchFields?](./kibana-plugin-core-public.savedobjectsfindoptions.searchfields.md) | string\[\] | (Optional) The fields to perform the parsed query against. See Elasticsearch Simple Query String fields argument for more information | +| [sortField?](./kibana-plugin-core-public.savedobjectsfindoptions.sortfield.md) | string | (Optional) | +| [sortOrder?](./kibana-plugin-core-public.savedobjectsfindoptions.sortorder.md) | estypes.SearchSortOrder | (Optional) | +| [type](./kibana-plugin-core-public.savedobjectsfindoptions.type.md) | string \| string\[\] | | +| [typeToNamespacesMap?](./kibana-plugin-core-public.savedobjectsfindoptions.typetonamespacesmap.md) | Map<string, string\[\] \| undefined> | (Optional) This map defines each type to search for, and the namespace(s) to search for the type in; this is only intended to be used by a saved object client wrapper. If this is defined, it supersedes the type and namespaces fields when building the Elasticsearch query. Any types that are not included in this map will be excluded entirely. If a type is included but its value is undefined, the operation will search for that type in the Default namespace. | diff --git a/docs/development/core/public/kibana-plugin-core-public.savedobjectsfindoptionsreference.md b/docs/development/core/public/kibana-plugin-core-public.savedobjectsfindoptionsreference.md index cdfefd01e6f83..cab03bf71393c 100644 --- a/docs/development/core/public/kibana-plugin-core-public.savedobjectsfindoptionsreference.md +++ b/docs/development/core/public/kibana-plugin-core-public.savedobjectsfindoptionsreference.md @@ -15,6 +15,6 @@ export interface SavedObjectsFindOptionsReference | Property | Type | Description | | --- | --- | --- | -| [id](./kibana-plugin-core-public.savedobjectsfindoptionsreference.id.md) | string | | -| [type](./kibana-plugin-core-public.savedobjectsfindoptionsreference.type.md) | string | | +| [id](./kibana-plugin-core-public.savedobjectsfindoptionsreference.id.md) | string | | +| [type](./kibana-plugin-core-public.savedobjectsfindoptionsreference.type.md) | string | | diff --git a/docs/development/core/public/kibana-plugin-core-public.savedobjectsfindresponsepublic.md b/docs/development/core/public/kibana-plugin-core-public.savedobjectsfindresponsepublic.md index 6f2276194f054..dd26960a95766 100644 --- a/docs/development/core/public/kibana-plugin-core-public.savedobjectsfindresponsepublic.md +++ b/docs/development/core/public/kibana-plugin-core-public.savedobjectsfindresponsepublic.md @@ -13,13 +13,14 @@ Return type of the Saved Objects `find()` method. ```typescript export interface SavedObjectsFindResponsePublic extends SavedObjectsBatchResponse ``` +Extends: SavedObjectsBatchResponse<T> ## Properties | Property | Type | Description | | --- | --- | --- | -| [aggregations](./kibana-plugin-core-public.savedobjectsfindresponsepublic.aggregations.md) | A | | -| [page](./kibana-plugin-core-public.savedobjectsfindresponsepublic.page.md) | number | | -| [perPage](./kibana-plugin-core-public.savedobjectsfindresponsepublic.perpage.md) | number | | -| [total](./kibana-plugin-core-public.savedobjectsfindresponsepublic.total.md) | number | | +| [aggregations?](./kibana-plugin-core-public.savedobjectsfindresponsepublic.aggregations.md) | A | (Optional) | +| [page](./kibana-plugin-core-public.savedobjectsfindresponsepublic.page.md) | number | | +| [perPage](./kibana-plugin-core-public.savedobjectsfindresponsepublic.perpage.md) | number | | +| [total](./kibana-plugin-core-public.savedobjectsfindresponsepublic.total.md) | number | | diff --git a/docs/development/core/public/kibana-plugin-core-public.savedobjectsimportactionrequiredwarning.md b/docs/development/core/public/kibana-plugin-core-public.savedobjectsimportactionrequiredwarning.md index 2ecce7233aa57..fe148fdc2ff1a 100644 --- a/docs/development/core/public/kibana-plugin-core-public.savedobjectsimportactionrequiredwarning.md +++ b/docs/development/core/public/kibana-plugin-core-public.savedobjectsimportactionrequiredwarning.md @@ -18,8 +18,8 @@ export interface SavedObjectsImportActionRequiredWarning | Property | Type | Description | | --- | --- | --- | -| [actionPath](./kibana-plugin-core-public.savedobjectsimportactionrequiredwarning.actionpath.md) | string | The path (without the basePath) that the user should be redirect to address this warning. | -| [buttonLabel](./kibana-plugin-core-public.savedobjectsimportactionrequiredwarning.buttonlabel.md) | string | An optional label to use for the link button. If unspecified, a default label will be used. | -| [message](./kibana-plugin-core-public.savedobjectsimportactionrequiredwarning.message.md) | string | The translated message to display to the user. | -| [type](./kibana-plugin-core-public.savedobjectsimportactionrequiredwarning.type.md) | 'action_required' | | +| [actionPath](./kibana-plugin-core-public.savedobjectsimportactionrequiredwarning.actionpath.md) | string | The path (without the basePath) that the user should be redirect to address this warning. | +| [buttonLabel?](./kibana-plugin-core-public.savedobjectsimportactionrequiredwarning.buttonlabel.md) | string | (Optional) An optional label to use for the link button. If unspecified, a default label will be used. | +| [message](./kibana-plugin-core-public.savedobjectsimportactionrequiredwarning.message.md) | string | The translated message to display to the user. | +| [type](./kibana-plugin-core-public.savedobjectsimportactionrequiredwarning.type.md) | 'action\_required' | | diff --git a/docs/development/core/public/kibana-plugin-core-public.savedobjectsimportambiguousconflicterror.md b/docs/development/core/public/kibana-plugin-core-public.savedobjectsimportambiguousconflicterror.md index 76dfacf132f0a..2d136bb870de7 100644 --- a/docs/development/core/public/kibana-plugin-core-public.savedobjectsimportambiguousconflicterror.md +++ b/docs/development/core/public/kibana-plugin-core-public.savedobjectsimportambiguousconflicterror.md @@ -16,6 +16,6 @@ export interface SavedObjectsImportAmbiguousConflictError | Property | Type | Description | | --- | --- | --- | -| [destinations](./kibana-plugin-core-public.savedobjectsimportambiguousconflicterror.destinations.md) | Array<{
id: string;
title?: string;
updatedAt?: string;
}> | | -| [type](./kibana-plugin-core-public.savedobjectsimportambiguousconflicterror.type.md) | 'ambiguous_conflict' | | +| [destinations](./kibana-plugin-core-public.savedobjectsimportambiguousconflicterror.destinations.md) | Array<{ id: string; title?: string; updatedAt?: string; }> | | +| [type](./kibana-plugin-core-public.savedobjectsimportambiguousconflicterror.type.md) | 'ambiguous\_conflict' | | diff --git a/docs/development/core/public/kibana-plugin-core-public.savedobjectsimportconflicterror.md b/docs/development/core/public/kibana-plugin-core-public.savedobjectsimportconflicterror.md index b0320b05ecadc..57737986ba4ca 100644 --- a/docs/development/core/public/kibana-plugin-core-public.savedobjectsimportconflicterror.md +++ b/docs/development/core/public/kibana-plugin-core-public.savedobjectsimportconflicterror.md @@ -16,6 +16,6 @@ export interface SavedObjectsImportConflictError | Property | Type | Description | | --- | --- | --- | -| [destinationId](./kibana-plugin-core-public.savedobjectsimportconflicterror.destinationid.md) | string | | -| [type](./kibana-plugin-core-public.savedobjectsimportconflicterror.type.md) | 'conflict' | | +| [destinationId?](./kibana-plugin-core-public.savedobjectsimportconflicterror.destinationid.md) | string | (Optional) | +| [type](./kibana-plugin-core-public.savedobjectsimportconflicterror.type.md) | 'conflict' | | diff --git a/docs/development/core/public/kibana-plugin-core-public.savedobjectsimportfailure.md b/docs/development/core/public/kibana-plugin-core-public.savedobjectsimportfailure.md index f9219c9037e0a..be1a20b3c71a4 100644 --- a/docs/development/core/public/kibana-plugin-core-public.savedobjectsimportfailure.md +++ b/docs/development/core/public/kibana-plugin-core-public.savedobjectsimportfailure.md @@ -16,10 +16,10 @@ export interface SavedObjectsImportFailure | Property | Type | Description | | --- | --- | --- | -| [error](./kibana-plugin-core-public.savedobjectsimportfailure.error.md) | SavedObjectsImportConflictError | SavedObjectsImportAmbiguousConflictError | SavedObjectsImportUnsupportedTypeError | SavedObjectsImportMissingReferencesError | SavedObjectsImportUnknownError | | -| [id](./kibana-plugin-core-public.savedobjectsimportfailure.id.md) | string | | -| [meta](./kibana-plugin-core-public.savedobjectsimportfailure.meta.md) | {
title?: string;
icon?: string;
} | | -| [overwrite](./kibana-plugin-core-public.savedobjectsimportfailure.overwrite.md) | boolean | If overwrite is specified, an attempt was made to overwrite an existing object. | -| [title](./kibana-plugin-core-public.savedobjectsimportfailure.title.md) | string | | -| [type](./kibana-plugin-core-public.savedobjectsimportfailure.type.md) | string | | +| [error](./kibana-plugin-core-public.savedobjectsimportfailure.error.md) | SavedObjectsImportConflictError \| SavedObjectsImportAmbiguousConflictError \| SavedObjectsImportUnsupportedTypeError \| SavedObjectsImportMissingReferencesError \| SavedObjectsImportUnknownError | | +| [id](./kibana-plugin-core-public.savedobjectsimportfailure.id.md) | string | | +| [meta](./kibana-plugin-core-public.savedobjectsimportfailure.meta.md) | { title?: string; icon?: string; } | | +| [overwrite?](./kibana-plugin-core-public.savedobjectsimportfailure.overwrite.md) | boolean | (Optional) If overwrite is specified, an attempt was made to overwrite an existing object. | +| [title?](./kibana-plugin-core-public.savedobjectsimportfailure.title.md) | string | (Optional) | +| [type](./kibana-plugin-core-public.savedobjectsimportfailure.type.md) | string | | diff --git a/docs/development/core/public/kibana-plugin-core-public.savedobjectsimportmissingreferenceserror.md b/docs/development/core/public/kibana-plugin-core-public.savedobjectsimportmissingreferenceserror.md index 1fea85ea239d5..6c03ab263084c 100644 --- a/docs/development/core/public/kibana-plugin-core-public.savedobjectsimportmissingreferenceserror.md +++ b/docs/development/core/public/kibana-plugin-core-public.savedobjectsimportmissingreferenceserror.md @@ -16,6 +16,6 @@ export interface SavedObjectsImportMissingReferencesError | Property | Type | Description | | --- | --- | --- | -| [references](./kibana-plugin-core-public.savedobjectsimportmissingreferenceserror.references.md) | Array<{
type: string;
id: string;
}> | | -| [type](./kibana-plugin-core-public.savedobjectsimportmissingreferenceserror.type.md) | 'missing_references' | | +| [references](./kibana-plugin-core-public.savedobjectsimportmissingreferenceserror.references.md) | Array<{ type: string; id: string; }> | | +| [type](./kibana-plugin-core-public.savedobjectsimportmissingreferenceserror.type.md) | 'missing\_references' | | diff --git a/docs/development/core/public/kibana-plugin-core-public.savedobjectsimportresponse.md b/docs/development/core/public/kibana-plugin-core-public.savedobjectsimportresponse.md index 3be800498a9b7..5b6139723a101 100644 --- a/docs/development/core/public/kibana-plugin-core-public.savedobjectsimportresponse.md +++ b/docs/development/core/public/kibana-plugin-core-public.savedobjectsimportresponse.md @@ -16,9 +16,9 @@ export interface SavedObjectsImportResponse | Property | Type | Description | | --- | --- | --- | -| [errors](./kibana-plugin-core-public.savedobjectsimportresponse.errors.md) | SavedObjectsImportFailure[] | | -| [success](./kibana-plugin-core-public.savedobjectsimportresponse.success.md) | boolean | | -| [successCount](./kibana-plugin-core-public.savedobjectsimportresponse.successcount.md) | number | | -| [successResults](./kibana-plugin-core-public.savedobjectsimportresponse.successresults.md) | SavedObjectsImportSuccess[] | | -| [warnings](./kibana-plugin-core-public.savedobjectsimportresponse.warnings.md) | SavedObjectsImportWarning[] | | +| [errors?](./kibana-plugin-core-public.savedobjectsimportresponse.errors.md) | SavedObjectsImportFailure\[\] | (Optional) | +| [success](./kibana-plugin-core-public.savedobjectsimportresponse.success.md) | boolean | | +| [successCount](./kibana-plugin-core-public.savedobjectsimportresponse.successcount.md) | number | | +| [successResults?](./kibana-plugin-core-public.savedobjectsimportresponse.successresults.md) | SavedObjectsImportSuccess\[\] | (Optional) | +| [warnings](./kibana-plugin-core-public.savedobjectsimportresponse.warnings.md) | SavedObjectsImportWarning\[\] | | diff --git a/docs/development/core/public/kibana-plugin-core-public.savedobjectsimportretry.md b/docs/development/core/public/kibana-plugin-core-public.savedobjectsimportretry.md index b0bda93ef8b72..80a3145ae7e55 100644 --- a/docs/development/core/public/kibana-plugin-core-public.savedobjectsimportretry.md +++ b/docs/development/core/public/kibana-plugin-core-public.savedobjectsimportretry.md @@ -16,11 +16,11 @@ export interface SavedObjectsImportRetry | Property | Type | Description | | --- | --- | --- | -| [createNewCopy](./kibana-plugin-core-public.savedobjectsimportretry.createnewcopy.md) | boolean | If createNewCopy is specified, the new object has a new (undefined) origin ID. This is only needed for the case where createNewCopies mode is disabled and ambiguous source conflicts are detected. | -| [destinationId](./kibana-plugin-core-public.savedobjectsimportretry.destinationid.md) | string | The object ID that will be created or overwritten. If not specified, the id field will be used. | -| [id](./kibana-plugin-core-public.savedobjectsimportretry.id.md) | string | | -| [ignoreMissingReferences](./kibana-plugin-core-public.savedobjectsimportretry.ignoremissingreferences.md) | boolean | If ignoreMissingReferences is specified, reference validation will be skipped for this object. | -| [overwrite](./kibana-plugin-core-public.savedobjectsimportretry.overwrite.md) | boolean | | -| [replaceReferences](./kibana-plugin-core-public.savedobjectsimportretry.replacereferences.md) | Array<{
type: string;
from: string;
to: string;
}> | | -| [type](./kibana-plugin-core-public.savedobjectsimportretry.type.md) | string | | +| [createNewCopy?](./kibana-plugin-core-public.savedobjectsimportretry.createnewcopy.md) | boolean | (Optional) If createNewCopy is specified, the new object has a new (undefined) origin ID. This is only needed for the case where createNewCopies mode is disabled and ambiguous source conflicts are detected. | +| [destinationId?](./kibana-plugin-core-public.savedobjectsimportretry.destinationid.md) | string | (Optional) The object ID that will be created or overwritten. If not specified, the id field will be used. | +| [id](./kibana-plugin-core-public.savedobjectsimportretry.id.md) | string | | +| [ignoreMissingReferences?](./kibana-plugin-core-public.savedobjectsimportretry.ignoremissingreferences.md) | boolean | (Optional) If ignoreMissingReferences is specified, reference validation will be skipped for this object. | +| [overwrite](./kibana-plugin-core-public.savedobjectsimportretry.overwrite.md) | boolean | | +| [replaceReferences](./kibana-plugin-core-public.savedobjectsimportretry.replacereferences.md) | Array<{ type: string; from: string; to: string; }> | | +| [type](./kibana-plugin-core-public.savedobjectsimportretry.type.md) | string | | diff --git a/docs/development/core/public/kibana-plugin-core-public.savedobjectsimportsimplewarning.md b/docs/development/core/public/kibana-plugin-core-public.savedobjectsimportsimplewarning.md index 4d6d984777c80..304779a1589f9 100644 --- a/docs/development/core/public/kibana-plugin-core-public.savedobjectsimportsimplewarning.md +++ b/docs/development/core/public/kibana-plugin-core-public.savedobjectsimportsimplewarning.md @@ -16,6 +16,6 @@ export interface SavedObjectsImportSimpleWarning | Property | Type | Description | | --- | --- | --- | -| [message](./kibana-plugin-core-public.savedobjectsimportsimplewarning.message.md) | string | The translated message to display to the user | -| [type](./kibana-plugin-core-public.savedobjectsimportsimplewarning.type.md) | 'simple' | | +| [message](./kibana-plugin-core-public.savedobjectsimportsimplewarning.message.md) | string | The translated message to display to the user | +| [type](./kibana-plugin-core-public.savedobjectsimportsimplewarning.type.md) | 'simple' | | diff --git a/docs/development/core/public/kibana-plugin-core-public.savedobjectsimportsuccess.md b/docs/development/core/public/kibana-plugin-core-public.savedobjectsimportsuccess.md index 4872deb5ee0db..57ca4b7a787f6 100644 --- a/docs/development/core/public/kibana-plugin-core-public.savedobjectsimportsuccess.md +++ b/docs/development/core/public/kibana-plugin-core-public.savedobjectsimportsuccess.md @@ -16,10 +16,10 @@ export interface SavedObjectsImportSuccess | Property | Type | Description | | --- | --- | --- | -| [createNewCopy](./kibana-plugin-core-public.savedobjectsimportsuccess.createnewcopy.md) | boolean | | -| [destinationId](./kibana-plugin-core-public.savedobjectsimportsuccess.destinationid.md) | string | If destinationId is specified, the new object has a new ID that is different from the import ID. | -| [id](./kibana-plugin-core-public.savedobjectsimportsuccess.id.md) | string | | -| [meta](./kibana-plugin-core-public.savedobjectsimportsuccess.meta.md) | {
title?: string;
icon?: string;
} | | -| [overwrite](./kibana-plugin-core-public.savedobjectsimportsuccess.overwrite.md) | boolean | If overwrite is specified, this object overwrote an existing one (or will do so, in the case of a pending resolution). | -| [type](./kibana-plugin-core-public.savedobjectsimportsuccess.type.md) | string | | +| [createNewCopy?](./kibana-plugin-core-public.savedobjectsimportsuccess.createnewcopy.md) | boolean | (Optional) | +| [destinationId?](./kibana-plugin-core-public.savedobjectsimportsuccess.destinationid.md) | string | (Optional) If destinationId is specified, the new object has a new ID that is different from the import ID. | +| [id](./kibana-plugin-core-public.savedobjectsimportsuccess.id.md) | string | | +| [meta](./kibana-plugin-core-public.savedobjectsimportsuccess.meta.md) | { title?: string; icon?: string; } | | +| [overwrite?](./kibana-plugin-core-public.savedobjectsimportsuccess.overwrite.md) | boolean | (Optional) If overwrite is specified, this object overwrote an existing one (or will do so, in the case of a pending resolution). | +| [type](./kibana-plugin-core-public.savedobjectsimportsuccess.type.md) | string | | diff --git a/docs/development/core/public/kibana-plugin-core-public.savedobjectsimportunknownerror.md b/docs/development/core/public/kibana-plugin-core-public.savedobjectsimportunknownerror.md index 8ed3369d50d74..fc78e04dee8ac 100644 --- a/docs/development/core/public/kibana-plugin-core-public.savedobjectsimportunknownerror.md +++ b/docs/development/core/public/kibana-plugin-core-public.savedobjectsimportunknownerror.md @@ -16,7 +16,7 @@ export interface SavedObjectsImportUnknownError | Property | Type | Description | | --- | --- | --- | -| [message](./kibana-plugin-core-public.savedobjectsimportunknownerror.message.md) | string | | -| [statusCode](./kibana-plugin-core-public.savedobjectsimportunknownerror.statuscode.md) | number | | -| [type](./kibana-plugin-core-public.savedobjectsimportunknownerror.type.md) | 'unknown' | | +| [message](./kibana-plugin-core-public.savedobjectsimportunknownerror.message.md) | string | | +| [statusCode](./kibana-plugin-core-public.savedobjectsimportunknownerror.statuscode.md) | number | | +| [type](./kibana-plugin-core-public.savedobjectsimportunknownerror.type.md) | 'unknown' | | diff --git a/docs/development/core/public/kibana-plugin-core-public.savedobjectsimportunsupportedtypeerror.md b/docs/development/core/public/kibana-plugin-core-public.savedobjectsimportunsupportedtypeerror.md index afd5ae3110087..de805f05a12e9 100644 --- a/docs/development/core/public/kibana-plugin-core-public.savedobjectsimportunsupportedtypeerror.md +++ b/docs/development/core/public/kibana-plugin-core-public.savedobjectsimportunsupportedtypeerror.md @@ -16,5 +16,5 @@ export interface SavedObjectsImportUnsupportedTypeError | Property | Type | Description | | --- | --- | --- | -| [type](./kibana-plugin-core-public.savedobjectsimportunsupportedtypeerror.type.md) | 'unsupported_type' | | +| [type](./kibana-plugin-core-public.savedobjectsimportunsupportedtypeerror.type.md) | 'unsupported\_type' | | diff --git a/docs/development/core/public/kibana-plugin-core-public.savedobjectsresolveresponse.md b/docs/development/core/public/kibana-plugin-core-public.savedobjectsresolveresponse.md index cdc79d8ac363d..6364493a9ef09 100644 --- a/docs/development/core/public/kibana-plugin-core-public.savedobjectsresolveresponse.md +++ b/docs/development/core/public/kibana-plugin-core-public.savedobjectsresolveresponse.md @@ -15,7 +15,7 @@ export interface SavedObjectsResolveResponse | Property | Type | Description | | --- | --- | --- | -| [alias\_target\_id](./kibana-plugin-core-public.savedobjectsresolveresponse.alias_target_id.md) | string | The ID of the object that the legacy URL alias points to. This is only defined when the outcome is 'aliasMatch' or 'conflict'. | -| [outcome](./kibana-plugin-core-public.savedobjectsresolveresponse.outcome.md) | 'exactMatch' | 'aliasMatch' | 'conflict' | The outcome for a successful resolve call is one of the following values:\* 'exactMatch' -- One document exactly matched the given ID. \* 'aliasMatch' -- One document with a legacy URL alias matched the given ID; in this case the saved_object.id field is different than the given ID. \* 'conflict' -- Two documents matched the given ID, one was an exact match and another with a legacy URL alias; in this case the saved_object object is the exact match, and the saved_object.id field is the same as the given ID. | -| [saved\_object](./kibana-plugin-core-public.savedobjectsresolveresponse.saved_object.md) | SavedObject<T> | The saved object that was found. | +| [alias\_target\_id?](./kibana-plugin-core-public.savedobjectsresolveresponse.alias_target_id.md) | string | (Optional) The ID of the object that the legacy URL alias points to. This is only defined when the outcome is 'aliasMatch' or 'conflict'. | +| [outcome](./kibana-plugin-core-public.savedobjectsresolveresponse.outcome.md) | 'exactMatch' \| 'aliasMatch' \| 'conflict' | The outcome for a successful resolve call is one of the following values:\* 'exactMatch' -- One document exactly matched the given ID. \* 'aliasMatch' -- One document with a legacy URL alias matched the given ID; in this case the saved_object.id field is different than the given ID. \* 'conflict' -- Two documents matched the given ID, one was an exact match and another with a legacy URL alias; in this case the saved_object object is the exact match, and the saved_object.id field is the same as the given ID. | +| [saved\_object](./kibana-plugin-core-public.savedobjectsresolveresponse.saved_object.md) | SavedObject<T> | The saved object that was found. | diff --git a/docs/development/core/public/kibana-plugin-core-public.savedobjectsstart.md b/docs/development/core/public/kibana-plugin-core-public.savedobjectsstart.md index 0aa47301e8eb1..0ce0da309afbd 100644 --- a/docs/development/core/public/kibana-plugin-core-public.savedobjectsstart.md +++ b/docs/development/core/public/kibana-plugin-core-public.savedobjectsstart.md @@ -15,5 +15,5 @@ export interface SavedObjectsStart | Property | Type | Description | | --- | --- | --- | -| [client](./kibana-plugin-core-public.savedobjectsstart.client.md) | SavedObjectsClientContract | [SavedObjectsClient](./kibana-plugin-core-public.savedobjectsclient.md) | +| [client](./kibana-plugin-core-public.savedobjectsstart.client.md) | SavedObjectsClientContract | [SavedObjectsClient](./kibana-plugin-core-public.savedobjectsclient.md) | diff --git a/docs/development/core/public/kibana-plugin-core-public.savedobjectsupdateoptions.md b/docs/development/core/public/kibana-plugin-core-public.savedobjectsupdateoptions.md index d9cc801148d9e..4a9b85e7b67e6 100644 --- a/docs/development/core/public/kibana-plugin-core-public.savedobjectsupdateoptions.md +++ b/docs/development/core/public/kibana-plugin-core-public.savedobjectsupdateoptions.md @@ -15,7 +15,7 @@ export interface SavedObjectsUpdateOptions | Property | Type | Description | | --- | --- | --- | -| [references](./kibana-plugin-core-public.savedobjectsupdateoptions.references.md) | SavedObjectReference[] | | -| [upsert](./kibana-plugin-core-public.savedobjectsupdateoptions.upsert.md) | Attributes | | -| [version](./kibana-plugin-core-public.savedobjectsupdateoptions.version.md) | string | | +| [references?](./kibana-plugin-core-public.savedobjectsupdateoptions.references.md) | SavedObjectReference\[\] | (Optional) | +| [upsert?](./kibana-plugin-core-public.savedobjectsupdateoptions.upsert.md) | Attributes | (Optional) | +| [version?](./kibana-plugin-core-public.savedobjectsupdateoptions.version.md) | string | (Optional) | diff --git a/docs/development/core/public/kibana-plugin-core-public.scopedhistory._constructor_.md b/docs/development/core/public/kibana-plugin-core-public.scopedhistory._constructor_.md index 2cf647086b3e2..32b0950aa1065 100644 --- a/docs/development/core/public/kibana-plugin-core-public.scopedhistory._constructor_.md +++ b/docs/development/core/public/kibana-plugin-core-public.scopedhistory._constructor_.md @@ -16,6 +16,6 @@ constructor(parentHistory: History, basePath: string); | Parameter | Type | Description | | --- | --- | --- | -| parentHistory | History | | -| basePath | string | | +| parentHistory | History | | +| basePath | string | | diff --git a/docs/development/core/public/kibana-plugin-core-public.scopedhistory.md b/docs/development/core/public/kibana-plugin-core-public.scopedhistory.md index 15ed4e74c4dc5..0d04fc3d6a860 100644 --- a/docs/development/core/public/kibana-plugin-core-public.scopedhistory.md +++ b/docs/development/core/public/kibana-plugin-core-public.scopedhistory.md @@ -15,6 +15,7 @@ The [createSubHistory](./kibana-plugin-core-public.scopedhistory.createsubhistor ```typescript export declare class ScopedHistory implements History ``` +Implements: History<HistoryLocationState> ## Constructors @@ -26,16 +27,16 @@ export declare class ScopedHistory implements Hi | Property | Modifiers | Type | Description | | --- | --- | --- | --- | -| [action](./kibana-plugin-core-public.scopedhistory.action.md) | | Action | The last action dispatched on the history stack. | -| [block](./kibana-plugin-core-public.scopedhistory.block.md) | | (prompt?: string | boolean | History.TransitionPromptHook<HistoryLocationState> | undefined) => UnregisterCallback | Add a block prompt requesting user confirmation when navigating away from the current page. | -| [createHref](./kibana-plugin-core-public.scopedhistory.createhref.md) | | (location: LocationDescriptorObject<HistoryLocationState>, { prependBasePath }?: {
prependBasePath?: boolean | undefined;
}) => Href | Creates an href (string) to the location. If prependBasePath is true (default), it will prepend the location's path with the scoped history basePath. | -| [createSubHistory](./kibana-plugin-core-public.scopedhistory.createsubhistory.md) | | <SubHistoryLocationState = unknown>(basePath: string) => ScopedHistory<SubHistoryLocationState> | Creates a ScopedHistory for a subpath of this ScopedHistory. Useful for applications that may have sub-apps that do not need access to the containing application's history. | -| [go](./kibana-plugin-core-public.scopedhistory.go.md) | | (n: number) => void | Send the user forward or backwards in the history stack. | -| [goBack](./kibana-plugin-core-public.scopedhistory.goback.md) | | () => void | Send the user one location back in the history stack. Equivalent to calling [ScopedHistory.go(-1)](./kibana-plugin-core-public.scopedhistory.go.md). If no more entries are available backwards, this is a no-op. | -| [goForward](./kibana-plugin-core-public.scopedhistory.goforward.md) | | () => void | Send the user one location forward in the history stack. Equivalent to calling [ScopedHistory.go(1)](./kibana-plugin-core-public.scopedhistory.go.md). If no more entries are available forwards, this is a no-op. | -| [length](./kibana-plugin-core-public.scopedhistory.length.md) | | number | The number of entries in the history stack, including all entries forwards and backwards from the current location. | -| [listen](./kibana-plugin-core-public.scopedhistory.listen.md) | | (listener: (location: Location<HistoryLocationState>, action: Action) => void) => UnregisterCallback | Adds a listener for location updates. | -| [location](./kibana-plugin-core-public.scopedhistory.location.md) | | Location<HistoryLocationState> | The current location of the history stack. | -| [push](./kibana-plugin-core-public.scopedhistory.push.md) | | (pathOrLocation: Path | LocationDescriptorObject<HistoryLocationState>, state?: HistoryLocationState | undefined) => void | Pushes a new location onto the history stack. If there are forward entries in the stack, they will be removed. | -| [replace](./kibana-plugin-core-public.scopedhistory.replace.md) | | (pathOrLocation: Path | LocationDescriptorObject<HistoryLocationState>, state?: HistoryLocationState | undefined) => void | Replaces the current location in the history stack. Does not remove forward or backward entries. | +| [action](./kibana-plugin-core-public.scopedhistory.action.md) | | Action | The last action dispatched on the history stack. | +| [block](./kibana-plugin-core-public.scopedhistory.block.md) | | (prompt?: string \| boolean \| History.TransitionPromptHook<HistoryLocationState> \| undefined) => UnregisterCallback | Add a block prompt requesting user confirmation when navigating away from the current page. | +| [createHref](./kibana-plugin-core-public.scopedhistory.createhref.md) | | (location: LocationDescriptorObject<HistoryLocationState>, { prependBasePath }?: { prependBasePath?: boolean \| undefined; }) => Href | Creates an href (string) to the location. If prependBasePath is true (default), it will prepend the location's path with the scoped history basePath. | +| [createSubHistory](./kibana-plugin-core-public.scopedhistory.createsubhistory.md) | | <SubHistoryLocationState = unknown>(basePath: string) => ScopedHistory<SubHistoryLocationState> | Creates a ScopedHistory for a subpath of this ScopedHistory. Useful for applications that may have sub-apps that do not need access to the containing application's history. | +| [go](./kibana-plugin-core-public.scopedhistory.go.md) | | (n: number) => void | Send the user forward or backwards in the history stack. | +| [goBack](./kibana-plugin-core-public.scopedhistory.goback.md) | | () => void | Send the user one location back in the history stack. Equivalent to calling [ScopedHistory.go(-1)](./kibana-plugin-core-public.scopedhistory.go.md). If no more entries are available backwards, this is a no-op. | +| [goForward](./kibana-plugin-core-public.scopedhistory.goforward.md) | | () => void | Send the user one location forward in the history stack. Equivalent to calling [ScopedHistory.go(1)](./kibana-plugin-core-public.scopedhistory.go.md). If no more entries are available forwards, this is a no-op. | +| [length](./kibana-plugin-core-public.scopedhistory.length.md) | | number | The number of entries in the history stack, including all entries forwards and backwards from the current location. | +| [listen](./kibana-plugin-core-public.scopedhistory.listen.md) | | (listener: (location: Location<HistoryLocationState>, action: Action) => void) => UnregisterCallback | Adds a listener for location updates. | +| [location](./kibana-plugin-core-public.scopedhistory.location.md) | | Location<HistoryLocationState> | The current location of the history stack. | +| [push](./kibana-plugin-core-public.scopedhistory.push.md) | | (pathOrLocation: Path \| LocationDescriptorObject<HistoryLocationState>, state?: HistoryLocationState \| undefined) => void | Pushes a new location onto the history stack. If there are forward entries in the stack, they will be removed. | +| [replace](./kibana-plugin-core-public.scopedhistory.replace.md) | | (pathOrLocation: Path \| LocationDescriptorObject<HistoryLocationState>, state?: HistoryLocationState \| undefined) => void | Replaces the current location in the history stack. Does not remove forward or backward entries. | diff --git a/docs/development/core/public/kibana-plugin-core-public.simplesavedobject._constructor_.md b/docs/development/core/public/kibana-plugin-core-public.simplesavedobject._constructor_.md index c73a3a200cc24..f53b6e5292861 100644 --- a/docs/development/core/public/kibana-plugin-core-public.simplesavedobject._constructor_.md +++ b/docs/development/core/public/kibana-plugin-core-public.simplesavedobject._constructor_.md @@ -16,6 +16,6 @@ constructor(client: SavedObjectsClientContract, { id, type, version, attributes, | Parameter | Type | Description | | --- | --- | --- | -| client | SavedObjectsClientContract | | -| { id, type, version, attributes, error, references, migrationVersion, coreMigrationVersion, namespaces, } | SavedObjectType<T> | | +| client | SavedObjectsClientContract | | +| { id, type, version, attributes, error, references, migrationVersion, coreMigrationVersion, namespaces, } | SavedObjectType<T> | | diff --git a/docs/development/core/public/kibana-plugin-core-public.simplesavedobject.delete.md b/docs/development/core/public/kibana-plugin-core-public.simplesavedobject.delete.md index 909ff2e7d3435..cb848bff56430 100644 --- a/docs/development/core/public/kibana-plugin-core-public.simplesavedobject.delete.md +++ b/docs/development/core/public/kibana-plugin-core-public.simplesavedobject.delete.md @@ -11,5 +11,5 @@ delete(): Promise<{}>; ``` Returns: -`Promise<{}>` +Promise<{}> diff --git a/docs/development/core/public/kibana-plugin-core-public.simplesavedobject.get.md b/docs/development/core/public/kibana-plugin-core-public.simplesavedobject.get.md index caa9ab9857a6f..9a9c27d78c06c 100644 --- a/docs/development/core/public/kibana-plugin-core-public.simplesavedobject.get.md +++ b/docs/development/core/public/kibana-plugin-core-public.simplesavedobject.get.md @@ -14,9 +14,9 @@ get(key: string): any; | Parameter | Type | Description | | --- | --- | --- | -| key | string | | +| key | string | | Returns: -`any` +any diff --git a/docs/development/core/public/kibana-plugin-core-public.simplesavedobject.has.md b/docs/development/core/public/kibana-plugin-core-public.simplesavedobject.has.md index 960a7b6cfd16a..acd0ff02c7d23 100644 --- a/docs/development/core/public/kibana-plugin-core-public.simplesavedobject.has.md +++ b/docs/development/core/public/kibana-plugin-core-public.simplesavedobject.has.md @@ -14,9 +14,9 @@ has(key: string): boolean; | Parameter | Type | Description | | --- | --- | --- | -| key | string | | +| key | string | | Returns: -`boolean` +boolean diff --git a/docs/development/core/public/kibana-plugin-core-public.simplesavedobject.md b/docs/development/core/public/kibana-plugin-core-public.simplesavedobject.md index e15a4d4ea6d09..2aac93f9b5bc1 100644 --- a/docs/development/core/public/kibana-plugin-core-public.simplesavedobject.md +++ b/docs/development/core/public/kibana-plugin-core-public.simplesavedobject.md @@ -24,15 +24,15 @@ export declare class SimpleSavedObject | Property | Modifiers | Type | Description | | --- | --- | --- | --- | -| [\_version](./kibana-plugin-core-public.simplesavedobject._version.md) | | SavedObjectType<T>['version'] | | -| [attributes](./kibana-plugin-core-public.simplesavedobject.attributes.md) | | T | | -| [coreMigrationVersion](./kibana-plugin-core-public.simplesavedobject.coremigrationversion.md) | | SavedObjectType<T>['coreMigrationVersion'] | | -| [error](./kibana-plugin-core-public.simplesavedobject.error.md) | | SavedObjectType<T>['error'] | | -| [id](./kibana-plugin-core-public.simplesavedobject.id.md) | | SavedObjectType<T>['id'] | | -| [migrationVersion](./kibana-plugin-core-public.simplesavedobject.migrationversion.md) | | SavedObjectType<T>['migrationVersion'] | | -| [namespaces](./kibana-plugin-core-public.simplesavedobject.namespaces.md) | | SavedObjectType<T>['namespaces'] | Space(s) that this saved object exists in. This attribute is not used for "global" saved object types which are registered with namespaceType: 'agnostic'. | -| [references](./kibana-plugin-core-public.simplesavedobject.references.md) | | SavedObjectType<T>['references'] | | -| [type](./kibana-plugin-core-public.simplesavedobject.type.md) | | SavedObjectType<T>['type'] | | +| [\_version?](./kibana-plugin-core-public.simplesavedobject._version.md) | | SavedObjectType<T>\['version'\] | (Optional) | +| [attributes](./kibana-plugin-core-public.simplesavedobject.attributes.md) | | T | | +| [coreMigrationVersion](./kibana-plugin-core-public.simplesavedobject.coremigrationversion.md) | | SavedObjectType<T>\['coreMigrationVersion'\] | | +| [error](./kibana-plugin-core-public.simplesavedobject.error.md) | | SavedObjectType<T>\['error'\] | | +| [id](./kibana-plugin-core-public.simplesavedobject.id.md) | | SavedObjectType<T>\['id'\] | | +| [migrationVersion](./kibana-plugin-core-public.simplesavedobject.migrationversion.md) | | SavedObjectType<T>\['migrationVersion'\] | | +| [namespaces](./kibana-plugin-core-public.simplesavedobject.namespaces.md) | | SavedObjectType<T>\['namespaces'\] | Space(s) that this saved object exists in. This attribute is not used for "global" saved object types which are registered with namespaceType: 'agnostic'. | +| [references](./kibana-plugin-core-public.simplesavedobject.references.md) | | SavedObjectType<T>\['references'\] | | +| [type](./kibana-plugin-core-public.simplesavedobject.type.md) | | SavedObjectType<T>\['type'\] | | ## Methods diff --git a/docs/development/core/public/kibana-plugin-core-public.simplesavedobject.save.md b/docs/development/core/public/kibana-plugin-core-public.simplesavedobject.save.md index c985c714b9c5c..fdd262c70d4e6 100644 --- a/docs/development/core/public/kibana-plugin-core-public.simplesavedobject.save.md +++ b/docs/development/core/public/kibana-plugin-core-public.simplesavedobject.save.md @@ -11,5 +11,5 @@ save(): Promise>; ``` Returns: -`Promise>` +Promise<SimpleSavedObject<T>> diff --git a/docs/development/core/public/kibana-plugin-core-public.simplesavedobject.set.md b/docs/development/core/public/kibana-plugin-core-public.simplesavedobject.set.md index 09549e92b6a05..e3a6621f520bd 100644 --- a/docs/development/core/public/kibana-plugin-core-public.simplesavedobject.set.md +++ b/docs/development/core/public/kibana-plugin-core-public.simplesavedobject.set.md @@ -14,10 +14,10 @@ set(key: string, value: any): T; | Parameter | Type | Description | | --- | --- | --- | -| key | string | | -| value | any | | +| key | string | | +| value | any | | Returns: -`T` +T diff --git a/docs/development/core/public/kibana-plugin-core-public.toastoptions.md b/docs/development/core/public/kibana-plugin-core-public.toastoptions.md index 0d85c482c2288..c140a2e52b036 100644 --- a/docs/development/core/public/kibana-plugin-core-public.toastoptions.md +++ b/docs/development/core/public/kibana-plugin-core-public.toastoptions.md @@ -16,5 +16,5 @@ export interface ToastOptions | Property | Type | Description | | --- | --- | --- | -| [toastLifeTimeMs](./kibana-plugin-core-public.toastoptions.toastlifetimems.md) | number | How long should the toast remain on screen. | +| [toastLifeTimeMs?](./kibana-plugin-core-public.toastoptions.toastlifetimems.md) | number | (Optional) How long should the toast remain on screen. | diff --git a/docs/development/core/public/kibana-plugin-core-public.toastsapi._constructor_.md b/docs/development/core/public/kibana-plugin-core-public.toastsapi._constructor_.md index 71faf9a13b640..c50cc4d6469ea 100644 --- a/docs/development/core/public/kibana-plugin-core-public.toastsapi._constructor_.md +++ b/docs/development/core/public/kibana-plugin-core-public.toastsapi._constructor_.md @@ -18,5 +18,5 @@ constructor(deps: { | Parameter | Type | Description | | --- | --- | --- | -| deps | {
uiSettings: IUiSettingsClient;
} | | +| deps | { uiSettings: IUiSettingsClient; } | | diff --git a/docs/development/core/public/kibana-plugin-core-public.toastsapi.add.md b/docs/development/core/public/kibana-plugin-core-public.toastsapi.add.md index 8cd3829c6f6ac..7bee5af0c3be4 100644 --- a/docs/development/core/public/kibana-plugin-core-public.toastsapi.add.md +++ b/docs/development/core/public/kibana-plugin-core-public.toastsapi.add.md @@ -16,11 +16,11 @@ add(toastOrTitle: ToastInput): Toast; | Parameter | Type | Description | | --- | --- | --- | -| toastOrTitle | ToastInput | a [ToastInput](./kibana-plugin-core-public.toastinput.md) | +| toastOrTitle | ToastInput | a [ToastInput](./kibana-plugin-core-public.toastinput.md) | Returns: -`Toast` +Toast a [Toast](./kibana-plugin-core-public.toast.md) diff --git a/docs/development/core/public/kibana-plugin-core-public.toastsapi.adddanger.md b/docs/development/core/public/kibana-plugin-core-public.toastsapi.adddanger.md index 420100a1209ab..f73a84996ff92 100644 --- a/docs/development/core/public/kibana-plugin-core-public.toastsapi.adddanger.md +++ b/docs/development/core/public/kibana-plugin-core-public.toastsapi.adddanger.md @@ -16,12 +16,12 @@ addDanger(toastOrTitle: ToastInput, options?: ToastOptions): Toast; | Parameter | Type | Description | | --- | --- | --- | -| toastOrTitle | ToastInput | a [ToastInput](./kibana-plugin-core-public.toastinput.md) | -| options | ToastOptions | a [ToastOptions](./kibana-plugin-core-public.toastoptions.md) | +| toastOrTitle | ToastInput | a [ToastInput](./kibana-plugin-core-public.toastinput.md) | +| options | ToastOptions | a [ToastOptions](./kibana-plugin-core-public.toastoptions.md) | Returns: -`Toast` +Toast a [Toast](./kibana-plugin-core-public.toast.md) diff --git a/docs/development/core/public/kibana-plugin-core-public.toastsapi.adderror.md b/docs/development/core/public/kibana-plugin-core-public.toastsapi.adderror.md index e5f851a225664..c1520ea392f70 100644 --- a/docs/development/core/public/kibana-plugin-core-public.toastsapi.adderror.md +++ b/docs/development/core/public/kibana-plugin-core-public.toastsapi.adderror.md @@ -16,12 +16,12 @@ addError(error: Error, options: ErrorToastOptions): Toast; | Parameter | Type | Description | | --- | --- | --- | -| error | Error | an Error instance. | -| options | ErrorToastOptions | [ErrorToastOptions](./kibana-plugin-core-public.errortoastoptions.md) | +| error | Error | an Error instance. | +| options | ErrorToastOptions | [ErrorToastOptions](./kibana-plugin-core-public.errortoastoptions.md) | Returns: -`Toast` +Toast a [Toast](./kibana-plugin-core-public.toast.md) diff --git a/docs/development/core/public/kibana-plugin-core-public.toastsapi.addinfo.md b/docs/development/core/public/kibana-plugin-core-public.toastsapi.addinfo.md index 76508d26b4ae9..7029482663155 100644 --- a/docs/development/core/public/kibana-plugin-core-public.toastsapi.addinfo.md +++ b/docs/development/core/public/kibana-plugin-core-public.toastsapi.addinfo.md @@ -16,12 +16,12 @@ addInfo(toastOrTitle: ToastInput, options?: ToastOptions): Toast; | Parameter | Type | Description | | --- | --- | --- | -| toastOrTitle | ToastInput | a [ToastInput](./kibana-plugin-core-public.toastinput.md) | -| options | ToastOptions | a [ToastOptions](./kibana-plugin-core-public.toastoptions.md) | +| toastOrTitle | ToastInput | a [ToastInput](./kibana-plugin-core-public.toastinput.md) | +| options | ToastOptions | a [ToastOptions](./kibana-plugin-core-public.toastoptions.md) | Returns: -`Toast` +Toast a [Toast](./kibana-plugin-core-public.toast.md) diff --git a/docs/development/core/public/kibana-plugin-core-public.toastsapi.addsuccess.md b/docs/development/core/public/kibana-plugin-core-public.toastsapi.addsuccess.md index c79f48042514a..b9cf4da3b43af 100644 --- a/docs/development/core/public/kibana-plugin-core-public.toastsapi.addsuccess.md +++ b/docs/development/core/public/kibana-plugin-core-public.toastsapi.addsuccess.md @@ -16,12 +16,12 @@ addSuccess(toastOrTitle: ToastInput, options?: ToastOptions): Toast; | Parameter | Type | Description | | --- | --- | --- | -| toastOrTitle | ToastInput | a [ToastInput](./kibana-plugin-core-public.toastinput.md) | -| options | ToastOptions | a [ToastOptions](./kibana-plugin-core-public.toastoptions.md) | +| toastOrTitle | ToastInput | a [ToastInput](./kibana-plugin-core-public.toastinput.md) | +| options | ToastOptions | a [ToastOptions](./kibana-plugin-core-public.toastoptions.md) | Returns: -`Toast` +Toast a [Toast](./kibana-plugin-core-public.toast.md) diff --git a/docs/development/core/public/kibana-plugin-core-public.toastsapi.addwarning.md b/docs/development/core/public/kibana-plugin-core-public.toastsapi.addwarning.md index 6154af148332d..790af0d26220a 100644 --- a/docs/development/core/public/kibana-plugin-core-public.toastsapi.addwarning.md +++ b/docs/development/core/public/kibana-plugin-core-public.toastsapi.addwarning.md @@ -16,12 +16,12 @@ addWarning(toastOrTitle: ToastInput, options?: ToastOptions): Toast; | Parameter | Type | Description | | --- | --- | --- | -| toastOrTitle | ToastInput | a [ToastInput](./kibana-plugin-core-public.toastinput.md) | -| options | ToastOptions | a [ToastOptions](./kibana-plugin-core-public.toastoptions.md) | +| toastOrTitle | ToastInput | a [ToastInput](./kibana-plugin-core-public.toastinput.md) | +| options | ToastOptions | a [ToastOptions](./kibana-plugin-core-public.toastoptions.md) | Returns: -`Toast` +Toast a [Toast](./kibana-plugin-core-public.toast.md) diff --git a/docs/development/core/public/kibana-plugin-core-public.toastsapi.get_.md b/docs/development/core/public/kibana-plugin-core-public.toastsapi.get_.md index 90b32a8b48e5c..275d30fd54e0f 100644 --- a/docs/development/core/public/kibana-plugin-core-public.toastsapi.get_.md +++ b/docs/development/core/public/kibana-plugin-core-public.toastsapi.get_.md @@ -13,5 +13,5 @@ get$(): Rx.Observable; ``` Returns: -`Rx.Observable` +Rx.Observable<Toast\[\]> diff --git a/docs/development/core/public/kibana-plugin-core-public.toastsapi.md b/docs/development/core/public/kibana-plugin-core-public.toastsapi.md index ca4c08989128a..4d7f9dcacfa6f 100644 --- a/docs/development/core/public/kibana-plugin-core-public.toastsapi.md +++ b/docs/development/core/public/kibana-plugin-core-public.toastsapi.md @@ -11,6 +11,7 @@ Methods for adding and removing global toast messages. ```typescript export declare class ToastsApi implements IToasts ``` +Implements: IToasts ## Constructors diff --git a/docs/development/core/public/kibana-plugin-core-public.toastsapi.remove.md b/docs/development/core/public/kibana-plugin-core-public.toastsapi.remove.md index 360fb94522821..aeac9f46b7901 100644 --- a/docs/development/core/public/kibana-plugin-core-public.toastsapi.remove.md +++ b/docs/development/core/public/kibana-plugin-core-public.toastsapi.remove.md @@ -16,9 +16,9 @@ remove(toastOrId: Toast | string): void; | Parameter | Type | Description | | --- | --- | --- | -| toastOrId | Toast | string | a [Toast](./kibana-plugin-core-public.toast.md) returned by [ToastsApi.add()](./kibana-plugin-core-public.toastsapi.add.md) or its id | +| toastOrId | Toast \| string | a [Toast](./kibana-plugin-core-public.toast.md) returned by [ToastsApi.add()](./kibana-plugin-core-public.toastsapi.add.md) or its id | Returns: -`void` +void diff --git a/docs/development/core/public/kibana-plugin-core-public.uisettingsparams.md b/docs/development/core/public/kibana-plugin-core-public.uisettingsparams.md index 54d1a6612f4ba..325ce96f36ca3 100644 --- a/docs/development/core/public/kibana-plugin-core-public.uisettingsparams.md +++ b/docs/development/core/public/kibana-plugin-core-public.uisettingsparams.md @@ -16,18 +16,18 @@ export interface UiSettingsParams | Property | Type | Description | | --- | --- | --- | -| [category](./kibana-plugin-core-public.uisettingsparams.category.md) | string[] | used to group the configured setting in the UI | -| [deprecation](./kibana-plugin-core-public.uisettingsparams.deprecation.md) | DeprecationSettings | optional deprecation information. Used to generate a deprecation warning. | -| [description](./kibana-plugin-core-public.uisettingsparams.description.md) | string | description provided to a user in UI | -| [metric](./kibana-plugin-core-public.uisettingsparams.metric.md) | {
type: UiCounterMetricType;
name: string;
} | Metric to track once this property changes | -| [name](./kibana-plugin-core-public.uisettingsparams.name.md) | string | title in the UI | -| [optionLabels](./kibana-plugin-core-public.uisettingsparams.optionlabels.md) | Record<string, string> | text labels for 'select' type UI element | -| [options](./kibana-plugin-core-public.uisettingsparams.options.md) | string[] | array of permitted values for this setting | -| [order](./kibana-plugin-core-public.uisettingsparams.order.md) | number | index of the settings within its category (ascending order, smallest will be displayed first). Used for ordering in the UI. settings without order defined will be displayed last and ordered by name | -| [readonly](./kibana-plugin-core-public.uisettingsparams.readonly.md) | boolean | a flag indicating that value cannot be changed | -| [requiresPageReload](./kibana-plugin-core-public.uisettingsparams.requirespagereload.md) | boolean | a flag indicating whether new value applying requires page reloading | -| [schema](./kibana-plugin-core-public.uisettingsparams.schema.md) | Type<T> | | -| [sensitive](./kibana-plugin-core-public.uisettingsparams.sensitive.md) | boolean | a flag indicating that value might contain user sensitive data. used by telemetry to mask the value of the setting when sent. | -| [type](./kibana-plugin-core-public.uisettingsparams.type.md) | UiSettingsType | defines a type of UI element [UiSettingsType](./kibana-plugin-core-public.uisettingstype.md) | -| [value](./kibana-plugin-core-public.uisettingsparams.value.md) | T | default value to fall back to if a user doesn't provide any | +| [category?](./kibana-plugin-core-public.uisettingsparams.category.md) | string\[\] | (Optional) used to group the configured setting in the UI | +| [deprecation?](./kibana-plugin-core-public.uisettingsparams.deprecation.md) | DeprecationSettings | (Optional) optional deprecation information. Used to generate a deprecation warning. | +| [description?](./kibana-plugin-core-public.uisettingsparams.description.md) | string | (Optional) description provided to a user in UI | +| [metric?](./kibana-plugin-core-public.uisettingsparams.metric.md) | { type: UiCounterMetricType; name: string; } | (Optional) Metric to track once this property changes | +| [name?](./kibana-plugin-core-public.uisettingsparams.name.md) | string | (Optional) title in the UI | +| [optionLabels?](./kibana-plugin-core-public.uisettingsparams.optionlabels.md) | Record<string, string> | (Optional) text labels for 'select' type UI element | +| [options?](./kibana-plugin-core-public.uisettingsparams.options.md) | string\[\] | (Optional) array of permitted values for this setting | +| [order?](./kibana-plugin-core-public.uisettingsparams.order.md) | number | (Optional) index of the settings within its category (ascending order, smallest will be displayed first). Used for ordering in the UI. settings without order defined will be displayed last and ordered by name | +| [readonly?](./kibana-plugin-core-public.uisettingsparams.readonly.md) | boolean | (Optional) a flag indicating that value cannot be changed | +| [requiresPageReload?](./kibana-plugin-core-public.uisettingsparams.requirespagereload.md) | boolean | (Optional) a flag indicating whether new value applying requires page reloading | +| [schema](./kibana-plugin-core-public.uisettingsparams.schema.md) | Type<T> | | +| [sensitive?](./kibana-plugin-core-public.uisettingsparams.sensitive.md) | boolean | (Optional) a flag indicating that value might contain user sensitive data. used by telemetry to mask the value of the setting when sent. | +| [type?](./kibana-plugin-core-public.uisettingsparams.type.md) | UiSettingsType | (Optional) defines a type of UI element [UiSettingsType](./kibana-plugin-core-public.uisettingstype.md) | +| [value?](./kibana-plugin-core-public.uisettingsparams.value.md) | T | (Optional) default value to fall back to if a user doesn't provide any | diff --git a/docs/development/core/public/kibana-plugin-core-public.userprovidedvalues.md b/docs/development/core/public/kibana-plugin-core-public.userprovidedvalues.md index e59a75500f558..eb8f0124c3341 100644 --- a/docs/development/core/public/kibana-plugin-core-public.userprovidedvalues.md +++ b/docs/development/core/public/kibana-plugin-core-public.userprovidedvalues.md @@ -16,6 +16,6 @@ export interface UserProvidedValues | Property | Type | Description | | --- | --- | --- | -| [isOverridden](./kibana-plugin-core-public.userprovidedvalues.isoverridden.md) | boolean | | -| [userValue](./kibana-plugin-core-public.userprovidedvalues.uservalue.md) | T | | +| [isOverridden?](./kibana-plugin-core-public.userprovidedvalues.isoverridden.md) | boolean | (Optional) | +| [userValue?](./kibana-plugin-core-public.userprovidedvalues.uservalue.md) | T | (Optional) | diff --git a/docs/development/core/server/kibana-plugin-core-server.appcategory.md b/docs/development/core/server/kibana-plugin-core-server.appcategory.md index a761bf4e5b393..ca5e8b354d451 100644 --- a/docs/development/core/server/kibana-plugin-core-server.appcategory.md +++ b/docs/development/core/server/kibana-plugin-core-server.appcategory.md @@ -16,9 +16,9 @@ export interface AppCategory | Property | Type | Description | | --- | --- | --- | -| [ariaLabel](./kibana-plugin-core-server.appcategory.arialabel.md) | string | If the visual label isn't appropriate for screen readers, can override it here | -| [euiIconType](./kibana-plugin-core-server.appcategory.euiicontype.md) | string | Define an icon to be used for the category If the category is only 1 item, and no icon is defined, will default to the product icon Defaults to initials if no icon is defined | -| [id](./kibana-plugin-core-server.appcategory.id.md) | string | Unique identifier for the categories | -| [label](./kibana-plugin-core-server.appcategory.label.md) | string | Label used for category name. Also used as aria-label if one isn't set. | -| [order](./kibana-plugin-core-server.appcategory.order.md) | number | The order that categories will be sorted in Prefer large steps between categories to allow for further editing (Default categories are in steps of 1000) | +| [ariaLabel?](./kibana-plugin-core-server.appcategory.arialabel.md) | string | (Optional) If the visual label isn't appropriate for screen readers, can override it here | +| [euiIconType?](./kibana-plugin-core-server.appcategory.euiicontype.md) | string | (Optional) Define an icon to be used for the category If the category is only 1 item, and no icon is defined, will default to the product icon Defaults to initials if no icon is defined | +| [id](./kibana-plugin-core-server.appcategory.id.md) | string | Unique identifier for the categories | +| [label](./kibana-plugin-core-server.appcategory.label.md) | string | Label used for category name. Also used as aria-label if one isn't set. | +| [order?](./kibana-plugin-core-server.appcategory.order.md) | number | (Optional) The order that categories will be sorted in Prefer large steps between categories to allow for further editing (Default categories are in steps of 1000) | diff --git a/docs/development/core/server/kibana-plugin-core-server.asyncplugin.md b/docs/development/core/server/kibana-plugin-core-server.asyncplugin.md index 1ad1d87220b74..c5c664e07f297 100644 --- a/docs/development/core/server/kibana-plugin-core-server.asyncplugin.md +++ b/docs/development/core/server/kibana-plugin-core-server.asyncplugin.md @@ -6,7 +6,7 @@ > Warning: This API is now obsolete. > -> Asynchronous lifecycles are deprecated, and should be migrated to sync [plugin](./kibana-plugin-core-server.plugin.md) +> Asynchronous lifecycles are deprecated, and should be migrated to sync > A plugin with asynchronous lifecycle methods. @@ -23,5 +23,5 @@ export interface AsyncPlugin(Optional) | diff --git a/docs/development/core/server/kibana-plugin-core-server.asyncplugin.setup.md b/docs/development/core/server/kibana-plugin-core-server.asyncplugin.setup.md index 1d033b7b88b05..73752d6c9bd20 100644 --- a/docs/development/core/server/kibana-plugin-core-server.asyncplugin.setup.md +++ b/docs/development/core/server/kibana-plugin-core-server.asyncplugin.setup.md @@ -14,10 +14,10 @@ setup(core: CoreSetup, plugins: TPluginsSetup): TSetup | Promise; | Parameter | Type | Description | | --- | --- | --- | -| core | CoreSetup | | -| plugins | TPluginsSetup | | +| core | CoreSetup | | +| plugins | TPluginsSetup | | Returns: -`TSetup | Promise` +TSetup \| Promise<TSetup> diff --git a/docs/development/core/server/kibana-plugin-core-server.asyncplugin.start.md b/docs/development/core/server/kibana-plugin-core-server.asyncplugin.start.md index 3cce90f01603b..98cf74341062a 100644 --- a/docs/development/core/server/kibana-plugin-core-server.asyncplugin.start.md +++ b/docs/development/core/server/kibana-plugin-core-server.asyncplugin.start.md @@ -14,10 +14,10 @@ start(core: CoreStart, plugins: TPluginsStart): TStart | Promise; | Parameter | Type | Description | | --- | --- | --- | -| core | CoreStart | | -| plugins | TPluginsStart | | +| core | CoreStart | | +| plugins | TPluginsStart | | Returns: -`TStart | Promise` +TStart \| Promise<TStart> diff --git a/docs/development/core/server/kibana-plugin-core-server.asyncplugin.stop.md b/docs/development/core/server/kibana-plugin-core-server.asyncplugin.stop.md index 9272fc2c4eba0..80c554f343346 100644 --- a/docs/development/core/server/kibana-plugin-core-server.asyncplugin.stop.md +++ b/docs/development/core/server/kibana-plugin-core-server.asyncplugin.stop.md @@ -11,5 +11,5 @@ stop?(): void; ``` Returns: -`void` +void diff --git a/docs/development/core/server/kibana-plugin-core-server.authenticated.md b/docs/development/core/server/kibana-plugin-core-server.authenticated.md index ffbf942926015..90b480d4a026c 100644 --- a/docs/development/core/server/kibana-plugin-core-server.authenticated.md +++ b/docs/development/core/server/kibana-plugin-core-server.authenticated.md @@ -10,10 +10,11 @@ ```typescript export interface Authenticated extends AuthResultParams ``` +Extends: AuthResultParams ## Properties | Property | Type | Description | | --- | --- | --- | -| [type](./kibana-plugin-core-server.authenticated.type.md) | AuthResultType.authenticated | | +| [type](./kibana-plugin-core-server.authenticated.type.md) | AuthResultType.authenticated | | diff --git a/docs/development/core/server/kibana-plugin-core-server.authnothandled.md b/docs/development/core/server/kibana-plugin-core-server.authnothandled.md index c1eaa6899135b..297f60e4bd8ac 100644 --- a/docs/development/core/server/kibana-plugin-core-server.authnothandled.md +++ b/docs/development/core/server/kibana-plugin-core-server.authnothandled.md @@ -15,5 +15,5 @@ export interface AuthNotHandled | Property | Type | Description | | --- | --- | --- | -| [type](./kibana-plugin-core-server.authnothandled.type.md) | AuthResultType.notHandled | | +| [type](./kibana-plugin-core-server.authnothandled.type.md) | AuthResultType.notHandled | | diff --git a/docs/development/core/server/kibana-plugin-core-server.authredirected.md b/docs/development/core/server/kibana-plugin-core-server.authredirected.md index 7b5a4d60fb0bb..4da08e49056f7 100644 --- a/docs/development/core/server/kibana-plugin-core-server.authredirected.md +++ b/docs/development/core/server/kibana-plugin-core-server.authredirected.md @@ -10,10 +10,11 @@ ```typescript export interface AuthRedirected extends AuthRedirectedParams ``` +Extends: AuthRedirectedParams ## Properties | Property | Type | Description | | --- | --- | --- | -| [type](./kibana-plugin-core-server.authredirected.type.md) | AuthResultType.redirected | | +| [type](./kibana-plugin-core-server.authredirected.type.md) | AuthResultType.redirected | | diff --git a/docs/development/core/server/kibana-plugin-core-server.authredirectedparams.md b/docs/development/core/server/kibana-plugin-core-server.authredirectedparams.md index 2e1f04ef4efc6..2f6ef7c6f6ba4 100644 --- a/docs/development/core/server/kibana-plugin-core-server.authredirectedparams.md +++ b/docs/development/core/server/kibana-plugin-core-server.authredirectedparams.md @@ -16,5 +16,5 @@ export interface AuthRedirectedParams | Property | Type | Description | | --- | --- | --- | -| [headers](./kibana-plugin-core-server.authredirectedparams.headers.md) | {
location: string;
} & ResponseHeaders | Headers to attach for auth redirect. Must include "location" header | +| [headers](./kibana-plugin-core-server.authredirectedparams.headers.md) | { location: string; } & ResponseHeaders | Headers to attach for auth redirect. Must include "location" header | diff --git a/docs/development/core/server/kibana-plugin-core-server.authresultparams.md b/docs/development/core/server/kibana-plugin-core-server.authresultparams.md index db4ead393c047..7e907a9bf7a77 100644 --- a/docs/development/core/server/kibana-plugin-core-server.authresultparams.md +++ b/docs/development/core/server/kibana-plugin-core-server.authresultparams.md @@ -16,7 +16,7 @@ export interface AuthResultParams | Property | Type | Description | | --- | --- | --- | -| [requestHeaders](./kibana-plugin-core-server.authresultparams.requestheaders.md) | AuthHeaders | Auth specific headers to attach to a request object. Used to perform a request to Elasticsearch on behalf of an authenticated user. | -| [responseHeaders](./kibana-plugin-core-server.authresultparams.responseheaders.md) | AuthHeaders | Auth specific headers to attach to a response object. Used to send back authentication mechanism related headers to a client when needed. | -| [state](./kibana-plugin-core-server.authresultparams.state.md) | Record<string, any> | Data to associate with an incoming request. Any downstream plugin may get access to the data. | +| [requestHeaders?](./kibana-plugin-core-server.authresultparams.requestheaders.md) | AuthHeaders | (Optional) Auth specific headers to attach to a request object. Used to perform a request to Elasticsearch on behalf of an authenticated user. | +| [responseHeaders?](./kibana-plugin-core-server.authresultparams.responseheaders.md) | AuthHeaders | (Optional) Auth specific headers to attach to a response object. Used to send back authentication mechanism related headers to a client when needed. | +| [state?](./kibana-plugin-core-server.authresultparams.state.md) | Record<string, any> | (Optional) Data to associate with an incoming request. Any downstream plugin may get access to the data. | diff --git a/docs/development/core/server/kibana-plugin-core-server.authtoolkit.md b/docs/development/core/server/kibana-plugin-core-server.authtoolkit.md index 5f8b98ab2e894..24b561d04bbb7 100644 --- a/docs/development/core/server/kibana-plugin-core-server.authtoolkit.md +++ b/docs/development/core/server/kibana-plugin-core-server.authtoolkit.md @@ -16,7 +16,7 @@ export interface AuthToolkit | Property | Type | Description | | --- | --- | --- | -| [authenticated](./kibana-plugin-core-server.authtoolkit.authenticated.md) | (data?: AuthResultParams) => AuthResult | Authentication is successful with given credentials, allow request to pass through | -| [notHandled](./kibana-plugin-core-server.authtoolkit.nothandled.md) | () => AuthResult | User has no credentials. Allows user to access a resource when authRequired is 'optional' Rejects a request when authRequired: true | -| [redirected](./kibana-plugin-core-server.authtoolkit.redirected.md) | (headers: {
location: string;
} & ResponseHeaders) => AuthResult | Redirects user to another location to complete authentication when authRequired: true Allows user to access a resource without redirection when authRequired: 'optional' | +| [authenticated](./kibana-plugin-core-server.authtoolkit.authenticated.md) | (data?: AuthResultParams) => AuthResult | Authentication is successful with given credentials, allow request to pass through | +| [notHandled](./kibana-plugin-core-server.authtoolkit.nothandled.md) | () => AuthResult | User has no credentials. Allows user to access a resource when authRequired is 'optional' Rejects a request when authRequired: true | +| [redirected](./kibana-plugin-core-server.authtoolkit.redirected.md) | (headers: { location: string; } & ResponseHeaders) => AuthResult | Redirects user to another location to complete authentication when authRequired: true Allows user to access a resource without redirection when authRequired: 'optional' | diff --git a/docs/development/core/server/kibana-plugin-core-server.basedeprecationdetails.md b/docs/development/core/server/kibana-plugin-core-server.basedeprecationdetails.md index 3e47865062352..bcd96e35295ac 100644 --- a/docs/development/core/server/kibana-plugin-core-server.basedeprecationdetails.md +++ b/docs/development/core/server/kibana-plugin-core-server.basedeprecationdetails.md @@ -16,11 +16,11 @@ export interface BaseDeprecationDetails | Property | Type | Description | | --- | --- | --- | -| [correctiveActions](./kibana-plugin-core-server.basedeprecationdetails.correctiveactions.md) | {
api?: {
path: string;
method: 'POST' | 'PUT';
body?: {
[key: string]: any;
};
omitContextFromBody?: boolean;
};
manualSteps: string[];
} | corrective action needed to fix this deprecation. | -| [deprecationType](./kibana-plugin-core-server.basedeprecationdetails.deprecationtype.md) | 'config' | 'feature' | (optional) Used to identify between different deprecation types. Example use case: in Upgrade Assistant, we may want to allow the user to sort by deprecation type or show each type in a separate tab.Feel free to add new types if necessary. Predefined types are necessary to reduce having similar definitions with different keywords across kibana deprecations. | -| [documentationUrl](./kibana-plugin-core-server.basedeprecationdetails.documentationurl.md) | string | (optional) link to the documentation for more details on the deprecation. | -| [level](./kibana-plugin-core-server.basedeprecationdetails.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.basedeprecationdetails.message.md) | string | The description message to be displayed for the deprecation. Check the README for writing deprecations in src/core/server/deprecations/README.mdx | -| [requireRestart](./kibana-plugin-core-server.basedeprecationdetails.requirerestart.md) | boolean | (optional) specify the fix for this deprecation requires a full kibana restart. | -| [title](./kibana-plugin-core-server.basedeprecationdetails.title.md) | string | The title of the deprecation. Check the README for writing deprecations in src/core/server/deprecations/README.mdx | +| [correctiveActions](./kibana-plugin-core-server.basedeprecationdetails.correctiveactions.md) | { api?: { path: string; method: 'POST' \| 'PUT'; body?: { \[key: string\]: any; }; omitContextFromBody?: boolean; }; manualSteps: string\[\]; } | corrective action needed to fix this deprecation. | +| [deprecationType?](./kibana-plugin-core-server.basedeprecationdetails.deprecationtype.md) | 'config' \| 'feature' | (Optional) (optional) Used to identify between different deprecation types. Example use case: in Upgrade Assistant, we may want to allow the user to sort by deprecation type or show each type in a separate tab.Feel free to add new types if necessary. Predefined types are necessary to reduce having similar definitions with different keywords across kibana deprecations. | +| [documentationUrl?](./kibana-plugin-core-server.basedeprecationdetails.documentationurl.md) | string | (Optional) (optional) link to the documentation for more details on the deprecation. | +| [level](./kibana-plugin-core-server.basedeprecationdetails.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.basedeprecationdetails.message.md) | string | The description message to be displayed for the deprecation. Check the README for writing deprecations in src/core/server/deprecations/README.mdx | +| [requireRestart?](./kibana-plugin-core-server.basedeprecationdetails.requirerestart.md) | boolean | (Optional) (optional) specify the fix for this deprecation requires a full kibana restart. | +| [title](./kibana-plugin-core-server.basedeprecationdetails.title.md) | string | The title of the deprecation. Check the README for writing deprecations in src/core/server/deprecations/README.mdx | diff --git a/docs/development/core/server/kibana-plugin-core-server.basepath.md b/docs/development/core/server/kibana-plugin-core-server.basepath.md index f4bac88cd85f5..4fae861cf1659 100644 --- a/docs/development/core/server/kibana-plugin-core-server.basepath.md +++ b/docs/development/core/server/kibana-plugin-core-server.basepath.md @@ -20,10 +20,10 @@ The constructor for this class is marked as internal. Third-party code should no | Property | Modifiers | Type | Description | | --- | --- | --- | --- | -| [get](./kibana-plugin-core-server.basepath.get.md) | | (request: KibanaRequest) => string | returns basePath value, specific for an incoming request. | -| [prepend](./kibana-plugin-core-server.basepath.prepend.md) | | (path: string) => string | Prepends path with the basePath. | -| [publicBaseUrl](./kibana-plugin-core-server.basepath.publicbaseurl.md) | | string | The server's publicly exposed base URL, if configured. Includes protocol, host, port (optional) and the [BasePath.serverBasePath](./kibana-plugin-core-server.basepath.serverbasepath.md). | -| [remove](./kibana-plugin-core-server.basepath.remove.md) | | (path: string) => string | Removes the prepended basePath from the path. | -| [serverBasePath](./kibana-plugin-core-server.basepath.serverbasepath.md) | | string | returns the server's basePathSee [BasePath.get](./kibana-plugin-core-server.basepath.get.md) for getting the basePath value for a specific request | -| [set](./kibana-plugin-core-server.basepath.set.md) | | (request: KibanaRequest, requestSpecificBasePath: string) => void | sets basePath value, specific for an incoming request. | +| [get](./kibana-plugin-core-server.basepath.get.md) | | (request: KibanaRequest) => string | returns basePath value, specific for an incoming request. | +| [prepend](./kibana-plugin-core-server.basepath.prepend.md) | | (path: string) => string | Prepends path with the basePath. | +| [publicBaseUrl?](./kibana-plugin-core-server.basepath.publicbaseurl.md) | | string | (Optional) The server's publicly exposed base URL, if configured. Includes protocol, host, port (optional) and the [BasePath.serverBasePath](./kibana-plugin-core-server.basepath.serverbasepath.md). | +| [remove](./kibana-plugin-core-server.basepath.remove.md) | | (path: string) => string | Removes the prepended basePath from the path. | +| [serverBasePath](./kibana-plugin-core-server.basepath.serverbasepath.md) | | string | returns the server's basePathSee [BasePath.get](./kibana-plugin-core-server.basepath.get.md) for getting the basePath value for a specific request | +| [set](./kibana-plugin-core-server.basepath.set.md) | | (request: KibanaRequest, requestSpecificBasePath: string) => void | sets basePath value, specific for an incoming request. | diff --git a/docs/development/core/server/kibana-plugin-core-server.capabilities.md b/docs/development/core/server/kibana-plugin-core-server.capabilities.md index cf47ce4609ea1..a5900e96a86b5 100644 --- a/docs/development/core/server/kibana-plugin-core-server.capabilities.md +++ b/docs/development/core/server/kibana-plugin-core-server.capabilities.md @@ -16,7 +16,7 @@ export interface Capabilities | Property | Type | Description | | --- | --- | --- | -| [catalogue](./kibana-plugin-core-server.capabilities.catalogue.md) | Record<string, boolean> | Catalogue capabilities. Catalogue entries drive the visibility of the Kibana homepage options. | -| [management](./kibana-plugin-core-server.capabilities.management.md) | {
[sectionId: string]: Record<string, boolean>;
} | Management section capabilities. | -| [navLinks](./kibana-plugin-core-server.capabilities.navlinks.md) | Record<string, boolean> | Navigation link capabilities. | +| [catalogue](./kibana-plugin-core-server.capabilities.catalogue.md) | Record<string, boolean> | Catalogue capabilities. Catalogue entries drive the visibility of the Kibana homepage options. | +| [management](./kibana-plugin-core-server.capabilities.management.md) | { \[sectionId: string\]: Record<string, boolean>; } | Management section capabilities. | +| [navLinks](./kibana-plugin-core-server.capabilities.navlinks.md) | Record<string, boolean> | Navigation link capabilities. | diff --git a/docs/development/core/server/kibana-plugin-core-server.capabilitiessetup.registerprovider.md b/docs/development/core/server/kibana-plugin-core-server.capabilitiessetup.registerprovider.md index 01a6f3562e77e..9122f7e0f11ed 100644 --- a/docs/development/core/server/kibana-plugin-core-server.capabilitiessetup.registerprovider.md +++ b/docs/development/core/server/kibana-plugin-core-server.capabilitiessetup.registerprovider.md @@ -16,11 +16,11 @@ registerProvider(provider: CapabilitiesProvider): void; | Parameter | Type | Description | | --- | --- | --- | -| provider | CapabilitiesProvider | | +| provider | CapabilitiesProvider | | Returns: -`void` +void ## Example @@ -41,6 +41,5 @@ public setup(core: CoreSetup, deps: {}) { } }); } - ``` diff --git a/docs/development/core/server/kibana-plugin-core-server.capabilitiessetup.registerswitcher.md b/docs/development/core/server/kibana-plugin-core-server.capabilitiessetup.registerswitcher.md index 715f15ec812a3..c07703c1f365f 100644 --- a/docs/development/core/server/kibana-plugin-core-server.capabilitiessetup.registerswitcher.md +++ b/docs/development/core/server/kibana-plugin-core-server.capabilitiessetup.registerswitcher.md @@ -18,11 +18,11 @@ registerSwitcher(switcher: CapabilitiesSwitcher): void; | Parameter | Type | Description | | --- | --- | --- | -| switcher | CapabilitiesSwitcher | | +| switcher | CapabilitiesSwitcher | | Returns: -`void` +void ## Example @@ -54,6 +54,5 @@ public setup(core: CoreSetup, deps: {}) { return {}; // All capabilities will remain unchanged. }); } - ``` diff --git a/docs/development/core/server/kibana-plugin-core-server.capabilitiesstart.resolvecapabilities.md b/docs/development/core/server/kibana-plugin-core-server.capabilitiesstart.resolvecapabilities.md index d0e02499c580e..a9dc279526065 100644 --- a/docs/development/core/server/kibana-plugin-core-server.capabilitiesstart.resolvecapabilities.md +++ b/docs/development/core/server/kibana-plugin-core-server.capabilitiesstart.resolvecapabilities.md @@ -16,10 +16,10 @@ resolveCapabilities(request: KibanaRequest, options?: ResolveCapabilitiesOptions | Parameter | Type | Description | | --- | --- | --- | -| request | KibanaRequest | | -| options | ResolveCapabilitiesOptions | | +| request | KibanaRequest | | +| options | ResolveCapabilitiesOptions | | Returns: -`Promise` +Promise<Capabilities> diff --git a/docs/development/core/server/kibana-plugin-core-server.configdeprecationdetails.md b/docs/development/core/server/kibana-plugin-core-server.configdeprecationdetails.md index d1ccbb0b6164a..6065d1fc1889f 100644 --- a/docs/development/core/server/kibana-plugin-core-server.configdeprecationdetails.md +++ b/docs/development/core/server/kibana-plugin-core-server.configdeprecationdetails.md @@ -10,11 +10,12 @@ ```typescript export interface ConfigDeprecationDetails extends BaseDeprecationDetails ``` +Extends: BaseDeprecationDetails ## Properties | Property | Type | Description | | --- | --- | --- | -| [configPath](./kibana-plugin-core-server.configdeprecationdetails.configpath.md) | string | | -| [deprecationType](./kibana-plugin-core-server.configdeprecationdetails.deprecationtype.md) | 'config' | | +| [configPath](./kibana-plugin-core-server.configdeprecationdetails.configpath.md) | string | | +| [deprecationType](./kibana-plugin-core-server.configdeprecationdetails.deprecationtype.md) | 'config' | | diff --git a/docs/development/core/server/kibana-plugin-core-server.contextsetup.createcontextcontainer.md b/docs/development/core/server/kibana-plugin-core-server.contextsetup.createcontextcontainer.md index 0d12fc16af423..5e60dd7e2ffd7 100644 --- a/docs/development/core/server/kibana-plugin-core-server.contextsetup.createcontextcontainer.md +++ b/docs/development/core/server/kibana-plugin-core-server.contextsetup.createcontextcontainer.md @@ -13,5 +13,5 @@ createContextContainer(): IContextContainer; ``` Returns: -`IContextContainer` +IContextContainer diff --git a/docs/development/core/server/kibana-plugin-core-server.contextsetup.md b/docs/development/core/server/kibana-plugin-core-server.contextsetup.md index df3ee924fbcca..a15adccc97714 100644 --- a/docs/development/core/server/kibana-plugin-core-server.contextsetup.md +++ b/docs/development/core/server/kibana-plugin-core-server.contextsetup.md @@ -68,7 +68,6 @@ class MyPlugin { } } } - ``` ## Example @@ -127,7 +126,6 @@ class VizRenderingPlugin { }; } } - ``` ## Methods diff --git a/docs/development/core/server/kibana-plugin-core-server.corepreboot.md b/docs/development/core/server/kibana-plugin-core-server.corepreboot.md index 475b5f109d58c..3ac97d2ca3b37 100644 --- a/docs/development/core/server/kibana-plugin-core-server.corepreboot.md +++ b/docs/development/core/server/kibana-plugin-core-server.corepreboot.md @@ -16,7 +16,7 @@ export interface CorePreboot | Property | Type | Description | | --- | --- | --- | -| [elasticsearch](./kibana-plugin-core-server.corepreboot.elasticsearch.md) | ElasticsearchServicePreboot | [ElasticsearchServicePreboot](./kibana-plugin-core-server.elasticsearchservicepreboot.md) | -| [http](./kibana-plugin-core-server.corepreboot.http.md) | HttpServicePreboot | [HttpServicePreboot](./kibana-plugin-core-server.httpservicepreboot.md) | -| [preboot](./kibana-plugin-core-server.corepreboot.preboot.md) | PrebootServicePreboot | [PrebootServicePreboot](./kibana-plugin-core-server.prebootservicepreboot.md) | +| [elasticsearch](./kibana-plugin-core-server.corepreboot.elasticsearch.md) | ElasticsearchServicePreboot | [ElasticsearchServicePreboot](./kibana-plugin-core-server.elasticsearchservicepreboot.md) | +| [http](./kibana-plugin-core-server.corepreboot.http.md) | HttpServicePreboot | [HttpServicePreboot](./kibana-plugin-core-server.httpservicepreboot.md) | +| [preboot](./kibana-plugin-core-server.corepreboot.preboot.md) | PrebootServicePreboot | [PrebootServicePreboot](./kibana-plugin-core-server.prebootservicepreboot.md) | 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 b03101b4d9fe6..f4a8ece97e013 100644 --- a/docs/development/core/server/kibana-plugin-core-server.coresetup.md +++ b/docs/development/core/server/kibana-plugin-core-server.coresetup.md @@ -16,17 +16,17 @@ 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) | -| [executionContext](./kibana-plugin-core-server.coresetup.executioncontext.md) | ExecutionContextSetup | [ExecutionContextSetup](./kibana-plugin-core-server.executioncontextsetup.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) | -| [i18n](./kibana-plugin-core-server.coresetup.i18n.md) | I18nServiceSetup | [I18nServiceSetup](./kibana-plugin-core-server.i18nservicesetup.md) | -| [logging](./kibana-plugin-core-server.coresetup.logging.md) | LoggingServiceSetup | [LoggingServiceSetup](./kibana-plugin-core-server.loggingservicesetup.md) | -| [metrics](./kibana-plugin-core-server.coresetup.metrics.md) | MetricsServiceSetup | [MetricsServiceSetup](./kibana-plugin-core-server.metricsservicesetup.md) | -| [savedObjects](./kibana-plugin-core-server.coresetup.savedobjects.md) | SavedObjectsServiceSetup | [SavedObjectsServiceSetup](./kibana-plugin-core-server.savedobjectsservicesetup.md) | -| [status](./kibana-plugin-core-server.coresetup.status.md) | StatusServiceSetup | [StatusServiceSetup](./kibana-plugin-core-server.statusservicesetup.md) | -| [uiSettings](./kibana-plugin-core-server.coresetup.uisettings.md) | UiSettingsServiceSetup | [UiSettingsServiceSetup](./kibana-plugin-core-server.uisettingsservicesetup.md) | +| [capabilities](./kibana-plugin-core-server.coresetup.capabilities.md) | CapabilitiesSetup | [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) | +| [executionContext](./kibana-plugin-core-server.coresetup.executioncontext.md) | ExecutionContextSetup | [ExecutionContextSetup](./kibana-plugin-core-server.executioncontextsetup.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) | +| [i18n](./kibana-plugin-core-server.coresetup.i18n.md) | I18nServiceSetup | [I18nServiceSetup](./kibana-plugin-core-server.i18nservicesetup.md) | +| [logging](./kibana-plugin-core-server.coresetup.logging.md) | LoggingServiceSetup | [LoggingServiceSetup](./kibana-plugin-core-server.loggingservicesetup.md) | +| [metrics](./kibana-plugin-core-server.coresetup.metrics.md) | MetricsServiceSetup | [MetricsServiceSetup](./kibana-plugin-core-server.metricsservicesetup.md) | +| [savedObjects](./kibana-plugin-core-server.coresetup.savedobjects.md) | SavedObjectsServiceSetup | [SavedObjectsServiceSetup](./kibana-plugin-core-server.savedobjectsservicesetup.md) | +| [status](./kibana-plugin-core-server.coresetup.status.md) | StatusServiceSetup | [StatusServiceSetup](./kibana-plugin-core-server.statusservicesetup.md) | +| [uiSettings](./kibana-plugin-core-server.coresetup.uisettings.md) | UiSettingsServiceSetup | [UiSettingsServiceSetup](./kibana-plugin-core-server.uisettingsservicesetup.md) | diff --git a/docs/development/core/server/kibana-plugin-core-server.corestart.md b/docs/development/core/server/kibana-plugin-core-server.corestart.md index d7aaba9149cf5..8a4a9b2c33f4b 100644 --- a/docs/development/core/server/kibana-plugin-core-server.corestart.md +++ b/docs/development/core/server/kibana-plugin-core-server.corestart.md @@ -16,11 +16,11 @@ export interface CoreStart | Property | Type | Description | | --- | --- | --- | -| [capabilities](./kibana-plugin-core-server.corestart.capabilities.md) | CapabilitiesStart | [CapabilitiesStart](./kibana-plugin-core-server.capabilitiesstart.md) | -| [elasticsearch](./kibana-plugin-core-server.corestart.elasticsearch.md) | ElasticsearchServiceStart | [ElasticsearchServiceStart](./kibana-plugin-core-server.elasticsearchservicestart.md) | -| [executionContext](./kibana-plugin-core-server.corestart.executioncontext.md) | ExecutionContextStart | [ExecutionContextStart](./kibana-plugin-core-server.executioncontextstart.md) | -| [http](./kibana-plugin-core-server.corestart.http.md) | HttpServiceStart | [HttpServiceStart](./kibana-plugin-core-server.httpservicestart.md) | -| [metrics](./kibana-plugin-core-server.corestart.metrics.md) | MetricsServiceStart | [MetricsServiceStart](./kibana-plugin-core-server.metricsservicestart.md) | -| [savedObjects](./kibana-plugin-core-server.corestart.savedobjects.md) | SavedObjectsServiceStart | [SavedObjectsServiceStart](./kibana-plugin-core-server.savedobjectsservicestart.md) | -| [uiSettings](./kibana-plugin-core-server.corestart.uisettings.md) | UiSettingsServiceStart | [UiSettingsServiceStart](./kibana-plugin-core-server.uisettingsservicestart.md) | +| [capabilities](./kibana-plugin-core-server.corestart.capabilities.md) | CapabilitiesStart | [CapabilitiesStart](./kibana-plugin-core-server.capabilitiesstart.md) | +| [elasticsearch](./kibana-plugin-core-server.corestart.elasticsearch.md) | ElasticsearchServiceStart | [ElasticsearchServiceStart](./kibana-plugin-core-server.elasticsearchservicestart.md) | +| [executionContext](./kibana-plugin-core-server.corestart.executioncontext.md) | ExecutionContextStart | [ExecutionContextStart](./kibana-plugin-core-server.executioncontextstart.md) | +| [http](./kibana-plugin-core-server.corestart.http.md) | HttpServiceStart | [HttpServiceStart](./kibana-plugin-core-server.httpservicestart.md) | +| [metrics](./kibana-plugin-core-server.corestart.metrics.md) | MetricsServiceStart | [MetricsServiceStart](./kibana-plugin-core-server.metricsservicestart.md) | +| [savedObjects](./kibana-plugin-core-server.corestart.savedobjects.md) | SavedObjectsServiceStart | [SavedObjectsServiceStart](./kibana-plugin-core-server.savedobjectsservicestart.md) | +| [uiSettings](./kibana-plugin-core-server.corestart.uisettings.md) | UiSettingsServiceStart | [UiSettingsServiceStart](./kibana-plugin-core-server.uisettingsservicestart.md) | diff --git a/docs/development/core/server/kibana-plugin-core-server.corestatus.md b/docs/development/core/server/kibana-plugin-core-server.corestatus.md index 3fde86a18c58b..8d44ab49dfcb7 100644 --- a/docs/development/core/server/kibana-plugin-core-server.corestatus.md +++ b/docs/development/core/server/kibana-plugin-core-server.corestatus.md @@ -16,6 +16,6 @@ export interface CoreStatus | Property | Type | Description | | --- | --- | --- | -| [elasticsearch](./kibana-plugin-core-server.corestatus.elasticsearch.md) | ServiceStatus | | -| [savedObjects](./kibana-plugin-core-server.corestatus.savedobjects.md) | ServiceStatus | | +| [elasticsearch](./kibana-plugin-core-server.corestatus.elasticsearch.md) | ServiceStatus | | +| [savedObjects](./kibana-plugin-core-server.corestatus.savedobjects.md) | ServiceStatus | | diff --git a/docs/development/core/server/kibana-plugin-core-server.countresponse.md b/docs/development/core/server/kibana-plugin-core-server.countresponse.md index f8664f4878f46..53793dc87bf33 100644 --- a/docs/development/core/server/kibana-plugin-core-server.countresponse.md +++ b/docs/development/core/server/kibana-plugin-core-server.countresponse.md @@ -15,6 +15,6 @@ export interface CountResponse | Property | Type | Description | | --- | --- | --- | -| [\_shards](./kibana-plugin-core-server.countresponse._shards.md) | ShardsInfo | | -| [count](./kibana-plugin-core-server.countresponse.count.md) | number | | +| [\_shards](./kibana-plugin-core-server.countresponse._shards.md) | ShardsInfo | | +| [count](./kibana-plugin-core-server.countresponse.count.md) | number | | diff --git a/docs/development/core/server/kibana-plugin-core-server.cspconfig.__private_.md b/docs/development/core/server/kibana-plugin-core-server.cspconfig.__private_.md deleted file mode 100644 index 217066481d33c..0000000000000 --- a/docs/development/core/server/kibana-plugin-core-server.cspconfig.__private_.md +++ /dev/null @@ -1,11 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [CspConfig](./kibana-plugin-core-server.cspconfig.md) > ["\#private"](./kibana-plugin-core-server.cspconfig.__private_.md) - -## CspConfig."\#private" property - -Signature: - -```typescript -#private; -``` diff --git a/docs/development/core/server/kibana-plugin-core-server.cspconfig.md b/docs/development/core/server/kibana-plugin-core-server.cspconfig.md index 46f24dfda6739..3d2f4bb683742 100644 --- a/docs/development/core/server/kibana-plugin-core-server.cspconfig.md +++ b/docs/development/core/server/kibana-plugin-core-server.cspconfig.md @@ -11,6 +11,7 @@ CSP configuration for use in Kibana. ```typescript export declare class CspConfig implements ICspConfig ``` +Implements: ICspConfig ## Remarks @@ -20,10 +21,9 @@ The constructor for this class is marked as internal. Third-party code should no | Property | Modifiers | Type | Description | | --- | --- | --- | --- | -| ["\#private"](./kibana-plugin-core-server.cspconfig.__private_.md) | | | | -| [DEFAULT](./kibana-plugin-core-server.cspconfig.default.md) | static | CspConfig | | -| [disableEmbedding](./kibana-plugin-core-server.cspconfig.disableembedding.md) | | boolean | | -| [header](./kibana-plugin-core-server.cspconfig.header.md) | | string | | -| [strict](./kibana-plugin-core-server.cspconfig.strict.md) | | boolean | | -| [warnLegacyBrowsers](./kibana-plugin-core-server.cspconfig.warnlegacybrowsers.md) | | boolean | | +| [DEFAULT](./kibana-plugin-core-server.cspconfig.default.md) | static | CspConfig | | +| [disableEmbedding](./kibana-plugin-core-server.cspconfig.disableembedding.md) | | boolean | | +| [header](./kibana-plugin-core-server.cspconfig.header.md) | | string | | +| [strict](./kibana-plugin-core-server.cspconfig.strict.md) | | boolean | | +| [warnLegacyBrowsers](./kibana-plugin-core-server.cspconfig.warnlegacybrowsers.md) | | boolean | | diff --git a/docs/development/core/server/kibana-plugin-core-server.customhttpresponseoptions.md b/docs/development/core/server/kibana-plugin-core-server.customhttpresponseoptions.md index 82089c831d718..84cb55f5c1054 100644 --- a/docs/development/core/server/kibana-plugin-core-server.customhttpresponseoptions.md +++ b/docs/development/core/server/kibana-plugin-core-server.customhttpresponseoptions.md @@ -16,8 +16,8 @@ export interface CustomHttpResponseOptionsT | HTTP message to send to the client | -| [bypassErrorFormat](./kibana-plugin-core-server.customhttpresponseoptions.bypasserrorformat.md) | boolean | Bypass the default error formatting | -| [headers](./kibana-plugin-core-server.customhttpresponseoptions.headers.md) | ResponseHeaders | HTTP Headers with additional information about response | -| [statusCode](./kibana-plugin-core-server.customhttpresponseoptions.statuscode.md) | number | | +| [body?](./kibana-plugin-core-server.customhttpresponseoptions.body.md) | T | (Optional) HTTP message to send to the client | +| [bypassErrorFormat?](./kibana-plugin-core-server.customhttpresponseoptions.bypasserrorformat.md) | boolean | (Optional) Bypass the default error formatting | +| [headers?](./kibana-plugin-core-server.customhttpresponseoptions.headers.md) | ResponseHeaders | (Optional) HTTP Headers with additional information about response | +| [statusCode](./kibana-plugin-core-server.customhttpresponseoptions.statuscode.md) | number | | diff --git a/docs/development/core/server/kibana-plugin-core-server.deletedocumentresponse.md b/docs/development/core/server/kibana-plugin-core-server.deletedocumentresponse.md index e8ac7d2fd8ec1..fe6712ea3b61c 100644 --- a/docs/development/core/server/kibana-plugin-core-server.deletedocumentresponse.md +++ b/docs/development/core/server/kibana-plugin-core-server.deletedocumentresponse.md @@ -15,12 +15,12 @@ export interface DeleteDocumentResponse | Property | Type | Description | | --- | --- | --- | -| [\_id](./kibana-plugin-core-server.deletedocumentresponse._id.md) | string | | -| [\_index](./kibana-plugin-core-server.deletedocumentresponse._index.md) | string | | -| [\_shards](./kibana-plugin-core-server.deletedocumentresponse._shards.md) | ShardsResponse | | -| [\_type](./kibana-plugin-core-server.deletedocumentresponse._type.md) | string | | -| [\_version](./kibana-plugin-core-server.deletedocumentresponse._version.md) | number | | -| [error](./kibana-plugin-core-server.deletedocumentresponse.error.md) | {
type: string;
} | | -| [found](./kibana-plugin-core-server.deletedocumentresponse.found.md) | boolean | | -| [result](./kibana-plugin-core-server.deletedocumentresponse.result.md) | string | | +| [\_id](./kibana-plugin-core-server.deletedocumentresponse._id.md) | string | | +| [\_index](./kibana-plugin-core-server.deletedocumentresponse._index.md) | string | | +| [\_shards](./kibana-plugin-core-server.deletedocumentresponse._shards.md) | ShardsResponse | | +| [\_type](./kibana-plugin-core-server.deletedocumentresponse._type.md) | string | | +| [\_version](./kibana-plugin-core-server.deletedocumentresponse._version.md) | number | | +| [error?](./kibana-plugin-core-server.deletedocumentresponse.error.md) | { type: string; } | (Optional) | +| [found](./kibana-plugin-core-server.deletedocumentresponse.found.md) | boolean | | +| [result](./kibana-plugin-core-server.deletedocumentresponse.result.md) | string | | diff --git a/docs/development/core/server/kibana-plugin-core-server.deprecationsclient.md b/docs/development/core/server/kibana-plugin-core-server.deprecationsclient.md index 920dc820f7f81..0e724e251b266 100644 --- a/docs/development/core/server/kibana-plugin-core-server.deprecationsclient.md +++ b/docs/development/core/server/kibana-plugin-core-server.deprecationsclient.md @@ -16,5 +16,5 @@ export interface DeprecationsClient | Property | Type | Description | | --- | --- | --- | -| [getAllDeprecations](./kibana-plugin-core-server.deprecationsclient.getalldeprecations.md) | () => Promise<DomainDeprecationDetails[]> | | +| [getAllDeprecations](./kibana-plugin-core-server.deprecationsclient.getalldeprecations.md) | () => Promise<DomainDeprecationDetails\[\]> | | diff --git a/docs/development/core/server/kibana-plugin-core-server.deprecationsettings.md b/docs/development/core/server/kibana-plugin-core-server.deprecationsettings.md index 90fd8192b544b..917ca933d63a1 100644 --- a/docs/development/core/server/kibana-plugin-core-server.deprecationsettings.md +++ b/docs/development/core/server/kibana-plugin-core-server.deprecationsettings.md @@ -16,6 +16,6 @@ export interface DeprecationSettings | Property | Type | Description | | --- | --- | --- | -| [docLinksKey](./kibana-plugin-core-server.deprecationsettings.doclinkskey.md) | string | Key to documentation links | -| [message](./kibana-plugin-core-server.deprecationsettings.message.md) | string | Deprecation message | +| [docLinksKey](./kibana-plugin-core-server.deprecationsettings.doclinkskey.md) | string | Key to documentation links | +| [message](./kibana-plugin-core-server.deprecationsettings.message.md) | string | Deprecation message | diff --git a/docs/development/core/server/kibana-plugin-core-server.deprecationsservicesetup.md b/docs/development/core/server/kibana-plugin-core-server.deprecationsservicesetup.md index 7b2cbdecd146a..1fcefeea9b0ba 100644 --- a/docs/development/core/server/kibana-plugin-core-server.deprecationsservicesetup.md +++ b/docs/development/core/server/kibana-plugin-core-server.deprecationsservicesetup.md @@ -75,12 +75,11 @@ export class Plugin() { core.deprecations.registerDeprecations({ getDeprecations }); } } - ``` ## Properties | Property | Type | Description | | --- | --- | --- | -| [registerDeprecations](./kibana-plugin-core-server.deprecationsservicesetup.registerdeprecations.md) | (deprecationContext: RegisterDeprecationsConfig) => void | | +| [registerDeprecations](./kibana-plugin-core-server.deprecationsservicesetup.registerdeprecations.md) | (deprecationContext: RegisterDeprecationsConfig) => void | | diff --git a/docs/development/core/server/kibana-plugin-core-server.discoveredplugin.md b/docs/development/core/server/kibana-plugin-core-server.discoveredplugin.md index 042f2d1485618..cf98a0cd68dbd 100644 --- a/docs/development/core/server/kibana-plugin-core-server.discoveredplugin.md +++ b/docs/development/core/server/kibana-plugin-core-server.discoveredplugin.md @@ -16,10 +16,10 @@ export interface DiscoveredPlugin | Property | Type | Description | | --- | --- | --- | -| [configPath](./kibana-plugin-core-server.discoveredplugin.configpath.md) | ConfigPath | Root configuration path used by the plugin, defaults to "id" in snake\_case format. | -| [id](./kibana-plugin-core-server.discoveredplugin.id.md) | PluginName | Identifier of the plugin. | -| [optionalPlugins](./kibana-plugin-core-server.discoveredplugin.optionalplugins.md) | readonly PluginName[] | An optional list of the other plugins that if installed and enabled \*\*may be\*\* leveraged by this plugin for some additional functionality but otherwise are not required for this plugin to work properly. | -| [requiredBundles](./kibana-plugin-core-server.discoveredplugin.requiredbundles.md) | readonly PluginName[] | List of plugin ids that this plugin's UI code imports modules from that are not in requiredPlugins. | -| [requiredPlugins](./kibana-plugin-core-server.discoveredplugin.requiredplugins.md) | readonly PluginName[] | An optional list of the other plugins that \*\*must be\*\* installed and enabled for this plugin to function properly. | -| [type](./kibana-plugin-core-server.discoveredplugin.type.md) | PluginType | Type of the plugin, defaults to standard. | +| [configPath](./kibana-plugin-core-server.discoveredplugin.configpath.md) | ConfigPath | Root configuration path used by the plugin, defaults to "id" in snake\_case format. | +| [id](./kibana-plugin-core-server.discoveredplugin.id.md) | PluginName | Identifier of the plugin. | +| [optionalPlugins](./kibana-plugin-core-server.discoveredplugin.optionalplugins.md) | readonly PluginName\[\] | An optional list of the other plugins that if installed and enabled \*\*may be\*\* leveraged by this plugin for some additional functionality but otherwise are not required for this plugin to work properly. | +| [requiredBundles](./kibana-plugin-core-server.discoveredplugin.requiredbundles.md) | readonly PluginName\[\] | List of plugin ids that this plugin's UI code imports modules from that are not in requiredPlugins. | +| [requiredPlugins](./kibana-plugin-core-server.discoveredplugin.requiredplugins.md) | readonly PluginName\[\] | An optional list of the other plugins that \*\*must be\*\* installed and enabled for this plugin to function properly. | +| [type](./kibana-plugin-core-server.discoveredplugin.type.md) | PluginType | Type of the plugin, defaults to standard. | diff --git a/docs/development/core/server/kibana-plugin-core-server.elasticsearchconfig._constructor_.md b/docs/development/core/server/kibana-plugin-core-server.elasticsearchconfig._constructor_.md index 6661970725f75..a8d9f1183b3e5 100644 --- a/docs/development/core/server/kibana-plugin-core-server.elasticsearchconfig._constructor_.md +++ b/docs/development/core/server/kibana-plugin-core-server.elasticsearchconfig._constructor_.md @@ -16,5 +16,5 @@ constructor(rawConfig: ElasticsearchConfigType); | Parameter | Type | Description | | --- | --- | --- | -| rawConfig | ElasticsearchConfigType | | +| rawConfig | ElasticsearchConfigType | | diff --git a/docs/development/core/server/kibana-plugin-core-server.elasticsearchconfig.md b/docs/development/core/server/kibana-plugin-core-server.elasticsearchconfig.md index a9ed614ba7552..c75f6377f4038 100644 --- a/docs/development/core/server/kibana-plugin-core-server.elasticsearchconfig.md +++ b/docs/development/core/server/kibana-plugin-core-server.elasticsearchconfig.md @@ -22,20 +22,20 @@ export declare class ElasticsearchConfig | Property | Modifiers | Type | Description | | --- | --- | --- | --- | -| [apiVersion](./kibana-plugin-core-server.elasticsearchconfig.apiversion.md) | | string | Version of the Elasticsearch (6.7, 7.1 or master) client will be connecting to. | -| [customHeaders](./kibana-plugin-core-server.elasticsearchconfig.customheaders.md) | | ElasticsearchConfigType['customHeaders'] | Header names and values to send to Elasticsearch with every request. These headers cannot be overwritten by client-side headers and aren't affected by requestHeadersWhitelist configuration. | -| [healthCheckDelay](./kibana-plugin-core-server.elasticsearchconfig.healthcheckdelay.md) | | Duration | The interval between health check requests Kibana sends to the Elasticsearch. | -| [hosts](./kibana-plugin-core-server.elasticsearchconfig.hosts.md) | | string[] | Hosts that the client will connect to. If sniffing is enabled, this list will be used as seeds to discover the rest of your cluster. | -| [ignoreVersionMismatch](./kibana-plugin-core-server.elasticsearchconfig.ignoreversionmismatch.md) | | boolean | Whether to allow kibana to connect to a non-compatible elasticsearch node. | -| [password](./kibana-plugin-core-server.elasticsearchconfig.password.md) | | string | If Elasticsearch is protected with basic authentication, this setting provides the password that the Kibana server uses to perform its administrative functions. | -| [pingTimeout](./kibana-plugin-core-server.elasticsearchconfig.pingtimeout.md) | | Duration | Timeout after which PING HTTP request will be aborted and retried. | -| [requestHeadersWhitelist](./kibana-plugin-core-server.elasticsearchconfig.requestheaderswhitelist.md) | | string[] | List of Kibana client-side headers to send to Elasticsearch when request scoped cluster client is used. If this is an empty array then \*no\* client-side will be sent. | -| [requestTimeout](./kibana-plugin-core-server.elasticsearchconfig.requesttimeout.md) | | Duration | Timeout after which HTTP request will be aborted and retried. | -| [serviceAccountToken](./kibana-plugin-core-server.elasticsearchconfig.serviceaccounttoken.md) | | string | If Elasticsearch security features are enabled, this setting provides the service account token that the Kibana server users to perform its administrative functions.This is an alternative to specifying a username and password. | -| [shardTimeout](./kibana-plugin-core-server.elasticsearchconfig.shardtimeout.md) | | Duration | Timeout for Elasticsearch to wait for responses from shards. Set to 0 to disable. | -| [sniffInterval](./kibana-plugin-core-server.elasticsearchconfig.sniffinterval.md) | | false | Duration | Interval to perform a sniff operation and make sure the list of nodes is complete. If false then sniffing is disabled. | -| [sniffOnConnectionFault](./kibana-plugin-core-server.elasticsearchconfig.sniffonconnectionfault.md) | | boolean | Specifies whether the client should immediately sniff for a more current list of nodes when a connection dies. | -| [sniffOnStart](./kibana-plugin-core-server.elasticsearchconfig.sniffonstart.md) | | boolean | Specifies whether the client should attempt to detect the rest of the cluster when it is first instantiated. | -| [ssl](./kibana-plugin-core-server.elasticsearchconfig.ssl.md) | | Pick<SslConfigSchema, Exclude<keyof SslConfigSchema, 'certificateAuthorities' | 'keystore' | 'truststore'>> & {
certificateAuthorities?: string[];
} | Set of settings configure SSL connection between Kibana and Elasticsearch that are required when xpack.ssl.verification_mode in Elasticsearch is set to either certificate or full. | -| [username](./kibana-plugin-core-server.elasticsearchconfig.username.md) | | string | If Elasticsearch is protected with basic authentication, this setting provides the username that the Kibana server uses to perform its administrative functions. Cannot be used in conjunction with serviceAccountToken. | +| [apiVersion](./kibana-plugin-core-server.elasticsearchconfig.apiversion.md) | | string | Version of the Elasticsearch (6.7, 7.1 or master) client will be connecting to. | +| [customHeaders](./kibana-plugin-core-server.elasticsearchconfig.customheaders.md) | | ElasticsearchConfigType\['customHeaders'\] | Header names and values to send to Elasticsearch with every request. These headers cannot be overwritten by client-side headers and aren't affected by requestHeadersWhitelist configuration. | +| [healthCheckDelay](./kibana-plugin-core-server.elasticsearchconfig.healthcheckdelay.md) | | Duration | The interval between health check requests Kibana sends to the Elasticsearch. | +| [hosts](./kibana-plugin-core-server.elasticsearchconfig.hosts.md) | | string\[\] | Hosts that the client will connect to. If sniffing is enabled, this list will be used as seeds to discover the rest of your cluster. | +| [ignoreVersionMismatch](./kibana-plugin-core-server.elasticsearchconfig.ignoreversionmismatch.md) | | boolean | Whether to allow kibana to connect to a non-compatible elasticsearch node. | +| [password?](./kibana-plugin-core-server.elasticsearchconfig.password.md) | | string | (Optional) If Elasticsearch is protected with basic authentication, this setting provides the password that the Kibana server uses to perform its administrative functions. | +| [pingTimeout](./kibana-plugin-core-server.elasticsearchconfig.pingtimeout.md) | | Duration | Timeout after which PING HTTP request will be aborted and retried. | +| [requestHeadersWhitelist](./kibana-plugin-core-server.elasticsearchconfig.requestheaderswhitelist.md) | | string\[\] | List of Kibana client-side headers to send to Elasticsearch when request scoped cluster client is used. If this is an empty array then \*no\* client-side will be sent. | +| [requestTimeout](./kibana-plugin-core-server.elasticsearchconfig.requesttimeout.md) | | Duration | Timeout after which HTTP request will be aborted and retried. | +| [serviceAccountToken?](./kibana-plugin-core-server.elasticsearchconfig.serviceaccounttoken.md) | | string | (Optional) If Elasticsearch security features are enabled, this setting provides the service account token that the Kibana server users to perform its administrative functions.This is an alternative to specifying a username and password. | +| [shardTimeout](./kibana-plugin-core-server.elasticsearchconfig.shardtimeout.md) | | Duration | Timeout for Elasticsearch to wait for responses from shards. Set to 0 to disable. | +| [sniffInterval](./kibana-plugin-core-server.elasticsearchconfig.sniffinterval.md) | | false \| Duration | Interval to perform a sniff operation and make sure the list of nodes is complete. If false then sniffing is disabled. | +| [sniffOnConnectionFault](./kibana-plugin-core-server.elasticsearchconfig.sniffonconnectionfault.md) | | boolean | Specifies whether the client should immediately sniff for a more current list of nodes when a connection dies. | +| [sniffOnStart](./kibana-plugin-core-server.elasticsearchconfig.sniffonstart.md) | | boolean | Specifies whether the client should attempt to detect the rest of the cluster when it is first instantiated. | +| [ssl](./kibana-plugin-core-server.elasticsearchconfig.ssl.md) | | Pick<SslConfigSchema, Exclude<keyof SslConfigSchema, 'certificateAuthorities' \| 'keystore' \| 'truststore'>> & { certificateAuthorities?: string\[\]; } | Set of settings configure SSL connection between Kibana and Elasticsearch that are required when xpack.ssl.verification_mode in Elasticsearch is set to either certificate or full. | +| [username?](./kibana-plugin-core-server.elasticsearchconfig.username.md) | | string | (Optional) If Elasticsearch is protected with basic authentication, this setting provides the username that the Kibana server uses to perform its administrative functions. Cannot be used in conjunction with serviceAccountToken. | diff --git a/docs/development/core/server/kibana-plugin-core-server.elasticsearchconfigpreboot.md b/docs/development/core/server/kibana-plugin-core-server.elasticsearchconfigpreboot.md index bbccea80b672f..d7d3e8d70e8d7 100644 --- a/docs/development/core/server/kibana-plugin-core-server.elasticsearchconfigpreboot.md +++ b/docs/development/core/server/kibana-plugin-core-server.elasticsearchconfigpreboot.md @@ -16,6 +16,6 @@ export interface ElasticsearchConfigPreboot | Property | Type | Description | | --- | --- | --- | -| [credentialsSpecified](./kibana-plugin-core-server.elasticsearchconfigpreboot.credentialsspecified.md) | boolean | Indicates whether Elasticsearch configuration includes credentials (username, password or serviceAccountToken). | -| [hosts](./kibana-plugin-core-server.elasticsearchconfigpreboot.hosts.md) | string[] | Hosts that the client will connect to. If sniffing is enabled, this list will be used as seeds to discover the rest of your cluster. | +| [credentialsSpecified](./kibana-plugin-core-server.elasticsearchconfigpreboot.credentialsspecified.md) | boolean | Indicates whether Elasticsearch configuration includes credentials (username, password or serviceAccountToken). | +| [hosts](./kibana-plugin-core-server.elasticsearchconfigpreboot.hosts.md) | string\[\] | Hosts that the client will connect to. If sniffing is enabled, this list will be used as seeds to discover the rest of your cluster. | diff --git a/docs/development/core/server/kibana-plugin-core-server.elasticsearcherrordetails.md b/docs/development/core/server/kibana-plugin-core-server.elasticsearcherrordetails.md index 7dbf9e89f9b7c..570a161db20e0 100644 --- a/docs/development/core/server/kibana-plugin-core-server.elasticsearcherrordetails.md +++ b/docs/development/core/server/kibana-plugin-core-server.elasticsearcherrordetails.md @@ -15,5 +15,5 @@ export interface ElasticsearchErrorDetails | Property | Type | Description | | --- | --- | --- | -| [error](./kibana-plugin-core-server.elasticsearcherrordetails.error.md) | {
type: string;
reason?: string;
} | | +| [error?](./kibana-plugin-core-server.elasticsearcherrordetails.error.md) | { type: string; reason?: string; } | (Optional) | diff --git a/docs/development/core/server/kibana-plugin-core-server.elasticsearchservicepreboot.config.md b/docs/development/core/server/kibana-plugin-core-server.elasticsearchservicepreboot.config.md index 12a32b4544aba..c4284248ea894 100644 --- a/docs/development/core/server/kibana-plugin-core-server.elasticsearchservicepreboot.config.md +++ b/docs/development/core/server/kibana-plugin-core-server.elasticsearchservicepreboot.config.md @@ -17,6 +17,5 @@ readonly config: Readonly; ```js const { hosts, credentialsSpecified } = core.elasticsearch.config; - ``` diff --git a/docs/development/core/server/kibana-plugin-core-server.elasticsearchservicepreboot.createclient.md b/docs/development/core/server/kibana-plugin-core-server.elasticsearchservicepreboot.createclient.md index d14e3e4efa400..070cb7905b585 100644 --- a/docs/development/core/server/kibana-plugin-core-server.elasticsearchservicepreboot.createclient.md +++ b/docs/development/core/server/kibana-plugin-core-server.elasticsearchservicepreboot.createclient.md @@ -18,6 +18,5 @@ readonly createClient: (type: string, clientConfig?: PartialReadonly<ElasticsearchConfigPreboot> | A limited set of Elasticsearch configuration entries. | -| [createClient](./kibana-plugin-core-server.elasticsearchservicepreboot.createclient.md) | (type: string, clientConfig?: Partial<ElasticsearchClientConfig>) => ICustomClusterClient | Create application specific Elasticsearch cluster API client with customized config. See [IClusterClient](./kibana-plugin-core-server.iclusterclient.md). | +| [config](./kibana-plugin-core-server.elasticsearchservicepreboot.config.md) | Readonly<ElasticsearchConfigPreboot> | A limited set of Elasticsearch configuration entries. | +| [createClient](./kibana-plugin-core-server.elasticsearchservicepreboot.createclient.md) | (type: string, clientConfig?: Partial<ElasticsearchClientConfig>) => ICustomClusterClient | Create application specific Elasticsearch cluster API client with customized config. See [IClusterClient](./kibana-plugin-core-server.iclusterclient.md). | diff --git a/docs/development/core/server/kibana-plugin-core-server.elasticsearchservicesetup.md b/docs/development/core/server/kibana-plugin-core-server.elasticsearchservicesetup.md index e6a4161674f5b..d18620992075d 100644 --- a/docs/development/core/server/kibana-plugin-core-server.elasticsearchservicesetup.md +++ b/docs/development/core/server/kibana-plugin-core-server.elasticsearchservicesetup.md @@ -15,5 +15,5 @@ export interface ElasticsearchServiceSetup | Property | Type | Description | | --- | --- | --- | -| [legacy](./kibana-plugin-core-server.elasticsearchservicesetup.legacy.md) | {
readonly config$: Observable<ElasticsearchConfig>;
} | | +| [legacy](./kibana-plugin-core-server.elasticsearchservicesetup.legacy.md) | { readonly config$: Observable<ElasticsearchConfig>; } | | diff --git a/docs/development/core/server/kibana-plugin-core-server.elasticsearchservicestart.client.md b/docs/development/core/server/kibana-plugin-core-server.elasticsearchservicestart.client.md index 591f126c423e3..59f302170c53a 100644 --- a/docs/development/core/server/kibana-plugin-core-server.elasticsearchservicestart.client.md +++ b/docs/development/core/server/kibana-plugin-core-server.elasticsearchservicestart.client.md @@ -17,6 +17,5 @@ readonly client: IClusterClient; ```js const client = core.elasticsearch.client; - ``` diff --git a/docs/development/core/server/kibana-plugin-core-server.elasticsearchservicestart.createclient.md b/docs/development/core/server/kibana-plugin-core-server.elasticsearchservicestart.createclient.md index d4a13812ab533..26930c6f02b32 100644 --- a/docs/development/core/server/kibana-plugin-core-server.elasticsearchservicestart.createclient.md +++ b/docs/development/core/server/kibana-plugin-core-server.elasticsearchservicestart.createclient.md @@ -18,6 +18,5 @@ readonly createClient: (type: string, clientConfig?: PartialIClusterClient | A pre-configured [Elasticsearch client](./kibana-plugin-core-server.iclusterclient.md) | -| [createClient](./kibana-plugin-core-server.elasticsearchservicestart.createclient.md) | (type: string, clientConfig?: Partial<ElasticsearchClientConfig>) => ICustomClusterClient | Create application specific Elasticsearch cluster API client with customized config. See [IClusterClient](./kibana-plugin-core-server.iclusterclient.md). | -| [legacy](./kibana-plugin-core-server.elasticsearchservicestart.legacy.md) | {
readonly config$: Observable<ElasticsearchConfig>;
} | | +| [client](./kibana-plugin-core-server.elasticsearchservicestart.client.md) | IClusterClient | A pre-configured [Elasticsearch client](./kibana-plugin-core-server.iclusterclient.md) | +| [createClient](./kibana-plugin-core-server.elasticsearchservicestart.createclient.md) | (type: string, clientConfig?: Partial<ElasticsearchClientConfig>) => ICustomClusterClient | Create application specific Elasticsearch cluster API client with customized config. See [IClusterClient](./kibana-plugin-core-server.iclusterclient.md). | +| [legacy](./kibana-plugin-core-server.elasticsearchservicestart.legacy.md) | { readonly config$: Observable<ElasticsearchConfig>; } | | diff --git a/docs/development/core/server/kibana-plugin-core-server.elasticsearchstatusmeta.md b/docs/development/core/server/kibana-plugin-core-server.elasticsearchstatusmeta.md index 90aa2f0100d88..82748932c2102 100644 --- a/docs/development/core/server/kibana-plugin-core-server.elasticsearchstatusmeta.md +++ b/docs/development/core/server/kibana-plugin-core-server.elasticsearchstatusmeta.md @@ -15,7 +15,7 @@ export interface ElasticsearchStatusMeta | Property | Type | Description | | --- | --- | --- | -| [incompatibleNodes](./kibana-plugin-core-server.elasticsearchstatusmeta.incompatiblenodes.md) | NodesVersionCompatibility['incompatibleNodes'] | | -| [nodesInfoRequestError](./kibana-plugin-core-server.elasticsearchstatusmeta.nodesinforequesterror.md) | NodesVersionCompatibility['nodesInfoRequestError'] | | -| [warningNodes](./kibana-plugin-core-server.elasticsearchstatusmeta.warningnodes.md) | NodesVersionCompatibility['warningNodes'] | | +| [incompatibleNodes](./kibana-plugin-core-server.elasticsearchstatusmeta.incompatiblenodes.md) | NodesVersionCompatibility\['incompatibleNodes'\] | | +| [nodesInfoRequestError?](./kibana-plugin-core-server.elasticsearchstatusmeta.nodesinforequesterror.md) | NodesVersionCompatibility\['nodesInfoRequestError'\] | (Optional) | +| [warningNodes](./kibana-plugin-core-server.elasticsearchstatusmeta.warningnodes.md) | NodesVersionCompatibility\['warningNodes'\] | | diff --git a/docs/development/core/server/kibana-plugin-core-server.errorhttpresponseoptions.md b/docs/development/core/server/kibana-plugin-core-server.errorhttpresponseoptions.md index 8a3a9e3cc29f4..3a037e71ac1e2 100644 --- a/docs/development/core/server/kibana-plugin-core-server.errorhttpresponseoptions.md +++ b/docs/development/core/server/kibana-plugin-core-server.errorhttpresponseoptions.md @@ -16,6 +16,6 @@ export interface ErrorHttpResponseOptions | Property | Type | Description | | --- | --- | --- | -| [body](./kibana-plugin-core-server.errorhttpresponseoptions.body.md) | ResponseError | HTTP message to send to the client | -| [headers](./kibana-plugin-core-server.errorhttpresponseoptions.headers.md) | ResponseHeaders | HTTP Headers with additional information about response | +| [body?](./kibana-plugin-core-server.errorhttpresponseoptions.body.md) | ResponseError | (Optional) HTTP message to send to the client | +| [headers?](./kibana-plugin-core-server.errorhttpresponseoptions.headers.md) | ResponseHeaders | (Optional) HTTP Headers with additional information about response | diff --git a/docs/development/core/server/kibana-plugin-core-server.eventloopdelaysmonitor.collect.md b/docs/development/core/server/kibana-plugin-core-server.eventloopdelaysmonitor.collect.md index 0e07497baf887..36cb2d2d20944 100644 --- a/docs/development/core/server/kibana-plugin-core-server.eventloopdelaysmonitor.collect.md +++ b/docs/development/core/server/kibana-plugin-core-server.eventloopdelaysmonitor.collect.md @@ -13,7 +13,7 @@ collect(): IntervalHistogram; ``` Returns: -`IntervalHistogram` +IntervalHistogram {IntervalHistogram} diff --git a/docs/development/core/server/kibana-plugin-core-server.eventloopdelaysmonitor.reset.md b/docs/development/core/server/kibana-plugin-core-server.eventloopdelaysmonitor.reset.md index fdba7a79ebda0..a65cc7c99842d 100644 --- a/docs/development/core/server/kibana-plugin-core-server.eventloopdelaysmonitor.reset.md +++ b/docs/development/core/server/kibana-plugin-core-server.eventloopdelaysmonitor.reset.md @@ -13,5 +13,5 @@ reset(): void; ``` Returns: -`void` +void diff --git a/docs/development/core/server/kibana-plugin-core-server.eventloopdelaysmonitor.stop.md b/docs/development/core/server/kibana-plugin-core-server.eventloopdelaysmonitor.stop.md index 25b61434b0061..d63c963b384e6 100644 --- a/docs/development/core/server/kibana-plugin-core-server.eventloopdelaysmonitor.stop.md +++ b/docs/development/core/server/kibana-plugin-core-server.eventloopdelaysmonitor.stop.md @@ -13,5 +13,5 @@ stop(): void; ``` Returns: -`void` +void diff --git a/docs/development/core/server/kibana-plugin-core-server.executioncontextsetup.withcontext.md b/docs/development/core/server/kibana-plugin-core-server.executioncontextsetup.withcontext.md index 87da071203018..2bfef6db2f907 100644 --- a/docs/development/core/server/kibana-plugin-core-server.executioncontextsetup.withcontext.md +++ b/docs/development/core/server/kibana-plugin-core-server.executioncontextsetup.withcontext.md @@ -16,10 +16,10 @@ withContext(context: KibanaExecutionContext | undefined, fn: (...args: any[]) | Parameter | Type | Description | | --- | --- | --- | -| context | KibanaExecutionContext | undefined | | -| fn | (...args: any[]) => R | | +| context | KibanaExecutionContext \| undefined | | +| fn | (...args: any\[\]) => R | | Returns: -`R` +R diff --git a/docs/development/core/server/kibana-plugin-core-server.fakerequest.md b/docs/development/core/server/kibana-plugin-core-server.fakerequest.md index 93ab54f9ba753..6b8bfe33d19c4 100644 --- a/docs/development/core/server/kibana-plugin-core-server.fakerequest.md +++ b/docs/development/core/server/kibana-plugin-core-server.fakerequest.md @@ -16,5 +16,5 @@ export interface FakeRequest | Property | Type | Description | | --- | --- | --- | -| [headers](./kibana-plugin-core-server.fakerequest.headers.md) | Headers | Headers used for authentication against Elasticsearch | +| [headers](./kibana-plugin-core-server.fakerequest.headers.md) | Headers | Headers used for authentication against Elasticsearch | diff --git a/docs/development/core/server/kibana-plugin-core-server.featuredeprecationdetails.md b/docs/development/core/server/kibana-plugin-core-server.featuredeprecationdetails.md index bed3356e36129..c92f352ce7e5e 100644 --- a/docs/development/core/server/kibana-plugin-core-server.featuredeprecationdetails.md +++ b/docs/development/core/server/kibana-plugin-core-server.featuredeprecationdetails.md @@ -10,10 +10,11 @@ ```typescript export interface FeatureDeprecationDetails extends BaseDeprecationDetails ``` +Extends: BaseDeprecationDetails ## Properties | Property | Type | Description | | --- | --- | --- | -| [deprecationType](./kibana-plugin-core-server.featuredeprecationdetails.deprecationtype.md) | 'feature' | undefined | | +| [deprecationType?](./kibana-plugin-core-server.featuredeprecationdetails.deprecationtype.md) | 'feature' \| undefined | (Optional) | diff --git a/docs/development/core/server/kibana-plugin-core-server.getdeprecationscontext.md b/docs/development/core/server/kibana-plugin-core-server.getdeprecationscontext.md index 96dd2ceb524ce..2362966866852 100644 --- a/docs/development/core/server/kibana-plugin-core-server.getdeprecationscontext.md +++ b/docs/development/core/server/kibana-plugin-core-server.getdeprecationscontext.md @@ -15,6 +15,6 @@ export interface GetDeprecationsContext | Property | Type | Description | | --- | --- | --- | -| [esClient](./kibana-plugin-core-server.getdeprecationscontext.esclient.md) | IScopedClusterClient | | -| [savedObjectsClient](./kibana-plugin-core-server.getdeprecationscontext.savedobjectsclient.md) | SavedObjectsClientContract | | +| [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.getresponse.md b/docs/development/core/server/kibana-plugin-core-server.getresponse.md index bab3092c6b1fa..5068be8a5689a 100644 --- a/docs/development/core/server/kibana-plugin-core-server.getresponse.md +++ b/docs/development/core/server/kibana-plugin-core-server.getresponse.md @@ -15,13 +15,13 @@ export interface GetResponse | Property | Type | Description | | --- | --- | --- | -| [\_id](./kibana-plugin-core-server.getresponse._id.md) | string | | -| [\_index](./kibana-plugin-core-server.getresponse._index.md) | string | | -| [\_primary\_term](./kibana-plugin-core-server.getresponse._primary_term.md) | number | | -| [\_routing](./kibana-plugin-core-server.getresponse._routing.md) | string | | -| [\_seq\_no](./kibana-plugin-core-server.getresponse._seq_no.md) | number | | -| [\_source](./kibana-plugin-core-server.getresponse._source.md) | T | | -| [\_type](./kibana-plugin-core-server.getresponse._type.md) | string | | -| [\_version](./kibana-plugin-core-server.getresponse._version.md) | number | | -| [found](./kibana-plugin-core-server.getresponse.found.md) | boolean | | +| [\_id](./kibana-plugin-core-server.getresponse._id.md) | string | | +| [\_index](./kibana-plugin-core-server.getresponse._index.md) | string | | +| [\_primary\_term](./kibana-plugin-core-server.getresponse._primary_term.md) | number | | +| [\_routing?](./kibana-plugin-core-server.getresponse._routing.md) | string | (Optional) | +| [\_seq\_no](./kibana-plugin-core-server.getresponse._seq_no.md) | number | | +| [\_source](./kibana-plugin-core-server.getresponse._source.md) | T | | +| [\_type](./kibana-plugin-core-server.getresponse._type.md) | string | | +| [\_version](./kibana-plugin-core-server.getresponse._version.md) | number | | +| [found](./kibana-plugin-core-server.getresponse.found.md) | boolean | | diff --git a/docs/development/core/server/kibana-plugin-core-server.headers.md b/docs/development/core/server/kibana-plugin-core-server.headers.md deleted file mode 100644 index 5b2c40a81878e..0000000000000 --- a/docs/development/core/server/kibana-plugin-core-server.headers.md +++ /dev/null @@ -1,17 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [Headers](./kibana-plugin-core-server.headers.md) - -## Headers type - -Http request headers to read. - -Signature: - -```typescript -export declare type Headers = { - [header in KnownHeaders]?: string | string[] | undefined; -} & { - [header: string]: string | string[] | undefined; -}; -``` diff --git a/docs/development/core/server/kibana-plugin-core-server.headers_2.md b/docs/development/core/server/kibana-plugin-core-server.headers_2.md new file mode 100644 index 0000000000000..398827f2bf3d1 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.headers_2.md @@ -0,0 +1,17 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [Headers\_2](./kibana-plugin-core-server.headers_2.md) + +## Headers\_2 type + +Http request headers to read. + +Signature: + +```typescript +export declare type Headers = { + [header in KnownHeaders]?: string | string[] | undefined; +} & { + [header: string]: string | string[] | undefined; +}; +``` diff --git a/docs/development/core/server/kibana-plugin-core-server.httpauth.md b/docs/development/core/server/kibana-plugin-core-server.httpauth.md index d9d77809570ab..4b47be615c79c 100644 --- a/docs/development/core/server/kibana-plugin-core-server.httpauth.md +++ b/docs/development/core/server/kibana-plugin-core-server.httpauth.md @@ -15,6 +15,6 @@ export interface HttpAuth | Property | Type | Description | | --- | --- | --- | -| [get](./kibana-plugin-core-server.httpauth.get.md) | GetAuthState | Gets authentication state for a request. Returned by auth interceptor. [GetAuthState](./kibana-plugin-core-server.getauthstate.md) | -| [isAuthenticated](./kibana-plugin-core-server.httpauth.isauthenticated.md) | IsAuthenticated | Returns authentication status for a request. [IsAuthenticated](./kibana-plugin-core-server.isauthenticated.md) | +| [get](./kibana-plugin-core-server.httpauth.get.md) | GetAuthState | Gets authentication state for a request. Returned by auth interceptor. [GetAuthState](./kibana-plugin-core-server.getauthstate.md) | +| [isAuthenticated](./kibana-plugin-core-server.httpauth.isauthenticated.md) | IsAuthenticated | Returns authentication status for a request. [IsAuthenticated](./kibana-plugin-core-server.isauthenticated.md) | diff --git a/docs/development/core/server/kibana-plugin-core-server.httpresources.md b/docs/development/core/server/kibana-plugin-core-server.httpresources.md index 25acffc1a040f..0b1854d7cbcd9 100644 --- a/docs/development/core/server/kibana-plugin-core-server.httpresources.md +++ b/docs/development/core/server/kibana-plugin-core-server.httpresources.md @@ -16,5 +16,5 @@ export interface HttpResources | Property | Type | Description | | --- | --- | --- | -| [register](./kibana-plugin-core-server.httpresources.register.md) | <P, Q, B, Context extends RequestHandlerContext = RequestHandlerContext>(route: RouteConfig<P, Q, B, 'get'>, handler: HttpResourcesRequestHandler<P, Q, B, Context>) => void | To register a route handler executing passed function to form response. | +| [register](./kibana-plugin-core-server.httpresources.register.md) | <P, Q, B, Context extends RequestHandlerContext = RequestHandlerContext>(route: RouteConfig<P, Q, B, 'get'>, handler: HttpResourcesRequestHandler<P, Q, B, Context>) => void | To register a route handler executing passed function to form response. | diff --git a/docs/development/core/server/kibana-plugin-core-server.httpresourcesrenderoptions.md b/docs/development/core/server/kibana-plugin-core-server.httpresourcesrenderoptions.md index 6563e3c636a99..9fcdc1b338783 100644 --- a/docs/development/core/server/kibana-plugin-core-server.httpresourcesrenderoptions.md +++ b/docs/development/core/server/kibana-plugin-core-server.httpresourcesrenderoptions.md @@ -16,5 +16,5 @@ export interface HttpResourcesRenderOptions | Property | Type | Description | | --- | --- | --- | -| [headers](./kibana-plugin-core-server.httpresourcesrenderoptions.headers.md) | ResponseHeaders | HTTP Headers with additional information about response. | +| [headers?](./kibana-plugin-core-server.httpresourcesrenderoptions.headers.md) | ResponseHeaders | (Optional) HTTP Headers with additional information about response. | diff --git a/docs/development/core/server/kibana-plugin-core-server.httpresourcesservicetoolkit.md b/docs/development/core/server/kibana-plugin-core-server.httpresourcesservicetoolkit.md index 1c221e13f534f..05e7af5dcbedf 100644 --- a/docs/development/core/server/kibana-plugin-core-server.httpresourcesservicetoolkit.md +++ b/docs/development/core/server/kibana-plugin-core-server.httpresourcesservicetoolkit.md @@ -16,8 +16,8 @@ export interface HttpResourcesServiceToolkit | Property | Type | Description | | --- | --- | --- | -| [renderAnonymousCoreApp](./kibana-plugin-core-server.httpresourcesservicetoolkit.renderanonymouscoreapp.md) | (options?: HttpResourcesRenderOptions) => Promise<IKibanaResponse> | To respond with HTML page bootstrapping Kibana application without retrieving user-specific information. | -| [renderCoreApp](./kibana-plugin-core-server.httpresourcesservicetoolkit.rendercoreapp.md) | (options?: HttpResourcesRenderOptions) => Promise<IKibanaResponse> | To respond with HTML page bootstrapping Kibana application. | -| [renderHtml](./kibana-plugin-core-server.httpresourcesservicetoolkit.renderhtml.md) | (options: HttpResourcesResponseOptions) => IKibanaResponse | To respond with a custom HTML page. | -| [renderJs](./kibana-plugin-core-server.httpresourcesservicetoolkit.renderjs.md) | (options: HttpResourcesResponseOptions) => IKibanaResponse | To respond with a custom JS script file. | +| [renderAnonymousCoreApp](./kibana-plugin-core-server.httpresourcesservicetoolkit.renderanonymouscoreapp.md) | (options?: HttpResourcesRenderOptions) => Promise<IKibanaResponse> | To respond with HTML page bootstrapping Kibana application without retrieving user-specific information. | +| [renderCoreApp](./kibana-plugin-core-server.httpresourcesservicetoolkit.rendercoreapp.md) | (options?: HttpResourcesRenderOptions) => Promise<IKibanaResponse> | To respond with HTML page bootstrapping Kibana application. | +| [renderHtml](./kibana-plugin-core-server.httpresourcesservicetoolkit.renderhtml.md) | (options: HttpResourcesResponseOptions) => IKibanaResponse | To respond with a custom HTML page. | +| [renderJs](./kibana-plugin-core-server.httpresourcesservicetoolkit.renderjs.md) | (options: HttpResourcesResponseOptions) => IKibanaResponse | To respond with a custom JS script file. | diff --git a/docs/development/core/server/kibana-plugin-core-server.httpresponseoptions.md b/docs/development/core/server/kibana-plugin-core-server.httpresponseoptions.md index 497adc6a5ec5d..9d10d91244157 100644 --- a/docs/development/core/server/kibana-plugin-core-server.httpresponseoptions.md +++ b/docs/development/core/server/kibana-plugin-core-server.httpresponseoptions.md @@ -16,7 +16,7 @@ export interface HttpResponseOptions | Property | Type | Description | | --- | --- | --- | -| [body](./kibana-plugin-core-server.httpresponseoptions.body.md) | HttpResponsePayload | HTTP message to send to the client | -| [bypassErrorFormat](./kibana-plugin-core-server.httpresponseoptions.bypasserrorformat.md) | boolean | Bypass the default error formatting | -| [headers](./kibana-plugin-core-server.httpresponseoptions.headers.md) | ResponseHeaders | HTTP Headers with additional information about response | +| [body?](./kibana-plugin-core-server.httpresponseoptions.body.md) | HttpResponsePayload | (Optional) HTTP message to send to the client | +| [bypassErrorFormat?](./kibana-plugin-core-server.httpresponseoptions.bypasserrorformat.md) | boolean | (Optional) Bypass the default error formatting | +| [headers?](./kibana-plugin-core-server.httpresponseoptions.headers.md) | ResponseHeaders | (Optional) HTTP Headers with additional information about response | diff --git a/docs/development/core/server/kibana-plugin-core-server.httpserverinfo.md b/docs/development/core/server/kibana-plugin-core-server.httpserverinfo.md index 890bfaa834dc5..151cb5d272403 100644 --- a/docs/development/core/server/kibana-plugin-core-server.httpserverinfo.md +++ b/docs/development/core/server/kibana-plugin-core-server.httpserverinfo.md @@ -16,8 +16,8 @@ export interface HttpServerInfo | Property | Type | Description | | --- | --- | --- | -| [hostname](./kibana-plugin-core-server.httpserverinfo.hostname.md) | string | The hostname of the server | -| [name](./kibana-plugin-core-server.httpserverinfo.name.md) | string | The name of the Kibana server | -| [port](./kibana-plugin-core-server.httpserverinfo.port.md) | number | The port the server is listening on | -| [protocol](./kibana-plugin-core-server.httpserverinfo.protocol.md) | 'http' | 'https' | 'socket' | The protocol used by the server | +| [hostname](./kibana-plugin-core-server.httpserverinfo.hostname.md) | string | The hostname of the server | +| [name](./kibana-plugin-core-server.httpserverinfo.name.md) | string | The name of the Kibana server | +| [port](./kibana-plugin-core-server.httpserverinfo.port.md) | number | The port the server is listening on | +| [protocol](./kibana-plugin-core-server.httpserverinfo.protocol.md) | 'http' \| 'https' \| 'socket' | The protocol used by the server | diff --git a/docs/development/core/server/kibana-plugin-core-server.httpservicepreboot.md b/docs/development/core/server/kibana-plugin-core-server.httpservicepreboot.md index ab0fc365fc651..87c62b63014e1 100644 --- a/docs/development/core/server/kibana-plugin-core-server.httpservicepreboot.md +++ b/docs/development/core/server/kibana-plugin-core-server.httpservicepreboot.md @@ -23,7 +23,6 @@ const validate = { id: schema.string(), }), }; - ``` - Declare a function to respond to incoming request. The function will receive `request` object containing request details: url, headers, matched route, as well as validated `params`, `query`, `body`. And `response` object instructing HTTP server to create HTTP response with information sent back to the client as the response body, headers, and HTTP status. Any exception raised during the handler call will generate `500 Server error` response and log error details for further investigation. See below for returning custom error responses. @@ -40,7 +39,6 @@ const handler = async (context: RequestHandlerContext, request: KibanaRequest, r headers: { 'content-type': 'application/json' } }); } - ``` \* - Acquire `preboot` [IRouter](./kibana-plugin-core-server.irouter.md) instance and register route handler for GET request to 'path/{id}' path. @@ -65,15 +63,14 @@ httpPreboot.registerRoutes('my-plugin', (router) => { }); }); }); - ``` ## Properties | Property | Type | Description | | --- | --- | --- | -| [basePath](./kibana-plugin-core-server.httpservicepreboot.basepath.md) | IBasePath | Access or manipulate the Kibana base path See [IBasePath](./kibana-plugin-core-server.ibasepath.md). | -| [getServerInfo](./kibana-plugin-core-server.httpservicepreboot.getserverinfo.md) | () => HttpServerInfo | Provides common [information](./kibana-plugin-core-server.httpserverinfo.md) about the running preboot http server. | +| [basePath](./kibana-plugin-core-server.httpservicepreboot.basepath.md) | IBasePath | Access or manipulate the Kibana base path See [IBasePath](./kibana-plugin-core-server.ibasepath.md). | +| [getServerInfo](./kibana-plugin-core-server.httpservicepreboot.getserverinfo.md) | () => HttpServerInfo | Provides common [information](./kibana-plugin-core-server.httpserverinfo.md) about the running preboot http server. | ## Methods diff --git a/docs/development/core/server/kibana-plugin-core-server.httpservicepreboot.registerroutes.md b/docs/development/core/server/kibana-plugin-core-server.httpservicepreboot.registerroutes.md index c188f0ba0ce94..dd90074fad39a 100644 --- a/docs/development/core/server/kibana-plugin-core-server.httpservicepreboot.registerroutes.md +++ b/docs/development/core/server/kibana-plugin-core-server.httpservicepreboot.registerroutes.md @@ -16,12 +16,12 @@ registerRoutes(path: string, callback: (router: IRouter) => void): void; | Parameter | Type | Description | | --- | --- | --- | -| path | string | | -| callback | (router: IRouter) => void | | +| path | string | | +| callback | (router: IRouter) => void | | Returns: -`void` +void ## Remarks @@ -35,6 +35,5 @@ registerRoutes('my-plugin', (router) => { // handler is called when '/my-plugin/path' resource is requested with `GET` method router.get({ path: '/path', validate: false }, (context, req, res) => res.ok({ content: 'ok' })); }); - ``` diff --git a/docs/development/core/server/kibana-plugin-core-server.httpservicesetup.createrouter.md b/docs/development/core/server/kibana-plugin-core-server.httpservicesetup.createrouter.md index f009dd3fc2b16..7bdc7cd2e4e33 100644 --- a/docs/development/core/server/kibana-plugin-core-server.httpservicesetup.createrouter.md +++ b/docs/development/core/server/kibana-plugin-core-server.httpservicesetup.createrouter.md @@ -23,6 +23,5 @@ Each route can have only one handler function, which is executed when the route const router = createRouter(); // handler is called when '/path' resource is requested with `GET` method router.get({ path: '/path', validate: false }, (context, req, res) => res.ok({ content: 'ok' })); - ``` diff --git a/docs/development/core/server/kibana-plugin-core-server.httpservicesetup.md b/docs/development/core/server/kibana-plugin-core-server.httpservicesetup.md index dbc2a516fa17b..81ddeaaaa5a12 100644 --- a/docs/development/core/server/kibana-plugin-core-server.httpservicesetup.md +++ b/docs/development/core/server/kibana-plugin-core-server.httpservicesetup.md @@ -18,7 +18,6 @@ To handle an incoming request in your plugin you should: - Create a `Router` ins ```ts const router = httpSetup.createRouter(); - ``` - Use `@kbn/config-schema` package to create a schema to validate the request `params`, `query`, and `body`. Every incoming request will be validated against the created schema. If validation failed, the request is rejected with `400` status and `Bad request` error without calling the route's handler. To opt out of validating the request, specify `false`. @@ -29,7 +28,6 @@ const validate = { id: schema.string(), }), }; - ``` - Declare a function to respond to incoming request. The function will receive `request` object containing request details: url, headers, matched route, as well as validated `params`, `query`, `body`. And `response` object instructing HTTP server to create HTTP response with information sent back to the client as the response body, headers, and HTTP status. Unlike, `hapi` route handler in the Legacy platform, any exception raised during the handler call will generate `500 Server error` response and log error details for further investigation. See below for returning custom error responses. @@ -46,7 +44,6 @@ const handler = async (context: RequestHandlerContext, request: KibanaRequest, r } }); } - ``` - Register route handler for GET request to 'path/{id}' path @@ -74,23 +71,22 @@ async (context, request, response) => { } }); }); - ``` ## Properties | Property | Type | Description | | --- | --- | --- | -| [auth](./kibana-plugin-core-server.httpservicesetup.auth.md) | HttpAuth | Auth status. See [HttpAuth](./kibana-plugin-core-server.httpauth.md) | -| [basePath](./kibana-plugin-core-server.httpservicesetup.basepath.md) | IBasePath | Access or manipulate the Kibana base path See [IBasePath](./kibana-plugin-core-server.ibasepath.md). | -| [createCookieSessionStorageFactory](./kibana-plugin-core-server.httpservicesetup.createcookiesessionstoragefactory.md) | <T>(cookieOptions: SessionStorageCookieOptions<T>) => Promise<SessionStorageFactory<T>> | Creates cookie based session storage factory [SessionStorageFactory](./kibana-plugin-core-server.sessionstoragefactory.md) | -| [createRouter](./kibana-plugin-core-server.httpservicesetup.createrouter.md) | <Context extends RequestHandlerContext = RequestHandlerContext>() => IRouter<Context> | Provides ability to declare a handler function for a particular path and HTTP request method. | -| [csp](./kibana-plugin-core-server.httpservicesetup.csp.md) | ICspConfig | The CSP config used for Kibana. | -| [getServerInfo](./kibana-plugin-core-server.httpservicesetup.getserverinfo.md) | () => HttpServerInfo | Provides common [information](./kibana-plugin-core-server.httpserverinfo.md) about the running http server. | -| [registerAuth](./kibana-plugin-core-server.httpservicesetup.registerauth.md) | (handler: AuthenticationHandler) => void | To define custom authentication and/or authorization mechanism for incoming requests. | -| [registerOnPostAuth](./kibana-plugin-core-server.httpservicesetup.registeronpostauth.md) | (handler: OnPostAuthHandler) => void | To define custom logic after Auth interceptor did make sure a user has access to the requested resource. | -| [registerOnPreAuth](./kibana-plugin-core-server.httpservicesetup.registeronpreauth.md) | (handler: OnPreAuthHandler) => void | To define custom logic to perform for incoming requests before the Auth interceptor performs a check that user has access to requested resources. | -| [registerOnPreResponse](./kibana-plugin-core-server.httpservicesetup.registeronpreresponse.md) | (handler: OnPreResponseHandler) => void | To define custom logic to perform for the server response. | -| [registerOnPreRouting](./kibana-plugin-core-server.httpservicesetup.registeronprerouting.md) | (handler: OnPreRoutingHandler) => void | To define custom logic to perform for incoming requests before server performs a route lookup. | -| [registerRouteHandlerContext](./kibana-plugin-core-server.httpservicesetup.registerroutehandlercontext.md) | <Context extends RequestHandlerContext, ContextName extends keyof Context>(contextName: ContextName, provider: RequestHandlerContextProvider<Context, ContextName>) => RequestHandlerContextContainer | Register a context provider for a route handler. | +| [auth](./kibana-plugin-core-server.httpservicesetup.auth.md) | HttpAuth | Auth status. See [HttpAuth](./kibana-plugin-core-server.httpauth.md) | +| [basePath](./kibana-plugin-core-server.httpservicesetup.basepath.md) | IBasePath | Access or manipulate the Kibana base path See [IBasePath](./kibana-plugin-core-server.ibasepath.md). | +| [createCookieSessionStorageFactory](./kibana-plugin-core-server.httpservicesetup.createcookiesessionstoragefactory.md) | <T>(cookieOptions: SessionStorageCookieOptions<T>) => Promise<SessionStorageFactory<T>> | Creates cookie based session storage factory [SessionStorageFactory](./kibana-plugin-core-server.sessionstoragefactory.md) | +| [createRouter](./kibana-plugin-core-server.httpservicesetup.createrouter.md) | <Context extends RequestHandlerContext = RequestHandlerContext>() => IRouter<Context> | Provides ability to declare a handler function for a particular path and HTTP request method. | +| [csp](./kibana-plugin-core-server.httpservicesetup.csp.md) | ICspConfig | The CSP config used for Kibana. | +| [getServerInfo](./kibana-plugin-core-server.httpservicesetup.getserverinfo.md) | () => HttpServerInfo | Provides common [information](./kibana-plugin-core-server.httpserverinfo.md) about the running http server. | +| [registerAuth](./kibana-plugin-core-server.httpservicesetup.registerauth.md) | (handler: AuthenticationHandler) => void | To define custom authentication and/or authorization mechanism for incoming requests. | +| [registerOnPostAuth](./kibana-plugin-core-server.httpservicesetup.registeronpostauth.md) | (handler: OnPostAuthHandler) => void | To define custom logic after Auth interceptor did make sure a user has access to the requested resource. | +| [registerOnPreAuth](./kibana-plugin-core-server.httpservicesetup.registeronpreauth.md) | (handler: OnPreAuthHandler) => void | To define custom logic to perform for incoming requests before the Auth interceptor performs a check that user has access to requested resources. | +| [registerOnPreResponse](./kibana-plugin-core-server.httpservicesetup.registeronpreresponse.md) | (handler: OnPreResponseHandler) => void | To define custom logic to perform for the server response. | +| [registerOnPreRouting](./kibana-plugin-core-server.httpservicesetup.registeronprerouting.md) | (handler: OnPreRoutingHandler) => void | To define custom logic to perform for incoming requests before server performs a route lookup. | +| [registerRouteHandlerContext](./kibana-plugin-core-server.httpservicesetup.registerroutehandlercontext.md) | <Context extends RequestHandlerContext, ContextName extends keyof Context>(contextName: ContextName, provider: RequestHandlerContextProvider<Context, ContextName>) => RequestHandlerContextContainer | Register a context provider for a route handler. | diff --git a/docs/development/core/server/kibana-plugin-core-server.httpservicesetup.registerroutehandlercontext.md b/docs/development/core/server/kibana-plugin-core-server.httpservicesetup.registerroutehandlercontext.md index df3f80580f6da..c793080305d0c 100644 --- a/docs/development/core/server/kibana-plugin-core-server.httpservicesetup.registerroutehandlercontext.md +++ b/docs/development/core/server/kibana-plugin-core-server.httpservicesetup.registerroutehandlercontext.md @@ -37,6 +37,5 @@ registerRouteHandlerContext: HttpAuth | Auth status. See [HttpAuth](./kibana-plugin-core-server.httpauth.md) | -| [basePath](./kibana-plugin-core-server.httpservicestart.basepath.md) | IBasePath | Access or manipulate the Kibana base path See [IBasePath](./kibana-plugin-core-server.ibasepath.md). | -| [getServerInfo](./kibana-plugin-core-server.httpservicestart.getserverinfo.md) | () => HttpServerInfo | Provides common [information](./kibana-plugin-core-server.httpserverinfo.md) about the running http server. | +| [auth](./kibana-plugin-core-server.httpservicestart.auth.md) | HttpAuth | Auth status. See [HttpAuth](./kibana-plugin-core-server.httpauth.md) | +| [basePath](./kibana-plugin-core-server.httpservicestart.basepath.md) | IBasePath | Access or manipulate the Kibana base path See [IBasePath](./kibana-plugin-core-server.ibasepath.md). | +| [getServerInfo](./kibana-plugin-core-server.httpservicestart.getserverinfo.md) | () => HttpServerInfo | Provides common [information](./kibana-plugin-core-server.httpserverinfo.md) about the running http server. | diff --git a/docs/development/core/server/kibana-plugin-core-server.i18nservicesetup.getlocale.md b/docs/development/core/server/kibana-plugin-core-server.i18nservicesetup.getlocale.md index 2fe8e564e7ce5..fa98f34c6ac5e 100644 --- a/docs/development/core/server/kibana-plugin-core-server.i18nservicesetup.getlocale.md +++ b/docs/development/core/server/kibana-plugin-core-server.i18nservicesetup.getlocale.md @@ -13,5 +13,5 @@ getLocale(): string; ``` Returns: -`string` +string diff --git a/docs/development/core/server/kibana-plugin-core-server.i18nservicesetup.gettranslationfiles.md b/docs/development/core/server/kibana-plugin-core-server.i18nservicesetup.gettranslationfiles.md index 81caed287454e..ebdb0babc3af7 100644 --- a/docs/development/core/server/kibana-plugin-core-server.i18nservicesetup.gettranslationfiles.md +++ b/docs/development/core/server/kibana-plugin-core-server.i18nservicesetup.gettranslationfiles.md @@ -13,5 +13,5 @@ getTranslationFiles(): string[]; ``` Returns: -`string[]` +string\[\] diff --git a/docs/development/core/server/kibana-plugin-core-server.iclusterclient.md b/docs/development/core/server/kibana-plugin-core-server.iclusterclient.md index f6bacee322538..969a32d96a3a0 100644 --- a/docs/development/core/server/kibana-plugin-core-server.iclusterclient.md +++ b/docs/development/core/server/kibana-plugin-core-server.iclusterclient.md @@ -16,6 +16,6 @@ export interface IClusterClient | Property | Type | Description | | --- | --- | --- | -| [asInternalUser](./kibana-plugin-core-server.iclusterclient.asinternaluser.md) | ElasticsearchClient | A [client](./kibana-plugin-core-server.elasticsearchclient.md) to be used to query the ES cluster on behalf of the Kibana internal user | -| [asScoped](./kibana-plugin-core-server.iclusterclient.asscoped.md) | (request: ScopeableRequest) => IScopedClusterClient | Creates a [scoped cluster client](./kibana-plugin-core-server.iscopedclusterclient.md) bound to given [request](./kibana-plugin-core-server.scopeablerequest.md) | +| [asInternalUser](./kibana-plugin-core-server.iclusterclient.asinternaluser.md) | ElasticsearchClient | A [client](./kibana-plugin-core-server.elasticsearchclient.md) to be used to query the ES cluster on behalf of the Kibana internal user | +| [asScoped](./kibana-plugin-core-server.iclusterclient.asscoped.md) | (request: ScopeableRequest) => IScopedClusterClient | Creates a [scoped cluster client](./kibana-plugin-core-server.iscopedclusterclient.md) bound to given [request](./kibana-plugin-core-server.scopeablerequest.md) | diff --git a/docs/development/core/server/kibana-plugin-core-server.icontextcontainer.createhandler.md b/docs/development/core/server/kibana-plugin-core-server.icontextcontainer.createhandler.md index 7d7368426b1c2..ac13b86295f6d 100644 --- a/docs/development/core/server/kibana-plugin-core-server.icontextcontainer.createhandler.md +++ b/docs/development/core/server/kibana-plugin-core-server.icontextcontainer.createhandler.md @@ -16,12 +16,12 @@ createHandler(pluginOpaqueId: PluginOpaqueId, handler: RequestHandler): (...rest | Parameter | Type | Description | | --- | --- | --- | -| pluginOpaqueId | PluginOpaqueId | The plugin opaque ID for the plugin that registers this handler. | -| handler | RequestHandler | Handler function to pass context object to. | +| pluginOpaqueId | PluginOpaqueId | The plugin opaque ID for the plugin that registers this handler. | +| handler | RequestHandler | Handler function to pass context object to. | Returns: -`(...rest: HandlerParameters) => ShallowPromise>` +(...rest: HandlerParameters<RequestHandler>) => ShallowPromise<ReturnType<RequestHandler>> A function that takes `RequestHandler` parameters, calls `handler` with a new context, and returns a Promise of the `handler` return value. diff --git a/docs/development/core/server/kibana-plugin-core-server.icontextcontainer.md b/docs/development/core/server/kibana-plugin-core-server.icontextcontainer.md index 8b4d3f39e345e..99cddecb38d43 100644 --- a/docs/development/core/server/kibana-plugin-core-server.icontextcontainer.md +++ b/docs/development/core/server/kibana-plugin-core-server.icontextcontainer.md @@ -68,7 +68,6 @@ class MyPlugin { } } } - ``` ## Methods diff --git a/docs/development/core/server/kibana-plugin-core-server.icontextcontainer.registercontext.md b/docs/development/core/server/kibana-plugin-core-server.icontextcontainer.registercontext.md index 7f531fa8ba0d2..32b177df2b2ed 100644 --- a/docs/development/core/server/kibana-plugin-core-server.icontextcontainer.registercontext.md +++ b/docs/development/core/server/kibana-plugin-core-server.icontextcontainer.registercontext.md @@ -16,13 +16,13 @@ registerContextPluginOpaqueId | The plugin opaque ID for the plugin that registers this context. | -| contextName | ContextName | The key of the TContext object this provider supplies the value for. | -| provider | IContextProvider<Context, ContextName> | A [IContextProvider](./kibana-plugin-core-server.icontextprovider.md) to be called each time a new context is created. | +| pluginOpaqueId | PluginOpaqueId | The plugin opaque ID for the plugin that registers this context. | +| contextName | ContextName | The key of the TContext object this provider supplies the value for. | +| provider | IContextProvider<Context, ContextName> | A [IContextProvider](./kibana-plugin-core-server.icontextprovider.md) to be called each time a new context is created. | Returns: -`this` +this The [IContextContainer](./kibana-plugin-core-server.icontextcontainer.md) for method chaining. diff --git a/docs/development/core/server/kibana-plugin-core-server.icspconfig.md b/docs/development/core/server/kibana-plugin-core-server.icspconfig.md index 9da31cdc11e36..d5667900a41e2 100644 --- a/docs/development/core/server/kibana-plugin-core-server.icspconfig.md +++ b/docs/development/core/server/kibana-plugin-core-server.icspconfig.md @@ -16,8 +16,8 @@ export interface ICspConfig | Property | Type | Description | | --- | --- | --- | -| [disableEmbedding](./kibana-plugin-core-server.icspconfig.disableembedding.md) | boolean | Whether or not embedding (using iframes) should be allowed by the CSP. If embedding is disabled, a restrictive 'frame-ancestors' rule will be added to the default CSP rules. | -| [header](./kibana-plugin-core-server.icspconfig.header.md) | string | The CSP rules in a formatted directives string for use in a Content-Security-Policy header. | -| [strict](./kibana-plugin-core-server.icspconfig.strict.md) | boolean | Specify whether browsers that do not support CSP should be able to use Kibana. Use true to block and false to allow. | -| [warnLegacyBrowsers](./kibana-plugin-core-server.icspconfig.warnlegacybrowsers.md) | boolean | Specify whether users with legacy browsers should be warned about their lack of Kibana security compliance. | +| [disableEmbedding](./kibana-plugin-core-server.icspconfig.disableembedding.md) | boolean | Whether or not embedding (using iframes) should be allowed by the CSP. If embedding is disabled, a restrictive 'frame-ancestors' rule will be added to the default CSP rules. | +| [header](./kibana-plugin-core-server.icspconfig.header.md) | string | The CSP rules in a formatted directives string for use in a Content-Security-Policy header. | +| [strict](./kibana-plugin-core-server.icspconfig.strict.md) | boolean | Specify whether browsers that do not support CSP should be able to use Kibana. Use true to block and false to allow. | +| [warnLegacyBrowsers](./kibana-plugin-core-server.icspconfig.warnlegacybrowsers.md) | boolean | Specify whether users with legacy browsers should be warned about their lack of Kibana security compliance. | diff --git a/docs/development/core/server/kibana-plugin-core-server.icustomclusterclient.md b/docs/development/core/server/kibana-plugin-core-server.icustomclusterclient.md index 189a50b5d6c20..1c65137d1ddc1 100644 --- a/docs/development/core/server/kibana-plugin-core-server.icustomclusterclient.md +++ b/docs/development/core/server/kibana-plugin-core-server.icustomclusterclient.md @@ -11,10 +11,11 @@ See [IClusterClient](./kibana-plugin-core-server.iclusterclient.md) ```typescript export interface ICustomClusterClient extends IClusterClient ``` +Extends: IClusterClient ## Properties | Property | Type | Description | | --- | --- | --- | -| [close](./kibana-plugin-core-server.icustomclusterclient.close.md) | () => Promise<void> | Closes the cluster client. After that client cannot be used and one should create a new client instance to be able to interact with Elasticsearch API. | +| [close](./kibana-plugin-core-server.icustomclusterclient.close.md) | () => Promise<void> | Closes the cluster client. After that client cannot be used and one should create a new client instance to be able to interact with Elasticsearch API. | diff --git a/docs/development/core/server/kibana-plugin-core-server.iexecutioncontextcontainer.tojson.md b/docs/development/core/server/kibana-plugin-core-server.iexecutioncontextcontainer.tojson.md index 6b643f7f72c95..a561e6c319408 100644 --- a/docs/development/core/server/kibana-plugin-core-server.iexecutioncontextcontainer.tojson.md +++ b/docs/development/core/server/kibana-plugin-core-server.iexecutioncontextcontainer.tojson.md @@ -11,5 +11,5 @@ toJSON(): Readonly; ``` Returns: -`Readonly` +Readonly<KibanaExecutionContext> diff --git a/docs/development/core/server/kibana-plugin-core-server.iexecutioncontextcontainer.tostring.md b/docs/development/core/server/kibana-plugin-core-server.iexecutioncontextcontainer.tostring.md index 60f9f499cf36c..666da5bef3969 100644 --- a/docs/development/core/server/kibana-plugin-core-server.iexecutioncontextcontainer.tostring.md +++ b/docs/development/core/server/kibana-plugin-core-server.iexecutioncontextcontainer.tostring.md @@ -11,5 +11,5 @@ toString(): string; ``` Returns: -`string` +string diff --git a/docs/development/core/server/kibana-plugin-core-server.iexternalurlconfig.md b/docs/development/core/server/kibana-plugin-core-server.iexternalurlconfig.md index 8df4db4aa9b5e..b5490a9548dc1 100644 --- a/docs/development/core/server/kibana-plugin-core-server.iexternalurlconfig.md +++ b/docs/development/core/server/kibana-plugin-core-server.iexternalurlconfig.md @@ -16,5 +16,5 @@ export interface IExternalUrlConfig | Property | Type | Description | | --- | --- | --- | -| [policy](./kibana-plugin-core-server.iexternalurlconfig.policy.md) | IExternalUrlPolicy[] | A set of policies describing which external urls are allowed. | +| [policy](./kibana-plugin-core-server.iexternalurlconfig.policy.md) | IExternalUrlPolicy\[\] | A set of policies describing which external urls are allowed. | diff --git a/docs/development/core/server/kibana-plugin-core-server.iexternalurlpolicy.host.md b/docs/development/core/server/kibana-plugin-core-server.iexternalurlpolicy.host.md index d1380d9a37846..a549f80509474 100644 --- a/docs/development/core/server/kibana-plugin-core-server.iexternalurlpolicy.host.md +++ b/docs/development/core/server/kibana-plugin-core-server.iexternalurlpolicy.host.md @@ -19,6 +19,5 @@ host?: string; // allows access to all of google.com, using any protocol. allow: true, host: 'google.com' - ``` diff --git a/docs/development/core/server/kibana-plugin-core-server.iexternalurlpolicy.md b/docs/development/core/server/kibana-plugin-core-server.iexternalurlpolicy.md index d7fcc1d96bdfe..45f7798eaf336 100644 --- a/docs/development/core/server/kibana-plugin-core-server.iexternalurlpolicy.md +++ b/docs/development/core/server/kibana-plugin-core-server.iexternalurlpolicy.md @@ -16,7 +16,7 @@ export interface IExternalUrlPolicy | Property | Type | Description | | --- | --- | --- | -| [allow](./kibana-plugin-core-server.iexternalurlpolicy.allow.md) | boolean | Indicates if this policy allows or denies access to the described destination. | -| [host](./kibana-plugin-core-server.iexternalurlpolicy.host.md) | string | Optional host describing the external destination. May be combined with protocol. | -| [protocol](./kibana-plugin-core-server.iexternalurlpolicy.protocol.md) | string | Optional protocol describing the external destination. May be combined with host. | +| [allow](./kibana-plugin-core-server.iexternalurlpolicy.allow.md) | boolean | Indicates if this policy allows or denies access to the described destination. | +| [host?](./kibana-plugin-core-server.iexternalurlpolicy.host.md) | string | (Optional) Optional host describing the external destination. May be combined with protocol. | +| [protocol?](./kibana-plugin-core-server.iexternalurlpolicy.protocol.md) | string | (Optional) Optional protocol describing the external destination. May be combined with host. | diff --git a/docs/development/core/server/kibana-plugin-core-server.iexternalurlpolicy.protocol.md b/docs/development/core/server/kibana-plugin-core-server.iexternalurlpolicy.protocol.md index 54e5eb5bc68f5..86f6e6164de4e 100644 --- a/docs/development/core/server/kibana-plugin-core-server.iexternalurlpolicy.protocol.md +++ b/docs/development/core/server/kibana-plugin-core-server.iexternalurlpolicy.protocol.md @@ -19,6 +19,5 @@ protocol?: string; // allows access to all destinations over the `https` protocol. allow: true, protocol: 'https' - ``` diff --git a/docs/development/core/server/kibana-plugin-core-server.ikibanaresponse.md b/docs/development/core/server/kibana-plugin-core-server.ikibanaresponse.md index 339ae189f1513..c71f5360834e8 100644 --- a/docs/development/core/server/kibana-plugin-core-server.ikibanaresponse.md +++ b/docs/development/core/server/kibana-plugin-core-server.ikibanaresponse.md @@ -16,7 +16,7 @@ export interface IKibanaResponseHttpResponseOptions | | -| [payload](./kibana-plugin-core-server.ikibanaresponse.payload.md) | T | | -| [status](./kibana-plugin-core-server.ikibanaresponse.status.md) | number | | +| [options](./kibana-plugin-core-server.ikibanaresponse.options.md) | HttpResponseOptions | | +| [payload?](./kibana-plugin-core-server.ikibanaresponse.payload.md) | T | (Optional) | +| [status](./kibana-plugin-core-server.ikibanaresponse.status.md) | number | | diff --git a/docs/development/core/server/kibana-plugin-core-server.ikibanasocket.getpeercertificate.md b/docs/development/core/server/kibana-plugin-core-server.ikibanasocket.getpeercertificate.md index 79dca660a5f30..9f0dce061bcfc 100644 --- a/docs/development/core/server/kibana-plugin-core-server.ikibanasocket.getpeercertificate.md +++ b/docs/development/core/server/kibana-plugin-core-server.ikibanasocket.getpeercertificate.md @@ -14,9 +14,9 @@ getPeerCertificate(detailed: true): DetailedPeerCertificate | null; | Parameter | Type | Description | | --- | --- | --- | -| detailed | true | | +| detailed | true | | Returns: -`DetailedPeerCertificate | null` +DetailedPeerCertificate \| null diff --git a/docs/development/core/server/kibana-plugin-core-server.ikibanasocket.getpeercertificate_1.md b/docs/development/core/server/kibana-plugin-core-server.ikibanasocket.getpeercertificate_1.md index 28c126fc4f733..363fce50251d8 100644 --- a/docs/development/core/server/kibana-plugin-core-server.ikibanasocket.getpeercertificate_1.md +++ b/docs/development/core/server/kibana-plugin-core-server.ikibanasocket.getpeercertificate_1.md @@ -14,9 +14,9 @@ getPeerCertificate(detailed: false): PeerCertificate | null; | Parameter | Type | Description | | --- | --- | --- | -| detailed | false | | +| detailed | false | | Returns: -`PeerCertificate | null` +PeerCertificate \| null diff --git a/docs/development/core/server/kibana-plugin-core-server.ikibanasocket.getpeercertificate_2.md b/docs/development/core/server/kibana-plugin-core-server.ikibanasocket.getpeercertificate_2.md index afeb36a9d5f3e..24b11b6966000 100644 --- a/docs/development/core/server/kibana-plugin-core-server.ikibanasocket.getpeercertificate_2.md +++ b/docs/development/core/server/kibana-plugin-core-server.ikibanasocket.getpeercertificate_2.md @@ -16,11 +16,11 @@ getPeerCertificate(detailed?: boolean): PeerCertificate | DetailedPeerCertificat | Parameter | Type | Description | | --- | --- | --- | -| detailed | boolean | If true; the full chain with issuer property will be returned. | +| detailed | boolean | If true; the full chain with issuer property will be returned. | Returns: -`PeerCertificate | DetailedPeerCertificate | null` +PeerCertificate \| DetailedPeerCertificate \| null An object representing the peer's certificate. diff --git a/docs/development/core/server/kibana-plugin-core-server.ikibanasocket.getprotocol.md b/docs/development/core/server/kibana-plugin-core-server.ikibanasocket.getprotocol.md index 720091174629a..d605f2fd21bef 100644 --- a/docs/development/core/server/kibana-plugin-core-server.ikibanasocket.getprotocol.md +++ b/docs/development/core/server/kibana-plugin-core-server.ikibanasocket.getprotocol.md @@ -13,5 +13,5 @@ getProtocol(): string | null; ``` Returns: -`string | null` +string \| null diff --git a/docs/development/core/server/kibana-plugin-core-server.ikibanasocket.md b/docs/development/core/server/kibana-plugin-core-server.ikibanasocket.md index 99923aecef8df..bc8f59df9d211 100644 --- a/docs/development/core/server/kibana-plugin-core-server.ikibanasocket.md +++ b/docs/development/core/server/kibana-plugin-core-server.ikibanasocket.md @@ -16,8 +16,8 @@ export interface IKibanaSocket | Property | Type | Description | | --- | --- | --- | -| [authorizationError](./kibana-plugin-core-server.ikibanasocket.authorizationerror.md) | Error | The reason why the peer's certificate has not been verified. This property becomes available only when authorized is false. | -| [authorized](./kibana-plugin-core-server.ikibanasocket.authorized.md) | boolean | Indicates whether or not the peer certificate was signed by one of the specified CAs. When TLS isn't used the value is undefined. | +| [authorizationError?](./kibana-plugin-core-server.ikibanasocket.authorizationerror.md) | Error | (Optional) The reason why the peer's certificate has not been verified. This property becomes available only when authorized is false. | +| [authorized?](./kibana-plugin-core-server.ikibanasocket.authorized.md) | boolean | (Optional) Indicates whether or not the peer certificate was signed by one of the specified CAs. When TLS isn't used the value is undefined. | ## Methods diff --git a/docs/development/core/server/kibana-plugin-core-server.ikibanasocket.renegotiate.md b/docs/development/core/server/kibana-plugin-core-server.ikibanasocket.renegotiate.md index f39d3c08d9f0b..b4addde9b3179 100644 --- a/docs/development/core/server/kibana-plugin-core-server.ikibanasocket.renegotiate.md +++ b/docs/development/core/server/kibana-plugin-core-server.ikibanasocket.renegotiate.md @@ -19,11 +19,11 @@ renegotiate(options: { | Parameter | Type | Description | | --- | --- | --- | -| options | {
rejectUnauthorized?: boolean;
requestCert?: boolean;
} | The options may contain the following fields: rejectUnauthorized, requestCert (See tls.createServer() for details). | +| options | { rejectUnauthorized?: boolean; requestCert?: boolean; } | The options may contain the following fields: rejectUnauthorized, requestCert (See tls.createServer() for details). | Returns: -`Promise` +Promise<void> A Promise that will be resolved if renegotiation succeeded, or will be rejected if renegotiation failed. diff --git a/docs/development/core/server/kibana-plugin-core-server.intervalhistogram.md b/docs/development/core/server/kibana-plugin-core-server.intervalhistogram.md index d7fb889dce322..39f2d570cd259 100644 --- a/docs/development/core/server/kibana-plugin-core-server.intervalhistogram.md +++ b/docs/development/core/server/kibana-plugin-core-server.intervalhistogram.md @@ -16,12 +16,12 @@ export interface IntervalHistogram | Property | Type | Description | | --- | --- | --- | -| [exceeds](./kibana-plugin-core-server.intervalhistogram.exceeds.md) | number | | -| [fromTimestamp](./kibana-plugin-core-server.intervalhistogram.fromtimestamp.md) | string | | -| [lastUpdatedAt](./kibana-plugin-core-server.intervalhistogram.lastupdatedat.md) | string | | -| [max](./kibana-plugin-core-server.intervalhistogram.max.md) | number | | -| [mean](./kibana-plugin-core-server.intervalhistogram.mean.md) | number | | -| [min](./kibana-plugin-core-server.intervalhistogram.min.md) | number | | -| [percentiles](./kibana-plugin-core-server.intervalhistogram.percentiles.md) | {
50: number;
75: number;
95: number;
99: number;
} | | -| [stddev](./kibana-plugin-core-server.intervalhistogram.stddev.md) | number | | +| [exceeds](./kibana-plugin-core-server.intervalhistogram.exceeds.md) | number | | +| [fromTimestamp](./kibana-plugin-core-server.intervalhistogram.fromtimestamp.md) | string | | +| [lastUpdatedAt](./kibana-plugin-core-server.intervalhistogram.lastupdatedat.md) | string | | +| [max](./kibana-plugin-core-server.intervalhistogram.max.md) | number | | +| [mean](./kibana-plugin-core-server.intervalhistogram.mean.md) | number | | +| [min](./kibana-plugin-core-server.intervalhistogram.min.md) | number | | +| [percentiles](./kibana-plugin-core-server.intervalhistogram.percentiles.md) | { 50: number; 75: number; 95: number; 99: number; } | | +| [stddev](./kibana-plugin-core-server.intervalhistogram.stddev.md) | number | | diff --git a/docs/development/core/server/kibana-plugin-core-server.irenderoptions.md b/docs/development/core/server/kibana-plugin-core-server.irenderoptions.md index 51712a3ea1871..b7c43bc77867d 100644 --- a/docs/development/core/server/kibana-plugin-core-server.irenderoptions.md +++ b/docs/development/core/server/kibana-plugin-core-server.irenderoptions.md @@ -15,5 +15,5 @@ export interface IRenderOptions | Property | Type | Description | | --- | --- | --- | -| [includeUserSettings](./kibana-plugin-core-server.irenderoptions.includeusersettings.md) | boolean | Set whether to output user settings in the page metadata. true by default. | +| [includeUserSettings?](./kibana-plugin-core-server.irenderoptions.includeusersettings.md) | boolean | (Optional) Set whether to output user settings in the page metadata. true by default. | diff --git a/docs/development/core/server/kibana-plugin-core-server.irouter.md b/docs/development/core/server/kibana-plugin-core-server.irouter.md index a0a27e828f865..a751ea399c5a9 100644 --- a/docs/development/core/server/kibana-plugin-core-server.irouter.md +++ b/docs/development/core/server/kibana-plugin-core-server.irouter.md @@ -16,11 +16,11 @@ export interface IRouterRouteRegistrar<'delete', Context> | Register a route handler for DELETE request. | -| [get](./kibana-plugin-core-server.irouter.get.md) | RouteRegistrar<'get', Context> | Register a route handler for GET request. | -| [handleLegacyErrors](./kibana-plugin-core-server.irouter.handlelegacyerrors.md) | RequestHandlerWrapper | Wrap a router handler to catch and converts legacy boom errors to proper custom errors. | -| [patch](./kibana-plugin-core-server.irouter.patch.md) | RouteRegistrar<'patch', Context> | Register a route handler for PATCH request. | -| [post](./kibana-plugin-core-server.irouter.post.md) | RouteRegistrar<'post', Context> | Register a route handler for POST request. | -| [put](./kibana-plugin-core-server.irouter.put.md) | RouteRegistrar<'put', Context> | Register a route handler for PUT request. | -| [routerPath](./kibana-plugin-core-server.irouter.routerpath.md) | string | Resulted path | +| [delete](./kibana-plugin-core-server.irouter.delete.md) | RouteRegistrar<'delete', Context> | Register a route handler for DELETE request. | +| [get](./kibana-plugin-core-server.irouter.get.md) | RouteRegistrar<'get', Context> | Register a route handler for GET request. | +| [handleLegacyErrors](./kibana-plugin-core-server.irouter.handlelegacyerrors.md) | RequestHandlerWrapper | Wrap a router handler to catch and converts legacy boom errors to proper custom errors. | +| [patch](./kibana-plugin-core-server.irouter.patch.md) | RouteRegistrar<'patch', Context> | Register a route handler for PATCH request. | +| [post](./kibana-plugin-core-server.irouter.post.md) | RouteRegistrar<'post', Context> | Register a route handler for POST request. | +| [put](./kibana-plugin-core-server.irouter.put.md) | RouteRegistrar<'put', Context> | Register a route handler for PUT request. | +| [routerPath](./kibana-plugin-core-server.irouter.routerpath.md) | string | Resulted path | diff --git a/docs/development/core/server/kibana-plugin-core-server.isavedobjectspointintimefinder.md b/docs/development/core/server/kibana-plugin-core-server.isavedobjectspointintimefinder.md index 950d6c078654c..748ffbdc3c4e8 100644 --- a/docs/development/core/server/kibana-plugin-core-server.isavedobjectspointintimefinder.md +++ b/docs/development/core/server/kibana-plugin-core-server.isavedobjectspointintimefinder.md @@ -15,6 +15,6 @@ export interface ISavedObjectsPointInTimeFinder | Property | Type | Description | | --- | --- | --- | -| [close](./kibana-plugin-core-server.isavedobjectspointintimefinder.close.md) | () => Promise<void> | Closes the Point-In-Time associated with this finder instance.Once you have retrieved all of the results you need, it is recommended to call close() to clean up the PIT and prevent Elasticsearch from consuming resources unnecessarily. This is only required if you are done iterating and have not yet paged through all of the results: the PIT will automatically be closed for you once you reach the last page of results, or if the underlying call to find fails for any reason. | -| [find](./kibana-plugin-core-server.isavedobjectspointintimefinder.find.md) | () => AsyncGenerator<SavedObjectsFindResponse<T, A>> | An async generator which wraps calls to savedObjectsClient.find and iterates over multiple pages of results using _pit and search_after. This will open a new Point-In-Time (PIT), and continue paging until a set of results is received that's smaller than the designated perPage size. | +| [close](./kibana-plugin-core-server.isavedobjectspointintimefinder.close.md) | () => Promise<void> | Closes the Point-In-Time associated with this finder instance.Once you have retrieved all of the results you need, it is recommended to call close() to clean up the PIT and prevent Elasticsearch from consuming resources unnecessarily. This is only required if you are done iterating and have not yet paged through all of the results: the PIT will automatically be closed for you once you reach the last page of results, or if the underlying call to find fails for any reason. | +| [find](./kibana-plugin-core-server.isavedobjectspointintimefinder.find.md) | () => AsyncGenerator<SavedObjectsFindResponse<T, A>> | An async generator which wraps calls to savedObjectsClient.find and iterates over multiple pages of results using _pit and search_after. This will open a new Point-In-Time (PIT), and continue paging until a set of results is received that's smaller than the designated perPage size. | diff --git a/docs/development/core/server/kibana-plugin-core-server.iscopedclusterclient.md b/docs/development/core/server/kibana-plugin-core-server.iscopedclusterclient.md index f39db268288a6..f0d75f2f08fe4 100644 --- a/docs/development/core/server/kibana-plugin-core-server.iscopedclusterclient.md +++ b/docs/development/core/server/kibana-plugin-core-server.iscopedclusterclient.md @@ -16,6 +16,6 @@ export interface IScopedClusterClient | Property | Type | Description | | --- | --- | --- | -| [asCurrentUser](./kibana-plugin-core-server.iscopedclusterclient.ascurrentuser.md) | ElasticsearchClient | A [client](./kibana-plugin-core-server.elasticsearchclient.md) to be used to query the elasticsearch cluster on behalf of the user that initiated the request to the Kibana server. | -| [asInternalUser](./kibana-plugin-core-server.iscopedclusterclient.asinternaluser.md) | ElasticsearchClient | A [client](./kibana-plugin-core-server.elasticsearchclient.md) to be used to query the elasticsearch cluster on behalf of the internal Kibana user. | +| [asCurrentUser](./kibana-plugin-core-server.iscopedclusterclient.ascurrentuser.md) | ElasticsearchClient | A [client](./kibana-plugin-core-server.elasticsearchclient.md) to be used to query the elasticsearch cluster on behalf of the user that initiated the request to the Kibana server. | +| [asInternalUser](./kibana-plugin-core-server.iscopedclusterclient.asinternaluser.md) | ElasticsearchClient | A [client](./kibana-plugin-core-server.elasticsearchclient.md) to be used to query the elasticsearch cluster on behalf of the internal Kibana user. | diff --git a/docs/development/core/server/kibana-plugin-core-server.iuisettingsclient.md b/docs/development/core/server/kibana-plugin-core-server.iuisettingsclient.md index dd4a69c13a2d9..ad7819719e14a 100644 --- a/docs/development/core/server/kibana-plugin-core-server.iuisettingsclient.md +++ b/docs/development/core/server/kibana-plugin-core-server.iuisettingsclient.md @@ -16,14 +16,14 @@ export interface IUiSettingsClient | Property | Type | Description | | --- | --- | --- | -| [get](./kibana-plugin-core-server.iuisettingsclient.get.md) | <T = any>(key: string) => Promise<T> | Retrieves uiSettings values set by the user with fallbacks to default values if not specified. | -| [getAll](./kibana-plugin-core-server.iuisettingsclient.getall.md) | <T = any>() => Promise<Record<string, T>> | Retrieves a set of all uiSettings values set by the user with fallbacks to default values if not specified. | -| [getRegistered](./kibana-plugin-core-server.iuisettingsclient.getregistered.md) | () => Readonly<Record<string, PublicUiSettingsParams>> | Returns registered uiSettings values [UiSettingsParams](./kibana-plugin-core-server.uisettingsparams.md) | -| [getUserProvided](./kibana-plugin-core-server.iuisettingsclient.getuserprovided.md) | <T = any>() => Promise<Record<string, UserProvidedValues<T>>> | Retrieves a set of all uiSettings values set by the user. | -| [isOverridden](./kibana-plugin-core-server.iuisettingsclient.isoverridden.md) | (key: string) => boolean | Shows whether the uiSettings value set by the user. | -| [isSensitive](./kibana-plugin-core-server.iuisettingsclient.issensitive.md) | (key: string) => boolean | Shows whether the uiSetting is a sensitive value. Used by telemetry to not send sensitive values. | -| [remove](./kibana-plugin-core-server.iuisettingsclient.remove.md) | (key: string) => Promise<void> | Removes uiSettings value by key. | -| [removeMany](./kibana-plugin-core-server.iuisettingsclient.removemany.md) | (keys: string[]) => Promise<void> | Removes multiple uiSettings values by keys. | -| [set](./kibana-plugin-core-server.iuisettingsclient.set.md) | (key: string, value: any) => Promise<void> | Writes uiSettings value and marks it as set by the user. | -| [setMany](./kibana-plugin-core-server.iuisettingsclient.setmany.md) | (changes: Record<string, any>) => Promise<void> | Writes multiple uiSettings values and marks them as set by the user. | +| [get](./kibana-plugin-core-server.iuisettingsclient.get.md) | <T = any>(key: string) => Promise<T> | Retrieves uiSettings values set by the user with fallbacks to default values if not specified. | +| [getAll](./kibana-plugin-core-server.iuisettingsclient.getall.md) | <T = any>() => Promise<Record<string, T>> | Retrieves a set of all uiSettings values set by the user with fallbacks to default values if not specified. | +| [getRegistered](./kibana-plugin-core-server.iuisettingsclient.getregistered.md) | () => Readonly<Record<string, PublicUiSettingsParams>> | Returns registered uiSettings values [UiSettingsParams](./kibana-plugin-core-server.uisettingsparams.md) | +| [getUserProvided](./kibana-plugin-core-server.iuisettingsclient.getuserprovided.md) | <T = any>() => Promise<Record<string, UserProvidedValues<T>>> | Retrieves a set of all uiSettings values set by the user. | +| [isOverridden](./kibana-plugin-core-server.iuisettingsclient.isoverridden.md) | (key: string) => boolean | Shows whether the uiSettings value set by the user. | +| [isSensitive](./kibana-plugin-core-server.iuisettingsclient.issensitive.md) | (key: string) => boolean | Shows whether the uiSetting is a sensitive value. Used by telemetry to not send sensitive values. | +| [remove](./kibana-plugin-core-server.iuisettingsclient.remove.md) | (key: string) => Promise<void> | Removes uiSettings value by key. | +| [removeMany](./kibana-plugin-core-server.iuisettingsclient.removemany.md) | (keys: string\[\]) => Promise<void> | Removes multiple uiSettings values by keys. | +| [set](./kibana-plugin-core-server.iuisettingsclient.set.md) | (key: string, value: any) => Promise<void> | Writes uiSettings value and marks it as set by the user. | +| [setMany](./kibana-plugin-core-server.iuisettingsclient.setmany.md) | (changes: Record<string, any>) => Promise<void> | Writes multiple uiSettings values and marks them as set by the user. | diff --git a/docs/development/core/server/kibana-plugin-core-server.kibanarequest._constructor_.md b/docs/development/core/server/kibana-plugin-core-server.kibanarequest._constructor_.md index b44607c1c4135..682d6c87629fc 100644 --- a/docs/development/core/server/kibana-plugin-core-server.kibanarequest._constructor_.md +++ b/docs/development/core/server/kibana-plugin-core-server.kibanarequest._constructor_.md @@ -16,9 +16,9 @@ constructor(request: Request, params: Params, query: Query, body: Body, withoutS | Parameter | Type | Description | | --- | --- | --- | -| request | Request | | -| params | Params | | -| query | Query | | -| body | Body | | -| withoutSecretHeaders | boolean | | +| request | Request | | +| params | Params | | +| query | Query | | +| body | Body | | +| withoutSecretHeaders | boolean | | diff --git a/docs/development/core/server/kibana-plugin-core-server.kibanarequest.md b/docs/development/core/server/kibana-plugin-core-server.kibanarequest.md index 4129662acb2b1..f4e2dda2d5499 100644 --- a/docs/development/core/server/kibana-plugin-core-server.kibanarequest.md +++ b/docs/development/core/server/kibana-plugin-core-server.kibanarequest.md @@ -22,17 +22,17 @@ export declare class KibanaRequest{
isAuthenticated: boolean;
} | | -| [body](./kibana-plugin-core-server.kibanarequest.body.md) | | Body | | -| [events](./kibana-plugin-core-server.kibanarequest.events.md) | | KibanaRequestEvents | Request events [KibanaRequestEvents](./kibana-plugin-core-server.kibanarequestevents.md) | -| [headers](./kibana-plugin-core-server.kibanarequest.headers.md) | | Headers | Readonly copy of incoming request headers. | -| [id](./kibana-plugin-core-server.kibanarequest.id.md) | | string | A identifier to identify this request. | -| [isSystemRequest](./kibana-plugin-core-server.kibanarequest.issystemrequest.md) | | boolean | Whether or not the request is a "system request" rather than an application-level request. Can be set on the client using the HttpFetchOptions#asSystemRequest option. | -| [params](./kibana-plugin-core-server.kibanarequest.params.md) | | Params | | -| [query](./kibana-plugin-core-server.kibanarequest.query.md) | | Query | | -| [rewrittenUrl](./kibana-plugin-core-server.kibanarequest.rewrittenurl.md) | | URL | URL rewritten in onPreRouting request interceptor. | -| [route](./kibana-plugin-core-server.kibanarequest.route.md) | | RecursiveReadonly<KibanaRequestRoute<Method>> | matched route details | -| [socket](./kibana-plugin-core-server.kibanarequest.socket.md) | | IKibanaSocket | [IKibanaSocket](./kibana-plugin-core-server.ikibanasocket.md) | -| [url](./kibana-plugin-core-server.kibanarequest.url.md) | | URL | a WHATWG URL standard object. | -| [uuid](./kibana-plugin-core-server.kibanarequest.uuid.md) | | string | A UUID to identify this request. | +| [auth](./kibana-plugin-core-server.kibanarequest.auth.md) | | { isAuthenticated: boolean; } | | +| [body](./kibana-plugin-core-server.kibanarequest.body.md) | | Body | | +| [events](./kibana-plugin-core-server.kibanarequest.events.md) | | KibanaRequestEvents | Request events [KibanaRequestEvents](./kibana-plugin-core-server.kibanarequestevents.md) | +| [headers](./kibana-plugin-core-server.kibanarequest.headers.md) | | Headers | Readonly copy of incoming request headers. | +| [id](./kibana-plugin-core-server.kibanarequest.id.md) | | string | A identifier to identify this request. | +| [isSystemRequest](./kibana-plugin-core-server.kibanarequest.issystemrequest.md) | | boolean | Whether or not the request is a "system request" rather than an application-level request. Can be set on the client using the HttpFetchOptions#asSystemRequest option. | +| [params](./kibana-plugin-core-server.kibanarequest.params.md) | | Params | | +| [query](./kibana-plugin-core-server.kibanarequest.query.md) | | Query | | +| [rewrittenUrl?](./kibana-plugin-core-server.kibanarequest.rewrittenurl.md) | | URL | (Optional) URL rewritten in onPreRouting request interceptor. | +| [route](./kibana-plugin-core-server.kibanarequest.route.md) | | RecursiveReadonly<KibanaRequestRoute<Method>> | matched route details | +| [socket](./kibana-plugin-core-server.kibanarequest.socket.md) | | IKibanaSocket | [IKibanaSocket](./kibana-plugin-core-server.ikibanasocket.md) | +| [url](./kibana-plugin-core-server.kibanarequest.url.md) | | URL | a WHATWG URL standard object. | +| [uuid](./kibana-plugin-core-server.kibanarequest.uuid.md) | | string | A UUID to identify this request. | diff --git a/docs/development/core/server/kibana-plugin-core-server.kibanarequestevents.md b/docs/development/core/server/kibana-plugin-core-server.kibanarequestevents.md index dfd7efd27cb5a..c61e4aeec7c85 100644 --- a/docs/development/core/server/kibana-plugin-core-server.kibanarequestevents.md +++ b/docs/development/core/server/kibana-plugin-core-server.kibanarequestevents.md @@ -16,6 +16,6 @@ export interface KibanaRequestEvents | Property | Type | Description | | --- | --- | --- | -| [aborted$](./kibana-plugin-core-server.kibanarequestevents.aborted_.md) | Observable<void> | Observable that emits once if and when the request has been aborted. | -| [completed$](./kibana-plugin-core-server.kibanarequestevents.completed_.md) | Observable<void> | Observable that emits once if and when the request has been completely handled. | +| [aborted$](./kibana-plugin-core-server.kibanarequestevents.aborted_.md) | Observable<void> | Observable that emits once if and when the request has been aborted. | +| [completed$](./kibana-plugin-core-server.kibanarequestevents.completed_.md) | Observable<void> | Observable that emits once if and when the request has been completely handled. | diff --git a/docs/development/core/server/kibana-plugin-core-server.kibanarequestroute.md b/docs/development/core/server/kibana-plugin-core-server.kibanarequestroute.md index 480b580abc8a7..196c352e21f8a 100644 --- a/docs/development/core/server/kibana-plugin-core-server.kibanarequestroute.md +++ b/docs/development/core/server/kibana-plugin-core-server.kibanarequestroute.md @@ -16,7 +16,7 @@ export interface KibanaRequestRoute | Property | Type | Description | | --- | --- | --- | -| [method](./kibana-plugin-core-server.kibanarequestroute.method.md) | Method | | -| [options](./kibana-plugin-core-server.kibanarequestroute.options.md) | KibanaRequestRouteOptions<Method> | | -| [path](./kibana-plugin-core-server.kibanarequestroute.path.md) | string | | +| [method](./kibana-plugin-core-server.kibanarequestroute.method.md) | Method | | +| [options](./kibana-plugin-core-server.kibanarequestroute.options.md) | KibanaRequestRouteOptions<Method> | | +| [path](./kibana-plugin-core-server.kibanarequestroute.path.md) | string | | 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 8ddc0da5f1b28..b2e2b4bc6003f 100644 --- a/docs/development/core/server/kibana-plugin-core-server.kibanaresponsefactory.md +++ b/docs/development/core/server/kibana-plugin-core-server.kibanaresponsefactory.md @@ -40,7 +40,6 @@ return response.ok({ body: Buffer.from(...) }); const stream = new Stream.PassThrough(); fs.createReadStream('./file').pipe(stream); return res.ok({ body: stream }); - ``` HTTP headers are configurable via response factory parameter `options` [HttpResponseOptions](./kibana-plugin-core-server.httpresponseoptions.md). @@ -51,7 +50,6 @@ return response.ok({ 'content-type': 'application/json' } }); - ``` 2. Redirection response. Redirection URL is configures via 'Location' header. @@ -62,7 +60,6 @@ return response.redirected({ location: '/new-url', }, }); - ``` 3. Error response. You may pass an error message to the client, where error message can be: - `string` send message text - `Error` send the message text of given Error object. - `{ message: string | Error, attributes: {data: Record, ...} }` - send message text and attach additional error data. @@ -100,7 +97,6 @@ try { }); } - ``` 4. Custom response. `ResponseFactory` may not cover your use case, so you can use the `custom` function to customize the response. @@ -112,6 +108,5 @@ return response.custom({ location: '/created-url' } }) - ``` diff --git a/docs/development/core/server/kibana-plugin-core-server.loggercontextconfiginput.md b/docs/development/core/server/kibana-plugin-core-server.loggercontextconfiginput.md index fb6922d839cb8..2628d1ada88f8 100644 --- a/docs/development/core/server/kibana-plugin-core-server.loggercontextconfiginput.md +++ b/docs/development/core/server/kibana-plugin-core-server.loggercontextconfiginput.md @@ -15,6 +15,6 @@ export interface LoggerContextConfigInput | Property | Type | Description | | --- | --- | --- | -| [appenders](./kibana-plugin-core-server.loggercontextconfiginput.appenders.md) | Record<string, AppenderConfigType> | Map<string, AppenderConfigType> | | -| [loggers](./kibana-plugin-core-server.loggercontextconfiginput.loggers.md) | LoggerConfigType[] | | +| [appenders?](./kibana-plugin-core-server.loggercontextconfiginput.appenders.md) | Record<string, AppenderConfigType> \| Map<string, AppenderConfigType> | (Optional) | +| [loggers?](./kibana-plugin-core-server.loggercontextconfiginput.loggers.md) | LoggerConfigType\[\] | (Optional) | diff --git a/docs/development/core/server/kibana-plugin-core-server.loggingservicesetup.configure.md b/docs/development/core/server/kibana-plugin-core-server.loggingservicesetup.configure.md index 52ab5f1098457..022cbb3b7cbae 100644 --- a/docs/development/core/server/kibana-plugin-core-server.loggingservicesetup.configure.md +++ b/docs/development/core/server/kibana-plugin-core-server.loggingservicesetup.configure.md @@ -16,11 +16,11 @@ configure(config$: Observable): void; | Parameter | Type | Description | | --- | --- | --- | -| config$ | Observable<LoggerContextConfigInput> | | +| config$ | Observable<LoggerContextConfigInput> | | Returns: -`void` +void ## Remarks @@ -37,6 +37,5 @@ core.logging.configure( loggers: [{ name: 'search', appenders: ['default'] }] }) ) - ``` diff --git a/docs/development/core/server/kibana-plugin-core-server.md b/docs/development/core/server/kibana-plugin-core-server.md index f22a0fb8283d7..2eed71cc6ecea 100644 --- a/docs/development/core/server/kibana-plugin-core-server.md +++ b/docs/development/core/server/kibana-plugin-core-server.md @@ -8,7 +8,7 @@ The Kibana Core APIs for server-side plugins. A plugin requires a `kibana.json` file at it's root directory that follows [the manfiest schema](./kibana-plugin-core-server.pluginmanifest.md) to define static plugin information required to load the plugin. -A plugin's `server/index` file must contain a named import, `plugin`, that implements [PluginInitializer](./kibana-plugin-core-server.plugininitializer.md) which returns an object that implements [Plugin](./kibana-plugin-core-server.plugin.md). +A plugin's `server/index` file must contain a named import, `plugin`, that implements [PluginInitializer](./kibana-plugin-core-server.plugininitializer.md) which returns an object that implements . The plugin integrates with the core system via lifecycle events: `setup`, `start`, and `stop`. In each lifecycle method, the plugin will receive the corresponding core services available (either [CoreSetup](./kibana-plugin-core-server.coresetup.md) or [CoreStart](./kibana-plugin-core-server.corestart.md)) and any interfaces returned by dependency plugins' lifecycle method. Anything returned by the plugin's lifecycle method will be exposed to downstream dependencies when their corresponding lifecycle methods are invoked. @@ -124,7 +124,7 @@ The plugin integrates with the core system via lifecycle events: `setup` | [OpsOsMetrics](./kibana-plugin-core-server.opsosmetrics.md) | OS related metrics | | [OpsProcessMetrics](./kibana-plugin-core-server.opsprocessmetrics.md) | Process related metrics | | [OpsServerMetrics](./kibana-plugin-core-server.opsservermetrics.md) | server related metrics | -| [Plugin](./kibana-plugin-core-server.plugin.md) | The interface that should be returned by a PluginInitializer for a standard plugin. | +| [Plugin\_2](./kibana-plugin-core-server.plugin_2.md) | The interface that should be returned by a PluginInitializer for a standard plugin. | | [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. | @@ -259,7 +259,7 @@ The plugin integrates with the core system via lifecycle events: `setup` | [HandlerContextType](./kibana-plugin-core-server.handlercontexttype.md) | Extracts the type of the first argument of a [HandlerFunction](./kibana-plugin-core-server.handlerfunction.md) to represent the type of the context. | | [HandlerFunction](./kibana-plugin-core-server.handlerfunction.md) | A function that accepts a context object and an optional number of additional arguments. Used for the generic types in [IContextContainer](./kibana-plugin-core-server.icontextcontainer.md) | | [HandlerParameters](./kibana-plugin-core-server.handlerparameters.md) | Extracts the types of the additional arguments of a [HandlerFunction](./kibana-plugin-core-server.handlerfunction.md), excluding the [HandlerContextType](./kibana-plugin-core-server.handlercontexttype.md). | -| [Headers](./kibana-plugin-core-server.headers.md) | Http request headers to read. | +| [Headers\_2](./kibana-plugin-core-server.headers_2.md) | Http request headers to read. | | [HttpResourcesRequestHandler](./kibana-plugin-core-server.httpresourcesrequesthandler.md) | Extended version of [RequestHandler](./kibana-plugin-core-server.requesthandler.md) having access to [HttpResourcesServiceToolkit](./kibana-plugin-core-server.httpresourcesservicetoolkit.md) to respond with HTML or JS resources. | | [HttpResourcesResponseOptions](./kibana-plugin-core-server.httpresourcesresponseoptions.md) | HTTP Resources response parameters | | [HttpResponsePayload](./kibana-plugin-core-server.httpresponsepayload.md) | Data send to the client as a response payload. | diff --git a/docs/development/core/server/kibana-plugin-core-server.metricsservicesetup.getopsmetrics_.md b/docs/development/core/server/kibana-plugin-core-server.metricsservicesetup.getopsmetrics_.md index 61107fbf20ad9..0db06b231f60e 100644 --- a/docs/development/core/server/kibana-plugin-core-server.metricsservicesetup.getopsmetrics_.md +++ b/docs/development/core/server/kibana-plugin-core-server.metricsservicesetup.getopsmetrics_.md @@ -19,6 +19,5 @@ getOpsMetrics$: () => Observable; core.metrics.getOpsMetrics$().subscribe(metrics => { // do something with the metrics }) - ``` diff --git a/docs/development/core/server/kibana-plugin-core-server.metricsservicesetup.md b/docs/development/core/server/kibana-plugin-core-server.metricsservicesetup.md index 5fcb1417dea0e..d9b05589ab6a7 100644 --- a/docs/development/core/server/kibana-plugin-core-server.metricsservicesetup.md +++ b/docs/development/core/server/kibana-plugin-core-server.metricsservicesetup.md @@ -16,6 +16,6 @@ export interface MetricsServiceSetup | Property | Type | Description | | --- | --- | --- | -| [collectionInterval](./kibana-plugin-core-server.metricsservicesetup.collectioninterval.md) | number | Interval metrics are collected in milliseconds | -| [getOpsMetrics$](./kibana-plugin-core-server.metricsservicesetup.getopsmetrics_.md) | () => Observable<OpsMetrics> | Retrieve an observable emitting the [OpsMetrics](./kibana-plugin-core-server.opsmetrics.md) gathered. The observable will emit an initial value during core's start phase, and a new value every fixed interval of time, based on the opts.interval configuration property. | +| [collectionInterval](./kibana-plugin-core-server.metricsservicesetup.collectioninterval.md) | number | Interval metrics are collected in milliseconds | +| [getOpsMetrics$](./kibana-plugin-core-server.metricsservicesetup.getopsmetrics_.md) | () => Observable<OpsMetrics> | Retrieve an observable emitting the [OpsMetrics](./kibana-plugin-core-server.opsmetrics.md) gathered. The observable will emit an initial value during core's start phase, and a new value every fixed interval of time, based on the opts.interval configuration property. | diff --git a/docs/development/core/server/kibana-plugin-core-server.nodesversioncompatibility.md b/docs/development/core/server/kibana-plugin-core-server.nodesversioncompatibility.md index cbdac9d5455b0..a282bf4b7b4b6 100644 --- a/docs/development/core/server/kibana-plugin-core-server.nodesversioncompatibility.md +++ b/docs/development/core/server/kibana-plugin-core-server.nodesversioncompatibility.md @@ -14,10 +14,10 @@ export interface NodesVersionCompatibility | Property | Type | Description | | --- | --- | --- | -| [incompatibleNodes](./kibana-plugin-core-server.nodesversioncompatibility.incompatiblenodes.md) | NodeInfo[] | | -| [isCompatible](./kibana-plugin-core-server.nodesversioncompatibility.iscompatible.md) | boolean | | -| [kibanaVersion](./kibana-plugin-core-server.nodesversioncompatibility.kibanaversion.md) | string | | -| [message](./kibana-plugin-core-server.nodesversioncompatibility.message.md) | string | | -| [nodesInfoRequestError](./kibana-plugin-core-server.nodesversioncompatibility.nodesinforequesterror.md) | Error | | -| [warningNodes](./kibana-plugin-core-server.nodesversioncompatibility.warningnodes.md) | NodeInfo[] | | +| [incompatibleNodes](./kibana-plugin-core-server.nodesversioncompatibility.incompatiblenodes.md) | NodeInfo\[\] | | +| [isCompatible](./kibana-plugin-core-server.nodesversioncompatibility.iscompatible.md) | boolean | | +| [kibanaVersion](./kibana-plugin-core-server.nodesversioncompatibility.kibanaversion.md) | string | | +| [message?](./kibana-plugin-core-server.nodesversioncompatibility.message.md) | string | (Optional) | +| [nodesInfoRequestError?](./kibana-plugin-core-server.nodesversioncompatibility.nodesinforequesterror.md) | Error | (Optional) | +| [warningNodes](./kibana-plugin-core-server.nodesversioncompatibility.warningnodes.md) | NodeInfo\[\] | | diff --git a/docs/development/core/server/kibana-plugin-core-server.onpostauthtoolkit.md b/docs/development/core/server/kibana-plugin-core-server.onpostauthtoolkit.md index ba9f7d60667ac..069f63fe01b77 100644 --- a/docs/development/core/server/kibana-plugin-core-server.onpostauthtoolkit.md +++ b/docs/development/core/server/kibana-plugin-core-server.onpostauthtoolkit.md @@ -16,5 +16,5 @@ export interface OnPostAuthToolkit | Property | Type | Description | | --- | --- | --- | -| [next](./kibana-plugin-core-server.onpostauthtoolkit.next.md) | () => OnPostAuthResult | To pass request to the next handler | +| [next](./kibana-plugin-core-server.onpostauthtoolkit.next.md) | () => OnPostAuthResult | To pass request to the next handler | diff --git a/docs/development/core/server/kibana-plugin-core-server.onpreauthtoolkit.md b/docs/development/core/server/kibana-plugin-core-server.onpreauthtoolkit.md index 8031dbc64fa6d..44eed5818c610 100644 --- a/docs/development/core/server/kibana-plugin-core-server.onpreauthtoolkit.md +++ b/docs/development/core/server/kibana-plugin-core-server.onpreauthtoolkit.md @@ -16,5 +16,5 @@ export interface OnPreAuthToolkit | Property | Type | Description | | --- | --- | --- | -| [next](./kibana-plugin-core-server.onpreauthtoolkit.next.md) | () => OnPreAuthResult | To pass request to the next handler | +| [next](./kibana-plugin-core-server.onpreauthtoolkit.next.md) | () => OnPreAuthResult | To pass request to the next handler | diff --git a/docs/development/core/server/kibana-plugin-core-server.onpreresponseextensions.md b/docs/development/core/server/kibana-plugin-core-server.onpreresponseextensions.md index eaaa94b936fd8..078ccc38a70c1 100644 --- a/docs/development/core/server/kibana-plugin-core-server.onpreresponseextensions.md +++ b/docs/development/core/server/kibana-plugin-core-server.onpreresponseextensions.md @@ -16,5 +16,5 @@ export interface OnPreResponseExtensions | Property | Type | Description | | --- | --- | --- | -| [headers](./kibana-plugin-core-server.onpreresponseextensions.headers.md) | ResponseHeaders | additional headers to attach to the response | +| [headers?](./kibana-plugin-core-server.onpreresponseextensions.headers.md) | ResponseHeaders | (Optional) additional headers to attach to the response | diff --git a/docs/development/core/server/kibana-plugin-core-server.onpreresponseinfo.md b/docs/development/core/server/kibana-plugin-core-server.onpreresponseinfo.md index 3e5c882b2fb29..60f7f39ed30a6 100644 --- a/docs/development/core/server/kibana-plugin-core-server.onpreresponseinfo.md +++ b/docs/development/core/server/kibana-plugin-core-server.onpreresponseinfo.md @@ -16,5 +16,5 @@ export interface OnPreResponseInfo | Property | Type | Description | | --- | --- | --- | -| [statusCode](./kibana-plugin-core-server.onpreresponseinfo.statuscode.md) | number | | +| [statusCode](./kibana-plugin-core-server.onpreresponseinfo.statuscode.md) | number | | diff --git a/docs/development/core/server/kibana-plugin-core-server.onpreresponserender.md b/docs/development/core/server/kibana-plugin-core-server.onpreresponserender.md index 0a7ce2d546701..a5afa1a709326 100644 --- a/docs/development/core/server/kibana-plugin-core-server.onpreresponserender.md +++ b/docs/development/core/server/kibana-plugin-core-server.onpreresponserender.md @@ -16,6 +16,6 @@ export interface OnPreResponseRender | Property | Type | Description | | --- | --- | --- | -| [body](./kibana-plugin-core-server.onpreresponserender.body.md) | string | the body to use in the response | -| [headers](./kibana-plugin-core-server.onpreresponserender.headers.md) | ResponseHeaders | additional headers to attach to the response | +| [body](./kibana-plugin-core-server.onpreresponserender.body.md) | string | the body to use in the response | +| [headers?](./kibana-plugin-core-server.onpreresponserender.headers.md) | ResponseHeaders | (Optional) additional headers to attach to the response | diff --git a/docs/development/core/server/kibana-plugin-core-server.onpreresponsetoolkit.md b/docs/development/core/server/kibana-plugin-core-server.onpreresponsetoolkit.md index 14070038132da..197b7b692e734 100644 --- a/docs/development/core/server/kibana-plugin-core-server.onpreresponsetoolkit.md +++ b/docs/development/core/server/kibana-plugin-core-server.onpreresponsetoolkit.md @@ -16,6 +16,6 @@ export interface OnPreResponseToolkit | Property | Type | Description | | --- | --- | --- | -| [next](./kibana-plugin-core-server.onpreresponsetoolkit.next.md) | (responseExtensions?: OnPreResponseExtensions) => OnPreResponseResult | To pass request to the next handler | -| [render](./kibana-plugin-core-server.onpreresponsetoolkit.render.md) | (responseRender: OnPreResponseRender) => OnPreResponseResult | To override the response with a different body | +| [next](./kibana-plugin-core-server.onpreresponsetoolkit.next.md) | (responseExtensions?: OnPreResponseExtensions) => OnPreResponseResult | To pass request to the next handler | +| [render](./kibana-plugin-core-server.onpreresponsetoolkit.render.md) | (responseRender: OnPreResponseRender) => OnPreResponseResult | To override the response with a different body | diff --git a/docs/development/core/server/kibana-plugin-core-server.onpreroutingtoolkit.md b/docs/development/core/server/kibana-plugin-core-server.onpreroutingtoolkit.md index c564896b46a27..e3bdeb3c451c4 100644 --- a/docs/development/core/server/kibana-plugin-core-server.onpreroutingtoolkit.md +++ b/docs/development/core/server/kibana-plugin-core-server.onpreroutingtoolkit.md @@ -16,6 +16,6 @@ export interface OnPreRoutingToolkit | Property | Type | Description | | --- | --- | --- | -| [next](./kibana-plugin-core-server.onpreroutingtoolkit.next.md) | () => OnPreRoutingResult | To pass request to the next handler | -| [rewriteUrl](./kibana-plugin-core-server.onpreroutingtoolkit.rewriteurl.md) | (url: string) => OnPreRoutingResult | Rewrite requested resources url before is was authenticated and routed to a handler | +| [next](./kibana-plugin-core-server.onpreroutingtoolkit.next.md) | () => OnPreRoutingResult | To pass request to the next handler | +| [rewriteUrl](./kibana-plugin-core-server.onpreroutingtoolkit.rewriteurl.md) | (url: string) => OnPreRoutingResult | Rewrite requested resources url before is was authenticated and routed to a handler | diff --git a/docs/development/core/server/kibana-plugin-core-server.opsmetrics.md b/docs/development/core/server/kibana-plugin-core-server.opsmetrics.md index 4774215cef071..dcecfd35a8d2d 100644 --- a/docs/development/core/server/kibana-plugin-core-server.opsmetrics.md +++ b/docs/development/core/server/kibana-plugin-core-server.opsmetrics.md @@ -16,11 +16,11 @@ export interface OpsMetrics | Property | Type | Description | | --- | --- | --- | -| [collected\_at](./kibana-plugin-core-server.opsmetrics.collected_at.md) | Date | Time metrics were recorded at. | -| [concurrent\_connections](./kibana-plugin-core-server.opsmetrics.concurrent_connections.md) | OpsServerMetrics['concurrent_connections'] | number of current concurrent connections to the server | -| [os](./kibana-plugin-core-server.opsmetrics.os.md) | OpsOsMetrics | OS related metrics | -| [process](./kibana-plugin-core-server.opsmetrics.process.md) | OpsProcessMetrics | Process related metrics. | -| [processes](./kibana-plugin-core-server.opsmetrics.processes.md) | OpsProcessMetrics[] | Process related metrics. Reports an array of objects for each kibana pid. | -| [requests](./kibana-plugin-core-server.opsmetrics.requests.md) | OpsServerMetrics['requests'] | server requests stats | -| [response\_times](./kibana-plugin-core-server.opsmetrics.response_times.md) | OpsServerMetrics['response_times'] | server response time stats | +| [collected\_at](./kibana-plugin-core-server.opsmetrics.collected_at.md) | Date | Time metrics were recorded at. | +| [concurrent\_connections](./kibana-plugin-core-server.opsmetrics.concurrent_connections.md) | OpsServerMetrics\['concurrent\_connections'\] | number of current concurrent connections to the server | +| [os](./kibana-plugin-core-server.opsmetrics.os.md) | OpsOsMetrics | OS related metrics | +| [process](./kibana-plugin-core-server.opsmetrics.process.md) | OpsProcessMetrics | Process related metrics. | +| [processes](./kibana-plugin-core-server.opsmetrics.processes.md) | OpsProcessMetrics\[\] | Process related metrics. Reports an array of objects for each kibana pid. | +| [requests](./kibana-plugin-core-server.opsmetrics.requests.md) | OpsServerMetrics\['requests'\] | server requests stats | +| [response\_times](./kibana-plugin-core-server.opsmetrics.response_times.md) | OpsServerMetrics\['response\_times'\] | server response time stats | diff --git a/docs/development/core/server/kibana-plugin-core-server.opsosmetrics.md b/docs/development/core/server/kibana-plugin-core-server.opsosmetrics.md index 8938608531139..08f205d48dd09 100644 --- a/docs/development/core/server/kibana-plugin-core-server.opsosmetrics.md +++ b/docs/development/core/server/kibana-plugin-core-server.opsosmetrics.md @@ -16,13 +16,13 @@ export interface OpsOsMetrics | Property | Type | Description | | --- | --- | --- | -| [cpu](./kibana-plugin-core-server.opsosmetrics.cpu.md) | {
control_group: string;
cfs_period_micros: number;
cfs_quota_micros: number;
stat: {
number_of_elapsed_periods: number;
number_of_times_throttled: number;
time_throttled_nanos: number;
};
} | cpu cgroup metrics, undefined when not running in a cgroup | -| [cpuacct](./kibana-plugin-core-server.opsosmetrics.cpuacct.md) | {
control_group: string;
usage_nanos: number;
} | cpu accounting metrics, undefined when not running in a cgroup | -| [distro](./kibana-plugin-core-server.opsosmetrics.distro.md) | string | The os distrib. Only present for linux platforms | -| [distroRelease](./kibana-plugin-core-server.opsosmetrics.distrorelease.md) | string | The os distrib release, prefixed by the os distrib. Only present for linux platforms | -| [load](./kibana-plugin-core-server.opsosmetrics.load.md) | {
'1m': number;
'5m': number;
'15m': number;
} | cpu load metrics | -| [memory](./kibana-plugin-core-server.opsosmetrics.memory.md) | {
total_in_bytes: number;
free_in_bytes: number;
used_in_bytes: number;
} | system memory usage metrics | -| [platform](./kibana-plugin-core-server.opsosmetrics.platform.md) | NodeJS.Platform | The os platform | -| [platformRelease](./kibana-plugin-core-server.opsosmetrics.platformrelease.md) | string | The os platform release, prefixed by the platform name | -| [uptime\_in\_millis](./kibana-plugin-core-server.opsosmetrics.uptime_in_millis.md) | number | the OS uptime | +| [cpu?](./kibana-plugin-core-server.opsosmetrics.cpu.md) | { control\_group: string; cfs\_period\_micros: number; cfs\_quota\_micros: number; stat: { number\_of\_elapsed\_periods: number; number\_of\_times\_throttled: number; time\_throttled\_nanos: number; }; } | (Optional) cpu cgroup metrics, undefined when not running in a cgroup | +| [cpuacct?](./kibana-plugin-core-server.opsosmetrics.cpuacct.md) | { control\_group: string; usage\_nanos: number; } | (Optional) cpu accounting metrics, undefined when not running in a cgroup | +| [distro?](./kibana-plugin-core-server.opsosmetrics.distro.md) | string | (Optional) The os distrib. Only present for linux platforms | +| [distroRelease?](./kibana-plugin-core-server.opsosmetrics.distrorelease.md) | string | (Optional) The os distrib release, prefixed by the os distrib. Only present for linux platforms | +| [load](./kibana-plugin-core-server.opsosmetrics.load.md) | { '1m': number; '5m': number; '15m': number; } | cpu load metrics | +| [memory](./kibana-plugin-core-server.opsosmetrics.memory.md) | { total\_in\_bytes: number; free\_in\_bytes: number; used\_in\_bytes: number; } | system memory usage metrics | +| [platform](./kibana-plugin-core-server.opsosmetrics.platform.md) | NodeJS.Platform | The os platform | +| [platformRelease](./kibana-plugin-core-server.opsosmetrics.platformrelease.md) | string | The os platform release, prefixed by the platform name | +| [uptime\_in\_millis](./kibana-plugin-core-server.opsosmetrics.uptime_in_millis.md) | number | the OS uptime | diff --git a/docs/development/core/server/kibana-plugin-core-server.opsprocessmetrics.md b/docs/development/core/server/kibana-plugin-core-server.opsprocessmetrics.md index 198b668afca60..43a4333d7bd2c 100644 --- a/docs/development/core/server/kibana-plugin-core-server.opsprocessmetrics.md +++ b/docs/development/core/server/kibana-plugin-core-server.opsprocessmetrics.md @@ -16,9 +16,9 @@ export interface OpsProcessMetrics | Property | Type | Description | | --- | --- | --- | -| [event\_loop\_delay\_histogram](./kibana-plugin-core-server.opsprocessmetrics.event_loop_delay_histogram.md) | IntervalHistogram | node event loop delay histogram since last collection | -| [event\_loop\_delay](./kibana-plugin-core-server.opsprocessmetrics.event_loop_delay.md) | number | mean event loop delay since last collection | -| [memory](./kibana-plugin-core-server.opsprocessmetrics.memory.md) | {
heap: {
total_in_bytes: number;
used_in_bytes: number;
size_limit: number;
};
resident_set_size_in_bytes: number;
} | process memory usage | -| [pid](./kibana-plugin-core-server.opsprocessmetrics.pid.md) | number | pid of the kibana process | -| [uptime\_in\_millis](./kibana-plugin-core-server.opsprocessmetrics.uptime_in_millis.md) | number | uptime of the kibana process | +| [event\_loop\_delay\_histogram](./kibana-plugin-core-server.opsprocessmetrics.event_loop_delay_histogram.md) | IntervalHistogram | node event loop delay histogram since last collection | +| [event\_loop\_delay](./kibana-plugin-core-server.opsprocessmetrics.event_loop_delay.md) | number | mean event loop delay since last collection | +| [memory](./kibana-plugin-core-server.opsprocessmetrics.memory.md) | { heap: { total\_in\_bytes: number; used\_in\_bytes: number; size\_limit: number; }; resident\_set\_size\_in\_bytes: number; } | process memory usage | +| [pid](./kibana-plugin-core-server.opsprocessmetrics.pid.md) | number | pid of the kibana process | +| [uptime\_in\_millis](./kibana-plugin-core-server.opsprocessmetrics.uptime_in_millis.md) | number | uptime of the kibana process | diff --git a/docs/development/core/server/kibana-plugin-core-server.opsservermetrics.md b/docs/development/core/server/kibana-plugin-core-server.opsservermetrics.md index ad6f64600a96e..ddabbd124627b 100644 --- a/docs/development/core/server/kibana-plugin-core-server.opsservermetrics.md +++ b/docs/development/core/server/kibana-plugin-core-server.opsservermetrics.md @@ -16,7 +16,7 @@ export interface OpsServerMetrics | Property | Type | Description | | --- | --- | --- | -| [concurrent\_connections](./kibana-plugin-core-server.opsservermetrics.concurrent_connections.md) | number | number of current concurrent connections to the server | -| [requests](./kibana-plugin-core-server.opsservermetrics.requests.md) | {
disconnects: number;
total: number;
statusCodes: Record<number, number>;
} | server requests stats | -| [response\_times](./kibana-plugin-core-server.opsservermetrics.response_times.md) | {
avg_in_millis: number;
max_in_millis: number;
} | server response time stats | +| [concurrent\_connections](./kibana-plugin-core-server.opsservermetrics.concurrent_connections.md) | number | number of current concurrent connections to the server | +| [requests](./kibana-plugin-core-server.opsservermetrics.requests.md) | { disconnects: number; total: number; statusCodes: Record<number, number>; } | server requests stats | +| [response\_times](./kibana-plugin-core-server.opsservermetrics.response_times.md) | { avg\_in\_millis: number; max\_in\_millis: number; } | server response time stats | diff --git a/docs/development/core/server/kibana-plugin-core-server.plugin.md b/docs/development/core/server/kibana-plugin-core-server.plugin.md deleted file mode 100644 index b1fce06d46f3a..0000000000000 --- a/docs/development/core/server/kibana-plugin-core-server.plugin.md +++ /dev/null @@ -1,22 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [Plugin](./kibana-plugin-core-server.plugin.md) - -## Plugin interface - -The interface that should be returned by a `PluginInitializer` for a `standard` plugin. - -Signature: - -```typescript -export interface Plugin -``` - -## Methods - -| Method | Description | -| --- | --- | -| [setup(core, plugins)](./kibana-plugin-core-server.plugin.setup.md) | | -| [start(core, plugins)](./kibana-plugin-core-server.plugin.start.md) | | -| [stop()](./kibana-plugin-core-server.plugin.stop.md) | | - diff --git a/docs/development/core/server/kibana-plugin-core-server.plugin.setup.md b/docs/development/core/server/kibana-plugin-core-server.plugin.setup.md deleted file mode 100644 index a8b0aae28d251..0000000000000 --- a/docs/development/core/server/kibana-plugin-core-server.plugin.setup.md +++ /dev/null @@ -1,23 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [Plugin](./kibana-plugin-core-server.plugin.md) > [setup](./kibana-plugin-core-server.plugin.setup.md) - -## Plugin.setup() method - -Signature: - -```typescript -setup(core: CoreSetup, plugins: TPluginsSetup): TSetup; -``` - -## Parameters - -| Parameter | Type | Description | -| --- | --- | --- | -| core | CoreSetup | | -| plugins | TPluginsSetup | | - -Returns: - -`TSetup` - diff --git a/docs/development/core/server/kibana-plugin-core-server.plugin.start.md b/docs/development/core/server/kibana-plugin-core-server.plugin.start.md deleted file mode 100644 index 851f84474fe11..0000000000000 --- a/docs/development/core/server/kibana-plugin-core-server.plugin.start.md +++ /dev/null @@ -1,23 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [Plugin](./kibana-plugin-core-server.plugin.md) > [start](./kibana-plugin-core-server.plugin.start.md) - -## Plugin.start() method - -Signature: - -```typescript -start(core: CoreStart, plugins: TPluginsStart): TStart; -``` - -## Parameters - -| Parameter | Type | Description | -| --- | --- | --- | -| core | CoreStart | | -| plugins | TPluginsStart | | - -Returns: - -`TStart` - diff --git a/docs/development/core/server/kibana-plugin-core-server.plugin.stop.md b/docs/development/core/server/kibana-plugin-core-server.plugin.stop.md deleted file mode 100644 index 5396e3d9c59f2..0000000000000 --- a/docs/development/core/server/kibana-plugin-core-server.plugin.stop.md +++ /dev/null @@ -1,15 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [Plugin](./kibana-plugin-core-server.plugin.md) > [stop](./kibana-plugin-core-server.plugin.stop.md) - -## Plugin.stop() method - -Signature: - -```typescript -stop?(): void; -``` -Returns: - -`void` - diff --git a/docs/development/core/server/kibana-plugin-core-server.plugin_2.md b/docs/development/core/server/kibana-plugin-core-server.plugin_2.md new file mode 100644 index 0000000000000..79dbbb56c86fd --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.plugin_2.md @@ -0,0 +1,22 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [Plugin\_2](./kibana-plugin-core-server.plugin_2.md) + +## Plugin\_2 interface + +The interface that should be returned by a `PluginInitializer` for a `standard` plugin. + +Signature: + +```typescript +export interface Plugin +``` + +## Methods + +| Method | Description | +| --- | --- | +| [setup(core, plugins)](./kibana-plugin-core-server.plugin_2.setup.md) | | +| [start(core, plugins)](./kibana-plugin-core-server.plugin_2.start.md) | | +| [stop()?](./kibana-plugin-core-server.plugin_2.stop.md) | (Optional) | + diff --git a/docs/development/core/server/kibana-plugin-core-server.plugin_2.setup.md b/docs/development/core/server/kibana-plugin-core-server.plugin_2.setup.md new file mode 100644 index 0000000000000..cedce40b58000 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.plugin_2.setup.md @@ -0,0 +1,23 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [Plugin\_2](./kibana-plugin-core-server.plugin_2.md) > [setup](./kibana-plugin-core-server.plugin_2.setup.md) + +## Plugin\_2.setup() method + +Signature: + +```typescript +setup(core: CoreSetup, plugins: TPluginsSetup): TSetup; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| core | CoreSetup | | +| plugins | TPluginsSetup | | + +Returns: + +TSetup + diff --git a/docs/development/core/server/kibana-plugin-core-server.plugin_2.start.md b/docs/development/core/server/kibana-plugin-core-server.plugin_2.start.md new file mode 100644 index 0000000000000..08eb5431a2cf2 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.plugin_2.start.md @@ -0,0 +1,23 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [Plugin\_2](./kibana-plugin-core-server.plugin_2.md) > [start](./kibana-plugin-core-server.plugin_2.start.md) + +## Plugin\_2.start() method + +Signature: + +```typescript +start(core: CoreStart, plugins: TPluginsStart): TStart; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| core | CoreStart | | +| plugins | TPluginsStart | | + +Returns: + +TStart + diff --git a/docs/development/core/server/kibana-plugin-core-server.plugin_2.stop.md b/docs/development/core/server/kibana-plugin-core-server.plugin_2.stop.md new file mode 100644 index 0000000000000..5b62a5e82b2ed --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.plugin_2.stop.md @@ -0,0 +1,15 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [Plugin\_2](./kibana-plugin-core-server.plugin_2.md) > [stop](./kibana-plugin-core-server.plugin_2.stop.md) + +## Plugin\_2.stop() method + +Signature: + +```typescript +stop?(): void; +``` +Returns: + +void + diff --git a/docs/development/core/server/kibana-plugin-core-server.pluginconfigdescriptor.md b/docs/development/core/server/kibana-plugin-core-server.pluginconfigdescriptor.md index 80e807a1361fd..b9cf0eea3362d 100644 --- a/docs/development/core/server/kibana-plugin-core-server.pluginconfigdescriptor.md +++ b/docs/development/core/server/kibana-plugin-core-server.pluginconfigdescriptor.md @@ -37,15 +37,14 @@ export const config: PluginConfigDescriptor = { unused('deprecatedProperty'), ], }; - ``` ## Properties | Property | Type | Description | | --- | --- | --- | -| [deprecations](./kibana-plugin-core-server.pluginconfigdescriptor.deprecations.md) | ConfigDeprecationProvider | Provider for the to apply to the plugin configuration. | -| [exposeToBrowser](./kibana-plugin-core-server.pluginconfigdescriptor.exposetobrowser.md) | {
[P in keyof T]?: boolean;
} | List of configuration properties that will be available on the client-side plugin. | -| [exposeToUsage](./kibana-plugin-core-server.pluginconfigdescriptor.exposetousage.md) | MakeUsageFromSchema<T> | Expose non-default configs to usage collection to be sent via telemetry. set a config to true to report the actual changed config value. set a config to false to report the changed config value as \[redacted\].All changed configs except booleans and numbers will be reported as \[redacted\] unless otherwise specified.[MakeUsageFromSchema](./kibana-plugin-core-server.makeusagefromschema.md) | -| [schema](./kibana-plugin-core-server.pluginconfigdescriptor.schema.md) | PluginConfigSchema<T> | Schema to use to validate the plugin configuration.[PluginConfigSchema](./kibana-plugin-core-server.pluginconfigschema.md) | +| [deprecations?](./kibana-plugin-core-server.pluginconfigdescriptor.deprecations.md) | ConfigDeprecationProvider | (Optional) Provider for the to apply to the plugin configuration. | +| [exposeToBrowser?](./kibana-plugin-core-server.pluginconfigdescriptor.exposetobrowser.md) | { \[P in keyof T\]?: boolean; } | (Optional) List of configuration properties that will be available on the client-side plugin. | +| [exposeToUsage?](./kibana-plugin-core-server.pluginconfigdescriptor.exposetousage.md) | MakeUsageFromSchema<T> | (Optional) Expose non-default configs to usage collection to be sent via telemetry. set a config to true to report the actual changed config value. set a config to false to report the changed config value as \[redacted\].All changed configs except booleans and numbers will be reported as \[redacted\] unless otherwise specified.[MakeUsageFromSchema](./kibana-plugin-core-server.makeusagefromschema.md) | +| [schema](./kibana-plugin-core-server.pluginconfigdescriptor.schema.md) | PluginConfigSchema<T> | Schema to use to validate the plugin configuration.[PluginConfigSchema](./kibana-plugin-core-server.pluginconfigschema.md) | diff --git a/docs/development/core/server/kibana-plugin-core-server.plugininitializercontext.logger.md b/docs/development/core/server/kibana-plugin-core-server.plugininitializercontext.logger.md index e5de046eccf1d..74bf5b0c1384c 100644 --- a/docs/development/core/server/kibana-plugin-core-server.plugininitializercontext.logger.md +++ b/docs/development/core/server/kibana-plugin-core-server.plugininitializercontext.logger.md @@ -27,6 +27,5 @@ export class MyPlugin implements Plugin { // `mySubLogger` context: `plugins.myPlugin.sub` } } - ``` diff --git a/docs/development/core/server/kibana-plugin-core-server.plugininitializercontext.md b/docs/development/core/server/kibana-plugin-core-server.plugininitializercontext.md index 9bc9d6d83674c..e2d115578d7c1 100644 --- a/docs/development/core/server/kibana-plugin-core-server.plugininitializercontext.md +++ b/docs/development/core/server/kibana-plugin-core-server.plugininitializercontext.md @@ -16,8 +16,8 @@ export interface PluginInitializerContext | Property | Type | Description | | --- | --- | --- | -| [config](./kibana-plugin-core-server.plugininitializercontext.config.md) | {
legacy: {
globalConfig$: Observable<SharedGlobalConfig>;
get: () => SharedGlobalConfig;
};
create: <T = ConfigSchema>() => Observable<T>;
get: <T = ConfigSchema>() => T;
} | Accessors for the plugin's configuration | -| [env](./kibana-plugin-core-server.plugininitializercontext.env.md) | {
mode: EnvironmentMode;
packageInfo: Readonly<PackageInfo>;
instanceUuid: string;
configs: readonly string[];
} | | -| [logger](./kibana-plugin-core-server.plugininitializercontext.logger.md) | LoggerFactory | instance already bound to the plugin's logging context | -| [opaqueId](./kibana-plugin-core-server.plugininitializercontext.opaqueid.md) | PluginOpaqueId | | +| [config](./kibana-plugin-core-server.plugininitializercontext.config.md) | { legacy: { globalConfig$: Observable<SharedGlobalConfig>; get: () => SharedGlobalConfig; }; create: <T = ConfigSchema>() => Observable<T>; get: <T = ConfigSchema>() => T; } | Accessors for the plugin's configuration | +| [env](./kibana-plugin-core-server.plugininitializercontext.env.md) | { mode: EnvironmentMode; packageInfo: Readonly<PackageInfo>; instanceUuid: string; configs: readonly string\[\]; } | | +| [logger](./kibana-plugin-core-server.plugininitializercontext.logger.md) | LoggerFactory | instance already bound to the plugin's logging context | +| [opaqueId](./kibana-plugin-core-server.plugininitializercontext.opaqueid.md) | PluginOpaqueId | | diff --git a/docs/development/core/server/kibana-plugin-core-server.pluginmanifest.md b/docs/development/core/server/kibana-plugin-core-server.pluginmanifest.md index e82599c11f51a..4c7c63b791a79 100644 --- a/docs/development/core/server/kibana-plugin-core-server.pluginmanifest.md +++ b/docs/development/core/server/kibana-plugin-core-server.pluginmanifest.md @@ -20,18 +20,18 @@ Should never be used in code outside of Core but is exported for documentation p | Property | Type | Description | | --- | --- | --- | -| [configPath](./kibana-plugin-core-server.pluginmanifest.configpath.md) | ConfigPath | Root used by the plugin, defaults to "id" in snake\_case format. | -| [description](./kibana-plugin-core-server.pluginmanifest.description.md) | string | TODO: make required once all plugins specify this. A brief description of what this plugin does and any capabilities it provides. | -| [extraPublicDirs](./kibana-plugin-core-server.pluginmanifest.extrapublicdirs.md) | string[] | Specifies directory names that can be imported by other ui-plugins built using the same instance of the @kbn/optimizer. A temporary measure we plan to replace with better mechanisms for sharing static code between plugins | -| [id](./kibana-plugin-core-server.pluginmanifest.id.md) | PluginName | Identifier of the plugin. Must be a string in camelCase. Part of a plugin public contract. Other plugins leverage it to access plugin API, navigate to the plugin, etc. | -| [kibanaVersion](./kibana-plugin-core-server.pluginmanifest.kibanaversion.md) | string | The version of Kibana the plugin is compatible with, defaults to "version". | -| [optionalPlugins](./kibana-plugin-core-server.pluginmanifest.optionalplugins.md) | readonly PluginName[] | An optional list of the other plugins that if installed and enabled \*\*may be\*\* leveraged by this plugin for some additional functionality but otherwise are not required for this plugin to work properly. | -| [owner](./kibana-plugin-core-server.pluginmanifest.owner.md) | {
readonly name: string;
readonly githubTeam?: string;
} | | -| [requiredBundles](./kibana-plugin-core-server.pluginmanifest.requiredbundles.md) | readonly string[] | List of plugin ids that this plugin's UI code imports modules from that are not in requiredPlugins. | -| [requiredPlugins](./kibana-plugin-core-server.pluginmanifest.requiredplugins.md) | readonly PluginName[] | An optional list of the other plugins that \*\*must be\*\* installed and enabled for this plugin to function properly. | -| [server](./kibana-plugin-core-server.pluginmanifest.server.md) | boolean | Specifies whether plugin includes some server-side specific functionality. | -| [serviceFolders](./kibana-plugin-core-server.pluginmanifest.servicefolders.md) | readonly string[] | Only used for the automatically generated API documentation. Specifying service folders will cause your plugin API reference to be broken up into sub sections. | -| [type](./kibana-plugin-core-server.pluginmanifest.type.md) | PluginType | Type of the plugin, defaults to standard. | -| [ui](./kibana-plugin-core-server.pluginmanifest.ui.md) | boolean | Specifies whether plugin includes some client/browser specific functionality that should be included into client bundle via public/ui_plugin.js file. | -| [version](./kibana-plugin-core-server.pluginmanifest.version.md) | string | Version of the plugin. | +| [configPath](./kibana-plugin-core-server.pluginmanifest.configpath.md) | ConfigPath | Root used by the plugin, defaults to "id" in snake\_case format. | +| [description?](./kibana-plugin-core-server.pluginmanifest.description.md) | string | (Optional) TODO: make required once all plugins specify this. A brief description of what this plugin does and any capabilities it provides. | +| [extraPublicDirs?](./kibana-plugin-core-server.pluginmanifest.extrapublicdirs.md) | string\[\] | (Optional) Specifies directory names that can be imported by other ui-plugins built using the same instance of the @kbn/optimizer. A temporary measure we plan to replace with better mechanisms for sharing static code between plugins | +| [id](./kibana-plugin-core-server.pluginmanifest.id.md) | PluginName | Identifier of the plugin. Must be a string in camelCase. Part of a plugin public contract. Other plugins leverage it to access plugin API, navigate to the plugin, etc. | +| [kibanaVersion](./kibana-plugin-core-server.pluginmanifest.kibanaversion.md) | string | The version of Kibana the plugin is compatible with, defaults to "version". | +| [optionalPlugins](./kibana-plugin-core-server.pluginmanifest.optionalplugins.md) | readonly PluginName\[\] | An optional list of the other plugins that if installed and enabled \*\*may be\*\* leveraged by this plugin for some additional functionality but otherwise are not required for this plugin to work properly. | +| [owner](./kibana-plugin-core-server.pluginmanifest.owner.md) | { readonly name: string; readonly githubTeam?: string; } | | +| [requiredBundles](./kibana-plugin-core-server.pluginmanifest.requiredbundles.md) | readonly string\[\] | List of plugin ids that this plugin's UI code imports modules from that are not in requiredPlugins. | +| [requiredPlugins](./kibana-plugin-core-server.pluginmanifest.requiredplugins.md) | readonly PluginName\[\] | An optional list of the other plugins that \*\*must be\*\* installed and enabled for this plugin to function properly. | +| [server](./kibana-plugin-core-server.pluginmanifest.server.md) | boolean | Specifies whether plugin includes some server-side specific functionality. | +| [serviceFolders?](./kibana-plugin-core-server.pluginmanifest.servicefolders.md) | readonly string\[\] | (Optional) Only used for the automatically generated API documentation. Specifying service folders will cause your plugin API reference to be broken up into sub sections. | +| [type](./kibana-plugin-core-server.pluginmanifest.type.md) | PluginType | Type of the plugin, defaults to standard. | +| [ui](./kibana-plugin-core-server.pluginmanifest.ui.md) | boolean | Specifies whether plugin includes some client/browser specific functionality that should be included into client bundle via public/ui_plugin.js file. | +| [version](./kibana-plugin-core-server.pluginmanifest.version.md) | string | Version of the plugin. | diff --git a/docs/development/core/server/kibana-plugin-core-server.prebootplugin.md b/docs/development/core/server/kibana-plugin-core-server.prebootplugin.md index df851daab7806..8bbb042965dde 100644 --- a/docs/development/core/server/kibana-plugin-core-server.prebootplugin.md +++ b/docs/development/core/server/kibana-plugin-core-server.prebootplugin.md @@ -17,5 +17,5 @@ export interface PrebootPlugin(Optional) | diff --git a/docs/development/core/server/kibana-plugin-core-server.prebootplugin.setup.md b/docs/development/core/server/kibana-plugin-core-server.prebootplugin.setup.md index 0ee2a26293e98..f55819eeaca57 100644 --- a/docs/development/core/server/kibana-plugin-core-server.prebootplugin.setup.md +++ b/docs/development/core/server/kibana-plugin-core-server.prebootplugin.setup.md @@ -14,10 +14,10 @@ setup(core: CorePreboot, plugins: TPluginsSetup): TSetup; | Parameter | Type | Description | | --- | --- | --- | -| core | CorePreboot | | -| plugins | TPluginsSetup | | +| core | CorePreboot | | +| plugins | TPluginsSetup | | Returns: -`TSetup` +TSetup diff --git a/docs/development/core/server/kibana-plugin-core-server.prebootplugin.stop.md b/docs/development/core/server/kibana-plugin-core-server.prebootplugin.stop.md index 89566b2ae6687..c93dcb7709a11 100644 --- a/docs/development/core/server/kibana-plugin-core-server.prebootplugin.stop.md +++ b/docs/development/core/server/kibana-plugin-core-server.prebootplugin.stop.md @@ -11,5 +11,5 @@ stop?(): void; ``` Returns: -`void` +void diff --git a/docs/development/core/server/kibana-plugin-core-server.prebootservicepreboot.md b/docs/development/core/server/kibana-plugin-core-server.prebootservicepreboot.md index bf503499b6298..c9c7c15ac3275 100644 --- a/docs/development/core/server/kibana-plugin-core-server.prebootservicepreboot.md +++ b/docs/development/core/server/kibana-plugin-core-server.prebootservicepreboot.md @@ -22,7 +22,6 @@ core.preboot.holdSetupUntilResolved('Just waiting for 5 seconds', setTimeout(resolve, 5000); }) ); - ``` If the supplied `Promise` resolves to an object with the `shouldReloadConfig` property set to `true`, Kibana will also reload its configuration from disk. @@ -33,13 +32,12 @@ core.preboot.holdSetupUntilResolved('Just waiting for 5 seconds before reloading setTimeout(() => resolve({ shouldReloadConfig: true }), 5000); }) ); - ``` ## Properties | Property | Type | Description | | --- | --- | --- | -| [holdSetupUntilResolved](./kibana-plugin-core-server.prebootservicepreboot.holdsetupuntilresolved.md) | (reason: string, promise: Promise<{
shouldReloadConfig: boolean;
} | undefined>) => void | Registers a Promise as a precondition before Kibana can proceed to setup. This method can be invoked multiple times and from multiple preboot plugins. Kibana will proceed to setup only when all registered Promises instances are resolved, or it will shut down if any of them is rejected. | -| [isSetupOnHold](./kibana-plugin-core-server.prebootservicepreboot.issetuponhold.md) | () => boolean | Indicates whether Kibana is currently on hold and cannot proceed to setup yet. | +| [holdSetupUntilResolved](./kibana-plugin-core-server.prebootservicepreboot.holdsetupuntilresolved.md) | (reason: string, promise: Promise<{ shouldReloadConfig: boolean; } \| undefined>) => void | Registers a Promise as a precondition before Kibana can proceed to setup. This method can be invoked multiple times and from multiple preboot plugins. Kibana will proceed to setup only when all registered Promises instances are resolved, or it will shut down if any of them is rejected. | +| [isSetupOnHold](./kibana-plugin-core-server.prebootservicepreboot.issetuponhold.md) | () => boolean | Indicates whether Kibana is currently on hold and cannot proceed to setup yet. | diff --git a/docs/development/core/server/kibana-plugin-core-server.registerdeprecationsconfig.md b/docs/development/core/server/kibana-plugin-core-server.registerdeprecationsconfig.md index 444c2653512de..b7787a1406319 100644 --- a/docs/development/core/server/kibana-plugin-core-server.registerdeprecationsconfig.md +++ b/docs/development/core/server/kibana-plugin-core-server.registerdeprecationsconfig.md @@ -15,5 +15,5 @@ export interface RegisterDeprecationsConfig | Property | Type | Description | | --- | --- | --- | -| [getDeprecations](./kibana-plugin-core-server.registerdeprecationsconfig.getdeprecations.md) | (context: GetDeprecationsContext) => MaybePromise<DeprecationsDetails[]> | | +| [getDeprecations](./kibana-plugin-core-server.registerdeprecationsconfig.getdeprecations.md) | (context: GetDeprecationsContext) => MaybePromise<DeprecationsDetails\[\]> | | diff --git a/docs/development/core/server/kibana-plugin-core-server.requesthandler.md b/docs/development/core/server/kibana-plugin-core-server.requesthandler.md index d32ac4d80c337..0ba0f72d7ab2f 100644 --- a/docs/development/core/server/kibana-plugin-core-server.requesthandler.md +++ b/docs/development/core/server/kibana-plugin-core-server.requesthandler.md @@ -37,6 +37,5 @@ router.get( return response.ok(data); } ); - ``` diff --git a/docs/development/core/server/kibana-plugin-core-server.requesthandlercontext.md b/docs/development/core/server/kibana-plugin-core-server.requesthandlercontext.md index 15a2e235fff29..0d705c9daa333 100644 --- a/docs/development/core/server/kibana-plugin-core-server.requesthandlercontext.md +++ b/docs/development/core/server/kibana-plugin-core-server.requesthandlercontext.md @@ -18,5 +18,5 @@ export interface RequestHandlerContext | Property | Type | Description | | --- | --- | --- | -| [core](./kibana-plugin-core-server.requesthandlercontext.core.md) | {
savedObjects: {
client: SavedObjectsClientContract;
typeRegistry: ISavedObjectTypeRegistry;
getClient: (options?: SavedObjectsClientProviderOptions) => SavedObjectsClientContract;
getExporter: (client: SavedObjectsClientContract) => ISavedObjectsExporter;
getImporter: (client: SavedObjectsClientContract) => ISavedObjectsImporter;
};
elasticsearch: {
client: IScopedClusterClient;
};
uiSettings: {
client: IUiSettingsClient;
};
deprecations: {
client: DeprecationsClient;
};
} | | +| [core](./kibana-plugin-core-server.requesthandlercontext.core.md) | { savedObjects: { client: SavedObjectsClientContract; typeRegistry: ISavedObjectTypeRegistry; getClient: (options?: SavedObjectsClientProviderOptions) => SavedObjectsClientContract; getExporter: (client: SavedObjectsClientContract) => ISavedObjectsExporter; getImporter: (client: SavedObjectsClientContract) => ISavedObjectsImporter; }; elasticsearch: { client: IScopedClusterClient; }; uiSettings: { client: IUiSettingsClient; }; deprecations: { client: DeprecationsClient; }; } | | diff --git a/docs/development/core/server/kibana-plugin-core-server.requesthandlerwrapper.md b/docs/development/core/server/kibana-plugin-core-server.requesthandlerwrapper.md index 76c7ee4f22902..6ae585b4eeb04 100644 --- a/docs/development/core/server/kibana-plugin-core-server.requesthandlerwrapper.md +++ b/docs/development/core/server/kibana-plugin-core-server.requesthandlerwrapper.md @@ -22,6 +22,5 @@ export const wrapper: RequestHandlerWrapper = handler => { ... }; } - ``` diff --git a/docs/development/core/server/kibana-plugin-core-server.resolvecapabilitiesoptions.md b/docs/development/core/server/kibana-plugin-core-server.resolvecapabilitiesoptions.md index f118c34c9be0f..e23d07d7683de 100644 --- a/docs/development/core/server/kibana-plugin-core-server.resolvecapabilitiesoptions.md +++ b/docs/development/core/server/kibana-plugin-core-server.resolvecapabilitiesoptions.md @@ -16,5 +16,5 @@ export interface ResolveCapabilitiesOptions | Property | Type | Description | | --- | --- | --- | -| [useDefaultCapabilities](./kibana-plugin-core-server.resolvecapabilitiesoptions.usedefaultcapabilities.md) | boolean | Indicates if capability switchers are supposed to return a default set of capabilities. | +| [useDefaultCapabilities](./kibana-plugin-core-server.resolvecapabilitiesoptions.usedefaultcapabilities.md) | boolean | Indicates if capability switchers are supposed to return a default set of capabilities. | diff --git a/docs/development/core/server/kibana-plugin-core-server.routeconfig.md b/docs/development/core/server/kibana-plugin-core-server.routeconfig.md index b61ac23e68cb8..6297e2745cd31 100644 --- a/docs/development/core/server/kibana-plugin-core-server.routeconfig.md +++ b/docs/development/core/server/kibana-plugin-core-server.routeconfig.md @@ -16,7 +16,7 @@ export interface RouteConfig | Property | Type | Description | | --- | --- | --- | -| [options](./kibana-plugin-core-server.routeconfig.options.md) | RouteConfigOptions<Method> | Additional route options [RouteConfigOptions](./kibana-plugin-core-server.routeconfigoptions.md). | -| [path](./kibana-plugin-core-server.routeconfig.path.md) | string | The endpoint \_within\_ the router path to register the route. | -| [validate](./kibana-plugin-core-server.routeconfig.validate.md) | RouteValidatorFullConfig<P, Q, B> | false | A schema created with @kbn/config-schema that every request will be validated against. | +| [options?](./kibana-plugin-core-server.routeconfig.options.md) | RouteConfigOptions<Method> | (Optional) Additional route options [RouteConfigOptions](./kibana-plugin-core-server.routeconfigoptions.md). | +| [path](./kibana-plugin-core-server.routeconfig.path.md) | string | The endpoint \_within\_ the router path to register the route. | +| [validate](./kibana-plugin-core-server.routeconfig.validate.md) | RouteValidatorFullConfig<P, Q, B> \| false | A schema created with @kbn/config-schema that every request will be validated against. | diff --git a/docs/development/core/server/kibana-plugin-core-server.routeconfig.validate.md b/docs/development/core/server/kibana-plugin-core-server.routeconfig.validate.md index 3bbabc04f2500..1f9cc216cad35 100644 --- a/docs/development/core/server/kibana-plugin-core-server.routeconfig.validate.md +++ b/docs/development/core/server/kibana-plugin-core-server.routeconfig.validate.md @@ -57,6 +57,5 @@ router.get({ console.log(req.params.id); // value myValidationLibrary.validate({ params: req.params }); }); - ``` diff --git a/docs/development/core/server/kibana-plugin-core-server.routeconfigoptions.md b/docs/development/core/server/kibana-plugin-core-server.routeconfigoptions.md index cf0fe32c14d1d..2dcd8ee5420ab 100644 --- a/docs/development/core/server/kibana-plugin-core-server.routeconfigoptions.md +++ b/docs/development/core/server/kibana-plugin-core-server.routeconfigoptions.md @@ -16,9 +16,9 @@ export interface RouteConfigOptions | Property | Type | Description | | --- | --- | --- | -| [authRequired](./kibana-plugin-core-server.routeconfigoptions.authrequired.md) | boolean | 'optional' | Defines authentication mode for a route: - true. A user has to have valid credentials to access a resource - false. A user can access a resource without any credentials. - 'optional'. A user can access a resource, and will be authenticated if provided credentials are valid. Can be useful when we grant access to a resource but want to identify a user if possible.Defaults to true if an auth mechanism is registered. | -| [body](./kibana-plugin-core-server.routeconfigoptions.body.md) | Method extends 'get' | 'options' ? undefined : RouteConfigOptionsBody | Additional body options [RouteConfigOptionsBody](./kibana-plugin-core-server.routeconfigoptionsbody.md). | -| [tags](./kibana-plugin-core-server.routeconfigoptions.tags.md) | readonly string[] | Additional metadata tag strings to attach to the route. | -| [timeout](./kibana-plugin-core-server.routeconfigoptions.timeout.md) | {
payload?: Method extends 'get' | 'options' ? undefined : number;
idleSocket?: number;
} | Defines per-route timeouts. | -| [xsrfRequired](./kibana-plugin-core-server.routeconfigoptions.xsrfrequired.md) | Method extends 'get' ? never : boolean | Defines xsrf protection requirements for a route: - true. Requires an incoming POST/PUT/DELETE request to contain kbn-xsrf header. - false. Disables xsrf protection.Set to true by default | +| [authRequired?](./kibana-plugin-core-server.routeconfigoptions.authrequired.md) | boolean \| 'optional' | (Optional) Defines authentication mode for a route: - true. A user has to have valid credentials to access a resource - false. A user can access a resource without any credentials. - 'optional'. A user can access a resource, and will be authenticated if provided credentials are valid. Can be useful when we grant access to a resource but want to identify a user if possible.Defaults to true if an auth mechanism is registered. | +| [body?](./kibana-plugin-core-server.routeconfigoptions.body.md) | Method extends 'get' \| 'options' ? undefined : RouteConfigOptionsBody | (Optional) Additional body options [RouteConfigOptionsBody](./kibana-plugin-core-server.routeconfigoptionsbody.md). | +| [tags?](./kibana-plugin-core-server.routeconfigoptions.tags.md) | readonly string\[\] | (Optional) Additional metadata tag strings to attach to the route. | +| [timeout?](./kibana-plugin-core-server.routeconfigoptions.timeout.md) | { payload?: Method extends 'get' \| 'options' ? undefined : number; idleSocket?: number; } | (Optional) Defines per-route timeouts. | +| [xsrfRequired?](./kibana-plugin-core-server.routeconfigoptions.xsrfrequired.md) | Method extends 'get' ? never : boolean | (Optional) Defines xsrf protection requirements for a route: - true. Requires an incoming POST/PUT/DELETE request to contain kbn-xsrf header. - false. Disables xsrf protection.Set to true by default | diff --git a/docs/development/core/server/kibana-plugin-core-server.routeconfigoptionsbody.md b/docs/development/core/server/kibana-plugin-core-server.routeconfigoptionsbody.md index d27c67891161a..fdae03f7cd7c9 100644 --- a/docs/development/core/server/kibana-plugin-core-server.routeconfigoptionsbody.md +++ b/docs/development/core/server/kibana-plugin-core-server.routeconfigoptionsbody.md @@ -16,8 +16,8 @@ export interface RouteConfigOptionsBody | Property | Type | Description | | --- | --- | --- | -| [accepts](./kibana-plugin-core-server.routeconfigoptionsbody.accepts.md) | RouteContentType | RouteContentType[] | string | string[] | A string or an array of strings with the allowed mime types for the endpoint. Use this settings to limit the set of allowed mime types. Note that allowing additional mime types not listed above will not enable them to be parsed, and if parse is true, the request will result in an error response.Default value: allows parsing of the following mime types: \* application/json \* application/\*+json \* application/octet-stream \* application/x-www-form-urlencoded \* multipart/form-data \* text/\* | -| [maxBytes](./kibana-plugin-core-server.routeconfigoptionsbody.maxbytes.md) | number | Limits the size of incoming payloads to the specified byte count. Allowing very large payloads may cause the server to run out of memory.Default value: The one set in the kibana.yml config file under the parameter server.maxPayload. | -| [output](./kibana-plugin-core-server.routeconfigoptionsbody.output.md) | typeof validBodyOutput[number] | The processed payload format. The value must be one of: \* 'data' - the incoming payload is read fully into memory. If parse is true, the payload is parsed (JSON, form-decoded, multipart) based on the 'Content-Type' header. If parse is false, a raw Buffer is returned. \* 'stream' - the incoming payload is made available via a Stream.Readable interface. If the payload is 'multipart/form-data' and parse is true, field values are presented as text while files are provided as streams. File streams from a 'multipart/form-data' upload will also have a hapi property containing the filename and headers properties. Note that payload streams for multipart payloads are a synthetic interface created on top of the entire multipart content loaded into memory. To avoid loading large multipart payloads into memory, set parse to false and handle the multipart payload in the handler using a streaming parser (e.g. pez).Default value: 'data', unless no validation.body is provided in the route definition. In that case the default is 'stream' to alleviate memory pressure. | -| [parse](./kibana-plugin-core-server.routeconfigoptionsbody.parse.md) | boolean | 'gunzip' | Determines if the incoming payload is processed or presented raw. Available values: \* true - if the request 'Content-Type' matches the allowed mime types set by allow (for the whole payload as well as parts), the payload is converted into an object when possible. If the format is unknown, a Bad Request (400) error response is sent. Any known content encoding is decoded. \* false - the raw payload is returned unmodified. \* 'gunzip' - the raw payload is returned unmodified after any known content encoding is decoded.Default value: true, unless no validation.body is provided in the route definition. In that case the default is false to alleviate memory pressure. | +| [accepts?](./kibana-plugin-core-server.routeconfigoptionsbody.accepts.md) | RouteContentType \| RouteContentType\[\] \| string \| string\[\] | (Optional) A string or an array of strings with the allowed mime types for the endpoint. Use this settings to limit the set of allowed mime types. Note that allowing additional mime types not listed above will not enable them to be parsed, and if parse is true, the request will result in an error response.Default value: allows parsing of the following mime types: \* application/json \* application/\*+json \* application/octet-stream \* application/x-www-form-urlencoded \* multipart/form-data \* text/\* | +| [maxBytes?](./kibana-plugin-core-server.routeconfigoptionsbody.maxbytes.md) | number | (Optional) Limits the size of incoming payloads to the specified byte count. Allowing very large payloads may cause the server to run out of memory.Default value: The one set in the kibana.yml config file under the parameter server.maxPayload. | +| [output?](./kibana-plugin-core-server.routeconfigoptionsbody.output.md) | typeof validBodyOutput\[number\] | (Optional) The processed payload format. The value must be one of: \* 'data' - the incoming payload is read fully into memory. If parse is true, the payload is parsed (JSON, form-decoded, multipart) based on the 'Content-Type' header. If parse is false, a raw Buffer is returned. \* 'stream' - the incoming payload is made available via a Stream.Readable interface. If the payload is 'multipart/form-data' and parse is true, field values are presented as text while files are provided as streams. File streams from a 'multipart/form-data' upload will also have a hapi property containing the filename and headers properties. Note that payload streams for multipart payloads are a synthetic interface created on top of the entire multipart content loaded into memory. To avoid loading large multipart payloads into memory, set parse to false and handle the multipart payload in the handler using a streaming parser (e.g. pez).Default value: 'data', unless no validation.body is provided in the route definition. In that case the default is 'stream' to alleviate memory pressure. | +| [parse?](./kibana-plugin-core-server.routeconfigoptionsbody.parse.md) | boolean \| 'gunzip' | (Optional) Determines if the incoming payload is processed or presented raw. Available values: \* true - if the request 'Content-Type' matches the allowed mime types set by allow (for the whole payload as well as parts), the payload is converted into an object when possible. If the format is unknown, a Bad Request (400) error response is sent. Any known content encoding is decoded. \* false - the raw payload is returned unmodified. \* 'gunzip' - the raw payload is returned unmodified after any known content encoding is decoded.Default value: true, unless no validation.body is provided in the route definition. In that case the default is false to alleviate memory pressure. | diff --git a/docs/development/core/server/kibana-plugin-core-server.routevalidationerror._constructor_.md b/docs/development/core/server/kibana-plugin-core-server.routevalidationerror._constructor_.md index ddacc1f7af2e6..ad1a4bae0dab1 100644 --- a/docs/development/core/server/kibana-plugin-core-server.routevalidationerror._constructor_.md +++ b/docs/development/core/server/kibana-plugin-core-server.routevalidationerror._constructor_.md @@ -16,6 +16,6 @@ constructor(error: Error | string, path?: string[]); | Parameter | Type | Description | | --- | --- | --- | -| error | Error | string | | -| path | string[] | | +| error | Error \| string | | +| path | string\[\] | | diff --git a/docs/development/core/server/kibana-plugin-core-server.routevalidationerror.md b/docs/development/core/server/kibana-plugin-core-server.routevalidationerror.md index 037e53c32bbc0..60a47236b4be5 100644 --- a/docs/development/core/server/kibana-plugin-core-server.routevalidationerror.md +++ b/docs/development/core/server/kibana-plugin-core-server.routevalidationerror.md @@ -11,6 +11,7 @@ Error to return when the validation is not successful. ```typescript export declare class RouteValidationError extends SchemaTypeError ``` +Extends: SchemaTypeError ## Constructors diff --git a/docs/development/core/server/kibana-plugin-core-server.routevalidationfunction.md b/docs/development/core/server/kibana-plugin-core-server.routevalidationfunction.md index 3ee61a07987b8..e3fd33552f7df 100644 --- a/docs/development/core/server/kibana-plugin-core-server.routevalidationfunction.md +++ b/docs/development/core/server/kibana-plugin-core-server.routevalidationfunction.md @@ -37,6 +37,5 @@ const myBodyValidation: RouteValidationFunction = (data, validat return badRequest('Wrong payload', ['body']); } } - ``` diff --git a/docs/development/core/server/kibana-plugin-core-server.routevalidationresultfactory.md b/docs/development/core/server/kibana-plugin-core-server.routevalidationresultfactory.md index eee77a91d73de..69e8b5e73136e 100644 --- a/docs/development/core/server/kibana-plugin-core-server.routevalidationresultfactory.md +++ b/docs/development/core/server/kibana-plugin-core-server.routevalidationresultfactory.md @@ -18,6 +18,6 @@ export interface RouteValidationResultFactory | Property | Type | Description | | --- | --- | --- | -| [badRequest](./kibana-plugin-core-server.routevalidationresultfactory.badrequest.md) | (error: Error | string, path?: string[]) => {
error: RouteValidationError;
} | | -| [ok](./kibana-plugin-core-server.routevalidationresultfactory.ok.md) | <T>(value: T) => {
value: T;
} | | +| [badRequest](./kibana-plugin-core-server.routevalidationresultfactory.badrequest.md) | (error: Error \| string, path?: string\[\]) => { error: RouteValidationError; } | | +| [ok](./kibana-plugin-core-server.routevalidationresultfactory.ok.md) | <T>(value: T) => { value: T; } | | diff --git a/docs/development/core/server/kibana-plugin-core-server.routevalidatorconfig.md b/docs/development/core/server/kibana-plugin-core-server.routevalidatorconfig.md index c8ed3612ec9f1..848bf6aa4b15e 100644 --- a/docs/development/core/server/kibana-plugin-core-server.routevalidatorconfig.md +++ b/docs/development/core/server/kibana-plugin-core-server.routevalidatorconfig.md @@ -16,7 +16,7 @@ export interface RouteValidatorConfig | Property | Type | Description | | --- | --- | --- | -| [body](./kibana-plugin-core-server.routevalidatorconfig.body.md) | RouteValidationSpec<B> | Validation logic for the body payload | -| [params](./kibana-plugin-core-server.routevalidatorconfig.params.md) | RouteValidationSpec<P> | Validation logic for the URL params | -| [query](./kibana-plugin-core-server.routevalidatorconfig.query.md) | RouteValidationSpec<Q> | Validation logic for the Query params | +| [body?](./kibana-plugin-core-server.routevalidatorconfig.body.md) | RouteValidationSpec<B> | (Optional) Validation logic for the body payload | +| [params?](./kibana-plugin-core-server.routevalidatorconfig.params.md) | RouteValidationSpec<P> | (Optional) Validation logic for the URL params | +| [query?](./kibana-plugin-core-server.routevalidatorconfig.query.md) | RouteValidationSpec<Q> | (Optional) Validation logic for the Query params | diff --git a/docs/development/core/server/kibana-plugin-core-server.routevalidatoroptions.md b/docs/development/core/server/kibana-plugin-core-server.routevalidatoroptions.md index 0d9a1369c3c3b..f054ca388762a 100644 --- a/docs/development/core/server/kibana-plugin-core-server.routevalidatoroptions.md +++ b/docs/development/core/server/kibana-plugin-core-server.routevalidatoroptions.md @@ -16,5 +16,5 @@ export interface RouteValidatorOptions | Property | Type | Description | | --- | --- | --- | -| [unsafe](./kibana-plugin-core-server.routevalidatoroptions.unsafe.md) | {
params?: boolean;
query?: boolean;
body?: boolean;
} | Set the unsafe config to avoid running some additional internal \*safe\* validations on top of your custom validation | +| [unsafe?](./kibana-plugin-core-server.routevalidatoroptions.unsafe.md) | { params?: boolean; query?: boolean; body?: boolean; } | (Optional) Set the unsafe config to avoid running some additional internal \*safe\* validations on top of your custom validation | diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobject.md b/docs/development/core/server/kibana-plugin-core-server.savedobject.md index 4c62b359b284d..cffb47659dc23 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobject.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobject.md @@ -14,15 +14,15 @@ export interface SavedObject | Property | Type | Description | | --- | --- | --- | -| [attributes](./kibana-plugin-core-server.savedobject.attributes.md) | T | The data for a Saved Object is stored as an object in the attributes property. | -| [coreMigrationVersion](./kibana-plugin-core-server.savedobject.coremigrationversion.md) | string | A semver value that is used when upgrading objects between Kibana versions. | -| [error](./kibana-plugin-core-server.savedobject.error.md) | SavedObjectError | | -| [id](./kibana-plugin-core-server.savedobject.id.md) | string | The ID of this Saved Object, guaranteed to be unique for all objects of the same type | -| [migrationVersion](./kibana-plugin-core-server.savedobject.migrationversion.md) | SavedObjectsMigrationVersion | Information about the migrations that have been applied to this SavedObject. When Kibana starts up, KibanaMigrator detects outdated documents and migrates them based on this value. For each migration that has been applied, the plugin's name is used as a key and the latest migration version as the value. | -| [namespaces](./kibana-plugin-core-server.savedobject.namespaces.md) | string[] | Space(s) that this saved object exists in. This attribute is not used for "global" saved object types which are registered with namespaceType: 'agnostic'. | -| [originId](./kibana-plugin-core-server.savedobject.originid.md) | string | The ID of the saved object this originated from. This is set if this object's id was regenerated; that can happen during migration from a legacy single-namespace type, or during import. It is only set during migration or create operations. This is used during import to ensure that ID regeneration is deterministic, so saved objects will be overwritten if they are imported multiple times into a given space. | -| [references](./kibana-plugin-core-server.savedobject.references.md) | SavedObjectReference[] | A reference to another saved object. | -| [type](./kibana-plugin-core-server.savedobject.type.md) | string | The type of Saved Object. Each plugin can define it's own custom Saved Object types. | -| [updated\_at](./kibana-plugin-core-server.savedobject.updated_at.md) | string | Timestamp of the last time this document had been updated. | -| [version](./kibana-plugin-core-server.savedobject.version.md) | string | An opaque version number which changes on each successful write operation. Can be used for implementing optimistic concurrency control. | +| [attributes](./kibana-plugin-core-server.savedobject.attributes.md) | T | The data for a Saved Object is stored as an object in the attributes property. | +| [coreMigrationVersion?](./kibana-plugin-core-server.savedobject.coremigrationversion.md) | string | (Optional) A semver value that is used when upgrading objects between Kibana versions. | +| [error?](./kibana-plugin-core-server.savedobject.error.md) | SavedObjectError | (Optional) | +| [id](./kibana-plugin-core-server.savedobject.id.md) | string | The ID of this Saved Object, guaranteed to be unique for all objects of the same type | +| [migrationVersion?](./kibana-plugin-core-server.savedobject.migrationversion.md) | SavedObjectsMigrationVersion | (Optional) Information about the migrations that have been applied to this SavedObject. When Kibana starts up, KibanaMigrator detects outdated documents and migrates them based on this value. For each migration that has been applied, the plugin's name is used as a key and the latest migration version as the value. | +| [namespaces?](./kibana-plugin-core-server.savedobject.namespaces.md) | string\[\] | (Optional) Space(s) that this saved object exists in. This attribute is not used for "global" saved object types which are registered with namespaceType: 'agnostic'. | +| [originId?](./kibana-plugin-core-server.savedobject.originid.md) | string | (Optional) The ID of the saved object this originated from. This is set if this object's id was regenerated; that can happen during migration from a legacy single-namespace type, or during import. It is only set during migration or create operations. This is used during import to ensure that ID regeneration is deterministic, so saved objects will be overwritten if they are imported multiple times into a given space. | +| [references](./kibana-plugin-core-server.savedobject.references.md) | SavedObjectReference\[\] | A reference to another saved object. | +| [type](./kibana-plugin-core-server.savedobject.type.md) | string | The type of Saved Object. Each plugin can define it's own custom Saved Object types. | +| [updated\_at?](./kibana-plugin-core-server.savedobject.updated_at.md) | string | (Optional) Timestamp of the last time this document had been updated. | +| [version?](./kibana-plugin-core-server.savedobject.version.md) | string | (Optional) An opaque version number which changes on each successful write operation. Can be used for implementing optimistic concurrency control. | diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectexportbaseoptions.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectexportbaseoptions.md index cd0c352086425..d2749cb85cd3a 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectexportbaseoptions.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectexportbaseoptions.md @@ -15,9 +15,9 @@ export interface SavedObjectExportBaseOptions | Property | Type | Description | | --- | --- | --- | -| [excludeExportDetails](./kibana-plugin-core-server.savedobjectexportbaseoptions.excludeexportdetails.md) | boolean | flag to not append [export details](./kibana-plugin-core-server.savedobjectsexportresultdetails.md) to the end of the export stream. | -| [includeNamespaces](./kibana-plugin-core-server.savedobjectexportbaseoptions.includenamespaces.md) | boolean | Flag to also include namespace information in the export stream. By default, namespace information is not included in exported objects. This is only intended to be used internally during copy-to-space operations, and it is not exposed as an option for the external HTTP route for exports. | -| [includeReferencesDeep](./kibana-plugin-core-server.savedobjectexportbaseoptions.includereferencesdeep.md) | boolean | flag to also include all related saved objects in the export stream. | -| [namespace](./kibana-plugin-core-server.savedobjectexportbaseoptions.namespace.md) | string | optional namespace to override the namespace used by the savedObjectsClient. | -| [request](./kibana-plugin-core-server.savedobjectexportbaseoptions.request.md) | KibanaRequest | The http request initiating the export. | +| [excludeExportDetails?](./kibana-plugin-core-server.savedobjectexportbaseoptions.excludeexportdetails.md) | boolean | (Optional) flag to not append [export details](./kibana-plugin-core-server.savedobjectsexportresultdetails.md) to the end of the export stream. | +| [includeNamespaces?](./kibana-plugin-core-server.savedobjectexportbaseoptions.includenamespaces.md) | boolean | (Optional) Flag to also include namespace information in the export stream. By default, namespace information is not included in exported objects. This is only intended to be used internally during copy-to-space operations, and it is not exposed as an option for the external HTTP route for exports. | +| [includeReferencesDeep?](./kibana-plugin-core-server.savedobjectexportbaseoptions.includereferencesdeep.md) | boolean | (Optional) flag to also include all related saved objects in the export stream. | +| [namespace?](./kibana-plugin-core-server.savedobjectexportbaseoptions.namespace.md) | string | (Optional) optional namespace to override the namespace used by the savedObjectsClient. | +| [request](./kibana-plugin-core-server.savedobjectexportbaseoptions.request.md) | KibanaRequest | The http request initiating the export. | diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectmigrationcontext.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectmigrationcontext.md index 21ca234fde185..3a265cc8e1d42 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectmigrationcontext.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectmigrationcontext.md @@ -16,8 +16,8 @@ export interface SavedObjectMigrationContext | Property | Type | Description | | --- | --- | --- | -| [convertToMultiNamespaceTypeVersion](./kibana-plugin-core-server.savedobjectmigrationcontext.converttomultinamespacetypeversion.md) | string | The version in which this object type is being converted to a multi-namespace type | -| [isSingleNamespaceType](./kibana-plugin-core-server.savedobjectmigrationcontext.issinglenamespacetype.md) | boolean | Whether this is a single-namespace type or not | -| [log](./kibana-plugin-core-server.savedobjectmigrationcontext.log.md) | SavedObjectsMigrationLogger | logger instance to be used by the migration handler | -| [migrationVersion](./kibana-plugin-core-server.savedobjectmigrationcontext.migrationversion.md) | string | The migration version that this migration function is defined for | +| [convertToMultiNamespaceTypeVersion?](./kibana-plugin-core-server.savedobjectmigrationcontext.converttomultinamespacetypeversion.md) | string | (Optional) The version in which this object type is being converted to a multi-namespace type | +| [isSingleNamespaceType](./kibana-plugin-core-server.savedobjectmigrationcontext.issinglenamespacetype.md) | boolean | Whether this is a single-namespace type or not | +| [log](./kibana-plugin-core-server.savedobjectmigrationcontext.log.md) | SavedObjectsMigrationLogger | logger instance to be used by the migration handler | +| [migrationVersion](./kibana-plugin-core-server.savedobjectmigrationcontext.migrationversion.md) | string | The migration version that this migration function is defined for | diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectmigrationfn.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectmigrationfn.md index a3294fb0a087a..1c96c63a3d4fe 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectmigrationfn.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectmigrationfn.md @@ -39,6 +39,5 @@ const migrateToV2: SavedObjectMigrationFn = }, }; }; - ``` diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectmigrationmap.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectmigrationmap.md index c07a41e28d45b..64575d34bfb10 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectmigrationmap.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectmigrationmap.md @@ -22,6 +22,5 @@ const migrationsMap: SavedObjectMigrationMap = { '1.0.0': migrateToV1, '2.1.0': migrateToV21 } - ``` diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectreference.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectreference.md index 1c8b580187395..bf21b13acfcfc 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectreference.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectreference.md @@ -16,7 +16,7 @@ export interface SavedObjectReference | Property | Type | Description | | --- | --- | --- | -| [id](./kibana-plugin-core-server.savedobjectreference.id.md) | string | | -| [name](./kibana-plugin-core-server.savedobjectreference.name.md) | string | | -| [type](./kibana-plugin-core-server.savedobjectreference.type.md) | string | | +| [id](./kibana-plugin-core-server.savedobjectreference.id.md) | string | | +| [name](./kibana-plugin-core-server.savedobjectreference.name.md) | string | | +| [type](./kibana-plugin-core-server.savedobjectreference.type.md) | string | | diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectreferencewithcontext.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectreferencewithcontext.md index 1f8b33c6e94e8..8cdfbb4fde480 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectreferencewithcontext.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectreferencewithcontext.md @@ -16,10 +16,10 @@ export interface SavedObjectReferenceWithContext | Property | Type | Description | | --- | --- | --- | -| [id](./kibana-plugin-core-server.savedobjectreferencewithcontext.id.md) | string | The ID of the referenced object | -| [inboundReferences](./kibana-plugin-core-server.savedobjectreferencewithcontext.inboundreferences.md) | Array<{
type: string;
id: string;
name: string;
}> | References to this object; note that this does not contain \_all inbound references everywhere for this object\_, it only contains inbound references for the scope of this operation | -| [isMissing](./kibana-plugin-core-server.savedobjectreferencewithcontext.ismissing.md) | boolean | Whether or not this object or reference is missing | -| [spaces](./kibana-plugin-core-server.savedobjectreferencewithcontext.spaces.md) | string[] | The space(s) that the referenced object exists in | -| [spacesWithMatchingAliases](./kibana-plugin-core-server.savedobjectreferencewithcontext.spaceswithmatchingaliases.md) | string[] | The space(s) that legacy URL aliases matching this type/id exist in | -| [type](./kibana-plugin-core-server.savedobjectreferencewithcontext.type.md) | string | The type of the referenced object | +| [id](./kibana-plugin-core-server.savedobjectreferencewithcontext.id.md) | string | The ID of the referenced object | +| [inboundReferences](./kibana-plugin-core-server.savedobjectreferencewithcontext.inboundreferences.md) | Array<{ type: string; id: string; name: string; }> | References to this object; note that this does not contain \_all inbound references everywhere for this object\_, it only contains inbound references for the scope of this operation | +| [isMissing?](./kibana-plugin-core-server.savedobjectreferencewithcontext.ismissing.md) | boolean | (Optional) Whether or not this object or reference is missing | +| [spaces](./kibana-plugin-core-server.savedobjectreferencewithcontext.spaces.md) | string\[\] | The space(s) that the referenced object exists in | +| [spacesWithMatchingAliases?](./kibana-plugin-core-server.savedobjectreferencewithcontext.spaceswithmatchingaliases.md) | string\[\] | (Optional) The space(s) that legacy URL aliases matching this type/id exist in | +| [type](./kibana-plugin-core-server.savedobjectreferencewithcontext.type.md) | string | The type of the referenced object | diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsbaseoptions.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsbaseoptions.md index 72f07c42949d1..6686ad7ca8bad 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectsbaseoptions.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsbaseoptions.md @@ -15,5 +15,5 @@ export interface SavedObjectsBaseOptions | Property | Type | Description | | --- | --- | --- | -| [namespace](./kibana-plugin-core-server.savedobjectsbaseoptions.namespace.md) | string | Specify the namespace for this operation | +| [namespace?](./kibana-plugin-core-server.savedobjectsbaseoptions.namespace.md) | string | (Optional) Specify the namespace for this operation | diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsbulkcreateobject.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsbulkcreateobject.md index 463c3fe81b702..441df5d50c612 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectsbulkcreateobject.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsbulkcreateobject.md @@ -15,13 +15,13 @@ export interface SavedObjectsBulkCreateObject | Property | Type | Description | | --- | --- | --- | -| [attributes](./kibana-plugin-core-server.savedobjectsbulkcreateobject.attributes.md) | T | | -| [coreMigrationVersion](./kibana-plugin-core-server.savedobjectsbulkcreateobject.coremigrationversion.md) | string | A semver value that is used when upgrading objects between Kibana versions. If undefined, this will be automatically set to the current Kibana version when the object is created. If this is set to a non-semver value, or it is set to a semver value greater than the current Kibana version, it will result in an error. | -| [id](./kibana-plugin-core-server.savedobjectsbulkcreateobject.id.md) | string | | -| [initialNamespaces](./kibana-plugin-core-server.savedobjectsbulkcreateobject.initialnamespaces.md) | string[] | Optional initial namespaces for the object to be created in. If this is defined, it will supersede the namespace ID that is in [SavedObjectsCreateOptions](./kibana-plugin-core-server.savedobjectscreateoptions.md).\* For shareable object types (registered with namespaceType: 'multiple'): this option can be used to specify one or more spaces, including the "All spaces" identifier ('*'). \* For isolated object types (registered with namespaceType: 'single' or namespaceType: 'multiple-isolated'): this option can only be used to specify a single space, and the "All spaces" identifier ('*') is not allowed. \* For global object types (registered with namespaceType: 'agnostic'): this option cannot be used. | -| [migrationVersion](./kibana-plugin-core-server.savedobjectsbulkcreateobject.migrationversion.md) | SavedObjectsMigrationVersion | Information about the migrations that have been applied to this SavedObject. When Kibana starts up, KibanaMigrator detects outdated documents and migrates them based on this value. For each migration that has been applied, the plugin's name is used as a key and the latest migration version as the value. | -| [originId](./kibana-plugin-core-server.savedobjectsbulkcreateobject.originid.md) | string | Optional ID of the original saved object, if this object's id was regenerated | -| [references](./kibana-plugin-core-server.savedobjectsbulkcreateobject.references.md) | SavedObjectReference[] | | -| [type](./kibana-plugin-core-server.savedobjectsbulkcreateobject.type.md) | string | | -| [version](./kibana-plugin-core-server.savedobjectsbulkcreateobject.version.md) | string | | +| [attributes](./kibana-plugin-core-server.savedobjectsbulkcreateobject.attributes.md) | T | | +| [coreMigrationVersion?](./kibana-plugin-core-server.savedobjectsbulkcreateobject.coremigrationversion.md) | string | (Optional) A semver value that is used when upgrading objects between Kibana versions. If undefined, this will be automatically set to the current Kibana version when the object is created. If this is set to a non-semver value, or it is set to a semver value greater than the current Kibana version, it will result in an error. | +| [id?](./kibana-plugin-core-server.savedobjectsbulkcreateobject.id.md) | string | (Optional) | +| [initialNamespaces?](./kibana-plugin-core-server.savedobjectsbulkcreateobject.initialnamespaces.md) | string\[\] | (Optional) Optional initial namespaces for the object to be created in. If this is defined, it will supersede the namespace ID that is in [SavedObjectsCreateOptions](./kibana-plugin-core-server.savedobjectscreateoptions.md).\* For shareable object types (registered with namespaceType: 'multiple'): this option can be used to specify one or more spaces, including the "All spaces" identifier ('*'). \* For isolated object types (registered with namespaceType: 'single' or namespaceType: 'multiple-isolated'): this option can only be used to specify a single space, and the "All spaces" identifier ('*') is not allowed. \* For global object types (registered with namespaceType: 'agnostic'): this option cannot be used. | +| [migrationVersion?](./kibana-plugin-core-server.savedobjectsbulkcreateobject.migrationversion.md) | SavedObjectsMigrationVersion | (Optional) Information about the migrations that have been applied to this SavedObject. When Kibana starts up, KibanaMigrator detects outdated documents and migrates them based on this value. For each migration that has been applied, the plugin's name is used as a key and the latest migration version as the value. | +| [originId?](./kibana-plugin-core-server.savedobjectsbulkcreateobject.originid.md) | string | (Optional) Optional ID of the original saved object, if this object's id was regenerated | +| [references?](./kibana-plugin-core-server.savedobjectsbulkcreateobject.references.md) | SavedObjectReference\[\] | (Optional) | +| [type](./kibana-plugin-core-server.savedobjectsbulkcreateobject.type.md) | string | | +| [version?](./kibana-plugin-core-server.savedobjectsbulkcreateobject.version.md) | string | (Optional) | diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsbulkgetobject.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsbulkgetobject.md index 0ad5f1d66ee52..0eb5b507a1f03 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectsbulkgetobject.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsbulkgetobject.md @@ -15,8 +15,8 @@ export interface SavedObjectsBulkGetObject | Property | Type | Description | | --- | --- | --- | -| [fields](./kibana-plugin-core-server.savedobjectsbulkgetobject.fields.md) | string[] | SavedObject fields to include in the response | -| [id](./kibana-plugin-core-server.savedobjectsbulkgetobject.id.md) | string | | -| [namespaces](./kibana-plugin-core-server.savedobjectsbulkgetobject.namespaces.md) | string[] | Optional namespace(s) for the object to be retrieved in. If this is defined, it will supersede the namespace ID that is in the top-level options.\* For shareable object types (registered with namespaceType: 'multiple'): this option can be used to specify one or more spaces, including the "All spaces" identifier ('*'). \* For isolated object types (registered with namespaceType: 'single' or namespaceType: 'multiple-isolated'): this option can only be used to specify a single space, and the "All spaces" identifier ('*') is not allowed. \* For global object types (registered with namespaceType: 'agnostic'): this option cannot be used. | -| [type](./kibana-plugin-core-server.savedobjectsbulkgetobject.type.md) | string | | +| [fields?](./kibana-plugin-core-server.savedobjectsbulkgetobject.fields.md) | string\[\] | (Optional) SavedObject fields to include in the response | +| [id](./kibana-plugin-core-server.savedobjectsbulkgetobject.id.md) | string | | +| [namespaces?](./kibana-plugin-core-server.savedobjectsbulkgetobject.namespaces.md) | string\[\] | (Optional) Optional namespace(s) for the object to be retrieved in. If this is defined, it will supersede the namespace ID that is in the top-level options.\* For shareable object types (registered with namespaceType: 'multiple'): this option can be used to specify one or more spaces, including the "All spaces" identifier ('*'). \* For isolated object types (registered with namespaceType: 'single' or namespaceType: 'multiple-isolated'): this option can only be used to specify a single space, and the "All spaces" identifier ('*') is not allowed. \* For global object types (registered with namespaceType: 'agnostic'): this option cannot be used. | +| [type](./kibana-plugin-core-server.savedobjectsbulkgetobject.type.md) | string | | diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsbulkresolveobject.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsbulkresolveobject.md index 3960511b21434..a81e18cf3593a 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectsbulkresolveobject.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsbulkresolveobject.md @@ -15,6 +15,6 @@ export interface SavedObjectsBulkResolveObject | Property | Type | Description | | --- | --- | --- | -| [id](./kibana-plugin-core-server.savedobjectsbulkresolveobject.id.md) | string | | -| [type](./kibana-plugin-core-server.savedobjectsbulkresolveobject.type.md) | string | | +| [id](./kibana-plugin-core-server.savedobjectsbulkresolveobject.id.md) | string | | +| [type](./kibana-plugin-core-server.savedobjectsbulkresolveobject.type.md) | string | | diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsbulkresolveresponse.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsbulkresolveresponse.md index 8384ecc1861f4..e280877d77cd6 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectsbulkresolveresponse.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsbulkresolveresponse.md @@ -15,5 +15,5 @@ export interface SavedObjectsBulkResolveResponse | Property | Type | Description | | --- | --- | --- | -| [resolved\_objects](./kibana-plugin-core-server.savedobjectsbulkresolveresponse.resolved_objects.md) | Array<SavedObjectsResolveResponse<T>> | | +| [resolved\_objects](./kibana-plugin-core-server.savedobjectsbulkresolveresponse.resolved_objects.md) | Array<SavedObjectsResolveResponse<T>> | | diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsbulkresponse.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsbulkresponse.md index bf08ae7816b93..e47350e4bf888 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectsbulkresponse.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsbulkresponse.md @@ -15,5 +15,5 @@ export interface SavedObjectsBulkResponse | Property | Type | Description | | --- | --- | --- | -| [saved\_objects](./kibana-plugin-core-server.savedobjectsbulkresponse.saved_objects.md) | Array<SavedObject<T>> | | +| [saved\_objects](./kibana-plugin-core-server.savedobjectsbulkresponse.saved_objects.md) | Array<SavedObject<T>> | | diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsbulkupdateobject.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsbulkupdateobject.md index dc30400bbd741..fa20d5d13d8f2 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectsbulkupdateobject.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsbulkupdateobject.md @@ -10,13 +10,14 @@ ```typescript export interface SavedObjectsBulkUpdateObject extends Pick, 'version' | 'references'> ``` +Extends: Pick<SavedObjectsUpdateOptions<T>, 'version' \| 'references'> ## Properties | Property | Type | Description | | --- | --- | --- | -| [attributes](./kibana-plugin-core-server.savedobjectsbulkupdateobject.attributes.md) | Partial<T> | The data for a Saved Object is stored as an object in the attributes property. | -| [id](./kibana-plugin-core-server.savedobjectsbulkupdateobject.id.md) | string | The ID of this Saved Object, guaranteed to be unique for all objects of the same type | -| [namespace](./kibana-plugin-core-server.savedobjectsbulkupdateobject.namespace.md) | string | Optional namespace string to use when searching for this object. If this is defined, it will supersede the namespace ID that is in [SavedObjectsBulkUpdateOptions](./kibana-plugin-core-server.savedobjectsbulkupdateoptions.md).Note: the default namespace's string representation is 'default', and its ID representation is undefined. | -| [type](./kibana-plugin-core-server.savedobjectsbulkupdateobject.type.md) | string | The type of this Saved Object. Each plugin can define it's own custom Saved Object types. | +| [attributes](./kibana-plugin-core-server.savedobjectsbulkupdateobject.attributes.md) | Partial<T> | The data for a Saved Object is stored as an object in the attributes property. | +| [id](./kibana-plugin-core-server.savedobjectsbulkupdateobject.id.md) | string | The ID of this Saved Object, guaranteed to be unique for all objects of the same type | +| [namespace?](./kibana-plugin-core-server.savedobjectsbulkupdateobject.namespace.md) | string | (Optional) Optional namespace string to use when searching for this object. If this is defined, it will supersede the namespace ID that is in [SavedObjectsBulkUpdateOptions](./kibana-plugin-core-server.savedobjectsbulkupdateoptions.md).Note: the default namespace's string representation is 'default', and its ID representation is undefined. | +| [type](./kibana-plugin-core-server.savedobjectsbulkupdateobject.type.md) | string | The type of this Saved Object. Each plugin can define it's own custom Saved Object types. | diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsbulkupdateoptions.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsbulkupdateoptions.md index bf5d1fc536f94..97285b326dbae 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectsbulkupdateoptions.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsbulkupdateoptions.md @@ -10,10 +10,11 @@ ```typescript export interface SavedObjectsBulkUpdateOptions extends SavedObjectsBaseOptions ``` +Extends: SavedObjectsBaseOptions ## Properties | Property | Type | Description | | --- | --- | --- | -| [refresh](./kibana-plugin-core-server.savedobjectsbulkupdateoptions.refresh.md) | MutatingOperationRefreshSetting | The Elasticsearch Refresh setting for this operation | +| [refresh?](./kibana-plugin-core-server.savedobjectsbulkupdateoptions.refresh.md) | MutatingOperationRefreshSetting | (Optional) The Elasticsearch Refresh setting for this operation | diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsbulkupdateresponse.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsbulkupdateresponse.md index 361575b3b1ce2..e1a1af2da25cc 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectsbulkupdateresponse.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsbulkupdateresponse.md @@ -15,5 +15,5 @@ export interface SavedObjectsBulkUpdateResponse | Property | Type | Description | | --- | --- | --- | -| [saved\_objects](./kibana-plugin-core-server.savedobjectsbulkupdateresponse.saved_objects.md) | Array<SavedObjectsUpdateResponse<T>> | | +| [saved\_objects](./kibana-plugin-core-server.savedobjectsbulkupdateresponse.saved_objects.md) | Array<SavedObjectsUpdateResponse<T>> | | diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectscheckconflictsobject.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectscheckconflictsobject.md index c327cc4a20551..af7d9ff74db25 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectscheckconflictsobject.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectscheckconflictsobject.md @@ -15,6 +15,6 @@ export interface SavedObjectsCheckConflictsObject | Property | Type | Description | | --- | --- | --- | -| [id](./kibana-plugin-core-server.savedobjectscheckconflictsobject.id.md) | string | | -| [type](./kibana-plugin-core-server.savedobjectscheckconflictsobject.type.md) | string | | +| [id](./kibana-plugin-core-server.savedobjectscheckconflictsobject.id.md) | string | | +| [type](./kibana-plugin-core-server.savedobjectscheckconflictsobject.type.md) | string | | diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectscheckconflictsresponse.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectscheckconflictsresponse.md index 499398586e7dd..68bbdbe67c273 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectscheckconflictsresponse.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectscheckconflictsresponse.md @@ -15,5 +15,5 @@ export interface SavedObjectsCheckConflictsResponse | Property | Type | Description | | --- | --- | --- | -| [errors](./kibana-plugin-core-server.savedobjectscheckconflictsresponse.errors.md) | Array<{
id: string;
type: string;
error: SavedObjectError;
}> | | +| [errors](./kibana-plugin-core-server.savedobjectscheckconflictsresponse.errors.md) | Array<{ id: string; type: string; error: SavedObjectError; }> | | diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsclient.bulkcreate.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsclient.bulkcreate.md index b175fe84fc4c3..a88d82ef49e7d 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectsclient.bulkcreate.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsclient.bulkcreate.md @@ -16,10 +16,10 @@ bulkCreate(objects: Array>, options | Parameter | Type | Description | | --- | --- | --- | -| objects | Array<SavedObjectsBulkCreateObject<T>> | | -| options | SavedObjectsCreateOptions | | +| objects | Array<SavedObjectsBulkCreateObject<T>> | | +| options | SavedObjectsCreateOptions | | Returns: -`Promise>` +Promise<SavedObjectsBulkResponse<T>> diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsclient.bulkget.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsclient.bulkget.md index 82cf65a78bcff..077cb08843acc 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectsclient.bulkget.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsclient.bulkget.md @@ -16,12 +16,12 @@ bulkGet(objects?: SavedObjectsBulkGetObject[], options?: SavedObjec | Parameter | Type | Description | | --- | --- | --- | -| objects | SavedObjectsBulkGetObject[] | an array of ids, or an array of objects containing id, type and optionally fields | -| options | SavedObjectsBaseOptions | | +| objects | SavedObjectsBulkGetObject\[\] | an array of ids, or an array of objects containing id, type and optionally fields | +| options | SavedObjectsBaseOptions | | Returns: -`Promise>` +Promise<SavedObjectsBulkResponse<T>> ## Example diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsclient.bulkresolve.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsclient.bulkresolve.md index 0525b361ebecf..3cf6e4d8d76a5 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectsclient.bulkresolve.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsclient.bulkresolve.md @@ -16,12 +16,12 @@ bulkResolve(objects: SavedObjectsBulkResolveObject[], options?: Sav | Parameter | Type | Description | | --- | --- | --- | -| objects | SavedObjectsBulkResolveObject[] | an array of objects containing id, type | -| options | SavedObjectsBaseOptions | | +| objects | SavedObjectsBulkResolveObject\[\] | an array of objects containing id, type | +| options | SavedObjectsBaseOptions | | Returns: -`Promise>` +Promise<SavedObjectsBulkResolveResponse<T>> ## Example diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsclient.bulkupdate.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsclient.bulkupdate.md index cfd178a03f98f..6c4034357a4ea 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectsclient.bulkupdate.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsclient.bulkupdate.md @@ -16,10 +16,10 @@ bulkUpdate(objects: Array>, options | Parameter | Type | Description | | --- | --- | --- | -| objects | Array<SavedObjectsBulkUpdateObject<T>> | | -| options | SavedObjectsBulkUpdateOptions | | +| objects | Array<SavedObjectsBulkUpdateObject<T>> | | +| options | SavedObjectsBulkUpdateOptions | | Returns: -`Promise>` +Promise<SavedObjectsBulkUpdateResponse<T>> diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsclient.checkconflicts.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsclient.checkconflicts.md index 5cffb0c498b0b..69d52ee098a30 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectsclient.checkconflicts.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsclient.checkconflicts.md @@ -16,10 +16,10 @@ checkConflicts(objects?: SavedObjectsCheckConflictsObject[], options?: SavedObje | Parameter | Type | Description | | --- | --- | --- | -| objects | SavedObjectsCheckConflictsObject[] | | -| options | SavedObjectsBaseOptions | | +| objects | SavedObjectsCheckConflictsObject\[\] | | +| options | SavedObjectsBaseOptions | | Returns: -`Promise` +Promise<SavedObjectsCheckConflictsResponse> diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsclient.closepointintime.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsclient.closepointintime.md index 79c7d18adf306..beb5ea847bf45 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectsclient.closepointintime.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsclient.closepointintime.md @@ -18,10 +18,10 @@ closePointInTime(id: string, options?: SavedObjectsClosePointInTimeOptions): Pro | Parameter | Type | Description | | --- | --- | --- | -| id | string | | -| options | SavedObjectsClosePointInTimeOptions | | +| id | string | | +| options | SavedObjectsClosePointInTimeOptions | | Returns: -`Promise` +Promise<SavedObjectsClosePointInTimeResponse> diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsclient.collectmultinamespacereferences.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsclient.collectmultinamespacereferences.md index 155167d32a738..64ccd4187597c 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectsclient.collectmultinamespacereferences.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsclient.collectmultinamespacereferences.md @@ -16,10 +16,10 @@ collectMultiNamespaceReferences(objects: SavedObjectsCollectMultiNamespaceRefere | Parameter | Type | Description | | --- | --- | --- | -| objects | SavedObjectsCollectMultiNamespaceReferencesObject[] | | -| options | SavedObjectsCollectMultiNamespaceReferencesOptions | | +| objects | SavedObjectsCollectMultiNamespaceReferencesObject\[\] | | +| options | SavedObjectsCollectMultiNamespaceReferencesOptions | | Returns: -`Promise` +Promise<SavedObjectsCollectMultiNamespaceReferencesResponse> diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsclient.create.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsclient.create.md index e2575c20b31f8..9f9b72984bbb6 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectsclient.create.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsclient.create.md @@ -16,11 +16,11 @@ create(type: string, attributes: T, options?: SavedObjectsCreateOpt | Parameter | Type | Description | | --- | --- | --- | -| type | string | | -| attributes | T | | -| options | SavedObjectsCreateOptions | | +| type | string | | +| attributes | T | | +| options | SavedObjectsCreateOptions | | Returns: -`Promise>` +Promise<SavedObject<T>> diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsclient.createpointintimefinder.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsclient.createpointintimefinder.md index 39d09807e4f3b..eab4312b1daa4 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectsclient.createpointintimefinder.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsclient.createpointintimefinder.md @@ -22,12 +22,12 @@ createPointInTimeFinder(findOptions: SavedObjectsCreat | Parameter | Type | Description | | --- | --- | --- | -| findOptions | SavedObjectsCreatePointInTimeFinderOptions | | -| dependencies | SavedObjectsCreatePointInTimeFinderDependencies | | +| findOptions | SavedObjectsCreatePointInTimeFinderOptions | | +| dependencies | SavedObjectsCreatePointInTimeFinderDependencies | | Returns: -`ISavedObjectsPointInTimeFinder` +ISavedObjectsPointInTimeFinder<T, A> ## Example @@ -48,6 +48,5 @@ for await (const response of finder.find()) { await finder.close(); } } - ``` diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsclient.delete.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsclient.delete.md index 07e635efd0e8c..64ed7778d3bec 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectsclient.delete.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsclient.delete.md @@ -16,11 +16,11 @@ delete(type: string, id: string, options?: SavedObjectsDeleteOptions): Promise<{ | Parameter | Type | Description | | --- | --- | --- | -| type | string | | -| id | string | | -| options | SavedObjectsDeleteOptions | | +| type | string | | +| id | string | | +| options | SavedObjectsDeleteOptions | | Returns: -`Promise<{}>` +Promise<{}> diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsclient.find.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsclient.find.md index 56d76125108d1..dc48f7481dc01 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectsclient.find.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsclient.find.md @@ -16,9 +16,9 @@ find(options: SavedObjectsFindOptions): PromiseSavedObjectsFindOptions | | +| options | SavedObjectsFindOptions | | Returns: -`Promise>` +Promise<SavedObjectsFindResponse<T, A>> diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsclient.get.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsclient.get.md index 166265ca320b8..00b6dd28bb7aa 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectsclient.get.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsclient.get.md @@ -16,11 +16,11 @@ get(type: string, id: string, options?: SavedObjectsBaseOptions): P | Parameter | Type | Description | | --- | --- | --- | -| type | string | The type of SavedObject to retrieve | -| id | string | The ID of the SavedObject to retrieve | -| options | SavedObjectsBaseOptions | | +| type | string | The type of SavedObject to retrieve | +| id | string | The ID of the SavedObject to retrieve | +| options | SavedObjectsBaseOptions | | Returns: -`Promise>` +Promise<SavedObject<T>> diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsclient.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsclient.md index e92b6d8e151b1..c77bcfac2f0e7 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectsclient.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsclient.md @@ -18,8 +18,8 @@ The constructor for this class is marked as internal. Third-party code should no | Property | Modifiers | Type | Description | | --- | --- | --- | --- | -| [errors](./kibana-plugin-core-server.savedobjectsclient.errors.md) | | typeof SavedObjectsErrorHelpers | | -| [errors](./kibana-plugin-core-server.savedobjectsclient.errors.md) | static | typeof SavedObjectsErrorHelpers | | +| [errors](./kibana-plugin-core-server.savedobjectsclient.errors.md) | | typeof SavedObjectsErrorHelpers | | +| [errors](./kibana-plugin-core-server.savedobjectsclient.errors.md) | static | typeof SavedObjectsErrorHelpers | | ## Methods diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsclient.openpointintimefortype.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsclient.openpointintimefortype.md index c76159ffa5032..c449fc7b1c3f6 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectsclient.openpointintimefortype.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsclient.openpointintimefortype.md @@ -18,10 +18,10 @@ openPointInTimeForType(type: string | string[], options?: SavedObjectsOpenPointI | Parameter | Type | Description | | --- | --- | --- | -| type | string | string[] | | -| options | SavedObjectsOpenPointInTimeOptions | | +| type | string \| string\[\] | | +| options | SavedObjectsOpenPointInTimeOptions | | Returns: -`Promise` +Promise<SavedObjectsOpenPointInTimeResponse> diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsclient.removereferencesto.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsclient.removereferencesto.md index 002992a17c313..560c210b0105b 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectsclient.removereferencesto.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsclient.removereferencesto.md @@ -16,11 +16,11 @@ removeReferencesTo(type: string, id: string, options?: SavedObjectsRemoveReferen | Parameter | Type | Description | | --- | --- | --- | -| type | string | | -| id | string | | -| options | SavedObjectsRemoveReferencesToOptions | | +| type | string | | +| id | string | | +| options | SavedObjectsRemoveReferencesToOptions | | Returns: -`Promise` +Promise<SavedObjectsRemoveReferencesToResponse> diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsclient.resolve.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsclient.resolve.md index b9a63f0b8c05a..31eabe46d6cb3 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectsclient.resolve.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsclient.resolve.md @@ -16,11 +16,11 @@ resolve(type: string, id: string, options?: SavedObjectsBaseOptions | Parameter | Type | Description | | --- | --- | --- | -| type | string | The type of SavedObject to retrieve | -| id | string | The ID of the SavedObject to retrieve | -| options | SavedObjectsBaseOptions | | +| type | string | The type of SavedObject to retrieve | +| id | string | The ID of the SavedObject to retrieve | +| options | SavedObjectsBaseOptions | | Returns: -`Promise>` +Promise<SavedObjectsResolveResponse<T>> diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsclient.update.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsclient.update.md index 8c4e5962e1dba..20a67387813ff 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectsclient.update.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsclient.update.md @@ -16,12 +16,12 @@ update(type: string, id: string, attributes: Partial, options?: | Parameter | Type | Description | | --- | --- | --- | -| type | string | | -| id | string | | -| attributes | Partial<T> | | -| options | SavedObjectsUpdateOptions<T> | | +| type | string | | +| id | string | | +| attributes | Partial<T> | | +| options | SavedObjectsUpdateOptions<T> | | Returns: -`Promise>` +Promise<SavedObjectsUpdateResponse<T>> diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsclient.updateobjectsspaces.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsclient.updateobjectsspaces.md index 7ababbbe1f535..09012607fd932 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectsclient.updateobjectsspaces.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsclient.updateobjectsspaces.md @@ -16,12 +16,12 @@ updateObjectsSpaces(objects: SavedObjectsUpdateObjectsSpacesObject[], spacesToAd | Parameter | Type | Description | | --- | --- | --- | -| objects | SavedObjectsUpdateObjectsSpacesObject[] | | -| spacesToAdd | string[] | | -| spacesToRemove | string[] | | -| options | SavedObjectsUpdateObjectsSpacesOptions | | +| objects | SavedObjectsUpdateObjectsSpacesObject\[\] | | +| spacesToAdd | string\[\] | | +| spacesToRemove | string\[\] | | +| options | SavedObjectsUpdateObjectsSpacesOptions | | Returns: -`Promise` +Promise<import("./lib").SavedObjectsUpdateObjectsSpacesResponse> diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsclientprovideroptions.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsclientprovideroptions.md index be1f73f064843..a02f54214163b 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectsclientprovideroptions.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsclientprovideroptions.md @@ -16,6 +16,6 @@ export interface SavedObjectsClientProviderOptions | Property | Type | Description | | --- | --- | --- | -| [excludedWrappers](./kibana-plugin-core-server.savedobjectsclientprovideroptions.excludedwrappers.md) | string[] | | -| [includedHiddenTypes](./kibana-plugin-core-server.savedobjectsclientprovideroptions.includedhiddentypes.md) | string[] | | +| [excludedWrappers?](./kibana-plugin-core-server.savedobjectsclientprovideroptions.excludedwrappers.md) | string\[\] | (Optional) | +| [includedHiddenTypes?](./kibana-plugin-core-server.savedobjectsclientprovideroptions.includedhiddentypes.md) | string\[\] | (Optional) | diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsclientwrapperoptions.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsclientwrapperoptions.md index 5b0a9edd24f35..16d104e4a8dff 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectsclientwrapperoptions.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsclientwrapperoptions.md @@ -16,7 +16,7 @@ export interface SavedObjectsClientWrapperOptions | Property | Type | Description | | --- | --- | --- | -| [client](./kibana-plugin-core-server.savedobjectsclientwrapperoptions.client.md) | SavedObjectsClientContract | | -| [request](./kibana-plugin-core-server.savedobjectsclientwrapperoptions.request.md) | KibanaRequest | | -| [typeRegistry](./kibana-plugin-core-server.savedobjectsclientwrapperoptions.typeregistry.md) | ISavedObjectTypeRegistry | | +| [client](./kibana-plugin-core-server.savedobjectsclientwrapperoptions.client.md) | SavedObjectsClientContract | | +| [request](./kibana-plugin-core-server.savedobjectsclientwrapperoptions.request.md) | KibanaRequest | | +| [typeRegistry](./kibana-plugin-core-server.savedobjectsclientwrapperoptions.typeregistry.md) | ISavedObjectTypeRegistry | | diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsclosepointintimeresponse.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsclosepointintimeresponse.md index 43ecd1298d5d9..27010232bd46b 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectsclosepointintimeresponse.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsclosepointintimeresponse.md @@ -15,6 +15,6 @@ export interface SavedObjectsClosePointInTimeResponse | Property | Type | Description | | --- | --- | --- | -| [num\_freed](./kibana-plugin-core-server.savedobjectsclosepointintimeresponse.num_freed.md) | number | The number of search contexts that have been successfully closed. | -| [succeeded](./kibana-plugin-core-server.savedobjectsclosepointintimeresponse.succeeded.md) | boolean | If true, all search contexts associated with the PIT id are successfully closed. | +| [num\_freed](./kibana-plugin-core-server.savedobjectsclosepointintimeresponse.num_freed.md) | number | The number of search contexts that have been successfully closed. | +| [succeeded](./kibana-plugin-core-server.savedobjectsclosepointintimeresponse.succeeded.md) | boolean | If true, all search contexts associated with the PIT id are successfully closed. | diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectscollectmultinamespacereferencesobject.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectscollectmultinamespacereferencesobject.md index b4e6379234f79..5f419a63e8c70 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectscollectmultinamespacereferencesobject.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectscollectmultinamespacereferencesobject.md @@ -18,6 +18,6 @@ export interface SavedObjectsCollectMultiNamespaceReferencesObject | Property | Type | Description | | --- | --- | --- | -| [id](./kibana-plugin-core-server.savedobjectscollectmultinamespacereferencesobject.id.md) | string | | -| [type](./kibana-plugin-core-server.savedobjectscollectmultinamespacereferencesobject.type.md) | string | | +| [id](./kibana-plugin-core-server.savedobjectscollectmultinamespacereferencesobject.id.md) | string | | +| [type](./kibana-plugin-core-server.savedobjectscollectmultinamespacereferencesobject.type.md) | string | | diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectscollectmultinamespacereferencesoptions.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectscollectmultinamespacereferencesoptions.md index 9311a66269753..57298e40a88ba 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectscollectmultinamespacereferencesoptions.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectscollectmultinamespacereferencesoptions.md @@ -11,10 +11,11 @@ Options for collecting references. ```typescript export interface SavedObjectsCollectMultiNamespaceReferencesOptions extends SavedObjectsBaseOptions ``` +Extends: SavedObjectsBaseOptions ## Properties | Property | Type | Description | | --- | --- | --- | -| [purpose](./kibana-plugin-core-server.savedobjectscollectmultinamespacereferencesoptions.purpose.md) | 'collectMultiNamespaceReferences' | 'updateObjectsSpaces' | Optional purpose used to determine filtering and authorization checks; default is 'collectMultiNamespaceReferences' | +| [purpose?](./kibana-plugin-core-server.savedobjectscollectmultinamespacereferencesoptions.purpose.md) | 'collectMultiNamespaceReferences' \| 'updateObjectsSpaces' | (Optional) Optional purpose used to determine filtering and authorization checks; default is 'collectMultiNamespaceReferences' | diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectscollectmultinamespacereferencesresponse.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectscollectmultinamespacereferencesresponse.md index bc72e73994468..514e9271aa17e 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectscollectmultinamespacereferencesresponse.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectscollectmultinamespacereferencesresponse.md @@ -16,5 +16,5 @@ export interface SavedObjectsCollectMultiNamespaceReferencesResponse | Property | Type | Description | | --- | --- | --- | -| [objects](./kibana-plugin-core-server.savedobjectscollectmultinamespacereferencesresponse.objects.md) | SavedObjectReferenceWithContext[] | | +| [objects](./kibana-plugin-core-server.savedobjectscollectmultinamespacereferencesresponse.objects.md) | SavedObjectReferenceWithContext\[\] | | diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectscreateoptions.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectscreateoptions.md index 7eaa9c51f5c82..646a0f6fcf548 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectscreateoptions.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectscreateoptions.md @@ -10,18 +10,19 @@ ```typescript export interface SavedObjectsCreateOptions extends SavedObjectsBaseOptions ``` +Extends: SavedObjectsBaseOptions ## Properties | Property | Type | Description | | --- | --- | --- | -| [coreMigrationVersion](./kibana-plugin-core-server.savedobjectscreateoptions.coremigrationversion.md) | string | A semver value that is used when upgrading objects between Kibana versions. If undefined, this will be automatically set to the current Kibana version when the object is created. If this is set to a non-semver value, or it is set to a semver value greater than the current Kibana version, it will result in an error. | -| [id](./kibana-plugin-core-server.savedobjectscreateoptions.id.md) | string | (not recommended) Specify an id for the document | -| [initialNamespaces](./kibana-plugin-core-server.savedobjectscreateoptions.initialnamespaces.md) | string[] | Optional initial namespaces for the object to be created in. If this is defined, it will supersede the namespace ID that is in [SavedObjectsCreateOptions](./kibana-plugin-core-server.savedobjectscreateoptions.md).\* For shareable object types (registered with namespaceType: 'multiple'): this option can be used to specify one or more spaces, including the "All spaces" identifier ('*'). \* For isolated object types (registered with namespaceType: 'single' or namespaceType: 'multiple-isolated'): this option can only be used to specify a single space, and the "All spaces" identifier ('*') is not allowed. \* For global object types (registered with namespaceType: 'agnostic'): this option cannot be used. | -| [migrationVersion](./kibana-plugin-core-server.savedobjectscreateoptions.migrationversion.md) | SavedObjectsMigrationVersion | Information about the migrations that have been applied to this SavedObject. When Kibana starts up, KibanaMigrator detects outdated documents and migrates them based on this value. For each migration that has been applied, the plugin's name is used as a key and the latest migration version as the value. | -| [originId](./kibana-plugin-core-server.savedobjectscreateoptions.originid.md) | string | Optional ID of the original saved object, if this object's id was regenerated | -| [overwrite](./kibana-plugin-core-server.savedobjectscreateoptions.overwrite.md) | boolean | Overwrite existing documents (defaults to false) | -| [references](./kibana-plugin-core-server.savedobjectscreateoptions.references.md) | SavedObjectReference[] | | -| [refresh](./kibana-plugin-core-server.savedobjectscreateoptions.refresh.md) | MutatingOperationRefreshSetting | The Elasticsearch Refresh setting for this operation | -| [version](./kibana-plugin-core-server.savedobjectscreateoptions.version.md) | string | An opaque version number which changes on each successful write operation. Can be used in conjunction with overwrite for implementing optimistic concurrency control. | +| [coreMigrationVersion?](./kibana-plugin-core-server.savedobjectscreateoptions.coremigrationversion.md) | string | (Optional) A semver value that is used when upgrading objects between Kibana versions. If undefined, this will be automatically set to the current Kibana version when the object is created. If this is set to a non-semver value, or it is set to a semver value greater than the current Kibana version, it will result in an error. | +| [id?](./kibana-plugin-core-server.savedobjectscreateoptions.id.md) | string | (Optional) (not recommended) Specify an id for the document | +| [initialNamespaces?](./kibana-plugin-core-server.savedobjectscreateoptions.initialnamespaces.md) | string\[\] | (Optional) Optional initial namespaces for the object to be created in. If this is defined, it will supersede the namespace ID that is in [SavedObjectsCreateOptions](./kibana-plugin-core-server.savedobjectscreateoptions.md).\* For shareable object types (registered with namespaceType: 'multiple'): this option can be used to specify one or more spaces, including the "All spaces" identifier ('*'). \* For isolated object types (registered with namespaceType: 'single' or namespaceType: 'multiple-isolated'): this option can only be used to specify a single space, and the "All spaces" identifier ('*') is not allowed. \* For global object types (registered with namespaceType: 'agnostic'): this option cannot be used. | +| [migrationVersion?](./kibana-plugin-core-server.savedobjectscreateoptions.migrationversion.md) | SavedObjectsMigrationVersion | (Optional) Information about the migrations that have been applied to this SavedObject. When Kibana starts up, KibanaMigrator detects outdated documents and migrates them based on this value. For each migration that has been applied, the plugin's name is used as a key and the latest migration version as the value. | +| [originId?](./kibana-plugin-core-server.savedobjectscreateoptions.originid.md) | string | (Optional) Optional ID of the original saved object, if this object's id was regenerated | +| [overwrite?](./kibana-plugin-core-server.savedobjectscreateoptions.overwrite.md) | boolean | (Optional) Overwrite existing documents (defaults to false) | +| [references?](./kibana-plugin-core-server.savedobjectscreateoptions.references.md) | SavedObjectReference\[\] | (Optional) | +| [refresh?](./kibana-plugin-core-server.savedobjectscreateoptions.refresh.md) | MutatingOperationRefreshSetting | (Optional) The Elasticsearch Refresh setting for this operation | +| [version?](./kibana-plugin-core-server.savedobjectscreateoptions.version.md) | string | (Optional) An opaque version number which changes on each successful write operation. Can be used in conjunction with overwrite for implementing optimistic concurrency control. | diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectscreatepointintimefinderdependencies.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectscreatepointintimefinderdependencies.md index 47c640bfabcb0..f647a9c1367b3 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectscreatepointintimefinderdependencies.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectscreatepointintimefinderdependencies.md @@ -15,5 +15,5 @@ export interface SavedObjectsCreatePointInTimeFinderDependencies | Property | Type | Description | | --- | --- | --- | -| [client](./kibana-plugin-core-server.savedobjectscreatepointintimefinderdependencies.client.md) | Pick<SavedObjectsClientContract, 'find' | 'openPointInTimeForType' | 'closePointInTime'> | | +| [client](./kibana-plugin-core-server.savedobjectscreatepointintimefinderdependencies.client.md) | Pick<SavedObjectsClientContract, 'find' \| 'openPointInTimeForType' \| 'closePointInTime'> | | diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsdeletebynamespaceoptions.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsdeletebynamespaceoptions.md index ba81a3e8c32d0..49b6c36cf3ed2 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectsdeletebynamespaceoptions.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsdeletebynamespaceoptions.md @@ -10,10 +10,11 @@ ```typescript export interface SavedObjectsDeleteByNamespaceOptions extends SavedObjectsBaseOptions ``` +Extends: SavedObjectsBaseOptions ## Properties | Property | Type | Description | | --- | --- | --- | -| [refresh](./kibana-plugin-core-server.savedobjectsdeletebynamespaceoptions.refresh.md) | boolean | The Elasticsearch supports only boolean flag for this operation | +| [refresh?](./kibana-plugin-core-server.savedobjectsdeletebynamespaceoptions.refresh.md) | boolean | (Optional) The Elasticsearch supports only boolean flag for this operation | diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsdeleteoptions.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsdeleteoptions.md index 245819e44d37d..e1bc1fcec3f2d 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectsdeleteoptions.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsdeleteoptions.md @@ -10,11 +10,12 @@ ```typescript export interface SavedObjectsDeleteOptions extends SavedObjectsBaseOptions ``` +Extends: SavedObjectsBaseOptions ## Properties | Property | Type | Description | | --- | --- | --- | -| [force](./kibana-plugin-core-server.savedobjectsdeleteoptions.force.md) | boolean | Force deletion of an object that exists in multiple namespaces | -| [refresh](./kibana-plugin-core-server.savedobjectsdeleteoptions.refresh.md) | MutatingOperationRefreshSetting | The Elasticsearch Refresh setting for this operation | +| [force?](./kibana-plugin-core-server.savedobjectsdeleteoptions.force.md) | boolean | (Optional) Force deletion of an object that exists in multiple namespaces | +| [refresh?](./kibana-plugin-core-server.savedobjectsdeleteoptions.refresh.md) | MutatingOperationRefreshSetting | (Optional) The Elasticsearch Refresh setting for this operation | diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectserrorhelpers.createbadrequesterror.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectserrorhelpers.createbadrequesterror.md index 03d5aceec6127..f101aa98d8bd3 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectserrorhelpers.createbadrequesterror.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectserrorhelpers.createbadrequesterror.md @@ -14,9 +14,9 @@ static createBadRequestError(reason?: string): DecoratedError; | Parameter | Type | Description | | --- | --- | --- | -| reason | string | | +| reason | string | | Returns: -`DecoratedError` +DecoratedError diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectserrorhelpers.createconflicterror.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectserrorhelpers.createconflicterror.md index 97d33c3060bb0..426de67ded2dc 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectserrorhelpers.createconflicterror.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectserrorhelpers.createconflicterror.md @@ -14,11 +14,11 @@ static createConflictError(type: string, id: string, reason?: string): Decorated | Parameter | Type | Description | | --- | --- | --- | -| type | string | | -| id | string | | -| reason | string | | +| type | string | | +| id | string | | +| reason | string | | Returns: -`DecoratedError` +DecoratedError diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectserrorhelpers.creategenericnotfounderror.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectserrorhelpers.creategenericnotfounderror.md index 9df39c82745d9..6ac403b442367 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectserrorhelpers.creategenericnotfounderror.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectserrorhelpers.creategenericnotfounderror.md @@ -14,10 +14,10 @@ static createGenericNotFoundError(type?: string | null, id?: string | null): Dec | Parameter | Type | Description | | --- | --- | --- | -| type | string | null | | -| id | string | null | | +| type | string \| null | | +| id | string \| null | | Returns: -`DecoratedError` +DecoratedError diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectserrorhelpers.createindexaliasnotfounderror.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectserrorhelpers.createindexaliasnotfounderror.md index 2b897db7bba4c..4f9fa9d4484bd 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectserrorhelpers.createindexaliasnotfounderror.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectserrorhelpers.createindexaliasnotfounderror.md @@ -14,9 +14,9 @@ static createIndexAliasNotFoundError(alias: string): DecoratedError; | Parameter | Type | Description | | --- | --- | --- | -| alias | string | | +| alias | string | | Returns: -`DecoratedError` +DecoratedError diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectserrorhelpers.createinvalidversionerror.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectserrorhelpers.createinvalidversionerror.md index c3eca80ef3efe..59e2a65694008 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectserrorhelpers.createinvalidversionerror.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectserrorhelpers.createinvalidversionerror.md @@ -14,9 +14,9 @@ static createInvalidVersionError(versionInput?: string): DecoratedError; | Parameter | Type | Description | | --- | --- | --- | -| versionInput | string | | +| versionInput | string | | Returns: -`DecoratedError` +DecoratedError diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectserrorhelpers.createtoomanyrequestserror.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectserrorhelpers.createtoomanyrequestserror.md index 6d93dc97a107e..3d4903c3482ed 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectserrorhelpers.createtoomanyrequestserror.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectserrorhelpers.createtoomanyrequestserror.md @@ -14,10 +14,10 @@ static createTooManyRequestsError(type: string, id: string): DecoratedError; | Parameter | Type | Description | | --- | --- | --- | -| type | string | | -| id | string | | +| type | string | | +| id | string | | Returns: -`DecoratedError` +DecoratedError diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectserrorhelpers.createunsupportedtypeerror.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectserrorhelpers.createunsupportedtypeerror.md index bcfd5d4296a45..4ca95c1565db6 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectserrorhelpers.createunsupportedtypeerror.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectserrorhelpers.createunsupportedtypeerror.md @@ -14,9 +14,9 @@ static createUnsupportedTypeError(type: string): DecoratedError; | Parameter | Type | Description | | --- | --- | --- | -| type | string | | +| type | string | | Returns: -`DecoratedError` +DecoratedError diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectserrorhelpers.decoratebadrequesterror.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectserrorhelpers.decoratebadrequesterror.md index 604f2bd93cc74..043950407519f 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectserrorhelpers.decoratebadrequesterror.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectserrorhelpers.decoratebadrequesterror.md @@ -14,10 +14,10 @@ static decorateBadRequestError(error: Error, reason?: string): DecoratedError; | Parameter | Type | Description | | --- | --- | --- | -| error | Error | | -| reason | string | | +| error | Error | | +| reason | string | | Returns: -`DecoratedError` +DecoratedError diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectserrorhelpers.decorateconflicterror.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectserrorhelpers.decorateconflicterror.md index 172f8f5ee6b4d..dfb981a0a656e 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectserrorhelpers.decorateconflicterror.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectserrorhelpers.decorateconflicterror.md @@ -14,10 +14,10 @@ static decorateConflictError(error: Error, reason?: string): DecoratedError; | Parameter | Type | Description | | --- | --- | --- | -| error | Error | | -| reason | string | | +| error | Error | | +| reason | string | | Returns: -`DecoratedError` +DecoratedError diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectserrorhelpers.decorateescannotexecutescripterror.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectserrorhelpers.decorateescannotexecutescripterror.md index 94060bba50067..18b019f1b5364 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectserrorhelpers.decorateescannotexecutescripterror.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectserrorhelpers.decorateescannotexecutescripterror.md @@ -14,10 +14,10 @@ static decorateEsCannotExecuteScriptError(error: Error, reason?: string): Decora | Parameter | Type | Description | | --- | --- | --- | -| error | Error | | -| reason | string | | +| error | Error | | +| reason | string | | Returns: -`DecoratedError` +DecoratedError diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectserrorhelpers.decorateesunavailableerror.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectserrorhelpers.decorateesunavailableerror.md index 54135f9875f5f..9d272b1e78454 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectserrorhelpers.decorateesunavailableerror.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectserrorhelpers.decorateesunavailableerror.md @@ -14,10 +14,10 @@ static decorateEsUnavailableError(error: Error, reason?: string): DecoratedError | Parameter | Type | Description | | --- | --- | --- | -| error | Error | | -| reason | string | | +| error | Error | | +| reason | string | | Returns: -`DecoratedError` +DecoratedError diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectserrorhelpers.decorateforbiddenerror.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectserrorhelpers.decorateforbiddenerror.md index 82d84defba6f7..11b53ec219334 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectserrorhelpers.decorateforbiddenerror.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectserrorhelpers.decorateforbiddenerror.md @@ -14,10 +14,10 @@ static decorateForbiddenError(error: Error, reason?: string): DecoratedError; | Parameter | Type | Description | | --- | --- | --- | -| error | Error | | -| reason | string | | +| error | Error | | +| reason | string | | Returns: -`DecoratedError` +DecoratedError diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectserrorhelpers.decorategeneralerror.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectserrorhelpers.decorategeneralerror.md index 310258bc9a6e9..595789611b5c3 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectserrorhelpers.decorategeneralerror.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectserrorhelpers.decorategeneralerror.md @@ -14,10 +14,10 @@ static decorateGeneralError(error: Error, reason?: string): DecoratedError; | Parameter | Type | Description | | --- | --- | --- | -| error | Error | | -| reason | string | | +| error | Error | | +| reason | string | | Returns: -`DecoratedError` +DecoratedError diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectserrorhelpers.decorateindexaliasnotfounderror.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectserrorhelpers.decorateindexaliasnotfounderror.md index c7e10fc42ead1..a2e74ca7769e0 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectserrorhelpers.decorateindexaliasnotfounderror.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectserrorhelpers.decorateindexaliasnotfounderror.md @@ -14,10 +14,10 @@ static decorateIndexAliasNotFoundError(error: Error, alias: string): DecoratedEr | Parameter | Type | Description | | --- | --- | --- | -| error | Error | | -| alias | string | | +| error | Error | | +| alias | string | | Returns: -`DecoratedError` +DecoratedError diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectserrorhelpers.decoratenotauthorizederror.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectserrorhelpers.decoratenotauthorizederror.md index 81405a6597f77..d50d5d9ebf45f 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectserrorhelpers.decoratenotauthorizederror.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectserrorhelpers.decoratenotauthorizederror.md @@ -14,10 +14,10 @@ static decorateNotAuthorizedError(error: Error, reason?: string): DecoratedError | Parameter | Type | Description | | --- | --- | --- | -| error | Error | | -| reason | string | | +| error | Error | | +| reason | string | | Returns: -`DecoratedError` +DecoratedError diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectserrorhelpers.decoraterequestentitytoolargeerror.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectserrorhelpers.decoraterequestentitytoolargeerror.md index edfa466aa9726..487d64f83ca30 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectserrorhelpers.decoraterequestentitytoolargeerror.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectserrorhelpers.decoraterequestentitytoolargeerror.md @@ -14,10 +14,10 @@ static decorateRequestEntityTooLargeError(error: Error, reason?: string): Decora | Parameter | Type | Description | | --- | --- | --- | -| error | Error | | -| reason | string | | +| error | Error | | +| reason | string | | Returns: -`DecoratedError` +DecoratedError diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectserrorhelpers.decoratetoomanyrequestserror.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectserrorhelpers.decoratetoomanyrequestserror.md index 46c94e1756edd..b85cf196c3cdb 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectserrorhelpers.decoratetoomanyrequestserror.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectserrorhelpers.decoratetoomanyrequestserror.md @@ -14,10 +14,10 @@ static decorateTooManyRequestsError(error: Error, reason?: string): DecoratedErr | Parameter | Type | Description | | --- | --- | --- | -| error | Error | | -| reason | string | | +| error | Error | | +| reason | string | | Returns: -`DecoratedError` +DecoratedError diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectserrorhelpers.isbadrequesterror.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectserrorhelpers.isbadrequesterror.md index 2fd613920fb10..5dd6a50b61e55 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectserrorhelpers.isbadrequesterror.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectserrorhelpers.isbadrequesterror.md @@ -14,9 +14,9 @@ static isBadRequestError(error: Error | DecoratedError): boolean; | Parameter | Type | Description | | --- | --- | --- | -| error | Error | DecoratedError | | +| error | Error \| DecoratedError | | Returns: -`boolean` +boolean diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectserrorhelpers.isconflicterror.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectserrorhelpers.isconflicterror.md index c8066c396f835..9762462af9ef3 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectserrorhelpers.isconflicterror.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectserrorhelpers.isconflicterror.md @@ -14,9 +14,9 @@ static isConflictError(error: Error | DecoratedError): boolean; | Parameter | Type | Description | | --- | --- | --- | -| error | Error | DecoratedError | | +| error | Error \| DecoratedError | | Returns: -`boolean` +boolean diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectserrorhelpers.isescannotexecutescripterror.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectserrorhelpers.isescannotexecutescripterror.md index debb94fe4f8d8..e007dd30f2bb0 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectserrorhelpers.isescannotexecutescripterror.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectserrorhelpers.isescannotexecutescripterror.md @@ -14,9 +14,9 @@ static isEsCannotExecuteScriptError(error: Error | DecoratedError): boolean; | Parameter | Type | Description | | --- | --- | --- | -| error | Error | DecoratedError | | +| error | Error \| DecoratedError | | Returns: -`boolean` +boolean diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectserrorhelpers.isesunavailableerror.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectserrorhelpers.isesunavailableerror.md index 6f38d414b30c3..a6fb911f5e0eb 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectserrorhelpers.isesunavailableerror.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectserrorhelpers.isesunavailableerror.md @@ -14,9 +14,9 @@ static isEsUnavailableError(error: Error | DecoratedError): boolean; | Parameter | Type | Description | | --- | --- | --- | -| error | Error | DecoratedError | | +| error | Error \| DecoratedError | | Returns: -`boolean` +boolean diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectserrorhelpers.isforbiddenerror.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectserrorhelpers.isforbiddenerror.md index 74cfecd49f4a9..e45ef7a7ed3f3 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectserrorhelpers.isforbiddenerror.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectserrorhelpers.isforbiddenerror.md @@ -14,9 +14,9 @@ static isForbiddenError(error: Error | DecoratedError): boolean; | Parameter | Type | Description | | --- | --- | --- | -| error | Error | DecoratedError | | +| error | Error \| DecoratedError | | Returns: -`boolean` +boolean diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectserrorhelpers.isgeneralerror.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectserrorhelpers.isgeneralerror.md index 4b4ede2f77a7e..cbec5d3b36a80 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectserrorhelpers.isgeneralerror.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectserrorhelpers.isgeneralerror.md @@ -14,9 +14,9 @@ static isGeneralError(error: Error | DecoratedError): boolean; | Parameter | Type | Description | | --- | --- | --- | -| error | Error | DecoratedError | | +| error | Error \| DecoratedError | | Returns: -`boolean` +boolean diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectserrorhelpers.isinvalidversionerror.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectserrorhelpers.isinvalidversionerror.md index 8fde00aad394a..8ad480147adf6 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectserrorhelpers.isinvalidversionerror.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectserrorhelpers.isinvalidversionerror.md @@ -14,9 +14,9 @@ static isInvalidVersionError(error: Error | DecoratedError): boolean; | Parameter | Type | Description | | --- | --- | --- | -| error | Error | DecoratedError | | +| error | Error \| DecoratedError | | Returns: -`boolean` +boolean diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectserrorhelpers.isnotauthorizederror.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectserrorhelpers.isnotauthorizederror.md index 39811943abe45..5e85718bde511 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectserrorhelpers.isnotauthorizederror.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectserrorhelpers.isnotauthorizederror.md @@ -14,9 +14,9 @@ static isNotAuthorizedError(error: Error | DecoratedError): boolean; | Parameter | Type | Description | | --- | --- | --- | -| error | Error | DecoratedError | | +| error | Error \| DecoratedError | | Returns: -`boolean` +boolean diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectserrorhelpers.isnotfounderror.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectserrorhelpers.isnotfounderror.md index d37fec7ca08be..05e848322ae9f 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectserrorhelpers.isnotfounderror.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectserrorhelpers.isnotfounderror.md @@ -14,9 +14,9 @@ static isNotFoundError(error: Error | DecoratedError): boolean; | Parameter | Type | Description | | --- | --- | --- | -| error | Error | DecoratedError | | +| error | Error \| DecoratedError | | Returns: -`boolean` +boolean diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectserrorhelpers.isrequestentitytoolargeerror.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectserrorhelpers.isrequestentitytoolargeerror.md index e2699c53b3836..d6674f0a588e9 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectserrorhelpers.isrequestentitytoolargeerror.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectserrorhelpers.isrequestentitytoolargeerror.md @@ -14,9 +14,9 @@ static isRequestEntityTooLargeError(error: Error | DecoratedError): boolean; | Parameter | Type | Description | | --- | --- | --- | -| error | Error | DecoratedError | | +| error | Error \| DecoratedError | | Returns: -`boolean` +boolean diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectserrorhelpers.issavedobjectsclienterror.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectserrorhelpers.issavedobjectsclienterror.md index 0dc0df0401b7b..0505c3a450a32 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectserrorhelpers.issavedobjectsclienterror.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectserrorhelpers.issavedobjectsclienterror.md @@ -14,9 +14,9 @@ static isSavedObjectsClientError(error: any): error is DecoratedError; | Parameter | Type | Description | | --- | --- | --- | -| error | any | | +| error | any | | Returns: -`error is DecoratedError` +error is DecoratedError diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectserrorhelpers.istoomanyrequestserror.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectserrorhelpers.istoomanyrequestserror.md index 4422966ee3e50..3f9c360710ae3 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectserrorhelpers.istoomanyrequestserror.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectserrorhelpers.istoomanyrequestserror.md @@ -14,9 +14,9 @@ static isTooManyRequestsError(error: Error | DecoratedError): boolean; | Parameter | Type | Description | | --- | --- | --- | -| error | Error | DecoratedError | | +| error | Error \| DecoratedError | | Returns: -`boolean` +boolean diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsexportbyobjectoptions.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsexportbyobjectoptions.md index cb20fc5400125..853125f2d1d1f 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectsexportbyobjectoptions.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsexportbyobjectoptions.md @@ -11,10 +11,11 @@ Options for the [export by objects API](./kibana-plugin-core-server.savedobjects ```typescript export interface SavedObjectsExportByObjectOptions extends SavedObjectExportBaseOptions ``` +Extends: SavedObjectExportBaseOptions ## Properties | Property | Type | Description | | --- | --- | --- | -| [objects](./kibana-plugin-core-server.savedobjectsexportbyobjectoptions.objects.md) | Array<{
id: string;
type: string;
}> | optional array of objects to export. | +| [objects](./kibana-plugin-core-server.savedobjectsexportbyobjectoptions.objects.md) | Array<{ id: string; type: string; }> | optional array of objects to export. | diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsexportbytypeoptions.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsexportbytypeoptions.md index 26ebfd658f19b..707db0dca30a7 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectsexportbytypeoptions.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsexportbytypeoptions.md @@ -11,12 +11,13 @@ Options for the [export by type API](./kibana-plugin-core-server.savedobjectsexp ```typescript export interface SavedObjectsExportByTypeOptions extends SavedObjectExportBaseOptions ``` +Extends: SavedObjectExportBaseOptions ## Properties | Property | Type | Description | | --- | --- | --- | -| [hasReference](./kibana-plugin-core-server.savedobjectsexportbytypeoptions.hasreference.md) | SavedObjectsFindOptionsReference[] | optional array of references to search object for. | -| [search](./kibana-plugin-core-server.savedobjectsexportbytypeoptions.search.md) | string | optional query string to filter exported objects. | -| [types](./kibana-plugin-core-server.savedobjectsexportbytypeoptions.types.md) | string[] | array of saved object types. | +| [hasReference?](./kibana-plugin-core-server.savedobjectsexportbytypeoptions.hasreference.md) | SavedObjectsFindOptionsReference\[\] | (Optional) optional array of references to search object for. | +| [search?](./kibana-plugin-core-server.savedobjectsexportbytypeoptions.search.md) | string | (Optional) optional query string to filter exported objects. | +| [types](./kibana-plugin-core-server.savedobjectsexportbytypeoptions.types.md) | string\[\] | array of saved object types. | diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsexporter.__private_.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsexporter.__private_.md deleted file mode 100644 index 23f49a703814f..0000000000000 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectsexporter.__private_.md +++ /dev/null @@ -1,11 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsExporter](./kibana-plugin-core-server.savedobjectsexporter.md) > ["\#private"](./kibana-plugin-core-server.savedobjectsexporter.__private_.md) - -## SavedObjectsExporter."\#private" property - -Signature: - -```typescript -#private; -``` diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsexporter._constructor_.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsexporter._constructor_.md index 3f3d708c590ee..1c2f60570348e 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectsexporter._constructor_.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsexporter._constructor_.md @@ -21,5 +21,5 @@ constructor({ savedObjectsClient, typeRegistry, exportSizeLimit, logger, }: { | Parameter | Type | Description | | --- | --- | --- | -| { savedObjectsClient, typeRegistry, exportSizeLimit, logger, } | {
savedObjectsClient: SavedObjectsClientContract;
typeRegistry: ISavedObjectTypeRegistry;
exportSizeLimit: number;
logger: Logger;
} | | +| { savedObjectsClient, typeRegistry, exportSizeLimit, logger, } | { savedObjectsClient: SavedObjectsClientContract; typeRegistry: ISavedObjectTypeRegistry; exportSizeLimit: number; logger: Logger; } | | diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsexporter.exportbyobjects.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsexporter.exportbyobjects.md index a7dc5a71b835d..b143d17fa59bd 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectsexporter.exportbyobjects.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsexporter.exportbyobjects.md @@ -18,11 +18,11 @@ exportByObjects(options: SavedObjectsExportByObjectOptions): PromiseSavedObjectsExportByObjectOptions | | +| options | SavedObjectsExportByObjectOptions | | Returns: -`Promise` +Promise<import("stream").Readable> ## Exceptions diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsexporter.exportbytypes.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsexporter.exportbytypes.md index 83da41bad7fe0..cf32d988f6eca 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectsexporter.exportbytypes.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsexporter.exportbytypes.md @@ -18,11 +18,11 @@ exportByTypes(options: SavedObjectsExportByTypeOptions): PromiseSavedObjectsExportByTypeOptions | | +| options | SavedObjectsExportByTypeOptions | | Returns: -`Promise` +Promise<import("stream").Readable> ## Exceptions diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsexporter.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsexporter.md index ce23e91633b07..66f52f4aa4902 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectsexporter.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsexporter.md @@ -17,12 +17,6 @@ export declare class SavedObjectsExporter | --- | --- | --- | | [(constructor)({ savedObjectsClient, typeRegistry, exportSizeLimit, logger, })](./kibana-plugin-core-server.savedobjectsexporter._constructor_.md) | | Constructs a new instance of the SavedObjectsExporter class | -## Properties - -| Property | Modifiers | Type | Description | -| --- | --- | --- | --- | -| ["\#private"](./kibana-plugin-core-server.savedobjectsexporter.__private_.md) | | | | - ## Methods | Method | Modifiers | Description | diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsexporterror._constructor_.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsexporterror._constructor_.md index 33bc6113d56e1..ee25dbf8c22e7 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectsexporterror._constructor_.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsexporterror._constructor_.md @@ -16,7 +16,7 @@ constructor(type: string, message: string, attributes?: Record | un | Parameter | Type | Description | | --- | --- | --- | -| type | string | | -| message | string | | -| attributes | Record<string, any> | undefined | | +| type | string | | +| message | string | | +| attributes | Record<string, any> \| undefined | | diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsexporterror.exportsizeexceeded.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsexporterror.exportsizeexceeded.md index c4097724b193d..b69c46383aae4 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectsexporterror.exportsizeexceeded.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsexporterror.exportsizeexceeded.md @@ -14,9 +14,9 @@ static exportSizeExceeded(limit: number): SavedObjectsExportError; | Parameter | Type | Description | | --- | --- | --- | -| limit | number | | +| limit | number | | Returns: -`SavedObjectsExportError` +SavedObjectsExportError diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsexporterror.invalidtransformerror.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsexporterror.invalidtransformerror.md index 103d1ff8a912b..a6f0190f27fb6 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectsexporterror.invalidtransformerror.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsexporterror.invalidtransformerror.md @@ -16,9 +16,9 @@ static invalidTransformError(objectKeys: string[]): SavedObjectsExportError; | Parameter | Type | Description | | --- | --- | --- | -| objectKeys | string[] | | +| objectKeys | string\[\] | | Returns: -`SavedObjectsExportError` +SavedObjectsExportError diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsexporterror.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsexporterror.md index 2a503f9377dac..4c70b8395d8a6 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectsexporterror.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsexporterror.md @@ -10,6 +10,7 @@ ```typescript export declare class SavedObjectsExportError extends Error ``` +Extends: Error ## Constructors @@ -21,8 +22,8 @@ export declare class SavedObjectsExportError extends Error | Property | Modifiers | Type | Description | | --- | --- | --- | --- | -| [attributes](./kibana-plugin-core-server.savedobjectsexporterror.attributes.md) | | Record<string, any> | undefined | | -| [type](./kibana-plugin-core-server.savedobjectsexporterror.type.md) | | string | | +| [attributes?](./kibana-plugin-core-server.savedobjectsexporterror.attributes.md) | | Record<string, any> \| undefined | (Optional) | +| [type](./kibana-plugin-core-server.savedobjectsexporterror.type.md) | | string | | ## Methods diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsexporterror.objectfetcherror.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsexporterror.objectfetcherror.md index afaa4693f3c70..172b9e0f3ef18 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectsexporterror.objectfetcherror.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsexporterror.objectfetcherror.md @@ -14,9 +14,9 @@ static objectFetchError(objects: SavedObject[]): SavedObjectsExportError; | Parameter | Type | Description | | --- | --- | --- | -| objects | SavedObject[] | | +| objects | SavedObject\[\] | | Returns: -`SavedObjectsExportError` +SavedObjectsExportError diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsexporterror.objecttransformerror.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsexporterror.objecttransformerror.md index 393cf20dbae16..46d415068e9e5 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectsexporterror.objecttransformerror.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsexporterror.objecttransformerror.md @@ -16,10 +16,10 @@ static objectTransformError(objects: SavedObject[], cause: Error): SavedObjectsE | Parameter | Type | Description | | --- | --- | --- | -| objects | SavedObject[] | | -| cause | Error | | +| objects | SavedObject\[\] | | +| cause | Error | | Returns: -`SavedObjectsExportError` +SavedObjectsExportError diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsexportexcludedobject.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsexportexcludedobject.md index 4766ae25a936d..053e9b8bec463 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectsexportexcludedobject.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsexportexcludedobject.md @@ -15,7 +15,7 @@ export interface SavedObjectsExportExcludedObject | Property | Type | Description | | --- | --- | --- | -| [id](./kibana-plugin-core-server.savedobjectsexportexcludedobject.id.md) | string | id of the excluded object | -| [reason](./kibana-plugin-core-server.savedobjectsexportexcludedobject.reason.md) | string | optional cause of the exclusion | -| [type](./kibana-plugin-core-server.savedobjectsexportexcludedobject.type.md) | string | type of the excluded object | +| [id](./kibana-plugin-core-server.savedobjectsexportexcludedobject.id.md) | string | id of the excluded object | +| [reason?](./kibana-plugin-core-server.savedobjectsexportexcludedobject.reason.md) | string | (Optional) optional cause of the exclusion | +| [type](./kibana-plugin-core-server.savedobjectsexportexcludedobject.type.md) | string | type of the excluded object | diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsexportresultdetails.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsexportresultdetails.md index f017f2329170b..872147dc81456 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectsexportresultdetails.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsexportresultdetails.md @@ -16,9 +16,9 @@ export interface SavedObjectsExportResultDetails | Property | Type | Description | | --- | --- | --- | -| [excludedObjects](./kibana-plugin-core-server.savedobjectsexportresultdetails.excludedobjects.md) | SavedObjectsExportExcludedObject[] | excluded objects details | -| [excludedObjectsCount](./kibana-plugin-core-server.savedobjectsexportresultdetails.excludedobjectscount.md) | number | number of objects that were excluded from the export | -| [exportedCount](./kibana-plugin-core-server.savedobjectsexportresultdetails.exportedcount.md) | number | number of successfully exported objects | -| [missingRefCount](./kibana-plugin-core-server.savedobjectsexportresultdetails.missingrefcount.md) | number | number of missing references | -| [missingReferences](./kibana-plugin-core-server.savedobjectsexportresultdetails.missingreferences.md) | Array<{
id: string;
type: string;
}> | missing references details | +| [excludedObjects](./kibana-plugin-core-server.savedobjectsexportresultdetails.excludedobjects.md) | SavedObjectsExportExcludedObject\[\] | excluded objects details | +| [excludedObjectsCount](./kibana-plugin-core-server.savedobjectsexportresultdetails.excludedobjectscount.md) | number | number of objects that were excluded from the export | +| [exportedCount](./kibana-plugin-core-server.savedobjectsexportresultdetails.exportedcount.md) | number | number of successfully exported objects | +| [missingRefCount](./kibana-plugin-core-server.savedobjectsexportresultdetails.missingrefcount.md) | number | number of missing references | +| [missingReferences](./kibana-plugin-core-server.savedobjectsexportresultdetails.missingreferences.md) | Array<{ id: string; type: string; }> | missing references details | diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsexporttransform.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsexporttransform.md index 2effed1ae9d70..2f83d5188e891 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectsexporttransform.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsexporttransform.md @@ -45,7 +45,6 @@ export class Plugin() { }); } } - ``` ## Example 2 @@ -81,6 +80,5 @@ export class Plugin() { }); } } - ``` diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsexporttransformcontext.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsexporttransformcontext.md index 271f0048842b2..c277308c6fc3b 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectsexporttransformcontext.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsexporttransformcontext.md @@ -16,5 +16,5 @@ export interface SavedObjectsExportTransformContext | Property | Type | Description | | --- | --- | --- | -| [request](./kibana-plugin-core-server.savedobjectsexporttransformcontext.request.md) | KibanaRequest | The request that initiated the export request. Can be used to create scoped services or client inside the [transformation](./kibana-plugin-core-server.savedobjectsexporttransform.md) | +| [request](./kibana-plugin-core-server.savedobjectsexporttransformcontext.request.md) | KibanaRequest | The request that initiated the export request. Can be used to create scoped services or client inside the [transformation](./kibana-plugin-core-server.savedobjectsexporttransform.md) | diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsfindoptions.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsfindoptions.md index d3696ee71049a..5f3bb46cc7a99 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectsfindoptions.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsfindoptions.md @@ -15,22 +15,22 @@ export interface SavedObjectsFindOptions | Property | Type | Description | | --- | --- | --- | -| [defaultSearchOperator](./kibana-plugin-core-server.savedobjectsfindoptions.defaultsearchoperator.md) | 'AND' | 'OR' | The search operator to use with the provided filter. Defaults to OR | -| [fields](./kibana-plugin-core-server.savedobjectsfindoptions.fields.md) | string[] | An array of fields to include in the results | -| [filter](./kibana-plugin-core-server.savedobjectsfindoptions.filter.md) | string | KueryNode | | -| [hasReference](./kibana-plugin-core-server.savedobjectsfindoptions.hasreference.md) | SavedObjectsFindOptionsReference | SavedObjectsFindOptionsReference[] | Search for documents having a reference to the specified objects. Use hasReferenceOperator to specify the operator to use when searching for multiple references. | -| [hasReferenceOperator](./kibana-plugin-core-server.savedobjectsfindoptions.hasreferenceoperator.md) | 'AND' | 'OR' | The operator to use when searching by multiple references using the hasReference option. Defaults to OR | -| [namespaces](./kibana-plugin-core-server.savedobjectsfindoptions.namespaces.md) | string[] | | -| [page](./kibana-plugin-core-server.savedobjectsfindoptions.page.md) | number | | -| [perPage](./kibana-plugin-core-server.savedobjectsfindoptions.perpage.md) | number | | -| [pit](./kibana-plugin-core-server.savedobjectsfindoptions.pit.md) | SavedObjectsPitParams | Search against a specific Point In Time (PIT) that you've opened with [SavedObjectsClient.openPointInTimeForType()](./kibana-plugin-core-server.savedobjectsclient.openpointintimefortype.md). | -| [preference](./kibana-plugin-core-server.savedobjectsfindoptions.preference.md) | string | An optional ES preference value to be used for the query \* | -| [rootSearchFields](./kibana-plugin-core-server.savedobjectsfindoptions.rootsearchfields.md) | string[] | The fields to perform the parsed query against. Unlike the searchFields argument, these are expected to be root fields and will not be modified. If used in conjunction with searchFields, both are concatenated together. | -| [search](./kibana-plugin-core-server.savedobjectsfindoptions.search.md) | string | Search documents using the Elasticsearch Simple Query String syntax. See Elasticsearch Simple Query String query argument for more information | -| [searchAfter](./kibana-plugin-core-server.savedobjectsfindoptions.searchafter.md) | estypes.Id[] | Use the sort values from the previous page to retrieve the next page of results. | -| [searchFields](./kibana-plugin-core-server.savedobjectsfindoptions.searchfields.md) | string[] | The fields to perform the parsed query against. See Elasticsearch Simple Query String fields argument for more information | -| [sortField](./kibana-plugin-core-server.savedobjectsfindoptions.sortfield.md) | string | | -| [sortOrder](./kibana-plugin-core-server.savedobjectsfindoptions.sortorder.md) | estypes.SearchSortOrder | | -| [type](./kibana-plugin-core-server.savedobjectsfindoptions.type.md) | string | string[] | | -| [typeToNamespacesMap](./kibana-plugin-core-server.savedobjectsfindoptions.typetonamespacesmap.md) | Map<string, string[] | undefined> | This map defines each type to search for, and the namespace(s) to search for the type in; this is only intended to be used by a saved object client wrapper. If this is defined, it supersedes the type and namespaces fields when building the Elasticsearch query. Any types that are not included in this map will be excluded entirely. If a type is included but its value is undefined, the operation will search for that type in the Default namespace. | +| [defaultSearchOperator?](./kibana-plugin-core-server.savedobjectsfindoptions.defaultsearchoperator.md) | 'AND' \| 'OR' | (Optional) The search operator to use with the provided filter. Defaults to OR | +| [fields?](./kibana-plugin-core-server.savedobjectsfindoptions.fields.md) | string\[\] | (Optional) An array of fields to include in the results | +| [filter?](./kibana-plugin-core-server.savedobjectsfindoptions.filter.md) | string \| KueryNode | (Optional) | +| [hasReference?](./kibana-plugin-core-server.savedobjectsfindoptions.hasreference.md) | SavedObjectsFindOptionsReference \| SavedObjectsFindOptionsReference\[\] | (Optional) Search for documents having a reference to the specified objects. Use hasReferenceOperator to specify the operator to use when searching for multiple references. | +| [hasReferenceOperator?](./kibana-plugin-core-server.savedobjectsfindoptions.hasreferenceoperator.md) | 'AND' \| 'OR' | (Optional) The operator to use when searching by multiple references using the hasReference option. Defaults to OR | +| [namespaces?](./kibana-plugin-core-server.savedobjectsfindoptions.namespaces.md) | string\[\] | (Optional) | +| [page?](./kibana-plugin-core-server.savedobjectsfindoptions.page.md) | number | (Optional) | +| [perPage?](./kibana-plugin-core-server.savedobjectsfindoptions.perpage.md) | number | (Optional) | +| [pit?](./kibana-plugin-core-server.savedobjectsfindoptions.pit.md) | SavedObjectsPitParams | (Optional) Search against a specific Point In Time (PIT) that you've opened with [SavedObjectsClient.openPointInTimeForType()](./kibana-plugin-core-server.savedobjectsclient.openpointintimefortype.md). | +| [preference?](./kibana-plugin-core-server.savedobjectsfindoptions.preference.md) | string | (Optional) An optional ES preference value to be used for the query \* | +| [rootSearchFields?](./kibana-plugin-core-server.savedobjectsfindoptions.rootsearchfields.md) | string\[\] | (Optional) The fields to perform the parsed query against. Unlike the searchFields argument, these are expected to be root fields and will not be modified. If used in conjunction with searchFields, both are concatenated together. | +| [search?](./kibana-plugin-core-server.savedobjectsfindoptions.search.md) | string | (Optional) Search documents using the Elasticsearch Simple Query String syntax. See Elasticsearch Simple Query String query argument for more information | +| [searchAfter?](./kibana-plugin-core-server.savedobjectsfindoptions.searchafter.md) | estypes.Id\[\] | (Optional) Use the sort values from the previous page to retrieve the next page of results. | +| [searchFields?](./kibana-plugin-core-server.savedobjectsfindoptions.searchfields.md) | string\[\] | (Optional) The fields to perform the parsed query against. See Elasticsearch Simple Query String fields argument for more information | +| [sortField?](./kibana-plugin-core-server.savedobjectsfindoptions.sortfield.md) | string | (Optional) | +| [sortOrder?](./kibana-plugin-core-server.savedobjectsfindoptions.sortorder.md) | estypes.SearchSortOrder | (Optional) | +| [type](./kibana-plugin-core-server.savedobjectsfindoptions.type.md) | string \| string\[\] | | +| [typeToNamespacesMap?](./kibana-plugin-core-server.savedobjectsfindoptions.typetonamespacesmap.md) | Map<string, string\[\] \| undefined> | (Optional) This map defines each type to search for, and the namespace(s) to search for the type in; this is only intended to be used by a saved object client wrapper. If this is defined, it supersedes the type and namespaces fields when building the Elasticsearch query. Any types that are not included in this map will be excluded entirely. If a type is included but its value is undefined, the operation will search for that type in the Default namespace. | diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsfindoptionsreference.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsfindoptionsreference.md index db04ef7b162a0..21eb9f06cc11d 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectsfindoptionsreference.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsfindoptionsreference.md @@ -15,6 +15,6 @@ export interface SavedObjectsFindOptionsReference | Property | Type | Description | | --- | --- | --- | -| [id](./kibana-plugin-core-server.savedobjectsfindoptionsreference.id.md) | string | | -| [type](./kibana-plugin-core-server.savedobjectsfindoptionsreference.type.md) | string | | +| [id](./kibana-plugin-core-server.savedobjectsfindoptionsreference.id.md) | string | | +| [type](./kibana-plugin-core-server.savedobjectsfindoptionsreference.type.md) | string | | diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsfindresponse.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsfindresponse.md index 8176baf44acbd..4afb825fd0f44 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectsfindresponse.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsfindresponse.md @@ -18,10 +18,10 @@ export interface SavedObjectsFindResponse | Property | Type | Description | | --- | --- | --- | -| [aggregations](./kibana-plugin-core-server.savedobjectsfindresponse.aggregations.md) | A | | -| [page](./kibana-plugin-core-server.savedobjectsfindresponse.page.md) | number | | -| [per\_page](./kibana-plugin-core-server.savedobjectsfindresponse.per_page.md) | number | | -| [pit\_id](./kibana-plugin-core-server.savedobjectsfindresponse.pit_id.md) | string | | -| [saved\_objects](./kibana-plugin-core-server.savedobjectsfindresponse.saved_objects.md) | Array<SavedObjectsFindResult<T>> | | -| [total](./kibana-plugin-core-server.savedobjectsfindresponse.total.md) | number | | +| [aggregations?](./kibana-plugin-core-server.savedobjectsfindresponse.aggregations.md) | A | (Optional) | +| [page](./kibana-plugin-core-server.savedobjectsfindresponse.page.md) | number | | +| [per\_page](./kibana-plugin-core-server.savedobjectsfindresponse.per_page.md) | number | | +| [pit\_id?](./kibana-plugin-core-server.savedobjectsfindresponse.pit_id.md) | string | (Optional) | +| [saved\_objects](./kibana-plugin-core-server.savedobjectsfindresponse.saved_objects.md) | Array<SavedObjectsFindResult<T>> | | +| [total](./kibana-plugin-core-server.savedobjectsfindresponse.total.md) | number | | diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsfindresult.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsfindresult.md index a729ce32e1c80..f2ba01697da17 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectsfindresult.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsfindresult.md @@ -10,11 +10,12 @@ ```typescript export interface SavedObjectsFindResult extends SavedObject ``` +Extends: SavedObject<T> ## Properties | Property | Type | Description | | --- | --- | --- | -| [score](./kibana-plugin-core-server.savedobjectsfindresult.score.md) | number | The Elasticsearch _score of this result. | -| [sort](./kibana-plugin-core-server.savedobjectsfindresult.sort.md) | string[] | The Elasticsearch sort value of this result. | +| [score](./kibana-plugin-core-server.savedobjectsfindresult.score.md) | number | The Elasticsearch _score of this result. | +| [sort?](./kibana-plugin-core-server.savedobjectsfindresult.sort.md) | string\[\] | (Optional) The Elasticsearch sort value of this result. | diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsfindresult.sort.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsfindresult.sort.md index e73d6b4926d89..5df1b3291b072 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectsfindresult.sort.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsfindresult.sort.md @@ -36,6 +36,5 @@ const page2 = await savedObjectsClient.find({ searchAfter: lastHit.sort, }); await savedObjectsClient.closePointInTime(page2.pit_id); - ``` diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsimportactionrequiredwarning.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsimportactionrequiredwarning.md index c8b96004662d4..c4ec5fdd2f8e6 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectsimportactionrequiredwarning.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsimportactionrequiredwarning.md @@ -18,8 +18,8 @@ export interface SavedObjectsImportActionRequiredWarning | Property | Type | Description | | --- | --- | --- | -| [actionPath](./kibana-plugin-core-server.savedobjectsimportactionrequiredwarning.actionpath.md) | string | The path (without the basePath) that the user should be redirect to address this warning. | -| [buttonLabel](./kibana-plugin-core-server.savedobjectsimportactionrequiredwarning.buttonlabel.md) | string | An optional label to use for the link button. If unspecified, a default label will be used. | -| [message](./kibana-plugin-core-server.savedobjectsimportactionrequiredwarning.message.md) | string | The translated message to display to the user. | -| [type](./kibana-plugin-core-server.savedobjectsimportactionrequiredwarning.type.md) | 'action_required' | | +| [actionPath](./kibana-plugin-core-server.savedobjectsimportactionrequiredwarning.actionpath.md) | string | The path (without the basePath) that the user should be redirect to address this warning. | +| [buttonLabel?](./kibana-plugin-core-server.savedobjectsimportactionrequiredwarning.buttonlabel.md) | string | (Optional) An optional label to use for the link button. If unspecified, a default label will be used. | +| [message](./kibana-plugin-core-server.savedobjectsimportactionrequiredwarning.message.md) | string | The translated message to display to the user. | +| [type](./kibana-plugin-core-server.savedobjectsimportactionrequiredwarning.type.md) | 'action\_required' | | diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsimportambiguousconflicterror.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsimportambiguousconflicterror.md index d2c0a397ebe8a..7d275fa199c5b 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectsimportambiguousconflicterror.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsimportambiguousconflicterror.md @@ -16,6 +16,6 @@ export interface SavedObjectsImportAmbiguousConflictError | Property | Type | Description | | --- | --- | --- | -| [destinations](./kibana-plugin-core-server.savedobjectsimportambiguousconflicterror.destinations.md) | Array<{
id: string;
title?: string;
updatedAt?: string;
}> | | -| [type](./kibana-plugin-core-server.savedobjectsimportambiguousconflicterror.type.md) | 'ambiguous_conflict' | | +| [destinations](./kibana-plugin-core-server.savedobjectsimportambiguousconflicterror.destinations.md) | Array<{ id: string; title?: string; updatedAt?: string; }> | | +| [type](./kibana-plugin-core-server.savedobjectsimportambiguousconflicterror.type.md) | 'ambiguous\_conflict' | | diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsimportconflicterror.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsimportconflicterror.md index 153cd55c9199e..9456e042035fe 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectsimportconflicterror.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsimportconflicterror.md @@ -16,6 +16,6 @@ export interface SavedObjectsImportConflictError | Property | Type | Description | | --- | --- | --- | -| [destinationId](./kibana-plugin-core-server.savedobjectsimportconflicterror.destinationid.md) | string | | -| [type](./kibana-plugin-core-server.savedobjectsimportconflicterror.type.md) | 'conflict' | | +| [destinationId?](./kibana-plugin-core-server.savedobjectsimportconflicterror.destinationid.md) | string | (Optional) | +| [type](./kibana-plugin-core-server.savedobjectsimportconflicterror.type.md) | 'conflict' | | diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsimporter.__private_.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsimporter.__private_.md deleted file mode 100644 index 2d780a957e087..0000000000000 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectsimporter.__private_.md +++ /dev/null @@ -1,11 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsImporter](./kibana-plugin-core-server.savedobjectsimporter.md) > ["\#private"](./kibana-plugin-core-server.savedobjectsimporter.__private_.md) - -## SavedObjectsImporter."\#private" property - -Signature: - -```typescript -#private; -``` diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsimporter._constructor_.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsimporter._constructor_.md index 67df4dbf09ad6..8451e35d1518e 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectsimporter._constructor_.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsimporter._constructor_.md @@ -20,5 +20,5 @@ constructor({ savedObjectsClient, typeRegistry, importSizeLimit, }: { | Parameter | Type | Description | | --- | --- | --- | -| { savedObjectsClient, typeRegistry, importSizeLimit, } | {
savedObjectsClient: SavedObjectsClientContract;
typeRegistry: ISavedObjectTypeRegistry;
importSizeLimit: number;
} | | +| { savedObjectsClient, typeRegistry, importSizeLimit, } | { savedObjectsClient: SavedObjectsClientContract; typeRegistry: ISavedObjectTypeRegistry; importSizeLimit: number; } | | diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsimporter.import.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsimporter.import.md index 5b1b2d733fa0e..1ca6058e7d742 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectsimporter.import.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsimporter.import.md @@ -16,11 +16,11 @@ import({ readStream, createNewCopies, namespace, overwrite, }: SavedObjectsImpor | Parameter | Type | Description | | --- | --- | --- | -| { readStream, createNewCopies, namespace, overwrite, } | SavedObjectsImportOptions | | +| { readStream, createNewCopies, namespace, overwrite, } | SavedObjectsImportOptions | | Returns: -`Promise` +Promise<SavedObjectsImportResponse> ## Exceptions diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsimporter.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsimporter.md index cd5c71077e666..18ce27ca2c0dc 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectsimporter.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsimporter.md @@ -17,12 +17,6 @@ export declare class SavedObjectsImporter | --- | --- | --- | | [(constructor)({ savedObjectsClient, typeRegistry, importSizeLimit, })](./kibana-plugin-core-server.savedobjectsimporter._constructor_.md) | | Constructs a new instance of the SavedObjectsImporter class | -## Properties - -| Property | Modifiers | Type | Description | -| --- | --- | --- | --- | -| ["\#private"](./kibana-plugin-core-server.savedobjectsimporter.__private_.md) | | | | - ## Methods | Method | Modifiers | Description | diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsimporter.resolveimporterrors.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsimporter.resolveimporterrors.md index 9418b581ad5b2..e1c95723ed3a5 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectsimporter.resolveimporterrors.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsimporter.resolveimporterrors.md @@ -16,11 +16,11 @@ resolveImportErrors({ readStream, createNewCopies, namespace, retries, }: SavedO | Parameter | Type | Description | | --- | --- | --- | -| { readStream, createNewCopies, namespace, retries, } | SavedObjectsResolveImportErrorsOptions | | +| { readStream, createNewCopies, namespace, retries, } | SavedObjectsResolveImportErrorsOptions | | Returns: -`Promise` +Promise<SavedObjectsImportResponse> ## Exceptions diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsimporterror.importsizeexceeded.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsimporterror.importsizeexceeded.md index 9dcc43633d9eb..421557445670e 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectsimporterror.importsizeexceeded.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsimporterror.importsizeexceeded.md @@ -14,9 +14,9 @@ static importSizeExceeded(limit: number): SavedObjectsImportError; | Parameter | Type | Description | | --- | --- | --- | -| limit | number | | +| limit | number | | Returns: -`SavedObjectsImportError` +SavedObjectsImportError diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsimporterror.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsimporterror.md index b37b6143e7b73..2e4fd1a01eb04 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectsimporterror.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsimporterror.md @@ -10,13 +10,14 @@ ```typescript export declare class SavedObjectsImportError extends Error ``` +Extends: Error ## Properties | Property | Modifiers | Type | Description | | --- | --- | --- | --- | -| [attributes](./kibana-plugin-core-server.savedobjectsimporterror.attributes.md) | | Record<string, any> | undefined | | -| [type](./kibana-plugin-core-server.savedobjectsimporterror.type.md) | | string | | +| [attributes?](./kibana-plugin-core-server.savedobjectsimporterror.attributes.md) | | Record<string, any> \| undefined | (Optional) | +| [type](./kibana-plugin-core-server.savedobjectsimporterror.type.md) | | string | | ## Methods diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsimporterror.nonuniqueimportobjects.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsimporterror.nonuniqueimportobjects.md index a4a1975af0b4c..29533db10302c 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectsimporterror.nonuniqueimportobjects.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsimporterror.nonuniqueimportobjects.md @@ -14,9 +14,9 @@ static nonUniqueImportObjects(nonUniqueEntries: string[]): SavedObjectsImportErr | Parameter | Type | Description | | --- | --- | --- | -| nonUniqueEntries | string[] | | +| nonUniqueEntries | string\[\] | | Returns: -`SavedObjectsImportError` +SavedObjectsImportError diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsimporterror.nonuniqueretrydestinations.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsimporterror.nonuniqueretrydestinations.md index a60f6c34cb7e2..4fd23c3f6d62d 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectsimporterror.nonuniqueretrydestinations.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsimporterror.nonuniqueretrydestinations.md @@ -14,9 +14,9 @@ static nonUniqueRetryDestinations(nonUniqueRetryDestinations: string[]): SavedOb | Parameter | Type | Description | | --- | --- | --- | -| nonUniqueRetryDestinations | string[] | | +| nonUniqueRetryDestinations | string\[\] | | Returns: -`SavedObjectsImportError` +SavedObjectsImportError diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsimporterror.nonuniqueretryobjects.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsimporterror.nonuniqueretryobjects.md index 187904ccf59a2..bf8a2c2c01760 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectsimporterror.nonuniqueretryobjects.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsimporterror.nonuniqueretryobjects.md @@ -14,9 +14,9 @@ static nonUniqueRetryObjects(nonUniqueRetryObjects: string[]): SavedObjectsImpor | Parameter | Type | Description | | --- | --- | --- | -| nonUniqueRetryObjects | string[] | | +| nonUniqueRetryObjects | string\[\] | | Returns: -`SavedObjectsImportError` +SavedObjectsImportError diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsimporterror.referencesfetcherror.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsimporterror.referencesfetcherror.md index c9392739838dc..4202f164900b3 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectsimporterror.referencesfetcherror.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsimporterror.referencesfetcherror.md @@ -14,9 +14,9 @@ static referencesFetchError(objects: SavedObject[]): SavedObjectsImportError; | Parameter | Type | Description | | --- | --- | --- | -| objects | SavedObject[] | | +| objects | SavedObject\[\] | | Returns: -`SavedObjectsImportError` +SavedObjectsImportError diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsimportfailure.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsimportfailure.md index 536f48f45e0c5..52db837479cf6 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectsimportfailure.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsimportfailure.md @@ -16,10 +16,10 @@ export interface SavedObjectsImportFailure | Property | Type | Description | | --- | --- | --- | -| [error](./kibana-plugin-core-server.savedobjectsimportfailure.error.md) | SavedObjectsImportConflictError | SavedObjectsImportAmbiguousConflictError | SavedObjectsImportUnsupportedTypeError | SavedObjectsImportMissingReferencesError | SavedObjectsImportUnknownError | | -| [id](./kibana-plugin-core-server.savedobjectsimportfailure.id.md) | string | | -| [meta](./kibana-plugin-core-server.savedobjectsimportfailure.meta.md) | {
title?: string;
icon?: string;
} | | -| [overwrite](./kibana-plugin-core-server.savedobjectsimportfailure.overwrite.md) | boolean | If overwrite is specified, an attempt was made to overwrite an existing object. | -| [title](./kibana-plugin-core-server.savedobjectsimportfailure.title.md) | string | | -| [type](./kibana-plugin-core-server.savedobjectsimportfailure.type.md) | string | | +| [error](./kibana-plugin-core-server.savedobjectsimportfailure.error.md) | SavedObjectsImportConflictError \| SavedObjectsImportAmbiguousConflictError \| SavedObjectsImportUnsupportedTypeError \| SavedObjectsImportMissingReferencesError \| SavedObjectsImportUnknownError | | +| [id](./kibana-plugin-core-server.savedobjectsimportfailure.id.md) | string | | +| [meta](./kibana-plugin-core-server.savedobjectsimportfailure.meta.md) | { title?: string; icon?: string; } | | +| [overwrite?](./kibana-plugin-core-server.savedobjectsimportfailure.overwrite.md) | boolean | (Optional) If overwrite is specified, an attempt was made to overwrite an existing object. | +| [title?](./kibana-plugin-core-server.savedobjectsimportfailure.title.md) | string | (Optional) | +| [type](./kibana-plugin-core-server.savedobjectsimportfailure.type.md) | string | | diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsimporthookresult.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsimporthookresult.md index 9756ce7fac350..eb16aa2fb0285 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectsimporthookresult.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsimporthookresult.md @@ -16,5 +16,5 @@ export interface SavedObjectsImportHookResult | Property | Type | Description | | --- | --- | --- | -| [warnings](./kibana-plugin-core-server.savedobjectsimporthookresult.warnings.md) | SavedObjectsImportWarning[] | An optional list of warnings to display in the UI when the import succeeds. | +| [warnings?](./kibana-plugin-core-server.savedobjectsimporthookresult.warnings.md) | SavedObjectsImportWarning\[\] | (Optional) An optional list of warnings to display in the UI when the import succeeds. | diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsimportmissingreferenceserror.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsimportmissingreferenceserror.md index 01557eff549f6..25d2a88e87e8b 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectsimportmissingreferenceserror.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsimportmissingreferenceserror.md @@ -16,6 +16,6 @@ export interface SavedObjectsImportMissingReferencesError | Property | Type | Description | | --- | --- | --- | -| [references](./kibana-plugin-core-server.savedobjectsimportmissingreferenceserror.references.md) | Array<{
type: string;
id: string;
}> | | -| [type](./kibana-plugin-core-server.savedobjectsimportmissingreferenceserror.type.md) | 'missing_references' | | +| [references](./kibana-plugin-core-server.savedobjectsimportmissingreferenceserror.references.md) | Array<{ type: string; id: string; }> | | +| [type](./kibana-plugin-core-server.savedobjectsimportmissingreferenceserror.type.md) | 'missing\_references' | | diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsimportoptions.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsimportoptions.md index ddda72938b13a..58d0f4bf982c3 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectsimportoptions.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsimportoptions.md @@ -16,8 +16,8 @@ export interface SavedObjectsImportOptions | Property | Type | Description | | --- | --- | --- | -| [createNewCopies](./kibana-plugin-core-server.savedobjectsimportoptions.createnewcopies.md) | boolean | If true, will create new copies of import objects, each with a random id and undefined originId. | -| [namespace](./kibana-plugin-core-server.savedobjectsimportoptions.namespace.md) | string | if specified, will import in given namespace, else will import as global object | -| [overwrite](./kibana-plugin-core-server.savedobjectsimportoptions.overwrite.md) | boolean | If true, will override existing object if present. Note: this has no effect when used with the createNewCopies option. | -| [readStream](./kibana-plugin-core-server.savedobjectsimportoptions.readstream.md) | Readable | The stream of [saved objects](./kibana-plugin-core-server.savedobject.md) to import | +| [createNewCopies](./kibana-plugin-core-server.savedobjectsimportoptions.createnewcopies.md) | boolean | If true, will create new copies of import objects, each with a random id and undefined originId. | +| [namespace?](./kibana-plugin-core-server.savedobjectsimportoptions.namespace.md) | string | (Optional) if specified, will import in given namespace, else will import as global object | +| [overwrite](./kibana-plugin-core-server.savedobjectsimportoptions.overwrite.md) | boolean | If true, will override existing object if present. Note: this has no effect when used with the createNewCopies option. | +| [readStream](./kibana-plugin-core-server.savedobjectsimportoptions.readstream.md) | Readable | The stream of [saved objects](./kibana-plugin-core-server.savedobject.md) to import | diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsimportresponse.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsimportresponse.md index 55f651197490f..e39b4b02bb55d 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectsimportresponse.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsimportresponse.md @@ -16,9 +16,9 @@ export interface SavedObjectsImportResponse | Property | Type | Description | | --- | --- | --- | -| [errors](./kibana-plugin-core-server.savedobjectsimportresponse.errors.md) | SavedObjectsImportFailure[] | | -| [success](./kibana-plugin-core-server.savedobjectsimportresponse.success.md) | boolean | | -| [successCount](./kibana-plugin-core-server.savedobjectsimportresponse.successcount.md) | number | | -| [successResults](./kibana-plugin-core-server.savedobjectsimportresponse.successresults.md) | SavedObjectsImportSuccess[] | | -| [warnings](./kibana-plugin-core-server.savedobjectsimportresponse.warnings.md) | SavedObjectsImportWarning[] | | +| [errors?](./kibana-plugin-core-server.savedobjectsimportresponse.errors.md) | SavedObjectsImportFailure\[\] | (Optional) | +| [success](./kibana-plugin-core-server.savedobjectsimportresponse.success.md) | boolean | | +| [successCount](./kibana-plugin-core-server.savedobjectsimportresponse.successcount.md) | number | | +| [successResults?](./kibana-plugin-core-server.savedobjectsimportresponse.successresults.md) | SavedObjectsImportSuccess\[\] | (Optional) | +| [warnings](./kibana-plugin-core-server.savedobjectsimportresponse.warnings.md) | SavedObjectsImportWarning\[\] | | diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsimportretry.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsimportretry.md index 70693e6f43a39..b79ec63ed86c0 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectsimportretry.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsimportretry.md @@ -16,11 +16,11 @@ export interface SavedObjectsImportRetry | Property | Type | Description | | --- | --- | --- | -| [createNewCopy](./kibana-plugin-core-server.savedobjectsimportretry.createnewcopy.md) | boolean | If createNewCopy is specified, the new object has a new (undefined) origin ID. This is only needed for the case where createNewCopies mode is disabled and ambiguous source conflicts are detected. | -| [destinationId](./kibana-plugin-core-server.savedobjectsimportretry.destinationid.md) | string | The object ID that will be created or overwritten. If not specified, the id field will be used. | -| [id](./kibana-plugin-core-server.savedobjectsimportretry.id.md) | string | | -| [ignoreMissingReferences](./kibana-plugin-core-server.savedobjectsimportretry.ignoremissingreferences.md) | boolean | If ignoreMissingReferences is specified, reference validation will be skipped for this object. | -| [overwrite](./kibana-plugin-core-server.savedobjectsimportretry.overwrite.md) | boolean | | -| [replaceReferences](./kibana-plugin-core-server.savedobjectsimportretry.replacereferences.md) | Array<{
type: string;
from: string;
to: string;
}> | | -| [type](./kibana-plugin-core-server.savedobjectsimportretry.type.md) | string | | +| [createNewCopy?](./kibana-plugin-core-server.savedobjectsimportretry.createnewcopy.md) | boolean | (Optional) If createNewCopy is specified, the new object has a new (undefined) origin ID. This is only needed for the case where createNewCopies mode is disabled and ambiguous source conflicts are detected. | +| [destinationId?](./kibana-plugin-core-server.savedobjectsimportretry.destinationid.md) | string | (Optional) The object ID that will be created or overwritten. If not specified, the id field will be used. | +| [id](./kibana-plugin-core-server.savedobjectsimportretry.id.md) | string | | +| [ignoreMissingReferences?](./kibana-plugin-core-server.savedobjectsimportretry.ignoremissingreferences.md) | boolean | (Optional) If ignoreMissingReferences is specified, reference validation will be skipped for this object. | +| [overwrite](./kibana-plugin-core-server.savedobjectsimportretry.overwrite.md) | boolean | | +| [replaceReferences](./kibana-plugin-core-server.savedobjectsimportretry.replacereferences.md) | Array<{ type: string; from: string; to: string; }> | | +| [type](./kibana-plugin-core-server.savedobjectsimportretry.type.md) | string | | diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsimportsimplewarning.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsimportsimplewarning.md index 52d46e4f8db80..7dc4285400478 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectsimportsimplewarning.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsimportsimplewarning.md @@ -16,6 +16,6 @@ export interface SavedObjectsImportSimpleWarning | Property | Type | Description | | --- | --- | --- | -| [message](./kibana-plugin-core-server.savedobjectsimportsimplewarning.message.md) | string | The translated message to display to the user | -| [type](./kibana-plugin-core-server.savedobjectsimportsimplewarning.type.md) | 'simple' | | +| [message](./kibana-plugin-core-server.savedobjectsimportsimplewarning.message.md) | string | The translated message to display to the user | +| [type](./kibana-plugin-core-server.savedobjectsimportsimplewarning.type.md) | 'simple' | | diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsimportsuccess.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsimportsuccess.md index 18a226f636b1d..74242ba6d75be 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectsimportsuccess.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsimportsuccess.md @@ -16,10 +16,10 @@ export interface SavedObjectsImportSuccess | Property | Type | Description | | --- | --- | --- | -| [createNewCopy](./kibana-plugin-core-server.savedobjectsimportsuccess.createnewcopy.md) | boolean | | -| [destinationId](./kibana-plugin-core-server.savedobjectsimportsuccess.destinationid.md) | string | If destinationId is specified, the new object has a new ID that is different from the import ID. | -| [id](./kibana-plugin-core-server.savedobjectsimportsuccess.id.md) | string | | -| [meta](./kibana-plugin-core-server.savedobjectsimportsuccess.meta.md) | {
title?: string;
icon?: string;
} | | -| [overwrite](./kibana-plugin-core-server.savedobjectsimportsuccess.overwrite.md) | boolean | If overwrite is specified, this object overwrote an existing one (or will do so, in the case of a pending resolution). | -| [type](./kibana-plugin-core-server.savedobjectsimportsuccess.type.md) | string | | +| [createNewCopy?](./kibana-plugin-core-server.savedobjectsimportsuccess.createnewcopy.md) | boolean | (Optional) | +| [destinationId?](./kibana-plugin-core-server.savedobjectsimportsuccess.destinationid.md) | string | (Optional) If destinationId is specified, the new object has a new ID that is different from the import ID. | +| [id](./kibana-plugin-core-server.savedobjectsimportsuccess.id.md) | string | | +| [meta](./kibana-plugin-core-server.savedobjectsimportsuccess.meta.md) | { title?: string; icon?: string; } | | +| [overwrite?](./kibana-plugin-core-server.savedobjectsimportsuccess.overwrite.md) | boolean | (Optional) If overwrite is specified, this object overwrote an existing one (or will do so, in the case of a pending resolution). | +| [type](./kibana-plugin-core-server.savedobjectsimportsuccess.type.md) | string | | diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsimportunknownerror.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsimportunknownerror.md index c178d363761ef..158afd45752fc 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectsimportunknownerror.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsimportunknownerror.md @@ -16,7 +16,7 @@ export interface SavedObjectsImportUnknownError | Property | Type | Description | | --- | --- | --- | -| [message](./kibana-plugin-core-server.savedobjectsimportunknownerror.message.md) | string | | -| [statusCode](./kibana-plugin-core-server.savedobjectsimportunknownerror.statuscode.md) | number | | -| [type](./kibana-plugin-core-server.savedobjectsimportunknownerror.type.md) | 'unknown' | | +| [message](./kibana-plugin-core-server.savedobjectsimportunknownerror.message.md) | string | | +| [statusCode](./kibana-plugin-core-server.savedobjectsimportunknownerror.statuscode.md) | number | | +| [type](./kibana-plugin-core-server.savedobjectsimportunknownerror.type.md) | 'unknown' | | diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsimportunsupportedtypeerror.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsimportunsupportedtypeerror.md index 3f580b27b7fde..48aff60c69d13 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectsimportunsupportedtypeerror.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsimportunsupportedtypeerror.md @@ -16,5 +16,5 @@ export interface SavedObjectsImportUnsupportedTypeError | Property | Type | Description | | --- | --- | --- | -| [type](./kibana-plugin-core-server.savedobjectsimportunsupportedtypeerror.type.md) | 'unsupported_type' | | +| [type](./kibana-plugin-core-server.savedobjectsimportunsupportedtypeerror.type.md) | 'unsupported\_type' | | diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsincrementcounterfield.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsincrementcounterfield.md index 10615c7da4c34..a45d48523f461 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectsincrementcounterfield.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsincrementcounterfield.md @@ -15,6 +15,6 @@ export interface SavedObjectsIncrementCounterField | Property | Type | Description | | --- | --- | --- | -| [fieldName](./kibana-plugin-core-server.savedobjectsincrementcounterfield.fieldname.md) | string | The field name to increment the counter by. | -| [incrementBy](./kibana-plugin-core-server.savedobjectsincrementcounterfield.incrementby.md) | number | The number to increment the field by (defaults to 1). | +| [fieldName](./kibana-plugin-core-server.savedobjectsincrementcounterfield.fieldname.md) | string | The field name to increment the counter by. | +| [incrementBy?](./kibana-plugin-core-server.savedobjectsincrementcounterfield.incrementby.md) | number | (Optional) The number to increment the field by (defaults to 1). | diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsincrementcounteroptions.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsincrementcounteroptions.md index 8da2458cf007e..8740ffb1be185 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectsincrementcounteroptions.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsincrementcounteroptions.md @@ -10,13 +10,14 @@ ```typescript export interface SavedObjectsIncrementCounterOptions extends SavedObjectsBaseOptions ``` +Extends: SavedObjectsBaseOptions ## Properties | Property | Type | Description | | --- | --- | --- | -| [initialize](./kibana-plugin-core-server.savedobjectsincrementcounteroptions.initialize.md) | boolean | (default=false) If true, sets all the counter fields to 0 if they don't already exist. Existing fields will be left as-is and won't be incremented. | -| [migrationVersion](./kibana-plugin-core-server.savedobjectsincrementcounteroptions.migrationversion.md) | SavedObjectsMigrationVersion | [SavedObjectsMigrationVersion](./kibana-plugin-core-server.savedobjectsmigrationversion.md) | -| [refresh](./kibana-plugin-core-server.savedobjectsincrementcounteroptions.refresh.md) | MutatingOperationRefreshSetting | (default='wait\_for') The Elasticsearch refresh setting for this operation. See [MutatingOperationRefreshSetting](./kibana-plugin-core-server.mutatingoperationrefreshsetting.md) | -| [upsertAttributes](./kibana-plugin-core-server.savedobjectsincrementcounteroptions.upsertattributes.md) | Attributes | Attributes to use when upserting the document if it doesn't exist. | +| [initialize?](./kibana-plugin-core-server.savedobjectsincrementcounteroptions.initialize.md) | boolean | (Optional) (default=false) If true, sets all the counter fields to 0 if they don't already exist. Existing fields will be left as-is and won't be incremented. | +| [migrationVersion?](./kibana-plugin-core-server.savedobjectsincrementcounteroptions.migrationversion.md) | SavedObjectsMigrationVersion | (Optional) [SavedObjectsMigrationVersion](./kibana-plugin-core-server.savedobjectsmigrationversion.md) | +| [refresh?](./kibana-plugin-core-server.savedobjectsincrementcounteroptions.refresh.md) | MutatingOperationRefreshSetting | (Optional) (default='wait\_for') The Elasticsearch refresh setting for this operation. See [MutatingOperationRefreshSetting](./kibana-plugin-core-server.mutatingoperationrefreshsetting.md) | +| [upsertAttributes?](./kibana-plugin-core-server.savedobjectsincrementcounteroptions.upsertattributes.md) | Attributes | (Optional) Attributes to use when upserting the document if it doesn't exist. | diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsmigrationlogger.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsmigrationlogger.md index 697f8823c4966..80f332c395159 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectsmigrationlogger.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsmigrationlogger.md @@ -15,9 +15,9 @@ export interface SavedObjectsMigrationLogger | Property | Type | Description | | --- | --- | --- | -| [debug](./kibana-plugin-core-server.savedobjectsmigrationlogger.debug.md) | (msg: string) => void | | -| [error](./kibana-plugin-core-server.savedobjectsmigrationlogger.error.md) | <Meta extends LogMeta = LogMeta>(msg: string, meta: Meta) => void | | -| [info](./kibana-plugin-core-server.savedobjectsmigrationlogger.info.md) | (msg: string) => void | | -| [warn](./kibana-plugin-core-server.savedobjectsmigrationlogger.warn.md) | (msg: string) => void | | -| [warning](./kibana-plugin-core-server.savedobjectsmigrationlogger.warning.md) | (msg: string) => void | | +| [debug](./kibana-plugin-core-server.savedobjectsmigrationlogger.debug.md) | (msg: string) => void | | +| [error](./kibana-plugin-core-server.savedobjectsmigrationlogger.error.md) | <Meta extends LogMeta = LogMeta>(msg: string, meta: Meta) => void | | +| [info](./kibana-plugin-core-server.savedobjectsmigrationlogger.info.md) | (msg: string) => void | | +| [warn](./kibana-plugin-core-server.savedobjectsmigrationlogger.warn.md) | (msg: string) => void | | +| [warning](./kibana-plugin-core-server.savedobjectsmigrationlogger.warning.md) | (msg: string) => void | | diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsopenpointintimeoptions.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsopenpointintimeoptions.md index fc825e3bf2937..331fb6cbe0a6e 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectsopenpointintimeoptions.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsopenpointintimeoptions.md @@ -15,7 +15,7 @@ export interface SavedObjectsOpenPointInTimeOptions | Property | Type | Description | | --- | --- | --- | -| [keepAlive](./kibana-plugin-core-server.savedobjectsopenpointintimeoptions.keepalive.md) | string | Optionally specify how long ES should keep the PIT alive until the next request. Defaults to 5m. | -| [namespaces](./kibana-plugin-core-server.savedobjectsopenpointintimeoptions.namespaces.md) | string[] | An optional list of namespaces to be used when opening the PIT.When the spaces plugin is enabled: - this will default to the user's current space (as determined by the URL) - if specified, the user's current space will be ignored - ['*'] will search across all available spaces | -| [preference](./kibana-plugin-core-server.savedobjectsopenpointintimeoptions.preference.md) | string | An optional ES preference value to be used for the query. | +| [keepAlive?](./kibana-plugin-core-server.savedobjectsopenpointintimeoptions.keepalive.md) | string | (Optional) Optionally specify how long ES should keep the PIT alive until the next request. Defaults to 5m. | +| [namespaces?](./kibana-plugin-core-server.savedobjectsopenpointintimeoptions.namespaces.md) | string\[\] | (Optional) An optional list of namespaces to be used when opening the PIT.When the spaces plugin is enabled: - this will default to the user's current space (as determined by the URL) - if specified, the user's current space will be ignored - ['*'] will search across all available spaces | +| [preference?](./kibana-plugin-core-server.savedobjectsopenpointintimeoptions.preference.md) | string | (Optional) An optional ES preference value to be used for the query. | diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsopenpointintimeresponse.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsopenpointintimeresponse.md index c4be2692763a5..e3804d63a1e6c 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectsopenpointintimeresponse.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsopenpointintimeresponse.md @@ -15,5 +15,5 @@ export interface SavedObjectsOpenPointInTimeResponse | Property | Type | Description | | --- | --- | --- | -| [id](./kibana-plugin-core-server.savedobjectsopenpointintimeresponse.id.md) | string | PIT ID returned from ES. | +| [id](./kibana-plugin-core-server.savedobjectsopenpointintimeresponse.id.md) | string | PIT ID returned from ES. | diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectspitparams.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectspitparams.md index 7bffca7cda281..3109a6bd88f7e 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectspitparams.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectspitparams.md @@ -15,6 +15,6 @@ export interface SavedObjectsPitParams | Property | Type | Description | | --- | --- | --- | -| [id](./kibana-plugin-core-server.savedobjectspitparams.id.md) | string | | -| [keepAlive](./kibana-plugin-core-server.savedobjectspitparams.keepalive.md) | string | | +| [id](./kibana-plugin-core-server.savedobjectspitparams.id.md) | string | | +| [keepAlive?](./kibana-plugin-core-server.savedobjectspitparams.keepalive.md) | string | (Optional) | diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsrawdoc.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsrawdoc.md index 54bca496b9930..8bce7fac941af 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectsrawdoc.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsrawdoc.md @@ -16,8 +16,8 @@ export interface SavedObjectsRawDoc | Property | Type | Description | | --- | --- | --- | -| [\_id](./kibana-plugin-core-server.savedobjectsrawdoc._id.md) | string | | -| [\_primary\_term](./kibana-plugin-core-server.savedobjectsrawdoc._primary_term.md) | number | | -| [\_seq\_no](./kibana-plugin-core-server.savedobjectsrawdoc._seq_no.md) | number | | -| [\_source](./kibana-plugin-core-server.savedobjectsrawdoc._source.md) | SavedObjectsRawDocSource | | +| [\_id](./kibana-plugin-core-server.savedobjectsrawdoc._id.md) | string | | +| [\_primary\_term?](./kibana-plugin-core-server.savedobjectsrawdoc._primary_term.md) | number | (Optional) | +| [\_seq\_no?](./kibana-plugin-core-server.savedobjectsrawdoc._seq_no.md) | number | (Optional) | +| [\_source](./kibana-plugin-core-server.savedobjectsrawdoc._source.md) | SavedObjectsRawDocSource | | diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsrawdocparseoptions.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsrawdocparseoptions.md index 708d1bc9c514d..dc2166258d0c0 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectsrawdocparseoptions.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsrawdocparseoptions.md @@ -16,5 +16,5 @@ export interface SavedObjectsRawDocParseOptions | Property | Type | Description | | --- | --- | --- | -| [namespaceTreatment](./kibana-plugin-core-server.savedobjectsrawdocparseoptions.namespacetreatment.md) | 'strict' | 'lax' | Optional setting to allow for lax handling of the raw document ID and namespace field. This is needed when a previously single-namespace object type is converted to a multi-namespace object type, and it is only intended to be used during upgrade migrations.If not specified, the default treatment is strict. | +| [namespaceTreatment?](./kibana-plugin-core-server.savedobjectsrawdocparseoptions.namespacetreatment.md) | 'strict' \| 'lax' | (Optional) Optional setting to allow for lax handling of the raw document ID and namespace field. This is needed when a previously single-namespace object type is converted to a multi-namespace object type, and it is only intended to be used during upgrade migrations.If not specified, the default treatment is strict. | diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsremovereferencestooptions.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsremovereferencestooptions.md index 0874aa460e220..c10f74297305d 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectsremovereferencestooptions.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsremovereferencestooptions.md @@ -10,10 +10,11 @@ ```typescript export interface SavedObjectsRemoveReferencesToOptions extends SavedObjectsBaseOptions ``` +Extends: SavedObjectsBaseOptions ## Properties | Property | Type | Description | | --- | --- | --- | -| [refresh](./kibana-plugin-core-server.savedobjectsremovereferencestooptions.refresh.md) | boolean | The Elasticsearch Refresh setting for this operation. Defaults to true | +| [refresh?](./kibana-plugin-core-server.savedobjectsremovereferencestooptions.refresh.md) | boolean | (Optional) The Elasticsearch Refresh setting for this operation. Defaults to true | diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsremovereferencestoresponse.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsremovereferencestoresponse.md index b5468a300d51d..cd10a9f916b03 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectsremovereferencestoresponse.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsremovereferencestoresponse.md @@ -10,10 +10,11 @@ ```typescript export interface SavedObjectsRemoveReferencesToResponse extends SavedObjectsBaseOptions ``` +Extends: SavedObjectsBaseOptions ## Properties | Property | Type | Description | | --- | --- | --- | -| [updated](./kibana-plugin-core-server.savedobjectsremovereferencestoresponse.updated.md) | number | The number of objects that have been updated by this operation | +| [updated](./kibana-plugin-core-server.savedobjectsremovereferencestoresponse.updated.md) | number | The number of objects that have been updated by this operation | diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsrepository.bulkcreate.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsrepository.bulkcreate.md index 17daf3ab1f042..e71a9d266a3db 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectsrepository.bulkcreate.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsrepository.bulkcreate.md @@ -16,12 +16,12 @@ bulkCreate(objects: Array>, options | Parameter | Type | Description | | --- | --- | --- | -| objects | Array<SavedObjectsBulkCreateObject<T>> | | -| options | SavedObjectsCreateOptions | | +| objects | Array<SavedObjectsBulkCreateObject<T>> | \[{ type, id, attributes, references, migrationVersion }\] | +| options | SavedObjectsCreateOptions | {boolean} \[options.overwrite=false\] - overwrites existing documents {string} \[options.namespace\] | Returns: -`Promise>` +Promise<SavedObjectsBulkResponse<T>> {promise} - {saved\_objects: \[\[{ id, type, version, references, attributes, error: { message } }\]} diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsrepository.bulkget.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsrepository.bulkget.md index 354ee4dfff62c..ab265132d606f 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectsrepository.bulkget.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsrepository.bulkget.md @@ -16,12 +16,12 @@ bulkGet(objects?: SavedObjectsBulkGetObject[], options?: SavedObjec | Parameter | Type | Description | | --- | --- | --- | -| objects | SavedObjectsBulkGetObject[] | | -| options | SavedObjectsBaseOptions | | +| objects | SavedObjectsBulkGetObject\[\] | an array of objects containing id, type and optionally fields | +| options | SavedObjectsBaseOptions | {string} \[options.namespace\] | Returns: -`Promise>` +Promise<SavedObjectsBulkResponse<T>> {promise} - { saved\_objects: \[{ id, type, version, attributes }\] } diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsrepository.bulkresolve.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsrepository.bulkresolve.md index f489972207a61..a67521753892d 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectsrepository.bulkresolve.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsrepository.bulkresolve.md @@ -16,12 +16,12 @@ bulkResolve(objects: SavedObjectsBulkResolveObject[], options?: Sav | Parameter | Type | Description | | --- | --- | --- | -| objects | SavedObjectsBulkResolveObject[] | | -| options | SavedObjectsBaseOptions | | +| objects | SavedObjectsBulkResolveObject\[\] | an array of objects containing id, type | +| options | SavedObjectsBaseOptions | {string} \[options.namespace\] | Returns: -`Promise>` +Promise<SavedObjectsBulkResolveResponse<T>> {promise} - { resolved\_objects: \[{ saved\_object, outcome }\] } diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsrepository.bulkupdate.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsrepository.bulkupdate.md index de61549b7680d..c4244a8e34e3c 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectsrepository.bulkupdate.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsrepository.bulkupdate.md @@ -16,12 +16,12 @@ bulkUpdate(objects: Array>, options | Parameter | Type | Description | | --- | --- | --- | -| objects | Array<SavedObjectsBulkUpdateObject<T>> | | -| options | SavedObjectsBulkUpdateOptions | | +| objects | Array<SavedObjectsBulkUpdateObject<T>> | \[{ type, id, attributes, options: { version, namespace } references }\] {string} options.version - ensures version matches that of persisted object {string} \[options.namespace\] | +| options | SavedObjectsBulkUpdateOptions | | Returns: -`Promise>` +Promise<SavedObjectsBulkUpdateResponse<T>> {promise} - {saved\_objects: \[\[{ id, type, version, references, attributes, error: { message } }\]} diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsrepository.checkconflicts.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsrepository.checkconflicts.md index 6e44bd704d6a7..48adf6a2dba4a 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectsrepository.checkconflicts.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsrepository.checkconflicts.md @@ -16,10 +16,10 @@ checkConflicts(objects?: SavedObjectsCheckConflictsObject[], options?: SavedObje | Parameter | Type | Description | | --- | --- | --- | -| objects | SavedObjectsCheckConflictsObject[] | | -| options | SavedObjectsBaseOptions | | +| objects | SavedObjectsCheckConflictsObject\[\] | | +| options | SavedObjectsBaseOptions | | Returns: -`Promise` +Promise<SavedObjectsCheckConflictsResponse> diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsrepository.closepointintime.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsrepository.closepointintime.md index b9d81c89bffd7..e25cd9dfcaae7 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectsrepository.closepointintime.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsrepository.closepointintime.md @@ -18,12 +18,12 @@ closePointInTime(id: string, options?: SavedObjectsClosePointInTimeOptions): Pro | Parameter | Type | Description | | --- | --- | --- | -| id | string | | -| options | SavedObjectsClosePointInTimeOptions | | +| id | string | | +| options | SavedObjectsClosePointInTimeOptions | [SavedObjectsClosePointInTimeOptions](./kibana-plugin-core-server.savedobjectsclosepointintimeoptions.md) | Returns: -`Promise` +Promise<SavedObjectsClosePointInTimeResponse> {promise} - [SavedObjectsClosePointInTimeResponse](./kibana-plugin-core-server.savedobjectsclosepointintimeresponse.md) @@ -55,6 +55,5 @@ const response = await repository.find({ }); await repository.closePointInTime(response.pit_id); - ``` diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsrepository.collectmultinamespacereferences.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsrepository.collectmultinamespacereferences.md index 450cd14a20524..b22b3bd8c0b53 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectsrepository.collectmultinamespacereferences.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsrepository.collectmultinamespacereferences.md @@ -16,10 +16,10 @@ collectMultiNamespaceReferences(objects: SavedObjectsCollectMultiNamespaceRefere | Parameter | Type | Description | | --- | --- | --- | -| objects | SavedObjectsCollectMultiNamespaceReferencesObject[] | | -| options | SavedObjectsCollectMultiNamespaceReferencesOptions | | +| objects | SavedObjectsCollectMultiNamespaceReferencesObject\[\] | The objects to get the references for. | +| options | SavedObjectsCollectMultiNamespaceReferencesOptions | | Returns: -`Promise` +Promise<import("./collect\_multi\_namespace\_references").SavedObjectsCollectMultiNamespaceReferencesResponse> diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsrepository.create.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsrepository.create.md index 316f72f626101..0c5412e2cd2df 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectsrepository.create.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsrepository.create.md @@ -16,13 +16,13 @@ create(type: string, attributes: T, options?: SavedObjectsCreateOpt | Parameter | Type | Description | | --- | --- | --- | -| type | string | | -| attributes | T | | -| options | SavedObjectsCreateOptions | | +| type | string | | +| attributes | T | | +| options | SavedObjectsCreateOptions | {string} \[options.id\] - force id on creation, not recommended {boolean} \[options.overwrite=false\] {object} \[options.migrationVersion=undefined\] {string} \[options.namespace\] {array} \[options.references=\[\]\] - \[{ name, type, id }\] | Returns: -`Promise>` +Promise<SavedObject<T>> {promise} - { id, type, version, attributes } diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsrepository.createpointintimefinder.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsrepository.createpointintimefinder.md index c92a1986966fd..4cbf51b85f26d 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectsrepository.createpointintimefinder.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsrepository.createpointintimefinder.md @@ -22,12 +22,12 @@ createPointInTimeFinder(findOptions: SavedObjectsCreat | Parameter | Type | Description | | --- | --- | --- | -| findOptions | SavedObjectsCreatePointInTimeFinderOptions | | -| dependencies | SavedObjectsCreatePointInTimeFinderDependencies | | +| findOptions | SavedObjectsCreatePointInTimeFinderOptions | | +| dependencies | SavedObjectsCreatePointInTimeFinderDependencies | | Returns: -`ISavedObjectsPointInTimeFinder` +ISavedObjectsPointInTimeFinder<T, A> ## Example @@ -48,6 +48,5 @@ for await (const response of finder.find()) { await finder.close(); } } - ``` diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsrepository.delete.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsrepository.delete.md index c10ab0ab57eb3..2caa59210b9d3 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectsrepository.delete.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsrepository.delete.md @@ -16,13 +16,13 @@ delete(type: string, id: string, options?: SavedObjectsDeleteOptions): Promise<{ | Parameter | Type | Description | | --- | --- | --- | -| type | string | | -| id | string | | -| options | SavedObjectsDeleteOptions | | +| type | string | | +| id | string | | +| options | SavedObjectsDeleteOptions | {string} \[options.namespace\] | Returns: -`Promise<{}>` +Promise<{}> {promise} diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsrepository.deletebynamespace.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsrepository.deletebynamespace.md index 3cdfb095d1334..2e9048bcbe188 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectsrepository.deletebynamespace.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsrepository.deletebynamespace.md @@ -16,12 +16,12 @@ deleteByNamespace(namespace: string, options?: SavedObjectsDeleteByNamespaceOpti | Parameter | Type | Description | | --- | --- | --- | -| namespace | string | | -| options | SavedObjectsDeleteByNamespaceOptions | | +| namespace | string | | +| options | SavedObjectsDeleteByNamespaceOptions | | Returns: -`Promise` +Promise<any> {promise} - { took, timed\_out, total, deleted, batches, version\_conflicts, noops, retries, failures } diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsrepository.find.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsrepository.find.md index 5c823b7567918..58c14917aa2c9 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectsrepository.find.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsrepository.find.md @@ -14,11 +14,11 @@ find(options: SavedObjectsFindOptions): PromiseSavedObjectsFindOptions | | +| options | SavedObjectsFindOptions | {(string\|Array)} \[options.type\] {string} \[options.search\] {string} \[options.defaultSearchOperator\] {Array} \[options.searchFields\] - see Elasticsearch Simple Query String Query field argument for more information {integer} \[options.page=1\] {integer} \[options.perPage=20\] {Array} \[options.searchAfter\] {string} \[options.sortField\] {string} \[options.sortOrder\] {Array} \[options.fields\] {string} \[options.namespace\] {object} \[options.hasReference\] - { type, id } {string} \[options.pit\] {string} \[options.preference\] | Returns: -`Promise>` +Promise<SavedObjectsFindResponse<T, A>> {promise} - { saved\_objects: \[{ id, type, version, attributes }\], total, per\_page, page } diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsrepository.get.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsrepository.get.md index f9368a1b400cb..9f2b1b924857b 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectsrepository.get.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsrepository.get.md @@ -16,13 +16,13 @@ get(type: string, id: string, options?: SavedObjectsBaseOptions): P | Parameter | Type | Description | | --- | --- | --- | -| type | string | | -| id | string | | -| options | SavedObjectsBaseOptions | | +| type | string | | +| id | string | | +| options | SavedObjectsBaseOptions | {string} \[options.namespace\] | Returns: -`Promise>` +Promise<SavedObject<T>> {promise} - { id, type, version, attributes } diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsrepository.incrementcounter.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsrepository.incrementcounter.md index 007b453817c8d..0a51ec9429fe9 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectsrepository.incrementcounter.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsrepository.incrementcounter.md @@ -16,14 +16,14 @@ incrementCounter(type: string, id: string, counterFields: Arraystring | The type of saved object whose fields should be incremented | -| id | string | The id of the document whose fields should be incremented | -| counterFields | Array<string | SavedObjectsIncrementCounterField> | An array of field names to increment or an array of [SavedObjectsIncrementCounterField](./kibana-plugin-core-server.savedobjectsincrementcounterfield.md) | -| options | SavedObjectsIncrementCounterOptions<T> | [SavedObjectsIncrementCounterOptions](./kibana-plugin-core-server.savedobjectsincrementcounteroptions.md) | +| type | string | The type of saved object whose fields should be incremented | +| id | string | The id of the document whose fields should be incremented | +| counterFields | Array<string \| SavedObjectsIncrementCounterField> | An array of field names to increment or an array of [SavedObjectsIncrementCounterField](./kibana-plugin-core-server.savedobjectsincrementcounterfield.md) | +| options | SavedObjectsIncrementCounterOptions<T> | [SavedObjectsIncrementCounterOptions](./kibana-plugin-core-server.savedobjectsincrementcounteroptions.md) | Returns: -`Promise>` +Promise<SavedObject<T>> The saved object after the specified fields were incremented @@ -65,6 +65,5 @@ repository.incrementCounter<{ appId: string }>( [ 'stats.apiCalls'], { upsertAttributes: { appId: 'myId' } } ) - ``` diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsrepository.openpointintimefortype.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsrepository.openpointintimefortype.md index b33765bb79dd8..e0eb4bc603a2d 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectsrepository.openpointintimefortype.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsrepository.openpointintimefortype.md @@ -18,12 +18,12 @@ openPointInTimeForType(type: string | string[], { keepAlive, preference }?: Save | Parameter | Type | Description | | --- | --- | --- | -| type | string | string[] | | -| { keepAlive, preference } | SavedObjectsOpenPointInTimeOptions | | +| type | string \| string\[\] | | +| { keepAlive, preference } | SavedObjectsOpenPointInTimeOptions | | Returns: -`Promise` +Promise<SavedObjectsOpenPointInTimeResponse> {promise} - { id: string } @@ -50,6 +50,5 @@ const page2 = await savedObjectsClient.find({ searchAfter: lastHit.sort, }); await savedObjectsClient.closePointInTime(page2.pit_id); - ``` diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsrepository.removereferencesto.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsrepository.removereferencesto.md index ff05926360938..6691bf69e58dc 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectsrepository.removereferencesto.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsrepository.removereferencesto.md @@ -16,13 +16,13 @@ removeReferencesTo(type: string, id: string, options?: SavedObjectsRemoveReferen | Parameter | Type | Description | | --- | --- | --- | -| type | string | | -| id | string | | -| options | SavedObjectsRemoveReferencesToOptions | | +| type | string | | +| id | string | | +| options | SavedObjectsRemoveReferencesToOptions | | Returns: -`Promise` +Promise<SavedObjectsRemoveReferencesToResponse> ## Remarks diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsrepository.resolve.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsrepository.resolve.md index 7d0a1c7d204be..bf558eca975fd 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectsrepository.resolve.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsrepository.resolve.md @@ -16,13 +16,13 @@ resolve(type: string, id: string, options?: SavedObjectsBaseOptions | Parameter | Type | Description | | --- | --- | --- | -| type | string | | -| id | string | | -| options | SavedObjectsBaseOptions | | +| type | string | | +| id | string | | +| options | SavedObjectsBaseOptions | {string} \[options.namespace\] | Returns: -`Promise>` +Promise<SavedObjectsResolveResponse<T>> {promise} - { saved\_object, outcome } diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsrepository.update.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsrepository.update.md index d0d48b8938db8..681ba9eb3f014 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectsrepository.update.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsrepository.update.md @@ -16,14 +16,14 @@ update(type: string, id: string, attributes: Partial, options?: | Parameter | Type | Description | | --- | --- | --- | -| type | string | | -| id | string | | -| attributes | Partial<T> | | -| options | SavedObjectsUpdateOptions<T> | | +| type | string | | +| id | string | | +| attributes | Partial<T> | | +| options | SavedObjectsUpdateOptions<T> | {string} options.version - ensures version matches that of persisted object {string} \[options.namespace\] {array} \[options.references\] - \[{ name, type, id }\] | Returns: -`Promise>` +Promise<SavedObjectsUpdateResponse<T>> {promise} diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsrepository.updateobjectsspaces.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsrepository.updateobjectsspaces.md index 6914c1b46b829..c226e8d2d2b1d 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectsrepository.updateobjectsspaces.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsrepository.updateobjectsspaces.md @@ -16,12 +16,12 @@ updateObjectsSpaces(objects: SavedObjectsUpdateObjectsSpacesObject[], spacesToAd | Parameter | Type | Description | | --- | --- | --- | -| objects | SavedObjectsUpdateObjectsSpacesObject[] | | -| spacesToAdd | string[] | | -| spacesToRemove | string[] | | -| options | SavedObjectsUpdateObjectsSpacesOptions | | +| objects | SavedObjectsUpdateObjectsSpacesObject\[\] | | +| spacesToAdd | string\[\] | | +| spacesToRemove | string\[\] | | +| options | SavedObjectsUpdateObjectsSpacesOptions | | Returns: -`Promise` +Promise<import("./update\_objects\_spaces").SavedObjectsUpdateObjectsSpacesResponse> diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsrepositoryfactory.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsrepositoryfactory.md index dec768b68cd3a..72aa79ed4df29 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectsrepositoryfactory.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsrepositoryfactory.md @@ -16,6 +16,6 @@ export interface SavedObjectsRepositoryFactory | Property | Type | Description | | --- | --- | --- | -| [createInternalRepository](./kibana-plugin-core-server.savedobjectsrepositoryfactory.createinternalrepository.md) | (includedHiddenTypes?: string[]) => ISavedObjectsRepository | Creates a [Saved Objects repository](./kibana-plugin-core-server.isavedobjectsrepository.md) that uses the internal Kibana user for authenticating with Elasticsearch. | -| [createScopedRepository](./kibana-plugin-core-server.savedobjectsrepositoryfactory.createscopedrepository.md) | (req: KibanaRequest, includedHiddenTypes?: string[]) => ISavedObjectsRepository | Creates a [Saved Objects repository](./kibana-plugin-core-server.isavedobjectsrepository.md) that uses the credentials from the passed in request to authenticate with Elasticsearch. | +| [createInternalRepository](./kibana-plugin-core-server.savedobjectsrepositoryfactory.createinternalrepository.md) | (includedHiddenTypes?: string\[\]) => ISavedObjectsRepository | Creates a [Saved Objects repository](./kibana-plugin-core-server.isavedobjectsrepository.md) that uses the internal Kibana user for authenticating with Elasticsearch. | +| [createScopedRepository](./kibana-plugin-core-server.savedobjectsrepositoryfactory.createscopedrepository.md) | (req: KibanaRequest, includedHiddenTypes?: string\[\]) => ISavedObjectsRepository | Creates a [Saved Objects repository](./kibana-plugin-core-server.isavedobjectsrepository.md) that uses the credentials from the passed in request to authenticate with Elasticsearch. | diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsresolveimporterrorsoptions.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsresolveimporterrorsoptions.md index dcd2305c831f4..7a005db4334ba 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectsresolveimporterrorsoptions.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsresolveimporterrorsoptions.md @@ -16,8 +16,8 @@ export interface SavedObjectsResolveImportErrorsOptions | Property | Type | Description | | --- | --- | --- | -| [createNewCopies](./kibana-plugin-core-server.savedobjectsresolveimporterrorsoptions.createnewcopies.md) | boolean | If true, will create new copies of import objects, each with a random id and undefined originId. | -| [namespace](./kibana-plugin-core-server.savedobjectsresolveimporterrorsoptions.namespace.md) | string | if specified, will import in given namespace | -| [readStream](./kibana-plugin-core-server.savedobjectsresolveimporterrorsoptions.readstream.md) | Readable | The stream of [saved objects](./kibana-plugin-core-server.savedobject.md) to resolve errors from | -| [retries](./kibana-plugin-core-server.savedobjectsresolveimporterrorsoptions.retries.md) | SavedObjectsImportRetry[] | saved object import references to retry | +| [createNewCopies](./kibana-plugin-core-server.savedobjectsresolveimporterrorsoptions.createnewcopies.md) | boolean | If true, will create new copies of import objects, each with a random id and undefined originId. | +| [namespace?](./kibana-plugin-core-server.savedobjectsresolveimporterrorsoptions.namespace.md) | string | (Optional) if specified, will import in given namespace | +| [readStream](./kibana-plugin-core-server.savedobjectsresolveimporterrorsoptions.readstream.md) | Readable | The stream of [saved objects](./kibana-plugin-core-server.savedobject.md) to resolve errors from | +| [retries](./kibana-plugin-core-server.savedobjectsresolveimporterrorsoptions.retries.md) | SavedObjectsImportRetry\[\] | saved object import references to retry | diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsresolveresponse.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsresolveresponse.md index bbffd9902c0e7..1eab71a7d7e75 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectsresolveresponse.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsresolveresponse.md @@ -15,7 +15,7 @@ export interface SavedObjectsResolveResponse | Property | Type | Description | | --- | --- | --- | -| [alias\_target\_id](./kibana-plugin-core-server.savedobjectsresolveresponse.alias_target_id.md) | string | The ID of the object that the legacy URL alias points to. This is only defined when the outcome is 'aliasMatch' or 'conflict'. | -| [outcome](./kibana-plugin-core-server.savedobjectsresolveresponse.outcome.md) | 'exactMatch' | 'aliasMatch' | 'conflict' | The outcome for a successful resolve call is one of the following values:\* 'exactMatch' -- One document exactly matched the given ID. \* 'aliasMatch' -- One document with a legacy URL alias matched the given ID; in this case the saved_object.id field is different than the given ID. \* 'conflict' -- Two documents matched the given ID, one was an exact match and another with a legacy URL alias; in this case the saved_object object is the exact match, and the saved_object.id field is the same as the given ID. | -| [saved\_object](./kibana-plugin-core-server.savedobjectsresolveresponse.saved_object.md) | SavedObject<T> | The saved object that was found. | +| [alias\_target\_id?](./kibana-plugin-core-server.savedobjectsresolveresponse.alias_target_id.md) | string | (Optional) The ID of the object that the legacy URL alias points to. This is only defined when the outcome is 'aliasMatch' or 'conflict'. | +| [outcome](./kibana-plugin-core-server.savedobjectsresolveresponse.outcome.md) | 'exactMatch' \| 'aliasMatch' \| 'conflict' | The outcome for a successful resolve call is one of the following values:\* 'exactMatch' -- One document exactly matched the given ID. \* 'aliasMatch' -- One document with a legacy URL alias matched the given ID; in this case the saved_object.id field is different than the given ID. \* 'conflict' -- Two documents matched the given ID, one was an exact match and another with a legacy URL alias; in this case the saved_object object is the exact match, and the saved_object.id field is the same as the given ID. | +| [saved\_object](./kibana-plugin-core-server.savedobjectsresolveresponse.saved_object.md) | SavedObject<T> | The saved object that was found. | diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsserializer.generaterawid.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsserializer.generaterawid.md index a9dfd84cf0b42..6172a05d5c8fa 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectsserializer.generaterawid.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsserializer.generaterawid.md @@ -16,11 +16,11 @@ generateRawId(namespace: string | undefined, type: string, id: string): string; | Parameter | Type | Description | | --- | --- | --- | -| namespace | string | undefined | | -| type | string | | -| id | string | | +| namespace | string \| undefined | The namespace of the saved object | +| type | string | The saved object type | +| id | string | The id of the saved object | Returns: -`string` +string diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsserializer.generaterawlegacyurlaliasid.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsserializer.generaterawlegacyurlaliasid.md index d33f42ee2cf5f..a0465b96f05b5 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectsserializer.generaterawlegacyurlaliasid.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsserializer.generaterawlegacyurlaliasid.md @@ -16,11 +16,11 @@ generateRawLegacyUrlAliasId(namespace: string, type: string, id: string): string | Parameter | Type | Description | | --- | --- | --- | -| namespace | string | | -| type | string | | -| id | string | | +| namespace | string | The namespace of the saved object | +| type | string | The saved object type | +| id | string | The id of the saved object | Returns: -`string` +string diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsserializer.israwsavedobject.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsserializer.israwsavedobject.md index 1094cc25ab557..00963e353aa20 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectsserializer.israwsavedobject.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsserializer.israwsavedobject.md @@ -16,10 +16,10 @@ isRawSavedObject(doc: SavedObjectsRawDoc, options?: SavedObjectsRawDocParseOptio | Parameter | Type | Description | | --- | --- | --- | -| doc | SavedObjectsRawDoc | | -| options | SavedObjectsRawDocParseOptions | | +| doc | SavedObjectsRawDoc | The raw ES document to be tested | +| options | SavedObjectsRawDocParseOptions | Options for parsing the raw document. | Returns: -`boolean` +boolean diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsserializer.rawtosavedobject.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsserializer.rawtosavedobject.md index d71db9caf6a3b..9ac0ae0feee09 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectsserializer.rawtosavedobject.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsserializer.rawtosavedobject.md @@ -16,10 +16,10 @@ rawToSavedObject(doc: SavedObjectsRawDoc, options?: SavedObjectsRaw | Parameter | Type | Description | | --- | --- | --- | -| doc | SavedObjectsRawDoc | | -| options | SavedObjectsRawDocParseOptions | | +| doc | SavedObjectsRawDoc | The raw ES document to be converted to saved object format. | +| options | SavedObjectsRawDocParseOptions | Options for parsing the raw document. | Returns: -`SavedObjectSanitizedDoc` +SavedObjectSanitizedDoc<T> diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsserializer.savedobjecttoraw.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsserializer.savedobjecttoraw.md index 16d499a7b7b38..560011fc09638 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectsserializer.savedobjecttoraw.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsserializer.savedobjecttoraw.md @@ -16,9 +16,9 @@ savedObjectToRaw(savedObj: SavedObjectSanitizedDoc): SavedObjectsRawDoc; | Parameter | Type | Description | | --- | --- | --- | -| savedObj | SavedObjectSanitizedDoc | | +| savedObj | SavedObjectSanitizedDoc | The saved object to be converted to raw ES format. | Returns: -`SavedObjectsRawDoc` +SavedObjectsRawDoc diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsservicesetup.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsservicesetup.md index 336d9f63f0ced..28afaacce7ce6 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectsservicesetup.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsservicesetup.md @@ -29,7 +29,6 @@ export class Plugin() { }) } } - ``` ## Example 2 @@ -44,15 +43,14 @@ export class Plugin() { core.savedObjects.registerType(mySoType); } } - ``` ## Properties | Property | Type | Description | | --- | --- | --- | -| [addClientWrapper](./kibana-plugin-core-server.savedobjectsservicesetup.addclientwrapper.md) | (priority: number, id: string, factory: SavedObjectsClientWrapperFactory) => void | Add a [client wrapper factory](./kibana-plugin-core-server.savedobjectsclientwrapperfactory.md) with the given priority. | -| [getKibanaIndex](./kibana-plugin-core-server.savedobjectsservicesetup.getkibanaindex.md) | () => string | Returns the default index used for saved objects. | -| [registerType](./kibana-plugin-core-server.savedobjectsservicesetup.registertype.md) | <Attributes = any>(type: SavedObjectsType<Attributes>) => void | Register a [savedObjects type](./kibana-plugin-core-server.savedobjectstype.md) definition.See the [mappings format](./kibana-plugin-core-server.savedobjectstypemappingdefinition.md) and [migration format](./kibana-plugin-core-server.savedobjectmigrationmap.md) for more details about these. | -| [setClientFactoryProvider](./kibana-plugin-core-server.savedobjectsservicesetup.setclientfactoryprovider.md) | (clientFactoryProvider: SavedObjectsClientFactoryProvider) => void | Set the default [factory provider](./kibana-plugin-core-server.savedobjectsclientfactoryprovider.md) for creating Saved Objects clients. Only one provider can be set, subsequent calls to this method will fail. | +| [addClientWrapper](./kibana-plugin-core-server.savedobjectsservicesetup.addclientwrapper.md) | (priority: number, id: string, factory: SavedObjectsClientWrapperFactory) => void | Add a [client wrapper factory](./kibana-plugin-core-server.savedobjectsclientwrapperfactory.md) with the given priority. | +| [getKibanaIndex](./kibana-plugin-core-server.savedobjectsservicesetup.getkibanaindex.md) | () => string | Returns the default index used for saved objects. | +| [registerType](./kibana-plugin-core-server.savedobjectsservicesetup.registertype.md) | <Attributes = any>(type: SavedObjectsType<Attributes>) => void | Register a [savedObjects type](./kibana-plugin-core-server.savedobjectstype.md) definition.See the [mappings format](./kibana-plugin-core-server.savedobjectstypemappingdefinition.md) and [migration format](./kibana-plugin-core-server.savedobjectmigrationmap.md) for more details about these. | +| [setClientFactoryProvider](./kibana-plugin-core-server.savedobjectsservicesetup.setclientfactoryprovider.md) | (clientFactoryProvider: SavedObjectsClientFactoryProvider) => void | Set the default [factory provider](./kibana-plugin-core-server.savedobjectsclientfactoryprovider.md) for creating Saved Objects clients. Only one provider can be set, subsequent calls to this method will fail. | diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsservicesetup.registertype.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsservicesetup.registertype.md index 7f74ce4d7bea7..3085224fdaa6b 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectsservicesetup.registertype.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsservicesetup.registertype.md @@ -51,6 +51,5 @@ export class Plugin() { core.savedObjects.registerType(myType); } } - ``` diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsservicestart.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsservicestart.md index 075a363fe1aa2..ae7480ab1e65b 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectsservicestart.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsservicestart.md @@ -16,11 +16,11 @@ export interface SavedObjectsServiceStart | Property | Type | Description | | --- | --- | --- | -| [createExporter](./kibana-plugin-core-server.savedobjectsservicestart.createexporter.md) | (client: SavedObjectsClientContract) => ISavedObjectsExporter | Creates an [exporter](./kibana-plugin-core-server.isavedobjectsexporter.md) bound to given client. | -| [createImporter](./kibana-plugin-core-server.savedobjectsservicestart.createimporter.md) | (client: SavedObjectsClientContract) => ISavedObjectsImporter | Creates an [importer](./kibana-plugin-core-server.isavedobjectsimporter.md) bound to given client. | -| [createInternalRepository](./kibana-plugin-core-server.savedobjectsservicestart.createinternalrepository.md) | (includedHiddenTypes?: string[]) => ISavedObjectsRepository | Creates a [Saved Objects repository](./kibana-plugin-core-server.isavedobjectsrepository.md) that uses the internal Kibana user for authenticating with Elasticsearch. | -| [createScopedRepository](./kibana-plugin-core-server.savedobjectsservicestart.createscopedrepository.md) | (req: KibanaRequest, includedHiddenTypes?: string[]) => ISavedObjectsRepository | Creates a [Saved Objects repository](./kibana-plugin-core-server.isavedobjectsrepository.md) that uses the credentials from the passed in request to authenticate with Elasticsearch. | -| [createSerializer](./kibana-plugin-core-server.savedobjectsservicestart.createserializer.md) | () => SavedObjectsSerializer | Creates a [serializer](./kibana-plugin-core-server.savedobjectsserializer.md) that is aware of all registered types. | -| [getScopedClient](./kibana-plugin-core-server.savedobjectsservicestart.getscopedclient.md) | (req: KibanaRequest, options?: SavedObjectsClientProviderOptions) => SavedObjectsClientContract | Creates a [Saved Objects client](./kibana-plugin-core-server.savedobjectsclientcontract.md) that uses the credentials from the passed in request to authenticate with Elasticsearch. If other plugins have registered Saved Objects client wrappers, these will be applied to extend the functionality of the client.A client that is already scoped to the incoming request is also exposed from the route handler context see [RequestHandlerContext](./kibana-plugin-core-server.requesthandlercontext.md). | -| [getTypeRegistry](./kibana-plugin-core-server.savedobjectsservicestart.gettyperegistry.md) | () => ISavedObjectTypeRegistry | Returns the [registry](./kibana-plugin-core-server.isavedobjecttyperegistry.md) containing all registered [saved object types](./kibana-plugin-core-server.savedobjectstype.md) | +| [createExporter](./kibana-plugin-core-server.savedobjectsservicestart.createexporter.md) | (client: SavedObjectsClientContract) => ISavedObjectsExporter | Creates an [exporter](./kibana-plugin-core-server.isavedobjectsexporter.md) bound to given client. | +| [createImporter](./kibana-plugin-core-server.savedobjectsservicestart.createimporter.md) | (client: SavedObjectsClientContract) => ISavedObjectsImporter | Creates an [importer](./kibana-plugin-core-server.isavedobjectsimporter.md) bound to given client. | +| [createInternalRepository](./kibana-plugin-core-server.savedobjectsservicestart.createinternalrepository.md) | (includedHiddenTypes?: string\[\]) => ISavedObjectsRepository | Creates a [Saved Objects repository](./kibana-plugin-core-server.isavedobjectsrepository.md) that uses the internal Kibana user for authenticating with Elasticsearch. | +| [createScopedRepository](./kibana-plugin-core-server.savedobjectsservicestart.createscopedrepository.md) | (req: KibanaRequest, includedHiddenTypes?: string\[\]) => ISavedObjectsRepository | Creates a [Saved Objects repository](./kibana-plugin-core-server.isavedobjectsrepository.md) that uses the credentials from the passed in request to authenticate with Elasticsearch. | +| [createSerializer](./kibana-plugin-core-server.savedobjectsservicestart.createserializer.md) | () => SavedObjectsSerializer | Creates a [serializer](./kibana-plugin-core-server.savedobjectsserializer.md) that is aware of all registered types. | +| [getScopedClient](./kibana-plugin-core-server.savedobjectsservicestart.getscopedclient.md) | (req: KibanaRequest, options?: SavedObjectsClientProviderOptions) => SavedObjectsClientContract | Creates a [Saved Objects client](./kibana-plugin-core-server.savedobjectsclientcontract.md) that uses the credentials from the passed in request to authenticate with Elasticsearch. If other plugins have registered Saved Objects client wrappers, these will be applied to extend the functionality of the client.A client that is already scoped to the incoming request is also exposed from the route handler context see [RequestHandlerContext](./kibana-plugin-core-server.requesthandlercontext.md). | +| [getTypeRegistry](./kibana-plugin-core-server.savedobjectsservicestart.gettyperegistry.md) | () => ISavedObjectTypeRegistry | Returns the [registry](./kibana-plugin-core-server.isavedobjecttyperegistry.md) containing all registered [saved object types](./kibana-plugin-core-server.savedobjectstype.md) | diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectstatusmeta.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectstatusmeta.md index 3a0b23d18632f..890ed36535b3f 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectstatusmeta.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectstatusmeta.md @@ -16,5 +16,5 @@ export interface SavedObjectStatusMeta | Property | Type | Description | | --- | --- | --- | -| [migratedIndices](./kibana-plugin-core-server.savedobjectstatusmeta.migratedindices.md) | {
[status: string]: number;
skipped: number;
migrated: number;
} | | +| [migratedIndices](./kibana-plugin-core-server.savedobjectstatusmeta.migratedindices.md) | { \[status: string\]: number; skipped: number; migrated: number; } | | diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectstype.converttomultinamespacetypeversion.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectstype.converttomultinamespacetypeversion.md index 20346919fc652..a3fac34153633 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectstype.converttomultinamespacetypeversion.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectstype.converttomultinamespacetypeversion.md @@ -19,7 +19,6 @@ Example of a single-namespace type in 7.12: namespaceType: 'single', mappings: {...} } - ``` Example after converting to a multi-namespace (isolated) type in 8.0: @@ -31,7 +30,6 @@ Example after converting to a multi-namespace (isolated) type in 8.0: mappings: {...}, convertToMultiNamespaceTypeVersion: '8.0.0' } - ``` Example after converting to a multi-namespace (shareable) type in 8.1: @@ -43,7 +41,6 @@ Example after converting to a multi-namespace (shareable) type in 8.1: mappings: {...}, convertToMultiNamespaceTypeVersion: '8.0.0' } - ``` Note: migration function(s) can be optionally specified for any of these versions and will not interfere with the conversion process. diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectstype.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectstype.md index bdb90be2bc8fc..3c76a898d06f6 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectstype.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectstype.md @@ -18,8 +18,8 @@ This is only internal for now, and will only be public when we expose the regist | Property | Type | Description | | --- | --- | --- | -| [convertToAliasScript](./kibana-plugin-core-server.savedobjectstype.converttoaliasscript.md) | string | If defined, will be used to convert the type to an alias. | -| [convertToMultiNamespaceTypeVersion](./kibana-plugin-core-server.savedobjectstype.converttomultinamespacetypeversion.md) | string | If defined, objects of this type will be converted to a 'multiple' or 'multiple-isolated' namespace type when migrating to this version.Requirements:1. This string value must be a valid semver version 2. This type must have previously specified [\`namespaceType: 'single'\`](./kibana-plugin-core-server.savedobjectsnamespacetype.md) 3. This type must also specify [\`namespaceType: 'multiple'\`](./kibana-plugin-core-server.savedobjectsnamespacetype.md) \*or\* [\`namespaceType: 'multiple-isolated'\`](./kibana-plugin-core-server.savedobjectsnamespacetype.md)Example of a single-namespace type in 7.12: +| [convertToAliasScript?](./kibana-plugin-core-server.savedobjectstype.converttoaliasscript.md) | string | (Optional) If defined, will be used to convert the type to an alias. | +| [convertToMultiNamespaceTypeVersion?](./kibana-plugin-core-server.savedobjectstype.converttomultinamespacetypeversion.md) | string | (Optional) If defined, objects of this type will be converted to a 'multiple' or 'multiple-isolated' namespace type when migrating to this version.Requirements:1. This string value must be a valid semver version 2. This type must have previously specified [\`namespaceType: 'single'\`](./kibana-plugin-core-server.savedobjectsnamespacetype.md) 3. This type must also specify [\`namespaceType: 'multiple'\`](./kibana-plugin-core-server.savedobjectsnamespacetype.md) \*or\* [\`namespaceType: 'multiple-isolated'\`](./kibana-plugin-core-server.savedobjectsnamespacetype.md)Example of a single-namespace type in 7.12: ```ts { name: 'foo', @@ -27,7 +27,6 @@ This is only internal for now, and will only be public when we expose the regist namespaceType: 'single', mappings: {...} } - ``` Example after converting to a multi-namespace (isolated) type in 8.0: ```ts @@ -38,7 +37,6 @@ Example after converting to a multi-namespace (isolated) type in 8.0: mappings: {...}, convertToMultiNamespaceTypeVersion: '8.0.0' } - ``` Example after converting to a multi-namespace (shareable) type in 8.1: ```ts @@ -49,15 +47,14 @@ Example after converting to a multi-namespace (shareable) type in 8.1: mappings: {...}, convertToMultiNamespaceTypeVersion: '8.0.0' } - ``` Note: migration function(s) can be optionally specified for any of these versions and will not interfere with the conversion process. | -| [excludeOnUpgrade](./kibana-plugin-core-server.savedobjectstype.excludeonupgrade.md) | SavedObjectTypeExcludeFromUpgradeFilterHook | If defined, allows a type to exclude unneeded documents from the migration process and effectively be deleted. See [SavedObjectTypeExcludeFromUpgradeFilterHook](./kibana-plugin-core-server.savedobjecttypeexcludefromupgradefilterhook.md) for more details. | -| [hidden](./kibana-plugin-core-server.savedobjectstype.hidden.md) | boolean | Is the type hidden by default. If true, repositories will not have access to this type unless explicitly declared as an extraType when creating the repository.See [createInternalRepository](./kibana-plugin-core-server.savedobjectsservicestart.createinternalrepository.md). | -| [indexPattern](./kibana-plugin-core-server.savedobjectstype.indexpattern.md) | string | If defined, the type instances will be stored in the given index instead of the default one. | -| [management](./kibana-plugin-core-server.savedobjectstype.management.md) | SavedObjectsTypeManagementDefinition<Attributes> | An optional [saved objects management section](./kibana-plugin-core-server.savedobjectstypemanagementdefinition.md) definition for the type. | -| [mappings](./kibana-plugin-core-server.savedobjectstype.mappings.md) | SavedObjectsTypeMappingDefinition | The [mapping definition](./kibana-plugin-core-server.savedobjectstypemappingdefinition.md) for the type. | -| [migrations](./kibana-plugin-core-server.savedobjectstype.migrations.md) | SavedObjectMigrationMap | (() => SavedObjectMigrationMap) | An optional map of [migrations](./kibana-plugin-core-server.savedobjectmigrationfn.md) or a function returning a map of [migrations](./kibana-plugin-core-server.savedobjectmigrationfn.md) to be used to migrate the type. | -| [name](./kibana-plugin-core-server.savedobjectstype.name.md) | string | The name of the type, which is also used as the internal id. | -| [namespaceType](./kibana-plugin-core-server.savedobjectstype.namespacetype.md) | SavedObjectsNamespaceType | The [namespace type](./kibana-plugin-core-server.savedobjectsnamespacetype.md) for the type. | +| [excludeOnUpgrade?](./kibana-plugin-core-server.savedobjectstype.excludeonupgrade.md) | SavedObjectTypeExcludeFromUpgradeFilterHook | (Optional) If defined, allows a type to exclude unneeded documents from the migration process and effectively be deleted. See [SavedObjectTypeExcludeFromUpgradeFilterHook](./kibana-plugin-core-server.savedobjecttypeexcludefromupgradefilterhook.md) for more details. | +| [hidden](./kibana-plugin-core-server.savedobjectstype.hidden.md) | boolean | Is the type hidden by default. If true, repositories will not have access to this type unless explicitly declared as an extraType when creating the repository.See [createInternalRepository](./kibana-plugin-core-server.savedobjectsservicestart.createinternalrepository.md). | +| [indexPattern?](./kibana-plugin-core-server.savedobjectstype.indexpattern.md) | string | (Optional) If defined, the type instances will be stored in the given index instead of the default one. | +| [management?](./kibana-plugin-core-server.savedobjectstype.management.md) | SavedObjectsTypeManagementDefinition<Attributes> | (Optional) An optional [saved objects management section](./kibana-plugin-core-server.savedobjectstypemanagementdefinition.md) definition for the type. | +| [mappings](./kibana-plugin-core-server.savedobjectstype.mappings.md) | SavedObjectsTypeMappingDefinition | The [mapping definition](./kibana-plugin-core-server.savedobjectstypemappingdefinition.md) for the type. | +| [migrations?](./kibana-plugin-core-server.savedobjectstype.migrations.md) | SavedObjectMigrationMap \| (() => SavedObjectMigrationMap) | (Optional) An optional map of [migrations](./kibana-plugin-core-server.savedobjectmigrationfn.md) or a function returning a map of [migrations](./kibana-plugin-core-server.savedobjectmigrationfn.md) to be used to migrate the type. | +| [name](./kibana-plugin-core-server.savedobjectstype.name.md) | string | The name of the type, which is also used as the internal id. | +| [namespaceType](./kibana-plugin-core-server.savedobjectstype.namespacetype.md) | SavedObjectsNamespaceType | The [namespace type](./kibana-plugin-core-server.savedobjectsnamespacetype.md) for the type. | diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectstypemanagementdefinition.isexportable.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectstypemanagementdefinition.isexportable.md index fef178e1d9847..c6dff60610990 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectstypemanagementdefinition.isexportable.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectstypemanagementdefinition.isexportable.md @@ -44,6 +44,5 @@ export class Plugin() { }); } } - ``` diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectstypemanagementdefinition.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectstypemanagementdefinition.md index 057eb6284bf9e..eeda40cd59664 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectstypemanagementdefinition.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectstypemanagementdefinition.md @@ -16,15 +16,15 @@ export interface SavedObjectsTypeManagementDefinition | Property | Type | Description | | --- | --- | --- | -| [defaultSearchField](./kibana-plugin-core-server.savedobjectstypemanagementdefinition.defaultsearchfield.md) | string | The default search field to use for this type. Defaults to id. | -| [displayName](./kibana-plugin-core-server.savedobjectstypemanagementdefinition.displayname.md) | string | When specified, will be used instead of the type's name in SO management section's labels. | -| [getEditUrl](./kibana-plugin-core-server.savedobjectstypemanagementdefinition.getediturl.md) | (savedObject: SavedObject<Attributes>) => string | Function returning the url to use to redirect to the editing page of this object. If not defined, editing will not be allowed. | -| [getInAppUrl](./kibana-plugin-core-server.savedobjectstypemanagementdefinition.getinappurl.md) | (savedObject: SavedObject<Attributes>) => {
path: string;
uiCapabilitiesPath: string;
} | Function returning the url to use to redirect to this object from the management section. If not defined, redirecting to the object will not be allowed. | -| [getTitle](./kibana-plugin-core-server.savedobjectstypemanagementdefinition.gettitle.md) | (savedObject: SavedObject<Attributes>) => string | Function returning the title to display in the management table. If not defined, will use the object's type and id to generate a label. | -| [icon](./kibana-plugin-core-server.savedobjectstypemanagementdefinition.icon.md) | string | The eui icon name to display in the management table. If not defined, the default icon will be used. | -| [importableAndExportable](./kibana-plugin-core-server.savedobjectstypemanagementdefinition.importableandexportable.md) | boolean | Is the type importable or exportable. Defaults to false. | -| [isExportable](./kibana-plugin-core-server.savedobjectstypemanagementdefinition.isexportable.md) | SavedObjectsExportablePredicate<Attributes> | Optional hook to specify whether an object should be exportable.If specified, isExportable will be called during export for each of this type's objects in the export, and the ones not matching the predicate will be excluded from the export.When implementing both isExportable and onExport, it is mandatory that isExportable returns the same value for an object before and after going though the export transform. E.g isExportable(objectBeforeTransform) === isExportable(objectAfterTransform) | -| [onExport](./kibana-plugin-core-server.savedobjectstypemanagementdefinition.onexport.md) | SavedObjectsExportTransform<Attributes> | An optional export transform function that can be used transform the objects of the registered type during the export process.It can be used to either mutate the exported objects, or add additional objects (of any type) to the export list.See [the transform type documentation](./kibana-plugin-core-server.savedobjectsexporttransform.md) for more info and examples.When implementing both isExportable and onExport, it is mandatory that isExportable returns the same value for an object before and after going though the export transform. E.g isExportable(objectBeforeTransform) === isExportable(objectAfterTransform) | -| [onImport](./kibana-plugin-core-server.savedobjectstypemanagementdefinition.onimport.md) | SavedObjectsImportHook<Attributes> | An optional [import hook](./kibana-plugin-core-server.savedobjectsimporthook.md) to use when importing given type.Import hooks are executed during the savedObjects import process and allow to interact with the imported objects. See the [hook documentation](./kibana-plugin-core-server.savedobjectsimporthook.md) for more info. | -| [visibleInManagement](./kibana-plugin-core-server.savedobjectstypemanagementdefinition.visibleinmanagement.md) | boolean | When set to false, the type will not be listed or searchable in the SO management section. Main usage of setting this property to false for a type is when objects from the type should be included in the export via references or export hooks, but should not directly appear in the SOM. Defaults to true. | +| [defaultSearchField?](./kibana-plugin-core-server.savedobjectstypemanagementdefinition.defaultsearchfield.md) | string | (Optional) The default search field to use for this type. Defaults to id. | +| [displayName?](./kibana-plugin-core-server.savedobjectstypemanagementdefinition.displayname.md) | string | (Optional) When specified, will be used instead of the type's name in SO management section's labels. | +| [getEditUrl?](./kibana-plugin-core-server.savedobjectstypemanagementdefinition.getediturl.md) | (savedObject: SavedObject<Attributes>) => string | (Optional) Function returning the url to use to redirect to the editing page of this object. If not defined, editing will not be allowed. | +| [getInAppUrl?](./kibana-plugin-core-server.savedobjectstypemanagementdefinition.getinappurl.md) | (savedObject: SavedObject<Attributes>) => { path: string; uiCapabilitiesPath: string; } | (Optional) Function returning the url to use to redirect to this object from the management section. If not defined, redirecting to the object will not be allowed. | +| [getTitle?](./kibana-plugin-core-server.savedobjectstypemanagementdefinition.gettitle.md) | (savedObject: SavedObject<Attributes>) => string | (Optional) Function returning the title to display in the management table. If not defined, will use the object's type and id to generate a label. | +| [icon?](./kibana-plugin-core-server.savedobjectstypemanagementdefinition.icon.md) | string | (Optional) The eui icon name to display in the management table. If not defined, the default icon will be used. | +| [importableAndExportable?](./kibana-plugin-core-server.savedobjectstypemanagementdefinition.importableandexportable.md) | boolean | (Optional) Is the type importable or exportable. Defaults to false. | +| [isExportable?](./kibana-plugin-core-server.savedobjectstypemanagementdefinition.isexportable.md) | SavedObjectsExportablePredicate<Attributes> | (Optional) Optional hook to specify whether an object should be exportable.If specified, isExportable will be called during export for each of this type's objects in the export, and the ones not matching the predicate will be excluded from the export.When implementing both isExportable and onExport, it is mandatory that isExportable returns the same value for an object before and after going though the export transform. E.g isExportable(objectBeforeTransform) === isExportable(objectAfterTransform) | +| [onExport?](./kibana-plugin-core-server.savedobjectstypemanagementdefinition.onexport.md) | SavedObjectsExportTransform<Attributes> | (Optional) An optional export transform function that can be used transform the objects of the registered type during the export process.It can be used to either mutate the exported objects, or add additional objects (of any type) to the export list.See [the transform type documentation](./kibana-plugin-core-server.savedobjectsexporttransform.md) for more info and examples.When implementing both isExportable and onExport, it is mandatory that isExportable returns the same value for an object before and after going though the export transform. E.g isExportable(objectBeforeTransform) === isExportable(objectAfterTransform) | +| [onImport?](./kibana-plugin-core-server.savedobjectstypemanagementdefinition.onimport.md) | SavedObjectsImportHook<Attributes> | (Optional) An optional [import hook](./kibana-plugin-core-server.savedobjectsimporthook.md) to use when importing given type.Import hooks are executed during the savedObjects import process and allow to interact with the imported objects. See the [hook documentation](./kibana-plugin-core-server.savedobjectsimporthook.md) for more info. | +| [visibleInManagement?](./kibana-plugin-core-server.savedobjectstypemanagementdefinition.visibleinmanagement.md) | boolean | (Optional) When set to false, the type will not be listed or searchable in the SO management section. Main usage of setting this property to false for a type is when objects from the type should be included in the export via references or export hooks, but should not directly appear in the SOM. Defaults to true. | diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectstypemanagementdefinition.onimport.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectstypemanagementdefinition.onimport.md index 332247b8eb8e1..c54570d79a7e2 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectstypemanagementdefinition.onimport.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectstypemanagementdefinition.onimport.md @@ -50,6 +50,5 @@ export class Plugin() { }); } } - ``` diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectstypemappingdefinition.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectstypemappingdefinition.md index 3d3b73880fa7f..7f4c82c23e2ca 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectstypemappingdefinition.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectstypemappingdefinition.md @@ -34,13 +34,12 @@ const typeDefinition: SavedObjectsTypeMappingDefinition = { }, } } - ``` ## Properties | Property | Type | Description | | --- | --- | --- | -| [dynamic](./kibana-plugin-core-server.savedobjectstypemappingdefinition.dynamic.md) | false | 'strict' | The dynamic property of the mapping, either false or 'strict'. If unspecified dynamic: 'strict' will be inherited from the top-level index mappings. | -| [properties](./kibana-plugin-core-server.savedobjectstypemappingdefinition.properties.md) | SavedObjectsMappingProperties | The underlying properties of the type mapping | +| [dynamic?](./kibana-plugin-core-server.savedobjectstypemappingdefinition.dynamic.md) | false \| 'strict' | (Optional) The dynamic property of the mapping, either false or 'strict'. If unspecified dynamic: 'strict' will be inherited from the top-level index mappings. | +| [properties](./kibana-plugin-core-server.savedobjectstypemappingdefinition.properties.md) | SavedObjectsMappingProperties | The underlying properties of the type mapping | diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsupdateobjectsspacesobject.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsupdateobjectsspacesobject.md index 847e40a8896b4..6fa04623c96a6 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectsupdateobjectsspacesobject.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsupdateobjectsspacesobject.md @@ -16,6 +16,6 @@ export interface SavedObjectsUpdateObjectsSpacesObject | Property | Type | Description | | --- | --- | --- | -| [id](./kibana-plugin-core-server.savedobjectsupdateobjectsspacesobject.id.md) | string | The type of the object to update | -| [type](./kibana-plugin-core-server.savedobjectsupdateobjectsspacesobject.type.md) | string | The ID of the object to update | +| [id](./kibana-plugin-core-server.savedobjectsupdateobjectsspacesobject.id.md) | string | The type of the object to update | +| [type](./kibana-plugin-core-server.savedobjectsupdateobjectsspacesobject.type.md) | string | The ID of the object to update | diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsupdateobjectsspacesoptions.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsupdateobjectsspacesoptions.md index 49ee013c5d2da..b8f17699b1841 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectsupdateobjectsspacesoptions.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsupdateobjectsspacesoptions.md @@ -11,10 +11,11 @@ Options for the update operation. ```typescript export interface SavedObjectsUpdateObjectsSpacesOptions extends SavedObjectsBaseOptions ``` +Extends: SavedObjectsBaseOptions ## Properties | Property | Type | Description | | --- | --- | --- | -| [refresh](./kibana-plugin-core-server.savedobjectsupdateobjectsspacesoptions.refresh.md) | MutatingOperationRefreshSetting | The Elasticsearch Refresh setting for this operation | +| [refresh?](./kibana-plugin-core-server.savedobjectsupdateobjectsspacesoptions.refresh.md) | MutatingOperationRefreshSetting | (Optional) The Elasticsearch Refresh setting for this operation | diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsupdateobjectsspacesresponse.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsupdateobjectsspacesresponse.md index bf53277887bda..aff67e0c54e66 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectsupdateobjectsspacesresponse.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsupdateobjectsspacesresponse.md @@ -16,5 +16,5 @@ export interface SavedObjectsUpdateObjectsSpacesResponse | Property | Type | Description | | --- | --- | --- | -| [objects](./kibana-plugin-core-server.savedobjectsupdateobjectsspacesresponse.objects.md) | SavedObjectsUpdateObjectsSpacesResponseObject[] | | +| [objects](./kibana-plugin-core-server.savedobjectsupdateobjectsspacesresponse.objects.md) | SavedObjectsUpdateObjectsSpacesResponseObject\[\] | | diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsupdateobjectsspacesresponseobject.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsupdateobjectsspacesresponseobject.md index 03802278ee5a3..5078473e9e6bd 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectsupdateobjectsspacesresponseobject.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsupdateobjectsspacesresponseobject.md @@ -16,8 +16,8 @@ export interface SavedObjectsUpdateObjectsSpacesResponseObject | Property | Type | Description | | --- | --- | --- | -| [error](./kibana-plugin-core-server.savedobjectsupdateobjectsspacesresponseobject.error.md) | SavedObjectError | Included if there was an error updating this object's spaces | -| [id](./kibana-plugin-core-server.savedobjectsupdateobjectsspacesresponseobject.id.md) | string | The ID of the referenced object | -| [spaces](./kibana-plugin-core-server.savedobjectsupdateobjectsspacesresponseobject.spaces.md) | string[] | The space(s) that the referenced object exists in | -| [type](./kibana-plugin-core-server.savedobjectsupdateobjectsspacesresponseobject.type.md) | string | The type of the referenced object | +| [error?](./kibana-plugin-core-server.savedobjectsupdateobjectsspacesresponseobject.error.md) | SavedObjectError | (Optional) Included if there was an error updating this object's spaces | +| [id](./kibana-plugin-core-server.savedobjectsupdateobjectsspacesresponseobject.id.md) | string | The ID of the referenced object | +| [spaces](./kibana-plugin-core-server.savedobjectsupdateobjectsspacesresponseobject.spaces.md) | string\[\] | The space(s) that the referenced object exists in | +| [type](./kibana-plugin-core-server.savedobjectsupdateobjectsspacesresponseobject.type.md) | string | The type of the referenced object | diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsupdateoptions.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsupdateoptions.md index 3111c1c8e65f1..b81a59c745e7b 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectsupdateoptions.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsupdateoptions.md @@ -10,13 +10,14 @@ ```typescript export interface SavedObjectsUpdateOptions extends SavedObjectsBaseOptions ``` +Extends: SavedObjectsBaseOptions ## Properties | Property | Type | Description | | --- | --- | --- | -| [references](./kibana-plugin-core-server.savedobjectsupdateoptions.references.md) | SavedObjectReference[] | A reference to another saved object. | -| [refresh](./kibana-plugin-core-server.savedobjectsupdateoptions.refresh.md) | MutatingOperationRefreshSetting | The Elasticsearch Refresh setting for this operation | -| [upsert](./kibana-plugin-core-server.savedobjectsupdateoptions.upsert.md) | Attributes | If specified, will be used to perform an upsert if the document doesn't exist | -| [version](./kibana-plugin-core-server.savedobjectsupdateoptions.version.md) | string | An opaque version number which changes on each successful write operation. Can be used for implementing optimistic concurrency control. | +| [references?](./kibana-plugin-core-server.savedobjectsupdateoptions.references.md) | SavedObjectReference\[\] | (Optional) A reference to another saved object. | +| [refresh?](./kibana-plugin-core-server.savedobjectsupdateoptions.refresh.md) | MutatingOperationRefreshSetting | (Optional) The Elasticsearch Refresh setting for this operation | +| [upsert?](./kibana-plugin-core-server.savedobjectsupdateoptions.upsert.md) | Attributes | (Optional) If specified, will be used to perform an upsert if the document doesn't exist | +| [version?](./kibana-plugin-core-server.savedobjectsupdateoptions.version.md) | string | (Optional) An opaque version number which changes on each successful write operation. Can be used for implementing optimistic concurrency control. | diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsupdateresponse.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsupdateresponse.md index d8130830eb1f7..5c773d92c6364 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectsupdateresponse.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsupdateresponse.md @@ -10,11 +10,12 @@ ```typescript export interface SavedObjectsUpdateResponse extends Omit, 'attributes' | 'references'> ``` +Extends: Omit<SavedObject<T>, 'attributes' \| 'references'> ## Properties | Property | Type | Description | | --- | --- | --- | -| [attributes](./kibana-plugin-core-server.savedobjectsupdateresponse.attributes.md) | Partial<T> | | -| [references](./kibana-plugin-core-server.savedobjectsupdateresponse.references.md) | SavedObjectReference[] | undefined | | +| [attributes](./kibana-plugin-core-server.savedobjectsupdateresponse.attributes.md) | Partial<T> | | +| [references](./kibana-plugin-core-server.savedobjectsupdateresponse.references.md) | SavedObjectReference\[\] \| undefined | | diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsutils.generateid.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsutils.generateid.md index f095184484992..887f2fb5d9fe1 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectsutils.generateid.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsutils.generateid.md @@ -13,5 +13,5 @@ static generateId(): string; ``` Returns: -`string` +string diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsutils.getconvertedobjectid.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsutils.getconvertedobjectid.md index c6a429d345ed1..502d9dcab8cf7 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectsutils.getconvertedobjectid.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsutils.getconvertedobjectid.md @@ -16,13 +16,13 @@ static getConvertedObjectId(namespace: string | undefined, type: string, id: str | Parameter | Type | Description | | --- | --- | --- | -| namespace | string | undefined | | -| type | string | | -| id | string | | +| namespace | string \| undefined | The namespace of the saved object before it is converted. | +| type | string | The type of the saved object before it is converted. | +| id | string | The ID of the saved object before it is converted. | Returns: -`string` +string {string} The ID of the saved object after it is converted. diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsutils.israndomid.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsutils.israndomid.md index 5a44321ee060f..75db00c449654 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectsutils.israndomid.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsutils.israndomid.md @@ -16,9 +16,9 @@ static isRandomId(id: string | undefined): boolean; | Parameter | Type | Description | | --- | --- | --- | -| id | string | undefined | | +| id | string \| undefined | The ID of a saved object. Use uuid.validate once upgraded to v5.3+ | Returns: -`boolean` +boolean diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsutils.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsutils.md index ab6382aca6a52..9a8c5cf9889b2 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectsutils.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsutils.md @@ -15,9 +15,9 @@ export declare class SavedObjectsUtils | Property | Modifiers | Type | Description | | --- | --- | --- | --- | -| [createEmptyFindResponse](./kibana-plugin-core-server.savedobjectsutils.createemptyfindresponse.md) | static | <T, A>({ page, perPage, }: SavedObjectsFindOptions) => SavedObjectsFindResponse<T, A> | Creates an empty response for a find operation. This is only intended to be used by saved objects client wrappers. | -| [namespaceIdToString](./kibana-plugin-core-server.savedobjectsutils.namespaceidtostring.md) | static | (namespace?: string | undefined) => string | Converts a given saved object namespace ID to its string representation. All namespace IDs have an identical string representation, with the exception of the undefined namespace ID (which has a namespace string of 'default'). | -| [namespaceStringToId](./kibana-plugin-core-server.savedobjectsutils.namespacestringtoid.md) | static | (namespace: string) => string | undefined | Converts a given saved object namespace string to its ID representation. All namespace strings have an identical ID representation, with the exception of the 'default' namespace string (which has a namespace ID of undefined). | +| [createEmptyFindResponse](./kibana-plugin-core-server.savedobjectsutils.createemptyfindresponse.md) | static | <T, A>({ page, perPage, }: SavedObjectsFindOptions) => SavedObjectsFindResponse<T, A> | Creates an empty response for a find operation. This is only intended to be used by saved objects client wrappers. | +| [namespaceIdToString](./kibana-plugin-core-server.savedobjectsutils.namespaceidtostring.md) | static | (namespace?: string \| undefined) => string | Converts a given saved object namespace ID to its string representation. All namespace IDs have an identical string representation, with the exception of the undefined namespace ID (which has a namespace string of 'default'). | +| [namespaceStringToId](./kibana-plugin-core-server.savedobjectsutils.namespacestringtoid.md) | static | (namespace: string) => string \| undefined | Converts a given saved object namespace string to its ID representation. All namespace strings have an identical ID representation, with the exception of the 'default' namespace string (which has a namespace ID of undefined). | ## Methods diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjecttyperegistry.getalltypes.md b/docs/development/core/server/kibana-plugin-core-server.savedobjecttyperegistry.getalltypes.md index 20d631ff74aca..7e4733f892955 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjecttyperegistry.getalltypes.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjecttyperegistry.getalltypes.md @@ -15,5 +15,5 @@ getAllTypes(): SavedObjectsType[]; ``` Returns: -`SavedObjectsType[]` +SavedObjectsType<any>\[\] diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjecttyperegistry.getimportableandexportabletypes.md b/docs/development/core/server/kibana-plugin-core-server.savedobjecttyperegistry.getimportableandexportabletypes.md index 1e29e632a6ec3..a20360128406a 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjecttyperegistry.getimportableandexportabletypes.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjecttyperegistry.getimportableandexportabletypes.md @@ -13,5 +13,5 @@ getImportableAndExportableTypes(): SavedObjectsType[]; ``` Returns: -`SavedObjectsType[]` +SavedObjectsType<any>\[\] diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjecttyperegistry.getindex.md b/docs/development/core/server/kibana-plugin-core-server.savedobjecttyperegistry.getindex.md index dca43c48ec46d..9da28c7f01278 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjecttyperegistry.getindex.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjecttyperegistry.getindex.md @@ -16,9 +16,9 @@ getIndex(type: string): string | undefined; | Parameter | Type | Description | | --- | --- | --- | -| type | string | | +| type | string | | Returns: -`string | undefined` +string \| undefined diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjecttyperegistry.gettype.md b/docs/development/core/server/kibana-plugin-core-server.savedobjecttyperegistry.gettype.md index 160aadb73cced..d6fc255958c8c 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjecttyperegistry.gettype.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjecttyperegistry.gettype.md @@ -16,9 +16,9 @@ getType(type: string): SavedObjectsType | undefined; | Parameter | Type | Description | | --- | --- | --- | -| type | string | | +| type | string | | Returns: -`SavedObjectsType | undefined` +SavedObjectsType<any> \| undefined diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjecttyperegistry.getvisibletypes.md b/docs/development/core/server/kibana-plugin-core-server.savedobjecttyperegistry.getvisibletypes.md index 05f22dcf7010b..9588e77e646fc 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjecttyperegistry.getvisibletypes.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjecttyperegistry.getvisibletypes.md @@ -15,5 +15,5 @@ getVisibleTypes(): SavedObjectsType[]; ``` Returns: -`SavedObjectsType[]` +SavedObjectsType<any>\[\] diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjecttyperegistry.ishidden.md b/docs/development/core/server/kibana-plugin-core-server.savedobjecttyperegistry.ishidden.md index aa19fa9b4364c..2d29e753218d7 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjecttyperegistry.ishidden.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjecttyperegistry.ishidden.md @@ -16,9 +16,9 @@ isHidden(type: string): boolean; | Parameter | Type | Description | | --- | --- | --- | -| type | string | | +| type | string | | Returns: -`boolean` +boolean diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjecttyperegistry.isimportableandexportable.md b/docs/development/core/server/kibana-plugin-core-server.savedobjecttyperegistry.isimportableandexportable.md index aeefc207da4fe..8487af6a58911 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjecttyperegistry.isimportableandexportable.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjecttyperegistry.isimportableandexportable.md @@ -16,9 +16,9 @@ isImportableAndExportable(type: string): boolean; | Parameter | Type | Description | | --- | --- | --- | -| type | string | | +| type | string | | Returns: -`boolean` +boolean diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjecttyperegistry.ismultinamespace.md b/docs/development/core/server/kibana-plugin-core-server.savedobjecttyperegistry.ismultinamespace.md index 0ff07ae2804ff..d4ec6de2392dd 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjecttyperegistry.ismultinamespace.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjecttyperegistry.ismultinamespace.md @@ -16,9 +16,9 @@ isMultiNamespace(type: string): boolean; | Parameter | Type | Description | | --- | --- | --- | -| type | string | | +| type | string | | Returns: -`boolean` +boolean diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjecttyperegistry.isnamespaceagnostic.md b/docs/development/core/server/kibana-plugin-core-server.savedobjecttyperegistry.isnamespaceagnostic.md index 859c7b9711816..d6eca4981f7ab 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjecttyperegistry.isnamespaceagnostic.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjecttyperegistry.isnamespaceagnostic.md @@ -16,9 +16,9 @@ isNamespaceAgnostic(type: string): boolean; | Parameter | Type | Description | | --- | --- | --- | -| type | string | | +| type | string | | Returns: -`boolean` +boolean diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjecttyperegistry.isshareable.md b/docs/development/core/server/kibana-plugin-core-server.savedobjecttyperegistry.isshareable.md index ee240268f9d67..0b67992e53080 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjecttyperegistry.isshareable.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjecttyperegistry.isshareable.md @@ -16,9 +16,9 @@ isShareable(type: string): boolean; | Parameter | Type | Description | | --- | --- | --- | -| type | string | | +| type | string | | Returns: -`boolean` +boolean diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjecttyperegistry.issinglenamespace.md b/docs/development/core/server/kibana-plugin-core-server.savedobjecttyperegistry.issinglenamespace.md index 18146b2fd6ea1..d1db00d0c8162 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjecttyperegistry.issinglenamespace.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjecttyperegistry.issinglenamespace.md @@ -16,9 +16,9 @@ isSingleNamespace(type: string): boolean; | Parameter | Type | Description | | --- | --- | --- | -| type | string | | +| type | string | | Returns: -`boolean` +boolean diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjecttyperegistry.registertype.md b/docs/development/core/server/kibana-plugin-core-server.savedobjecttyperegistry.registertype.md index 7108805852c86..c0442e2aaa4ce 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjecttyperegistry.registertype.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjecttyperegistry.registertype.md @@ -16,9 +16,9 @@ registerType(type: SavedObjectsType): void; | Parameter | Type | Description | | --- | --- | --- | -| type | SavedObjectsType | | +| type | SavedObjectsType | | Returns: -`void` +void diff --git a/docs/development/core/server/kibana-plugin-core-server.searchresponse.md b/docs/development/core/server/kibana-plugin-core-server.searchresponse.md index cbaab4632014d..7deca96e4054c 100644 --- a/docs/development/core/server/kibana-plugin-core-server.searchresponse.md +++ b/docs/development/core/server/kibana-plugin-core-server.searchresponse.md @@ -15,11 +15,11 @@ export interface SearchResponse | Property | Type | Description | | --- | --- | --- | -| [\_scroll\_id](./kibana-plugin-core-server.searchresponse._scroll_id.md) | string | | -| [\_shards](./kibana-plugin-core-server.searchresponse._shards.md) | ShardsResponse | | -| [aggregations](./kibana-plugin-core-server.searchresponse.aggregations.md) | any | | -| [hits](./kibana-plugin-core-server.searchresponse.hits.md) | {
total: number;
max_score: number;
hits: Array<{
_index: string;
_type: string;
_id: string;
_score: number;
_source: T;
_version?: number;
_explanation?: Explanation;
fields?: any;
highlight?: any;
inner_hits?: any;
matched_queries?: string[];
sort?: unknown[];
}>;
} | | -| [pit\_id](./kibana-plugin-core-server.searchresponse.pit_id.md) | string | | -| [timed\_out](./kibana-plugin-core-server.searchresponse.timed_out.md) | boolean | | -| [took](./kibana-plugin-core-server.searchresponse.took.md) | number | | +| [\_scroll\_id?](./kibana-plugin-core-server.searchresponse._scroll_id.md) | string | (Optional) | +| [\_shards](./kibana-plugin-core-server.searchresponse._shards.md) | ShardsResponse | | +| [aggregations?](./kibana-plugin-core-server.searchresponse.aggregations.md) | any | (Optional) | +| [hits](./kibana-plugin-core-server.searchresponse.hits.md) | { total: number; max\_score: number; hits: Array<{ \_index: string; \_type: string; \_id: string; \_score: number; \_source: T; \_version?: number; \_explanation?: Explanation; fields?: any; highlight?: any; inner\_hits?: any; matched\_queries?: string\[\]; sort?: unknown\[\]; }>; } | | +| [pit\_id?](./kibana-plugin-core-server.searchresponse.pit_id.md) | string | (Optional) | +| [timed\_out](./kibana-plugin-core-server.searchresponse.timed_out.md) | boolean | | +| [took](./kibana-plugin-core-server.searchresponse.took.md) | number | | diff --git a/docs/development/core/server/kibana-plugin-core-server.servicestatus.md b/docs/development/core/server/kibana-plugin-core-server.servicestatus.md index d35fc951c57ff..5c04cb33a7529 100644 --- a/docs/development/core/server/kibana-plugin-core-server.servicestatus.md +++ b/docs/development/core/server/kibana-plugin-core-server.servicestatus.md @@ -16,9 +16,9 @@ export interface ServiceStatus | unknown = unkn | Property | Type | Description | | --- | --- | --- | -| [detail](./kibana-plugin-core-server.servicestatus.detail.md) | string | A more detailed description of the service status. | -| [documentationUrl](./kibana-plugin-core-server.servicestatus.documentationurl.md) | string | A URL to open in a new tab about how to resolve or troubleshoot the problem. | -| [level](./kibana-plugin-core-server.servicestatus.level.md) | ServiceStatusLevel | The current availability level of the service. | -| [meta](./kibana-plugin-core-server.servicestatus.meta.md) | Meta | Any JSON-serializable data to be included in the HTTP API response. Useful for providing more fine-grained, machine-readable information about the service status. May include status information for underlying features. | -| [summary](./kibana-plugin-core-server.servicestatus.summary.md) | string | A high-level summary of the service status. | +| [detail?](./kibana-plugin-core-server.servicestatus.detail.md) | string | (Optional) A more detailed description of the service status. | +| [documentationUrl?](./kibana-plugin-core-server.servicestatus.documentationurl.md) | string | (Optional) A URL to open in a new tab about how to resolve or troubleshoot the problem. | +| [level](./kibana-plugin-core-server.servicestatus.level.md) | ServiceStatusLevel | The current availability level of the service. | +| [meta?](./kibana-plugin-core-server.servicestatus.meta.md) | Meta | (Optional) Any JSON-serializable data to be included in the HTTP API response. Useful for providing more fine-grained, machine-readable information about the service status. May include status information for underlying features. | +| [summary](./kibana-plugin-core-server.servicestatus.summary.md) | string | A high-level summary of the service status. | diff --git a/docs/development/core/server/kibana-plugin-core-server.sessioncookievalidationresult.md b/docs/development/core/server/kibana-plugin-core-server.sessioncookievalidationresult.md index 0c190c4819b5b..6c1a5e7af78a7 100644 --- a/docs/development/core/server/kibana-plugin-core-server.sessioncookievalidationresult.md +++ b/docs/development/core/server/kibana-plugin-core-server.sessioncookievalidationresult.md @@ -16,6 +16,6 @@ export interface SessionCookieValidationResult | Property | Type | Description | | --- | --- | --- | -| [isValid](./kibana-plugin-core-server.sessioncookievalidationresult.isvalid.md) | boolean | Whether the cookie is valid or not. | -| [path](./kibana-plugin-core-server.sessioncookievalidationresult.path.md) | string | The "Path" attribute of the cookie; if the cookie is invalid, this is used to clear it. | +| [isValid](./kibana-plugin-core-server.sessioncookievalidationresult.isvalid.md) | boolean | Whether the cookie is valid or not. | +| [path?](./kibana-plugin-core-server.sessioncookievalidationresult.path.md) | string | (Optional) The "Path" attribute of the cookie; if the cookie is invalid, this is used to clear it. | diff --git a/docs/development/core/server/kibana-plugin-core-server.sessionstorage.clear.md b/docs/development/core/server/kibana-plugin-core-server.sessionstorage.clear.md index 731050a488075..ac34c9b17be8d 100644 --- a/docs/development/core/server/kibana-plugin-core-server.sessionstorage.clear.md +++ b/docs/development/core/server/kibana-plugin-core-server.sessionstorage.clear.md @@ -13,5 +13,5 @@ clear(): void; ``` Returns: -`void` +void diff --git a/docs/development/core/server/kibana-plugin-core-server.sessionstorage.get.md b/docs/development/core/server/kibana-plugin-core-server.sessionstorage.get.md index 4e4b2d9aa21ca..9e867b9e0fcc8 100644 --- a/docs/development/core/server/kibana-plugin-core-server.sessionstorage.get.md +++ b/docs/development/core/server/kibana-plugin-core-server.sessionstorage.get.md @@ -13,5 +13,5 @@ get(): Promise; ``` Returns: -`Promise` +Promise<T \| null> diff --git a/docs/development/core/server/kibana-plugin-core-server.sessionstorage.set.md b/docs/development/core/server/kibana-plugin-core-server.sessionstorage.set.md index 15a4d9ec75948..a17aadf8fb984 100644 --- a/docs/development/core/server/kibana-plugin-core-server.sessionstorage.set.md +++ b/docs/development/core/server/kibana-plugin-core-server.sessionstorage.set.md @@ -16,9 +16,9 @@ set(sessionValue: T): void; | Parameter | Type | Description | | --- | --- | --- | -| sessionValue | T | value to put | +| sessionValue | T | value to put | Returns: -`void` +void diff --git a/docs/development/core/server/kibana-plugin-core-server.sessionstoragecookieoptions.md b/docs/development/core/server/kibana-plugin-core-server.sessionstoragecookieoptions.md index b5dad11117359..425daf32f5cb3 100644 --- a/docs/development/core/server/kibana-plugin-core-server.sessionstoragecookieoptions.md +++ b/docs/development/core/server/kibana-plugin-core-server.sessionstoragecookieoptions.md @@ -16,9 +16,9 @@ export interface SessionStorageCookieOptions | Property | Type | Description | | --- | --- | --- | -| [encryptionKey](./kibana-plugin-core-server.sessionstoragecookieoptions.encryptionkey.md) | string | A key used to encrypt a cookie's value. Should be at least 32 characters long. | -| [isSecure](./kibana-plugin-core-server.sessionstoragecookieoptions.issecure.md) | boolean | Flag indicating whether the cookie should be sent only via a secure connection. | -| [name](./kibana-plugin-core-server.sessionstoragecookieoptions.name.md) | string | Name of the session cookie. | -| [sameSite](./kibana-plugin-core-server.sessionstoragecookieoptions.samesite.md) | 'Strict' | 'Lax' | 'None' | Defines SameSite attribute of the Set-Cookie Header. https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie/SameSite | -| [validate](./kibana-plugin-core-server.sessionstoragecookieoptions.validate.md) | (sessionValue: T | T[]) => SessionCookieValidationResult | Function called to validate a cookie's decrypted value. | +| [encryptionKey](./kibana-plugin-core-server.sessionstoragecookieoptions.encryptionkey.md) | string | A key used to encrypt a cookie's value. Should be at least 32 characters long. | +| [isSecure](./kibana-plugin-core-server.sessionstoragecookieoptions.issecure.md) | boolean | Flag indicating whether the cookie should be sent only via a secure connection. | +| [name](./kibana-plugin-core-server.sessionstoragecookieoptions.name.md) | string | Name of the session cookie. | +| [sameSite?](./kibana-plugin-core-server.sessionstoragecookieoptions.samesite.md) | 'Strict' \| 'Lax' \| 'None' | (Optional) Defines SameSite attribute of the Set-Cookie Header. https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie/SameSite | +| [validate](./kibana-plugin-core-server.sessionstoragecookieoptions.validate.md) | (sessionValue: T \| T\[\]) => SessionCookieValidationResult | Function called to validate a cookie's decrypted value. | diff --git a/docs/development/core/server/kibana-plugin-core-server.sessionstoragefactory.md b/docs/development/core/server/kibana-plugin-core-server.sessionstoragefactory.md index 848558291eb0e..7bdea28beeda1 100644 --- a/docs/development/core/server/kibana-plugin-core-server.sessionstoragefactory.md +++ b/docs/development/core/server/kibana-plugin-core-server.sessionstoragefactory.md @@ -16,5 +16,5 @@ export interface SessionStorageFactory | Property | Type | Description | | --- | --- | --- | -| [asScoped](./kibana-plugin-core-server.sessionstoragefactory.asscoped.md) | (request: KibanaRequest) => SessionStorage<T> | | +| [asScoped](./kibana-plugin-core-server.sessionstoragefactory.asscoped.md) | (request: KibanaRequest) => SessionStorage<T> | | diff --git a/docs/development/core/server/kibana-plugin-core-server.shardsinfo.md b/docs/development/core/server/kibana-plugin-core-server.shardsinfo.md index 9eafe3792c14a..6c006c020d3fb 100644 --- a/docs/development/core/server/kibana-plugin-core-server.shardsinfo.md +++ b/docs/development/core/server/kibana-plugin-core-server.shardsinfo.md @@ -15,8 +15,8 @@ export interface ShardsInfo | Property | Type | Description | | --- | --- | --- | -| [failed](./kibana-plugin-core-server.shardsinfo.failed.md) | number | | -| [skipped](./kibana-plugin-core-server.shardsinfo.skipped.md) | number | | -| [successful](./kibana-plugin-core-server.shardsinfo.successful.md) | number | | -| [total](./kibana-plugin-core-server.shardsinfo.total.md) | number | | +| [failed](./kibana-plugin-core-server.shardsinfo.failed.md) | number | | +| [skipped](./kibana-plugin-core-server.shardsinfo.skipped.md) | number | | +| [successful](./kibana-plugin-core-server.shardsinfo.successful.md) | number | | +| [total](./kibana-plugin-core-server.shardsinfo.total.md) | number | | diff --git a/docs/development/core/server/kibana-plugin-core-server.shardsresponse.md b/docs/development/core/server/kibana-plugin-core-server.shardsresponse.md index 722ffd8efdb57..65e113f05212f 100644 --- a/docs/development/core/server/kibana-plugin-core-server.shardsresponse.md +++ b/docs/development/core/server/kibana-plugin-core-server.shardsresponse.md @@ -15,8 +15,8 @@ export interface ShardsResponse | Property | Type | Description | | --- | --- | --- | -| [failed](./kibana-plugin-core-server.shardsresponse.failed.md) | number | | -| [skipped](./kibana-plugin-core-server.shardsresponse.skipped.md) | number | | -| [successful](./kibana-plugin-core-server.shardsresponse.successful.md) | number | | -| [total](./kibana-plugin-core-server.shardsresponse.total.md) | number | | +| [failed](./kibana-plugin-core-server.shardsresponse.failed.md) | number | | +| [skipped](./kibana-plugin-core-server.shardsresponse.skipped.md) | number | | +| [successful](./kibana-plugin-core-server.shardsresponse.successful.md) | number | | +| [total](./kibana-plugin-core-server.shardsresponse.total.md) | number | | diff --git a/docs/development/core/server/kibana-plugin-core-server.statusservicesetup.md b/docs/development/core/server/kibana-plugin-core-server.statusservicesetup.md index f522d11a7ffef..5409772c369c2 100644 --- a/docs/development/core/server/kibana-plugin-core-server.statusservicesetup.md +++ b/docs/development/core/server/kibana-plugin-core-server.statusservicesetup.md @@ -30,7 +30,6 @@ core.status.set( }) ; ); ); - ``` ## Example 2 @@ -64,18 +63,17 @@ core.status.set( }) ) ); - ``` ## Properties | Property | Type | Description | | --- | --- | --- | -| [core$](./kibana-plugin-core-server.statusservicesetup.core_.md) | Observable<CoreStatus> | Current status for all Core services. | -| [dependencies$](./kibana-plugin-core-server.statusservicesetup.dependencies_.md) | Observable<Record<string, ServiceStatus>> | Current status for all plugins this plugin depends on. Each key of the Record is a plugin id. | -| [derivedStatus$](./kibana-plugin-core-server.statusservicesetup.derivedstatus_.md) | Observable<ServiceStatus> | The status of this plugin as derived from its dependencies. | -| [isStatusPageAnonymous](./kibana-plugin-core-server.statusservicesetup.isstatuspageanonymous.md) | () => boolean | Whether or not the status HTTP APIs are available to unauthenticated users when an authentication provider is present. | -| [overall$](./kibana-plugin-core-server.statusservicesetup.overall_.md) | Observable<ServiceStatus> | Overall system status for all of Kibana. | +| [core$](./kibana-plugin-core-server.statusservicesetup.core_.md) | Observable<CoreStatus> | Current status for all Core services. | +| [dependencies$](./kibana-plugin-core-server.statusservicesetup.dependencies_.md) | Observable<Record<string, ServiceStatus>> | Current status for all plugins this plugin depends on. Each key of the Record is a plugin id. | +| [derivedStatus$](./kibana-plugin-core-server.statusservicesetup.derivedstatus_.md) | Observable<ServiceStatus> | The status of this plugin as derived from its dependencies. | +| [isStatusPageAnonymous](./kibana-plugin-core-server.statusservicesetup.isstatuspageanonymous.md) | () => boolean | Whether or not the status HTTP APIs are available to unauthenticated users when an authentication provider is present. | +| [overall$](./kibana-plugin-core-server.statusservicesetup.overall_.md) | Observable<ServiceStatus> | Overall system status for all of Kibana. | ## Methods diff --git a/docs/development/core/server/kibana-plugin-core-server.statusservicesetup.set.md b/docs/development/core/server/kibana-plugin-core-server.statusservicesetup.set.md index bf08ca1682f3b..b60319e19529a 100644 --- a/docs/development/core/server/kibana-plugin-core-server.statusservicesetup.set.md +++ b/docs/development/core/server/kibana-plugin-core-server.statusservicesetup.set.md @@ -16,11 +16,11 @@ set(status$: Observable): void; | Parameter | Type | Description | | --- | --- | --- | -| status$ | Observable<ServiceStatus> | | +| status$ | Observable<ServiceStatus> | | Returns: -`void` +void ## Remarks diff --git a/docs/development/core/server/kibana-plugin-core-server.uisettingsparams.md b/docs/development/core/server/kibana-plugin-core-server.uisettingsparams.md index c0da909cfe5ec..531a0e75c97b0 100644 --- a/docs/development/core/server/kibana-plugin-core-server.uisettingsparams.md +++ b/docs/development/core/server/kibana-plugin-core-server.uisettingsparams.md @@ -16,18 +16,18 @@ export interface UiSettingsParams | Property | Type | Description | | --- | --- | --- | -| [category](./kibana-plugin-core-server.uisettingsparams.category.md) | string[] | used to group the configured setting in the UI | -| [deprecation](./kibana-plugin-core-server.uisettingsparams.deprecation.md) | DeprecationSettings | optional deprecation information. Used to generate a deprecation warning. | -| [description](./kibana-plugin-core-server.uisettingsparams.description.md) | string | description provided to a user in UI | -| [metric](./kibana-plugin-core-server.uisettingsparams.metric.md) | {
type: UiCounterMetricType;
name: string;
} | Metric to track once this property changes | -| [name](./kibana-plugin-core-server.uisettingsparams.name.md) | string | title in the UI | -| [optionLabels](./kibana-plugin-core-server.uisettingsparams.optionlabels.md) | Record<string, string> | text labels for 'select' type UI element | -| [options](./kibana-plugin-core-server.uisettingsparams.options.md) | string[] | array of permitted values for this setting | -| [order](./kibana-plugin-core-server.uisettingsparams.order.md) | number | index of the settings within its category (ascending order, smallest will be displayed first). Used for ordering in the UI. settings without order defined will be displayed last and ordered by name | -| [readonly](./kibana-plugin-core-server.uisettingsparams.readonly.md) | boolean | a flag indicating that value cannot be changed | -| [requiresPageReload](./kibana-plugin-core-server.uisettingsparams.requirespagereload.md) | boolean | a flag indicating whether new value applying requires page reloading | -| [schema](./kibana-plugin-core-server.uisettingsparams.schema.md) | Type<T> | | -| [sensitive](./kibana-plugin-core-server.uisettingsparams.sensitive.md) | boolean | a flag indicating that value might contain user sensitive data. used by telemetry to mask the value of the setting when sent. | -| [type](./kibana-plugin-core-server.uisettingsparams.type.md) | UiSettingsType | defines a type of UI element [UiSettingsType](./kibana-plugin-core-server.uisettingstype.md) | -| [value](./kibana-plugin-core-server.uisettingsparams.value.md) | T | default value to fall back to if a user doesn't provide any | +| [category?](./kibana-plugin-core-server.uisettingsparams.category.md) | string\[\] | (Optional) used to group the configured setting in the UI | +| [deprecation?](./kibana-plugin-core-server.uisettingsparams.deprecation.md) | DeprecationSettings | (Optional) optional deprecation information. Used to generate a deprecation warning. | +| [description?](./kibana-plugin-core-server.uisettingsparams.description.md) | string | (Optional) description provided to a user in UI | +| [metric?](./kibana-plugin-core-server.uisettingsparams.metric.md) | { type: UiCounterMetricType; name: string; } | (Optional) Metric to track once this property changes | +| [name?](./kibana-plugin-core-server.uisettingsparams.name.md) | string | (Optional) title in the UI | +| [optionLabels?](./kibana-plugin-core-server.uisettingsparams.optionlabels.md) | Record<string, string> | (Optional) text labels for 'select' type UI element | +| [options?](./kibana-plugin-core-server.uisettingsparams.options.md) | string\[\] | (Optional) array of permitted values for this setting | +| [order?](./kibana-plugin-core-server.uisettingsparams.order.md) | number | (Optional) index of the settings within its category (ascending order, smallest will be displayed first). Used for ordering in the UI. settings without order defined will be displayed last and ordered by name | +| [readonly?](./kibana-plugin-core-server.uisettingsparams.readonly.md) | boolean | (Optional) a flag indicating that value cannot be changed | +| [requiresPageReload?](./kibana-plugin-core-server.uisettingsparams.requirespagereload.md) | boolean | (Optional) a flag indicating whether new value applying requires page reloading | +| [schema](./kibana-plugin-core-server.uisettingsparams.schema.md) | Type<T> | | +| [sensitive?](./kibana-plugin-core-server.uisettingsparams.sensitive.md) | boolean | (Optional) a flag indicating that value might contain user sensitive data. used by telemetry to mask the value of the setting when sent. | +| [type?](./kibana-plugin-core-server.uisettingsparams.type.md) | UiSettingsType | (Optional) defines a type of UI element [UiSettingsType](./kibana-plugin-core-server.uisettingstype.md) | +| [value?](./kibana-plugin-core-server.uisettingsparams.value.md) | T | (Optional) default value to fall back to if a user doesn't provide any | diff --git a/docs/development/core/server/kibana-plugin-core-server.uisettingsservicesetup.register.md b/docs/development/core/server/kibana-plugin-core-server.uisettingsservicesetup.register.md index 24bfc32cb1139..97b06ddb00e70 100644 --- a/docs/development/core/server/kibana-plugin-core-server.uisettingsservicesetup.register.md +++ b/docs/development/core/server/kibana-plugin-core-server.uisettingsservicesetup.register.md @@ -16,11 +16,11 @@ register(settings: Record): void; | Parameter | Type | Description | | --- | --- | --- | -| settings | Record<string, UiSettingsParams> | | +| settings | Record<string, UiSettingsParams> | | Returns: -`void` +void ## Example @@ -35,6 +35,5 @@ setup(core: CoreSetup){ }, }]); } - ``` diff --git a/docs/development/core/server/kibana-plugin-core-server.uisettingsservicestart.asscopedtoclient.md b/docs/development/core/server/kibana-plugin-core-server.uisettingsservicestart.asscopedtoclient.md index 1703df00a5e71..1d76bc26a4150 100644 --- a/docs/development/core/server/kibana-plugin-core-server.uisettingsservicestart.asscopedtoclient.md +++ b/docs/development/core/server/kibana-plugin-core-server.uisettingsservicestart.asscopedtoclient.md @@ -18,11 +18,11 @@ asScopedToClient(savedObjectsClient: SavedObjectsClientContract): IUiSettingsCli | Parameter | Type | Description | | --- | --- | --- | -| savedObjectsClient | SavedObjectsClientContract | | +| savedObjectsClient | SavedObjectsClientContract | | Returns: -`IUiSettingsClient` +IUiSettingsClient ## Example @@ -32,6 +32,5 @@ start(core: CoreStart) { const soClient = core.savedObjects.getScopedClient(arbitraryRequest); const uiSettingsClient = core.uiSettings.asScopedToClient(soClient); } - ``` diff --git a/docs/development/core/server/kibana-plugin-core-server.userprovidedvalues.md b/docs/development/core/server/kibana-plugin-core-server.userprovidedvalues.md index eddfcb456826e..fe8aaf233fbf7 100644 --- a/docs/development/core/server/kibana-plugin-core-server.userprovidedvalues.md +++ b/docs/development/core/server/kibana-plugin-core-server.userprovidedvalues.md @@ -16,6 +16,6 @@ export interface UserProvidedValues | Property | Type | Description | | --- | --- | --- | -| [isOverridden](./kibana-plugin-core-server.userprovidedvalues.isoverridden.md) | boolean | | -| [userValue](./kibana-plugin-core-server.userprovidedvalues.uservalue.md) | T | | +| [isOverridden?](./kibana-plugin-core-server.userprovidedvalues.isoverridden.md) | boolean | (Optional) | +| [userValue?](./kibana-plugin-core-server.userprovidedvalues.uservalue.md) | T | (Optional) | diff --git a/docs/index-extra-title-page.html b/docs/index-extra-title-page.html index 2621848ebea8a..ff1c879c0f409 100644 --- a/docs/index-extra-title-page.html +++ b/docs/index-extra-title-page.html @@ -64,7 +64,7 @@
  • Create an index patternCreate a data view
  • diff --git a/docs/management/advanced-options.asciidoc b/docs/management/advanced-options.asciidoc index 56b7eb09252ed..7e7ff1137794c 100644 --- a/docs/management/advanced-options.asciidoc +++ b/docs/management/advanced-options.asciidoc @@ -2,7 +2,7 @@ == Advanced Settings *Advanced Settings* control the behavior of {kib}. For example, you can change the format used to display dates, -specify the default index pattern, and set the precision for displayed decimal values. +specify the default data view, and set the precision for displayed decimal values. . Open the main menu, then click *Stack Management > Advanced Settings*. . Scroll or search for the setting. @@ -134,10 +134,6 @@ value by the maximum number of aggregations in each visualization. [[history-limit]]`history:limit`:: In fields that have history, such as query inputs, show this many recent values. -[[indexpattern-placeholder]]`indexPattern:placeholder`:: -The default placeholder value to use in -*Management > Index Patterns > Create Index Pattern*. - [[metafields]]`metaFields`:: Fields that exist outside of `_source`. Kibana merges these fields into the document when displaying it. @@ -283,7 +279,7 @@ value is 5. [[context-tiebreakerfields]]`context:tieBreakerFields`:: A comma-separated list of fields to use for breaking a tie between documents that have the same timestamp value. The first field that is present and sortable -in the current index pattern is used. +in the current data view is used. [[defaultcolumns]]`defaultColumns`:: The columns that appear by default on the *Discover* page. The default is @@ -296,7 +292,7 @@ The number of rows to show in the *Discover* table. Specifies the maximum number of fields to show in the document column of the *Discover* table. [[discover-modify-columns-on-switch]]`discover:modifyColumnsOnSwitch`:: -When enabled, removes the columns that are not in the new index pattern. +When enabled, removes the columns that are not in the new data view. [[discover-sample-size]]`discover:sampleSize`:: Specifies the number of rows to display in the *Discover* table. @@ -314,7 +310,7 @@ does not have an effect when loading a saved search. When enabled, displays multi-fields in the expanded document view. [[discover-sort-defaultorder]]`discover:sort:defaultOrder`:: -The default sort direction for time-based index patterns. +The default sort direction for time-based data views. [[doctable-hidetimecolumn]]`doc_table:hideTimeColumn`:: Hides the "Time" column in *Discover* and in all saved searches on dashboards. @@ -391,8 +387,8 @@ A custom image to use in the footer of the PDF. ==== Rollup [horizontal] -[[rollups-enableindexpatterns]]`rollups:enableIndexPatterns`:: -Enables the creation of index patterns that capture rollup indices, which in +[[rollups-enabledataviews]]`rollups:enableDataViews`:: +Enables the creation of data views that capture rollup indices, which in turn enables visualizations based on rollup data. Refresh the page to apply the changes. @@ -408,7 +404,7 @@ to use when `courier:setRequestPreference` is set to "custom". [[courier-ignorefilteriffieldnotinindex]]`courier:ignoreFilterIfFieldNotInIndex`:: Skips filters that apply to fields that don't exist in the index for a visualization. Useful when dashboards consist of visualizations from multiple -index patterns. +data views. [[courier-maxconcurrentshardrequests]]`courier:maxConcurrentShardRequests`:: Controls the {ref}/search-multi-search.html[max_concurrent_shard_requests] diff --git a/docs/management/images/management-rollup-index-pattern.png b/docs/management/images/management-rollup-index-pattern.png deleted file mode 100644 index de7976e63f050..0000000000000 Binary files a/docs/management/images/management-rollup-index-pattern.png and /dev/null differ diff --git a/docs/management/manage-data-views.asciidoc b/docs/management/manage-data-views.asciidoc new file mode 100644 index 0000000000000..a092da669d45e --- /dev/null +++ b/docs/management/manage-data-views.asciidoc @@ -0,0 +1,272 @@ +[[managing-data-views]] +== Manage data views + +To customize the data fields in your data view, +you can add runtime fields to the existing documents, +add scripted fields to compute data on the fly, and change how {kib} displays the data fields. + +[float] +[[runtime-fields]] +=== Explore your data with runtime fields + +Runtime fields are fields that you add to documents after you've ingested your data, and are evaluated at query time. With runtime fields, you allow for a smaller index and faster ingest time so that you can use less resources and reduce your operating costs. +You can use runtime fields anywhere data views are used, for example, you can explore runtime fields in *Discover* and create visualizations with runtime fields for your dashboard. + +With runtime fields, you can: + +* Define fields for a specific use case without modifying the underlying schema. + +* Override the returned values from index fields. + +* Start working on your data without understanding the structure. + +* Add fields to existing documents without reindexing your data. + +WARNING: Runtime fields can impact {kib} performance. When you run a query, {es} uses the fields you index first to shorten the response time. +Index the fields that you commonly search for and filter on, such as `timestamp`, then use runtime fields to limit the number of fields {es} uses to calculate values. + +For detailed information on how to use runtime fields with {es}, refer to {ref}/runtime.html[Runtime fields]. + +[float] +[[create-runtime-fields]] +==== Add runtime fields + +To add runtime fields to your data views, open the data view you want to change, +then define the field values by emitting a single value using +the {ref}/modules-scripting-painless.html[Painless scripting language]. +You can also add runtime fields in <> and <>. + +. Open the main menu, then click *Stack Management > Data Views*. + +. Select the data view that you want to add the runtime field to, then click *Add field*. + +. Enter the field *Name*, then select the *Type*. + +. Select *Set custom label*, then enter the label you want to display where the data view is used, +such as *Discover*. + +. Select *Set value*, then define the script. The script must match the *Type*, or the data view fails anywhere it is used. + +. To help you define the script, use the *Preview*: + +* To view the other available fields, use the *Document ID* arrows. + +* To filter the fields list, enter the keyword in *Filter fields*. + +* To pin frequently used fields to the top of the list, hover over the field, +then click image:images/stackManagement-indexPatterns-pinRuntimeField-7.15.png[Icon to pin field to the top of the list]. + +. Click *Create field*. + +[float] +[[runtime-field-examples]] +==== Runtime field examples + +Try the runtime field examples on your own using the <> data. + +[float] +[[simple-hello-world-example]] +==== Return a keyword value + +Return `Hello World!`: + +[source,text] +---- +emit("Hello World!"); +---- + +[float] +[[perform-a-calculation-on-a-single-field]] +===== Perform a calculation on a single field + +Calculate kilobytes from bytes: + +[source,text] +---- +emit(doc['bytes'].value / 1024) +---- + +[float] +[[return-substring]] +===== Return a substring + +Return the string that appears after the last slash in the URL: + +[source,text] +---- +def path = doc["url.keyword"].value; +if (path != null) { + int lastSlashIndex = path.lastIndexOf('/'); + if (lastSlashIndex > 0) { + emit(path.substring(lastSlashIndex+1)); + return; + } +} +emit(""); +---- + +[float] +[[replace-nulls-with-blanks]] +===== Replace nulls with blanks + +Replace `null` values with `None`: + +[source,text] +---- +def source = doc['referer'].value; +if (source != null) { + emit(source); + return; +} +else { + emit("None"); +} +---- + +Specify the operating system condition: + +[source,text] +---- +def source = doc['machine.os.keyword'].value; +if (source != "") { + emit(source); +} +else { + emit("None"); +} +---- + +[float] +[[manage-runtime-fields]] +==== Manage runtime fields + +Edit the settings for runtime fields, or remove runtime fields from data views. + +. Open the main menu, then click *Stack Management > Data Views*. + +. Select the data view that contains the runtime field you want to manage, then open the runtime field edit options or delete the runtime field. + +[float] +[[scripted-fields]] +=== Add scripted fields to data views + +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 +query language. + +WARNING: Computing data on the fly with scripted fields can be very resource intensive and can have a direct impact on +{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 +{ref}/modules-scripting-painless.html[Painless] scripting language. + +You can reference any single value numeric field in your expressions, for example: + +---- +doc['field_name'].value +---- + +For more information on scripted fields and additional examples, refer to +https://www.elastic.co/blog/using-painless-kibana-scripted-fields[Using Painless in {kib} scripted fields] + +[float] +[[create-scripted-field]] +==== Create scripted fields + +Create and add scripted fields to your data views. + +. Open the main menu, then click *Stack Management > Data Views*. + +. Select the data view you want to add a scripted field to. + +. Select the *Scripted fields* tab, then click *Add scripted field*. + +. Enter a *Name* for the scripted field, then enter the *Script* you want to use to compute a value on the fly from your index data. + +. Click *Create field*. + +For more information about scripted fields in {es}, refer to {ref}/modules-scripting.html[Scripting]. + +[float] +[[update-scripted-field]] +==== Manage scripted fields + +. Open the main menu, then click *Stack Management > Data Views*. + +. Select the data view that contains the scripted field you want to manage. + +. Select the *Scripted fields* tab, then open the scripted field edit options or delete the scripted field. + +WARNING: Built-in validation is unsupported for scripted fields. When your scripts contain errors, you receive +exceptions when you view the dynamically generated data. + +[float] +[[managing-fields]] +=== Format data fields + +{kib} uses the same field types as {es}, however, some {es} field types are unsupported in {kib}. +To customize how {kib} displays data fields, use the formatting options. + +. Open the main menu, then click *Stack Management > Data Views*. + +. Click the data view that contains the field you want to change. + +. Find the field, then open the edit options (image:management/index-patterns/images/edit_icon.png[Data field edit icon]). + +. Select *Set custom label*, then enter a *Custom label* for the field. + +. Select *Set format*, then enter the *Format* for the field. + +[float] +[[string-field-formatters]] +==== String field formatters + +String fields support *String* and *Url* formatters. + +include::field-formatters/string-formatter.asciidoc[] + +include::field-formatters/url-formatter.asciidoc[] + +[float] +[[field-formatters-date]] +==== Date field formatters + +Date fields support *Date*, *String*, and *Url* formatters. + +The *Date* formatter enables you to choose the display format of date stamps using the https://momentjs.com/[moment.js] +standard format definitions. + +include::field-formatters/string-formatter.asciidoc[] + +include::field-formatters/url-formatter.asciidoc[] + +[float] +[[field-formatters-geopoint]] +==== Geographic point field formatters + +Geographic point fields support the *String* formatter. + +include::field-formatters/string-formatter.asciidoc[] + +[float] +[[field-formatters-numeric]] +==== Number field formatters + +Numeric fields support *Bytes*, *Color*, *Duration*, *Histogram*, *Number*, *Percentage*, *String*, and *Url* formatters. + +The *Bytes*, *Number*, and *Percentage* formatters enable you to choose the display formats of numbers in the field using +the <> syntax that {kib} maintains. + +The *Histogram* formatter is used only for the {ref}/histogram.html[histogram field type]. When you use the *Histogram* formatter, +you can apply the *Bytes*, *Number*, or *Percentage* format to aggregated data. + +include::field-formatters/url-formatter.asciidoc[] + +include::field-formatters/string-formatter.asciidoc[] + +include::field-formatters/duration-formatter.asciidoc[] + +include::field-formatters/color-formatter.asciidoc[] diff --git a/docs/management/manage-index-patterns.asciidoc b/docs/management/manage-index-patterns.asciidoc deleted file mode 100644 index 08527ffa75d4a..0000000000000 --- a/docs/management/manage-index-patterns.asciidoc +++ /dev/null @@ -1,264 +0,0 @@ -[[managing-index-patterns]] -== Manage index pattern data fields - -To customize the data fields in your index pattern, you can add runtime fields to the existing documents, add scrited fields to compute data on the fly, and change how {kib} displays the data fields. - -[float] -[[runtime-fields]] -=== Explore your data with runtime fields - -Runtime fields are fields that you add to documents after you've ingested your data, and are evaluated at query time. With runtime fields, you allow for a smaller index and faster ingest time so that you can use less resources and reduce your operating costs. You can use runtime fields anywhere index patterns are used, for example, you can explore runtime fields in *Discover* and create visualizations with runtime fields for your dashboard. - -With runtime fields, you can: - -* Define fields for a specific use case without modifying the underlying schema. - -* Override the returned values from index fields. - -* Start working on your data without understanding the structure. - -* Add fields to existing documents without reindexing your data. - -WARNING: Runtime fields can impact {kib} performance. When you run a query, {es} uses the fields you index first to shorten the response time. -Index the fields that you commonly search for and filter on, such as `timestamp`, then use runtime fields to limit the number of fields {es} uses to calculate values. - -For detailed information on how to use runtime fields with {es}, refer to {ref}/runtime.html[Runtime fields]. - -[float] -[[create-runtime-fields]] -==== Add runtime fields - -To add runtime fields to your index patterns, open the index pattern you want to change, then define the field values by emitting a single value using the {ref}/modules-scripting-painless.html[Painless scripting language]. You can also add runtime fields in <> and <>. - -. Open the main menu, then click *Stack Management > Index Patterns*. - -. Select the index pattern you want to add the runtime field to, then click *Add field*. - -. Enter the field *Name*, then select the *Type*. - -. Select *Set custom label*, then enter the label you want to display where the index pattern is used, such as *Discover*. - -. Select *Set value*, then define the script. The script must match the *Type*, or the index pattern fails anywhere it is used. - -. To help you define the script, use the *Preview*: - -* To view the other available fields, use the *Document ID* arrows. - -* To filter the fields list, enter the keyword in *Filter fields*. - -* To pin frequently used fields to the top of the list, hover over the field, then click image:images/stackManagement-indexPatterns-pinRuntimeField-7.15.png[Icon to pin field to the top of the list]. - -. Click *Create field*. - -[float] -[[runtime-field-examples]] -==== Runtime field examples - -Try the runtime field examples on your own using the <> data index pattern. - -[float] -[[simple-hello-world-example]] -==== Return a keyword value - -Return `Hello World!`: - -[source,text] ----- -emit("Hello World!"); ----- - -[float] -[[perform-a-calculation-on-a-single-field]] -===== Perform a calculation on a single field - -Calculate kilobytes from bytes: - -[source,text] ----- -emit(doc['bytes'].value / 1024) ----- - -[float] -[[return-substring]] -===== Return a substring - -Return the string that appears after the last slash in the URL: - -[source,text] ----- -def path = doc["url.keyword"].value; -if (path != null) { - int lastSlashIndex = path.lastIndexOf('/'); - if (lastSlashIndex > 0) { - emit(path.substring(lastSlashIndex+1)); - return; - } -} -emit(""); ----- - -[float] -[[replace-nulls-with-blanks]] -===== Replace nulls with blanks - -Replace `null` values with `None`: - -[source,text] ----- -def source = doc['referer'].value; -if (source != null) { - emit(source); - return; -} -else { - emit("None"); -} ----- - -Specify the operating system condition: - -[source,text] ----- -def source = doc['machine.os.keyword'].value; -if (source != "") { - emit(source); -} -else { - emit("None"); -} ----- - -[float] -[[manage-runtime-fields]] -==== Manage runtime fields - -Edit the settings for runtime fields, or remove runtime fields from index patterns. - -. Open the main menu, then click *Stack Management > Index Patterns*. - -. Select the index pattern that contains the runtime field you want to manage, then open the runtime field edit options or delete the runtime field. - -[float] -[[scripted-fields]] -=== Add scripted fields to index patterns - -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 -query language. - -WARNING: Computing data on the fly with scripted fields can be very resource intensive and can have a direct impact on -{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 -{ref}/modules-scripting-painless.html[Painless] scripting language. - -You can reference any single value numeric field in your expressions, for example: - ----- -doc['field_name'].value ----- - -For more information on scripted fields and additional examples, refer to -https://www.elastic.co/blog/using-painless-kibana-scripted-fields[Using Painless in {kib} scripted fields] - -[float] -[[create-scripted-field]] -==== Create scripted fields - -Create and add scripted fields to your index patterns. - -. Open the main menu, then click *Stack Management > Index Patterns*. - -. Select the index pattern you want to add a scripted field to. - -. Select the *Scripted fields* tab, then click *Add scripted field*. - -. Enter a *Name* for the scripted field, then enter the *Script* you want to use to compute a value on the fly from your index data. - -. Click *Create field*. - -For more information about scripted fields in {es}, refer to {ref}/modules-scripting.html[Scripting]. - -[float] -[[update-scripted-field]] -==== Manage scripted fields - -. Open the main menu, then click *Stack Management > Index Patterns*. - -. Select the index pattern that contains the scripted field you want to manage. - -. Select the *Scripted fields* tab, then open the scripted field edit options or delete the scripted field. - -WARNING: Built-in validation is unsupported for scripted fields. When your scripts contain errors, you receive -exceptions when you view the dynamically generated data. - -[float] -[[managing-fields]] -=== Format data fields - -{kib} uses the same field types as {es}, however, some {es} field types are unsupported in {kib}. -To customize how {kib} displays data fields, use the formatting options. - -. Open the main menu, then click *Stack Management > Index Patterns*. - -. Click the index pattern that contains the field you want to change. - -. Find the field, then open the edit options (image:management/index-patterns/images/edit_icon.png[Data field edit icon]). - -. Select *Set custom label*, then enter a *Custom label* for the field. - -. Select *Set format*, then enter the *Format* for the field. - -[float] -[[string-field-formatters]] -==== String field formatters - -String fields support *String* and *Url* formatters. - -include::field-formatters/string-formatter.asciidoc[] - -include::field-formatters/url-formatter.asciidoc[] - -[float] -[[field-formatters-date]] -==== Date field formatters - -Date fields support *Date*, *String*, and *Url* formatters. - -The *Date* formatter enables you to choose the display format of date stamps using the https://momentjs.com/[moment.js] -standard format definitions. - -include::field-formatters/string-formatter.asciidoc[] - -include::field-formatters/url-formatter.asciidoc[] - -[float] -[[field-formatters-geopoint]] -==== Geographic point field formatters - -Geographic point fields support the *String* formatter. - -include::field-formatters/string-formatter.asciidoc[] - -[float] -[[field-formatters-numeric]] -==== Number field formatters - -Numeric fields support *Bytes*, *Color*, *Duration*, *Histogram*, *Number*, *Percentage*, *String*, and *Url* formatters. - -The *Bytes*, *Number*, and *Percentage* formatters enable you to choose the display formats of numbers in the field using -the <> syntax that {kib} maintains. - -The *Histogram* formatter is used only for the {ref}/histogram.html[histogram field type]. When you use the *Histogram* formatter, -you can apply the *Bytes*, *Number*, or *Percentage* format to aggregated data. - -include::field-formatters/url-formatter.asciidoc[] - -include::field-formatters/string-formatter.asciidoc[] - -include::field-formatters/duration-formatter.asciidoc[] - -include::field-formatters/color-formatter.asciidoc[] \ No newline at end of file diff --git a/docs/management/managing-saved-objects.asciidoc b/docs/management/managing-saved-objects.asciidoc index 5b39c6ad1c4cd..b9859575051af 100644 --- a/docs/management/managing-saved-objects.asciidoc +++ b/docs/management/managing-saved-objects.asciidoc @@ -2,10 +2,10 @@ == Saved Objects The *Saved Objects* UI helps you keep track of and manage your saved objects. These objects -store data for later use, including dashboards, visualizations, maps, index patterns, +store data for later use, including dashboards, visualizations, maps, data views, Canvas workpads, and more. -To get started, open the main menu, then click *Stack Management > Saved Objects*. +To get started, open the main menu, then click *Stack Management > Saved Objects*. [role="screenshot"] image::images/management-saved-objects.png[Saved Objects] @@ -85,7 +85,7 @@ You have two options for exporting saved objects. * Click *Export x objects*, and export objects by type. This action creates an NDJSON with all your saved objects. By default, the NDJSON includes child objects that are related to the saved -objects. Exported dashboards include their associated index patterns. +objects. Exported dashboards include their associated data views. NOTE: The <> configuration setting limits the number of saved objects which may be exported. @@ -120,7 +120,7 @@ If you access an object whose index has been deleted, you can: * Recreate the index so you can continue using the object. * Delete the object and recreate it using a different index. * Change the index name in the object's `reference` array to point to an existing -index pattern. This is useful if the index you were working with has been renamed. +data view. This is useful if the index you were working with has been renamed. WARNING: Validation is not performed for object properties. Submitting an invalid change will render the object unusable. A more failsafe approach is to use diff --git a/docs/management/numeral.asciidoc b/docs/management/numeral.asciidoc index 893873eb1075a..d6c8fbc9011fc 100644 --- a/docs/management/numeral.asciidoc +++ b/docs/management/numeral.asciidoc @@ -9,7 +9,7 @@ they are now maintained by {kib}. Numeral formatting patterns are used in multiple places in {kib}, including: * <> -* <> +* <> * <> * <> diff --git a/docs/management/rollups/create_and_manage_rollups.asciidoc b/docs/management/rollups/create_and_manage_rollups.asciidoc index 51821a935d3f5..bdfd3f65b3c87 100644 --- a/docs/management/rollups/create_and_manage_rollups.asciidoc +++ b/docs/management/rollups/create_and_manage_rollups.asciidoc @@ -5,7 +5,7 @@ experimental::[] A rollup job is a periodic task that aggregates data from indices specified -by an index pattern, and then rolls it into a new index. Rollup indices are a good way to +by a data view, and then rolls it into a new index. Rollup indices are a good way to compactly store months or years of historical data for use in visualizations and reports. @@ -33,9 +33,9 @@ the process. You fill in the name, data flow, and how often you want to roll up the data. Then you define a date histogram aggregation for the rollup job and optionally define terms, histogram, and metrics aggregations. -When defining the index pattern, you must enter a name that is different than +When defining the data view, you must enter a name that is different than the output rollup index. Otherwise, the job -will attempt to capture the data in the rollup index. For example, if your index pattern is `metricbeat-*`, +will attempt to capture the data in the rollup index. For example, if your data view is `metricbeat-*`, you can name your rollup index `rollup-metricbeat`, but not `metricbeat-rollup`. [role="screenshot"] @@ -66,7 +66,7 @@ You can read more at {ref}/rollup-job-config.html[rollup job configuration]. This example creates a rollup job to capture log data from sample web logs. Before you start, <>. -In this example, you want data that is older than 7 days in the target index pattern `kibana_sample_data_logs` +In this example, you want data that is older than 7 days in the target data view `kibana_sample_data_logs` to roll up into the `rollup_logstash` index. You’ll bucket the rolled up data on an hourly basis, using 60m for the time bucket configuration. This allows for more granular queries, such as 2h and 12h. @@ -85,7 +85,7 @@ As you walk through the *Create rollup job* UI, enter the data: |Name |`logs_job` -|Index pattern +|Data view |`kibana_sample_data_logs` |Rollup index name @@ -139,27 +139,23 @@ rollup index, or you can remove or archive it using < Index Patterns*. +. Open the main menu, then click *Stack Management > Data Views*. -. Click *Create index pattern*, and select *Rollup index pattern* from the dropdown. -+ -[role="screenshot"] -image::images/management-rollup-index-pattern.png[][Create rollup index pattern] +. Click *Create data view*, and select *Rollup data view* from the dropdown. -. Enter *rollup_logstash,kibana_sample_logs* as your *Index Pattern* and `@timestamp` +. Enter *rollup_logstash,kibana_sample_logs* as your *Data View* and `@timestamp` as the *Time Filter field name*. + -The notation for a combination index pattern with both raw and rolled up data -is `rollup_logstash,kibana_sample_data_logs`. In this index pattern, `rollup_logstash` -matches the rolled up index pattern and `kibana_sample_data_logs` matches the index -pattern for raw data. +The notation for a combination data view with both raw and rolled up data +is `rollup_logstash,kibana_sample_data_logs`. In this data view, `rollup_logstash` +matches the rolled up data view and `kibana_sample_data_logs` matches the data view for raw data. . Open the main menu, click *Dashboard*, then *Create dashboard*. . Set the <> to *Last 90 days*. . On the dashboard, click *Create visualization*. - + . Choose `rollup_logstash,kibana_sample_data_logs` as your source to see both the raw and rolled up data. + diff --git a/docs/maps/asset-tracking-tutorial.asciidoc b/docs/maps/asset-tracking-tutorial.asciidoc index 4ba045681e148..ff62f5c019b74 100644 --- a/docs/maps/asset-tracking-tutorial.asciidoc +++ b/docs/maps/asset-tracking-tutorial.asciidoc @@ -156,16 +156,16 @@ image::maps/images/asset-tracking-tutorial/logstash_output.png[] . Leave the terminal window open and Logstash running throughout this tutorial. [float] -==== Step 3: Create a {kib} index pattern for the tri_met_tracks {es} index +==== Step 3: Create a data view for the tri_met_tracks {es} index -. In Kibana, open the main menu, and click *Stack Management > Index Patterns*. -. Click *Create index pattern*. -. Give the index pattern a name: *tri_met_tracks**. +. In {kib}, open the main menu, and click *Stack Management > Data Views*. +. Click *Create data view*. +. Give the data view a name: *tri_met_tracks**. . Click *Next step*. . Set the *Time field* to *time*. -. Click *Create index pattern*. +. Click *Create data view*. -{kib} shows the fields in your index pattern. +{kib} shows the fields in your data view. [role="screenshot"] image::maps/images/asset-tracking-tutorial/index_pattern.png[] @@ -174,7 +174,7 @@ image::maps/images/asset-tracking-tutorial/index_pattern.png[] ==== Step 4: Explore the Portland bus data . Open the main menu, and click *Discover*. -. Set the index pattern to *tri_met_tracks**. +. Set the data view to *tri_met_tracks**. . Open the <>, and set the time range to the last 15 minutes. . Expand a document and explore some of the fields that you will use later in this tutorial: `bearing`, `in_congestion`, `location`, and `vehicle_id`. @@ -202,7 +202,7 @@ Add a layer to show the bus routes for the last 15 minutes. . Click *Add layer*. . Click *Tracks*. -. Select the *tri_met_tracks** index pattern. +. Select the *tri_met_tracks** data view. . Define the tracks: .. Set *Entity* to *vehicle_id*. .. Set *Sort* to *time*. @@ -225,7 +225,7 @@ image::maps/images/asset-tracking-tutorial/tracks_layer.png[] Add a layer that uses attributes in the data to set the style and orientation of the buses. You’ll see the direction buses are headed and what traffic is like. . Click *Add layer*, and then select *Top Hits per entity*. -. Select the *tri_met_tracks** index pattern. +. Select the *tri_met_tracks** data view. . To display the most recent location per bus: .. Set *Entity* to *vehicle_id*. .. Set *Documents per entity* to 1. diff --git a/docs/maps/geojson-upload.asciidoc b/docs/maps/geojson-upload.asciidoc index 3c9bea11176cc..15ef3471e58d7 100644 --- a/docs/maps/geojson-upload.asciidoc +++ b/docs/maps/geojson-upload.asciidoc @@ -30,11 +30,11 @@ a preview of the data on the map. . Use the default *Index type* of {ref}/geo-point.html[geo_point] for point data, or override it and select {ref}/geo-shape.html[geo_shape]. All other shapes will default to a type of `geo_shape`. -. Leave the default *Index name* and *Index pattern* names (the name of the uploaded +. Leave the default *Index name* and *Data view* names (the name of the uploaded file minus its extension). You might need to change the index name if it is invalid. . Click *Import file*. + -Upon completing the indexing process and creating the associated index pattern, +Upon completing the indexing process and creating the associated data view, the Elasticsearch responses are shown on the *Layer add panel* and the indexed data appears on the map. The geospatial data on the map should be identical to the locally-previewed data, but now it's indexed data from Elasticsearch. diff --git a/docs/maps/indexing-geojson-data-tutorial.asciidoc b/docs/maps/indexing-geojson-data-tutorial.asciidoc index 434c9ab369a5b..50f2e9aed9248 100644 --- a/docs/maps/indexing-geojson-data-tutorial.asciidoc +++ b/docs/maps/indexing-geojson-data-tutorial.asciidoc @@ -58,8 +58,8 @@ auto-populate *Index type* with either {ref}/geo-point.html[geo_point] or . Click *Import file*. + You'll see activity as the GeoJSON Upload utility creates a new index -and index pattern for the data set. When the process is complete, you should -receive messages that the creation of the new index and index pattern +and data view for the data set. When the process is complete, you should +receive messages that the creation of the new index and data view were successful. . Click *Add layer*. diff --git a/docs/maps/maps-aggregations.asciidoc b/docs/maps/maps-aggregations.asciidoc index 7f4af952653e7..fced15771c386 100644 --- a/docs/maps/maps-aggregations.asciidoc +++ b/docs/maps/maps-aggregations.asciidoc @@ -62,7 +62,7 @@ To enable a grid aggregation layer: To enable a blended layer that dynamically shows clusters or documents: . Click *Add layer*, then select the *Documents* layer. -. Configure *Index pattern* and the *Geospatial field*. +. Configure *Data view* and the *Geospatial field*. . In *Scaling*, select *Show clusters when results exceed 10000*. @@ -77,7 +77,7 @@ then accumulates the most relevant documents based on sort order for each entry To enable top hits: . Click *Add layer*, then select the *Top hits per entity* layer. -. Configure *Index pattern* and *Geospatial field*. +. Configure *Data view* and *Geospatial field*. . 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/maps-getting-started.asciidoc b/docs/maps/maps-getting-started.asciidoc index 014be570253bb..89d06fce60183 100644 --- a/docs/maps/maps-getting-started.asciidoc +++ b/docs/maps/maps-getting-started.asciidoc @@ -49,7 +49,7 @@ and lighter shades will symbolize countries with less traffic. . From the **Layer** dropdown menu, select **World Countries**. . In **Statistics source**, set: -** **Index pattern** to **kibana_sample_data_logs** +** **Data view** to **kibana_sample_data_logs** ** **Join field** to **geo.dest** . Click **Add layer**. @@ -95,7 +95,7 @@ The layer is only visible when users zoom in. . Click **Add layer**, and then click **Documents**. -. Set **Index pattern** to **kibana_sample_data_logs**. +. Set **Data view** to **kibana_sample_data_logs**. . Set **Scaling** to *Limits results to 10000.* @@ -129,7 +129,7 @@ more total bytes transferred, and smaller circles will symbolize grids with less bytes transferred. . Click **Add layer**, and select **Clusters and grids**. -. Set **Index pattern** to **kibana_sample_data_logs**. +. Set **Data view** to **kibana_sample_data_logs**. . Click **Add layer**. . In **Layer settings**, set: ** **Name** to `Total Requests and Bytes` diff --git a/docs/maps/reverse-geocoding-tutorial.asciidoc b/docs/maps/reverse-geocoding-tutorial.asciidoc index 0c942f120a4da..8760d3ab4df8b 100644 --- a/docs/maps/reverse-geocoding-tutorial.asciidoc +++ b/docs/maps/reverse-geocoding-tutorial.asciidoc @@ -141,7 +141,7 @@ PUT kibana_sample_data_logs/_settings ---------------------------------- . Open the main menu, and click *Discover*. -. Set the index pattern to *kibana_sample_data_logs*. +. Set the data view to *kibana_sample_data_logs*. . Open the <>, and set the time range to the last 30 days. . Scan through the list of *Available fields* until you find the `csa.GEOID` field. You can also search for the field by name. . Click image:images/reverse-geocoding-tutorial/add-icon.png[Add icon] to toggle the field into the document table. @@ -162,10 +162,10 @@ Now that our web traffic contains CSA region identifiers, you'll visualize CSA r . Click *Choropleth*. . For *Boundaries source*: .. Select *Points, lines, and polygons from Elasticsearch*. -.. Set *Index pattern* to *csa*. +.. Set *Data view* to *csa*. .. Set *Join field* to *GEOID*. . For *Statistics source*: -.. Set *Index pattern* to *kibana_sample_data_logs*. +.. Set *Data view* to *kibana_sample_data_logs*. .. Set *Join field* to *csa.GEOID.keyword*. . Click *Add layer*. . Scroll to *Layer Style* and Set *Label* to *Fixed*. diff --git a/docs/maps/trouble-shooting.asciidoc b/docs/maps/trouble-shooting.asciidoc index 60bcabad3a6b4..13c8b97c30b3d 100644 --- a/docs/maps/trouble-shooting.asciidoc +++ b/docs/maps/trouble-shooting.asciidoc @@ -21,18 +21,18 @@ image::maps/images/inspector.png[] === Solutions to common problems [float] -==== Index not listed when adding layer +==== Data view not listed when adding layer * Verify your geospatial data is correctly mapped as {ref}/geo-point.html[geo_point] or {ref}/geo-shape.html[geo_shape]. - ** Run `GET myIndexPatternTitle/_field_caps?fields=myGeoFieldName` in <>, replacing `myIndexPatternTitle` and `myGeoFieldName` with your index pattern title and geospatial field name. + ** Run `GET myIndexName/_field_caps?fields=myGeoFieldName` in <>, replacing `myIndexName` and `myGeoFieldName` with your index and geospatial field name. ** Ensure response specifies `type` as `geo_point` or `geo_shape`. -* Verify your geospatial data is correctly mapped in your <>. - ** Open your index pattern in <>. +* Verify your geospatial data is correctly mapped in your <>. + ** Open your data view in <>. ** Ensure your geospatial field type is `geo_point` or `geo_shape`. ** Ensure your geospatial field is searchable and aggregatable. ** If your geospatial field type does not match your Elasticsearch mapping, click the *Refresh* button to refresh the field list from Elasticsearch. -* Index patterns with thousands of fields can exceed the default maximum payload size. -Increase <> for large index patterns. +* Data views with thousands of fields can exceed the default maximum payload size. +Increase <> for large data views. [float] ==== Features are not displayed diff --git a/docs/maps/vector-tooltips.asciidoc b/docs/maps/vector-tooltips.asciidoc index 2dda35aa28f76..2e4ee99d5b84f 100644 --- a/docs/maps/vector-tooltips.asciidoc +++ b/docs/maps/vector-tooltips.asciidoc @@ -18,7 +18,7 @@ image::maps/images/multifeature_tooltip.png[] ==== Format tooltips You can format the attributes in a tooltip by adding <> to your -index pattern. You can use field formatters to round numbers, provide units, +data view. You can use field formatters to round numbers, provide units, and even display images in your tooltip. [float] diff --git a/docs/redirects.asciidoc b/docs/redirects.asciidoc index 4010083d601b5..2b00ccd67dc96 100644 --- a/docs/redirects.asciidoc +++ b/docs/redirects.asciidoc @@ -363,3 +363,8 @@ This content has moved. Refer to <>. == Index patterns has been renamed to data views. This content has moved. Refer to <>. + +[role="exclude",id="managing-index-patterns"] +== Index patterns has been renamed to data views. + +This content has moved. Refer to <>. diff --git a/docs/settings/security-settings.asciidoc b/docs/settings/security-settings.asciidoc index 2ed3c21c482d5..56d08ee24efe1 100644 --- a/docs/settings/security-settings.asciidoc +++ b/docs/settings/security-settings.asciidoc @@ -332,7 +332,7 @@ For more details and a reference of audit events, refer to < type: rolling-file - fileName: ./data/audit.log + fileName: ./logs/audit.log policy: type: time-interval interval: 24h <2> diff --git a/docs/user/alerting/rule-types.asciidoc b/docs/user/alerting/rule-types.asciidoc index 4c1d3b94bdee6..ab2349f2fb102 100644 --- a/docs/user/alerting/rule-types.asciidoc +++ b/docs/user/alerting/rule-types.asciidoc @@ -26,6 +26,9 @@ see {subscriptions}[the subscription page]. | <> | Run a user-configured {es} query, compare the number of matches to a configured threshold, and schedule actions to run when the threshold condition is met. +| {ref}/transform-alerts.html[{transform-cap} rules] beta:[] +| beta:[] Run scheduled checks on a {ctransform} to check its health. If a {ctransform} meets the conditions, an alert is created and the associated action is triggered. + |=== [float] @@ -47,7 +50,7 @@ Domain rules are registered by *Observability*, *Security*, <> and < | Run an {es} query to determine if any documents are currently contained in any boundaries from a specified boundary index and generate alerts when a rule's conditions are met. | {ml-docs}/ml-configuring-alerts.html[{ml-cap} rules] beta:[] -| Run scheduled checks on an anomaly detection job to detect anomalies with certain conditions. If an anomaly meets the conditions, an alert is created and the associated action is triggered. +| beta:[] Run scheduled checks on an {anomaly-job} to detect anomalies with certain conditions. If an anomaly meets the conditions, an alert is created and the associated action is triggered. |=== diff --git a/docs/user/alerting/rule-types/es-query.asciidoc b/docs/user/alerting/rule-types/es-query.asciidoc index 86367a6a2e2c0..e3ce35687260f 100644 --- a/docs/user/alerting/rule-types/es-query.asciidoc +++ b/docs/user/alerting/rule-types/es-query.asciidoc @@ -17,7 +17,7 @@ Define properties to detect the condition. [role="screenshot"] image::user/alerting/images/rule-types-es-query-conditions.png[Five clauses define the condition to detect] -Index:: This clause requires an *index or index pattern* and a *time field* that will be used for the *time window*. +Index:: This clause requires an *index or data view* and a *time field* that will be used for the *time window*. Size:: This clause specifies the number of documents to pass to the configured actions when the the threshold condition is met. {es} query:: This clause specifies the ES DSL query to execute. The number of documents that match this query will be evaluated against the threshold condition. Aggregations are not supported at this time. diff --git a/docs/user/alerting/rule-types/geo-rule-types.asciidoc b/docs/user/alerting/rule-types/geo-rule-types.asciidoc index 244cf90c855a7..454c51ad69860 100644 --- a/docs/user/alerting/rule-types/geo-rule-types.asciidoc +++ b/docs/user/alerting/rule-types/geo-rule-types.asciidoc @@ -10,17 +10,17 @@ In the event that an entity is contained within a boundary, an alert may be gene ==== Requirements To create a Tracking containment rule, the following requirements must be present: -- *Tracks index or index pattern*: An index containing a `geo_point` field, `date` field, +- *Tracks index or data view*: An index containing a `geo_point` field, `date` field, and some form of entity identifier. An entity identifier is a `keyword` or `number` field that consistently identifies the entity to be tracked. The data in this index should be dynamically updating so that there are entity movements to alert upon. -- *Boundaries index or index pattern*: An index containing `geo_shape` data, such as boundary data and bounding box data. +- *Boundaries index or data view*: An index containing `geo_shape` data, such as boundary data and bounding box data. This data is presumed to be static (not updating). Shape data matching the query is harvested once when the rule is created and anytime after when the rule is re-enabled after disablement. By design, current interval entity locations (_current_ is determined by `date` in -the *Tracked index or index pattern*) are queried to determine if they are contained +the *Tracked index or data view*) are queried to determine if they are contained within any monitored boundaries. Entity data should be somewhat "real time", meaning the dates of new documents aren’t older than the current time minus the amount of the interval. If data older than @@ -39,13 +39,13 @@ as well as 2 Kuery bars used to provide additional filtering context for each of [role="screenshot"] image::user/alerting/images/alert-types-tracking-containment-conditions.png[Five clauses define the condition to detect] -Index (entity):: This clause requires an *index or index pattern*, a *time field* that will be used for the *time window*, and a *`geo_point` field* for tracking. +Index (entity):: This clause requires an *index or data view*, a *time field* that will be used for the *time window*, and a *`geo_point` field* for tracking. When entity:: This clause specifies which crossing option to track. The values *Entered*, *Exited*, and *Crossed* can be selected to indicate which crossing conditions should trigger a rule. *Entered* alerts on entry into a boundary, *Exited* alerts on exit from a boundary, and *Crossed* alerts on all boundary crossings whether they be entrances or exits. -Index (Boundary):: This clause requires an *index or index pattern*, a *`geo_shape` field* +Index (Boundary):: This clause requires an *index or data view*, a *`geo_shape` field* identifying boundaries, and an optional *Human-readable boundary name* for better alerting messages. diff --git a/docs/user/alerting/rule-types/index-threshold.asciidoc b/docs/user/alerting/rule-types/index-threshold.asciidoc index 8c45c158414f4..c65b0f66b1b63 100644 --- a/docs/user/alerting/rule-types/index-threshold.asciidoc +++ b/docs/user/alerting/rule-types/index-threshold.asciidoc @@ -17,7 +17,7 @@ Define properties to detect the condition. [role="screenshot"] image::user/alerting/images/rule-types-index-threshold-conditions.png[Five clauses define the condition to detect] -Index:: This clause requires an *index or index pattern* and a *time field* that will be used for the *time window*. +Index:: This clause requires an *index or data view* and a *time field* that will be used for the *time window*. When:: This clause specifies how the value to be compared to the threshold is calculated. The value is calculated by aggregating a numeric field a the *time window*. The aggregation options are: `count`, `average`, `sum`, `min`, and `max`. When using `count` the document count is used, and an aggregation field is not necessary. Over/Grouped Over:: This clause lets you configure whether the aggregation is applied over all documents, or should be split into groups using a grouping field. If grouping is used, an <> will be created for each group when it exceeds the threshold. To limit the number of alerts on high cardinality fields, you must specify the number of groups to check against the threshold. Only the *top* groups are checked. Threshold:: This clause defines a threshold value and a comparison operator (one of `is above`, `is above or equals`, `is below`, `is below or equals`, or `is between`). The result of the aggregation is compared to this threshold. diff --git a/docs/user/dashboard/images/lens_areaChartCumulativeNumberOfSalesOnWeekend_7.16.png b/docs/user/dashboard/images/lens_areaChartCumulativeNumberOfSalesOnWeekend_7.16.png new file mode 100644 index 0000000000000..82e0337ffed39 Binary files /dev/null and b/docs/user/dashboard/images/lens_areaChartCumulativeNumberOfSalesOnWeekend_7.16.png differ diff --git a/docs/user/dashboard/images/lens_areaPercentageNumberOfOrdersByCategory_7.16.png b/docs/user/dashboard/images/lens_areaPercentageNumberOfOrdersByCategory_7.16.png new file mode 100644 index 0000000000000..6addc8bc276e9 Binary files /dev/null and b/docs/user/dashboard/images/lens_areaPercentageNumberOfOrdersByCategory_7.16.png differ diff --git a/docs/user/dashboard/images/lens_barChartCustomTimeInterval_7.16.png b/docs/user/dashboard/images/lens_barChartCustomTimeInterval_7.16.png new file mode 100644 index 0000000000000..3aa5484cb6258 Binary files /dev/null and b/docs/user/dashboard/images/lens_barChartCustomTimeInterval_7.16.png differ diff --git a/docs/user/dashboard/images/lens_barChartDistributionOfNumberField_7.16.png b/docs/user/dashboard/images/lens_barChartDistributionOfNumberField_7.16.png new file mode 100644 index 0000000000000..631477e7d68cc Binary files /dev/null and b/docs/user/dashboard/images/lens_barChartDistributionOfNumberField_7.16.png differ diff --git a/docs/user/dashboard/images/lens_clickAndDragZoom_7.16.gif b/docs/user/dashboard/images/lens_clickAndDragZoom_7.16.gif new file mode 100644 index 0000000000000..65fed435dfa25 Binary files /dev/null and b/docs/user/dashboard/images/lens_clickAndDragZoom_7.16.gif differ diff --git a/docs/user/dashboard/images/lens_end_to_end_2_1_1.png b/docs/user/dashboard/images/lens_end_to_end_2_1_1.png index e996b58520d41..f1bee569f29c2 100644 Binary files a/docs/user/dashboard/images/lens_end_to_end_2_1_1.png and b/docs/user/dashboard/images/lens_end_to_end_2_1_1.png differ diff --git a/docs/user/dashboard/images/lens_end_to_end_6_1.png b/docs/user/dashboard/images/lens_end_to_end_6_1.png index 73299bac0354e..942c4d636d1fc 100644 Binary files a/docs/user/dashboard/images/lens_end_to_end_6_1.png and b/docs/user/dashboard/images/lens_end_to_end_6_1.png differ diff --git a/docs/user/dashboard/images/lens_indexPatternDropDown_7.16.png b/docs/user/dashboard/images/lens_indexPatternDropDown_7.16.png new file mode 100644 index 0000000000000..f8e797c7dd4b6 Binary files /dev/null and b/docs/user/dashboard/images/lens_indexPatternDropDown_7.16.png differ diff --git a/docs/user/dashboard/images/lens_index_pattern.png b/docs/user/dashboard/images/lens_index_pattern.png deleted file mode 100644 index 0c89e7ab7f814..0000000000000 Binary files a/docs/user/dashboard/images/lens_index_pattern.png and /dev/null differ diff --git a/docs/user/dashboard/images/lens_layerVisualizationTypeMenu_7.16.png b/docs/user/dashboard/images/lens_layerVisualizationTypeMenu_7.16.png new file mode 100644 index 0000000000000..6ee73e9a67662 Binary files /dev/null and b/docs/user/dashboard/images/lens_layerVisualizationTypeMenu_7.16.png differ diff --git a/docs/user/dashboard/images/lens_leftAxisMenu_7.16.png b/docs/user/dashboard/images/lens_leftAxisMenu_7.16.png new file mode 100644 index 0000000000000..054731adbeef5 Binary files /dev/null and b/docs/user/dashboard/images/lens_leftAxisMenu_7.16.png differ diff --git a/docs/user/dashboard/images/lens_lineChartMetricOverTime_7.16.png b/docs/user/dashboard/images/lens_lineChartMetricOverTime_7.16.png new file mode 100644 index 0000000000000..34fd8dae1407d Binary files /dev/null and b/docs/user/dashboard/images/lens_lineChartMetricOverTime_7.16.png differ diff --git a/docs/user/dashboard/images/lens_lineChartMultipleDataSeries_7.16.png b/docs/user/dashboard/images/lens_lineChartMultipleDataSeries_7.16.png new file mode 100644 index 0000000000000..373fc76b5db41 Binary files /dev/null and b/docs/user/dashboard/images/lens_lineChartMultipleDataSeries_7.16.png differ diff --git a/docs/user/dashboard/images/lens_logsDashboard_7.16.png b/docs/user/dashboard/images/lens_logsDashboard_7.16.png new file mode 100644 index 0000000000000..cdfe0accdbbb5 Binary files /dev/null and b/docs/user/dashboard/images/lens_logsDashboard_7.16.png differ diff --git a/docs/user/dashboard/images/lens_metricUniqueCountOfClientip_7.16.png b/docs/user/dashboard/images/lens_metricUniqueCountOfClientip_7.16.png new file mode 100644 index 0000000000000..bed6acf501a3a Binary files /dev/null and b/docs/user/dashboard/images/lens_metricUniqueCountOfClientip_7.16.png differ diff --git a/docs/user/dashboard/images/lens_metricUniqueVisitors_7.16.png b/docs/user/dashboard/images/lens_metricUniqueVisitors_7.16.png new file mode 100644 index 0000000000000..92fe4fb0676f2 Binary files /dev/null and b/docs/user/dashboard/images/lens_metricUniqueVisitors_7.16.png differ diff --git a/docs/user/dashboard/images/lens_mixedXYChart_7.16.png b/docs/user/dashboard/images/lens_mixedXYChart_7.16.png new file mode 100644 index 0000000000000..76fc96a44a402 Binary files /dev/null and b/docs/user/dashboard/images/lens_mixedXYChart_7.16.png differ diff --git a/docs/user/dashboard/images/lens_pieChartCompareSubsetOfDocs_7.16.png b/docs/user/dashboard/images/lens_pieChartCompareSubsetOfDocs_7.16.png new file mode 100644 index 0000000000000..f8e8ba98f691e Binary files /dev/null and b/docs/user/dashboard/images/lens_pieChartCompareSubsetOfDocs_7.16.png differ diff --git a/docs/user/dashboard/images/lens_referenceLine_7.16.png b/docs/user/dashboard/images/lens_referenceLine_7.16.png new file mode 100644 index 0000000000000..3df7e99e0aafe Binary files /dev/null and b/docs/user/dashboard/images/lens_referenceLine_7.16.png differ diff --git a/docs/user/dashboard/images/lens_tableTopFieldValues_7.16.png b/docs/user/dashboard/images/lens_tableTopFieldValues_7.16.png new file mode 100644 index 0000000000000..64417a9a6392c Binary files /dev/null and b/docs/user/dashboard/images/lens_tableTopFieldValues_7.16.png differ diff --git a/docs/user/dashboard/images/lens_timeSeriesDataTutorialDashboard_7.16.png b/docs/user/dashboard/images/lens_timeSeriesDataTutorialDashboard_7.16.png new file mode 100644 index 0000000000000..bce904c8606ca Binary files /dev/null and b/docs/user/dashboard/images/lens_timeSeriesDataTutorialDashboard_7.16.png differ diff --git a/docs/user/dashboard/images/lens_treemapMultiLevelChart_7.16.png b/docs/user/dashboard/images/lens_treemapMultiLevelChart_7.16.png new file mode 100644 index 0000000000000..6d772a32e9ef4 Binary files /dev/null and b/docs/user/dashboard/images/lens_treemapMultiLevelChart_7.16.png differ diff --git a/docs/user/dashboard/images/lens_visualizationTypeDropdown_7.16.png b/docs/user/dashboard/images/lens_visualizationTypeDropdown_7.16.png new file mode 100644 index 0000000000000..dce53da1f2cad Binary files /dev/null and b/docs/user/dashboard/images/lens_visualizationTypeDropdown_7.16.png differ diff --git a/docs/user/dashboard/lens-advanced.asciidoc b/docs/user/dashboard/lens-advanced.asciidoc index 02e0afd2c0311..324676ecb0a8e 100644 --- a/docs/user/dashboard/lens-advanced.asciidoc +++ b/docs/user/dashboard/lens-advanced.asciidoc @@ -2,18 +2,21 @@ == Analyze time series data In this tutorial, you'll use the ecommerce sample data to analyze sales trends, but you can use any type of data to complete the tutorial. -Before using this tutorial, review the <>. + +When you're done, you'll have a complete overview of the sample web logs data. [role="screenshot"] -image::images/final_time_series_analysis_dashboard.png[Final dashboard with ecommerce sample data, width=50%] +image::images/lens_timeSeriesDataTutorialDashboard_7.16.png[Final dashboard with ecommerce sample data] + +Before you begin, you should be familiar with the <>. [discrete] [[add-the-data-and-create-the-dashboard-advanced]] === Add the data and create the dashboard -Add the sample ecommerce data that you'll use to create the dashboard panels. +Add the sample ecommerce data, and create and set up the dashboard. -. Go to the {kib} *Home* page, then click *Try our sample data*. +. Go to the *Home* page, then click *Try sample data*. . On the *Sample eCommerce orders* card, click *Add data*. @@ -25,40 +28,30 @@ Create the dashboard where you'll display the visualization panels. [float] [[open-and-set-up-lens-advanced]] -=== Open and set up Lens +=== Open and set up the visualization editor -Open *Lens*, then make sure the correct fields appear. +Open the visualization editor, then make sure the correct fields appear. -. From the dashboard, click *Create visualization*. +. On the dashboard, click *Create visualization*. -. Make sure the *kibana_sample_data_ecommerce* index appears. -+ -If you are using your own data, select the <> that contains your data. +. Make sure the *kibana_sample_data_ecommerce* index appears, then set the <> to *Last 30 days*. [discrete] [[custom-time-interval]] -=== View a date histogram with a custom time interval - -It is common to use the automatic date histogram interval, but sometimes you want a larger or smaller -interval. For performance reasonse, *Lens* lets you choose the minimum time interval, not the exact time interval. The performance limit is controlled by the <> setting and the <>. +=== Create visualizations with custom time intervals -If you are using your own data, use one of the following options to see hourly sales over the last 30 days: +When you create visualizations with time series data, you can use the default time interval, or increase and decrease the interval. For performance reasons, the visualization editor allows you to choose the minimum time interval, but not the exact time interval. The interval limit is controlled by the <> setting and <>. -* View less than 30 days at a time, then use the time filter to select each day separately. - -* Increase `histogram:maxBars` to at least 720, which is the number of hours in 30 days. This affects all visualizations and can reduce performance. - -If you are using the sample data, use *Normalize unit*, which converts *Average sales per 12 hours* -into *Average sales per 12 hours (per hour)* by dividing the number of hours: - -. Set the <> to *Last 30 days*. +To analyze the data with a custom time interval, create a bar chart that shows you how many orders were made at your store every hour: . From the *Available fields* list, drag *Records* to the workspace. ++ +The visualization editor creates a bar chart. -. To zoom in on the data you want to view, click and drag your cursor across the bars. +. To zoom in on the data, click and drag your cursor across the bars. + [role="screenshot"] -image::images/lens_advanced_1_1.png[Added records to the workspace] +image::images/lens_clickAndDragZoom_7.16.gif[Cursor clicking and dragging across the bars to zoom in on the data] . In the layer pane, click *Count of Records*. @@ -67,32 +60,51 @@ image::images/lens_advanced_1_1.png[Added records to the workspace] .. Click *Add advanced options > Normalize by unit*. .. From the *Normalize by unit* dropdown, select *per hour*, then click *Close*. ++ +*Normalize unit* converts *Average sales per 12 hours* into *Average sales per 12 hours (per hour)* by dividing the number of hours. . To hide the *Horizontal axis* label, open the *Bottom Axis* menu, then deselect *Show*. -+ -You have a bar chart that shows you how many orders were made at your store every hour. + +To identify the 75th percentile of orders, add a reference line: + +. In the layer pane, click *Add layer > Add reference layer*. + +. Click *Static value*. + +. Click the *Percentile* function, then enter `75` in the *Percentile* field. + +. Configure the display options. + +.. In the *Display name* field, enter `75th`. + +.. Select *Show display name*. + +.. From the *Icon* dropdown, select *Tag*. + +.. In the *Color* field, enter `#E7664C`. + +. Click *Close*. + [role="screenshot"] -image::images/lens_advanced_1_2.png[Orders per day] +image::images/lens_barChartCustomTimeInterval_7.16.png[Orders per day] . Click *Save and return*. [discrete] [[add-a-data-layer-advanced]] -=== Monitor multiple series +=== Analyze multiple data series -It is often required to monitor multiple series within a time interval. These series can have similar configurations with minor differences. -*Lens* copies a function when you drag it to the *Drop a field or click to add* field within the same group. +You can create visualizations with multiple data series within the same time interval, even when the series have similar configurations with minor differences. -To quickly create many copies of a percentile metric that shows distribution of price over time: +To analyze multiple series, create a line chart that displays the price distribution of products sold over time: . On the dashboard, click *Create visualization*. -. Open the *Chart Type* dropdown, then select *Line*. +. Open the *Visualization type* dropdown, then select *Line*. . From the *Available fields* list, drag *products.price* to the workspace. -Create the 95th percentile. +Create the 95th price distribution percentile: . In the layer pane, click *Median of products.price*. @@ -100,9 +112,9 @@ Create the 95th percentile. . In the *Display name* field, enter `95th`, then click *Close*. -To create the 90th percentile, duplicate the `95th` percentile. +To copy a function, you drag it to the *Drop a field or click to add* field within the same group. To create the 90th percentile, duplicate the `95th` percentile: -. Drag the *95th* field to the *Drop a field or click to add* field in the *Vertical axis* group. +. Drag the *95th* field to *Add or drag-and-drop a field* for *Vertical axis*. + [role="screenshot"] image::images/lens_advanced_2_2.gif[Easily duplicate the items with drag and drop] @@ -111,22 +123,22 @@ image::images/lens_advanced_2_2.gif[Easily duplicate the items with drag and dro . In the *Display name* field enter `90th`, then click *Close*. -. Repeat the duplication steps to create the `50th` and `10th` percentiles. +. To create the `50th` and `10th` percentiles, repeat the duplication steps. . Open the *Left Axis* menu, then enter `Percentiles for product prices` in the *Axis name* field. + -You have a line chart that shows you the price distribution of products sold over time. -+ [role="screenshot"] -image::images/lens_advanced_2_3.png[Percentiles for product prices chart] +image::images/lens_lineChartMultipleDataSeries_7.16.png[Percentiles for product prices chart] . Click *Save and return*. [discrete] [[add-a-data-layer]] -==== Add multiple chart types or index patterns +=== Analyze multiple visualization types + +With layers, you can analyze your data with multiple visualization types. When you create layered visualizations, match the data on the horizontal axis so that it uses the same scale. -To overlay visualization types or index patterns, add layers. When you create layered charts, match the data on the horizontal axis so that it uses the same scale. +To analyze multiple visualization types, create an area chart that displays the average order prices, then add a line chart layer that displays the number of customers. . On the dashboard, click *Create visualization*. @@ -136,19 +148,19 @@ To overlay visualization types or index patterns, add layers. When you create la .. Click the *Average* function. -.. In the *Display name* field, enter `Average of prices`, then click *Close*. +.. In the *Display name* field, enter `Average price`, then click *Close*. -. Open the *Chart Type* dropdown, then select *Area*. +. Open the *Visualization type* dropdown, then select *Area*. -Create a new layer to overlay with custom traffic. +Add a layer to display the customer traffic: -. In the layer pane, click *+*. +. In the layer pane, click *Add layer > Add visualization layer*. . From the *Available fields* list, drag *customer_id* to the *Vertical Axis* field in the second layer. -. In the second layer, click *Unique count of customer_id*. +. In the layer pane, click *Unique count of customer_id*. -.. In the *Display name* field, enter `Unique customers`. +.. In the *Display name* field, enter `Number of customers`. .. In the *Series color* field, enter *#D36086*. @@ -156,12 +168,15 @@ Create a new layer to overlay with custom traffic. . From the *Available fields* list, drag *order_date* to the *Horizontal Axis* field in the second layer. -. In the second layer pane, open the *Chart type* menu, then click the line chart. +. In the second layer, open the *Layer visualization type* menu, then click *Line*. + [role="screenshot"] -image::images/lens_advanced_3_2.png[Change layer type] +image::images/lens_layerVisualizationTypeMenu_7.16.png[Layer visualization type menu] -. Open the *Legend* menu, then select the arrow that points up. +. To change the position of the legend, open the *Legend* menu, then select the *Alignment* arrow that points up. ++ +[role="screenshot"] +image::images/lens_mixedXYChart_7.16.png[Layer visualization type menu] . Click *Save and return*. @@ -169,35 +184,35 @@ image::images/lens_advanced_3_2.png[Change layer type] [[percentage-stacked-area]] === Compare the change in percentage over time -By default, *Lens* shows *date histograms* using a stacked chart visualization, which helps understand how distinct sets of documents perform over time. Sometimes it is useful to understand how the distributions of these sets change over time. -Combine *filters* and *date histogram* functions to see the change over time in specific -sets of documents. To view this as a percentage, use a *Stacked percentage* bar or area chart. +By default, the visualization editor displays time series data with stacked charts, which show how the different document sets change over time. + +To view change over time as a percentage, create an *Area percentage* chart that displays three order categories over time: . On the dashboard, click *Create visualization*. . From the *Available fields* list, drag *Records* to the workspace. -. Open the *Chart type* dropdown, then select *Area percentage*. +. Open the *Visualization type* dropdown, then select *Area percentage*. -For each category type, create a filter. +For each order category, create a filter: -. In the layer pane, click the *Drop a field or click to add* field for *Break down by*. +. In the layer pane, click *Add or drag-and-drop a field* for *Break down by*. . Click the *Filters* function. -. Click *All records*, enter the following, then press Return: +. Click *All records*, enter the following in the query bar, then press Return: * *KQL* — `category.keyword : *Clothing` * *Label* — `Clothing` -. Click *Add a filter*, enter the following, then press Return: +. Click *Add a filter*, enter the following in the query bar, then press Return: * *KQL* — `category.keyword : *Shoes` * *Label* — `Shoes` -. Click *Add a filter*, enter the following, then press Return: +. Click *Add a filter*, enter the following in the query bar, then press Return: * *KQL* — `category.keyword : *Accessories` @@ -205,10 +220,10 @@ For each category type, create a filter. . Click *Close*. -. Open the *Legend* menu, then select the arrow that points up. +. Open the *Legend* menu, then select the *Alignment* arrow that points up. + [role="screenshot"] -image::images/lens_advanced_4_1.png[Prices share by category] +image::images/lens_areaPercentageNumberOfOrdersByCategory_7.16.png[Prices share by category] . Click *Save and return*. @@ -220,9 +235,9 @@ To determine the number of orders made only on Saturday and Sunday, create an ar . On the dashboard, click *Create visualization*. -. Open the *Chart Type* dropdown, then select *Area*. +. Open the *Visualization type* dropdown, then select *Area*. -Configure the cumulative sum of the store orders. +Configure the cumulative sum of store orders: . From the *Available fields* list, drag *Records* to the workspace. @@ -230,15 +245,15 @@ Configure the cumulative sum of the store orders. . Click the *Cumulative sum* function. -. In the *Display name* field, enter `Cumulative orders during weekend days`, then click *Close*. +. In the *Display name* field, enter `Cumulative weekend orders`, then click *Close*. -Filter the results to display the data for only Saturday and Sunday. +Filter the results to display the data for only Saturday and Sunday: -. In the layer pane, click the *Drop a field or click to add* field for *Break down by*. +. In the layer pane, click *Add or drag-and-drop a field* for *Break down by*. . Click the *Filters* function. -. Click *All records*, enter the following, then press Return: +. Click *All records*, enter the following in the query bar, then press Return: * *KQL* — `day_of_week : "Saturday" or day_of_week : "Sunday"` @@ -249,7 +264,7 @@ The <> displays all documents where `day_of_week` matche . Open the *Legend* menu, then click *Hide*. + [role="screenshot"] -image::images/lens_advanced_5_2.png[Line chart with cumulative sum of orders made on the weekend] +image::images/lens_areaChartCumulativeNumberOfSalesOnWeekend_7.16.png[Area chart with cumulative sum of orders made on the weekend] . Click *Save and return*. @@ -257,30 +272,25 @@ image::images/lens_advanced_5_2.png[Line chart with cumulative sum of orders mad [[compare-time-ranges]] === Compare time ranges -*Lens* allows you to compare the selected time range with historical data using the *Time shift* option. +With *Time shift*, you can compare the data from different time ranges. To make sure the data correctly displays, choose a multiple of the date histogram interval when you use multiple time shifts. For example, you are unable to use a *36h* time shift for one series, and a *1d* time shift for the second series if the interval is *days*. -If multiple time shifts are used in a single chart, a multiple of the date histogram interval should be chosen, or the data points might not line up and gaps can appear. -For example, if a daily interval is used, shifting one series by *36h*, and another by *1d* is not recommended. You can reduce the interval to *12h*, or create two separate charts. - -To compare current sales numbers with sales from a week ago, follow these steps: +To compare two time ranges, create a line chart that compares the sales in the current week with sales from the previous week: . On the dashboard, click *Create visualization*. -. Open the *Chart Type* dropdown, then select *Line*. +. Open the *Visualization type* dropdown, then select *Line*. . From the *Available fields* list, drag *Records* to the workspace. -. In the layer pane, drag *Count of Records* to the *Drop a field or click to add* field in the *Vertical axis* group. +. To duplicate *Count of Records*, drag *Count of Records* to *Add or drag-and-drop a field* for *Vertical axis* in the layer pane. -To create a week-over-week comparison, shift the second *Count of Records* by one week. +To create a week-over-week comparison, shift *Count of Records [1]* by one week: . In the layer pane, click *Count of Records [1]*. -. Open the *Add advanced options* dropdown, then select *Time shift*. - -. Click *1 week ago*. +. Click *Add advanced options > Time shift*, select *1 week ago*, then click *Close*. + -To define custom time shifts, enter the time value, the time increment, then press Enter. For example, to use a one week time shift, enter *1w*. +To use custom time shifts, enter the time value and increment, then press Enter. For example, enter *1w* to use the *1 week ago* time shift. + [role="screenshot"] image::images/lens_time_shift.png[Line chart with week-over-week sales comparison] @@ -289,9 +299,11 @@ image::images/lens_time_shift.png[Line chart with week-over-week sales compariso [float] [[compare-time-as-percent]] -==== Compare time ranges as a percent change +==== Analyze the percent change between time ranges -To view the percent change in sales between the current time and the previous week, create a *Formula*. +With *Formula*, you can analyze the percent change in your data from different time ranges. + +To compare time range changes as a percent, create a bar chart that compares the sales in the current week with sales from the previous week: . On the dashboard, click *Create visualization*. @@ -299,11 +311,11 @@ To view the percent change in sales between the current time and the previous we . In the layer pane, click *Count of Records*. -.. Click *Formula*, then enter `count() / count(shift='1w') - 1`. +. Click *Formula*, then enter `count() / count(shift='1w') - 1`. -.. Open the *Value format* dropdown, select *Percent*, then enter `0` in the *D*ecimals* field. +. Open the *Value format* dropdown, select *Percent*, then enter `0` in the *Decimals* field. -.. In the *Display name* field, enter `Percent change`, then click *Close*. +. In the *Display name* field, enter `Percent of change`, then click *Close*. + [role="screenshot"] image::images/lens_percent_chage.png[Bar chart with percent change in sales between the current time and the previous week] @@ -312,34 +324,33 @@ image::images/lens_percent_chage.png[Bar chart with percent change in sales betw [discrete] [[view-customers-over-time-by-continents]] -=== Create a table of customers by category over time +=== Analyze the data in a table -Tables are useful when you want to display the actual field values. -You can build a date histogram table, and group the customer count metric by category, such as the continent registered in user accounts. +With tables, you can view and compare the field values, which is useful for displaying the locations of customer orders. -In *Lens* you can split the metric in a table leveraging the *Columns* field, where each data value from the aggregation is used as column of the table and the relative metric value is shown. +Create a date histogram table and group the customer count metric by category, such as the continent registered in user accounts: . On the dashboard, click *Create visualization*. -. Open the *Chart Type* dropdown, then click *Table*. +. Open the *Visualization type* dropdown, then select *Table*. . From the *Available fields* list, drag *customer_id* to the *Metrics* field in the layer pane. -. In the layer pane, click *Unique count of customer_id*. +.. In the layer pane, click *Unique count of customer_id*. -. In the *Display name* field, enter `Customers`, then click *Close*. +.. In the *Display name* field, enter `Customers`, then click *Close*. . From the *Available fields* list, drag *order_date* to the *Rows* field in the layer pane. -. In the layer pane, click the *order_date*. +.. In the layer pane, click the *order_date*. .. Select *Customize time interval*. .. Change the *Minimum interval* to *1 days*. -.. In the *Display name* field, enter `Sale`, then click *Close*. +.. In the *Display name* field, enter `Sales`, then click *Close*. -Add columns for each continent. +To split the metric, add columns for each continent using the *Columns* field: . From the *Available fields* list, drag *geoip.continent_name* to the *Columns* field in the layer pane. + @@ -360,3 +371,6 @@ Now that you have a complete overview of your ecommerce sales data, save the das . Select *Store time with dashboard*. . Click *Save*. + +[role="screenshot"] +image::images/lens_timeSeriesDataTutorialDashboard_7.16.png[Final dashboard with ecommerce sample data] diff --git a/docs/user/dashboard/lens.asciidoc b/docs/user/dashboard/lens.asciidoc index c3e0a5523a78d..23a6d1fbcfd3d 100644 --- a/docs/user/dashboard/lens.asciidoc +++ b/docs/user/dashboard/lens.asciidoc @@ -48,6 +48,8 @@ Choose the data you want to visualize. . If you want to learn more about the data a field contains, click the field. +. To visualize more than one index pattern, click *Add layer > Add visualization layer*, then select the index pattern. + Edit and delete. . To change the aggregation *Quick function* and display options, click the field in the layer pane. @@ -60,11 +62,11 @@ Edit and delete. Change the fields list to display a different index pattern, different time range, or add your own fields. -* To create a visualization with fields in a different index pattern, open the *Change index pattern* dropdown, then select the index pattern. +* To create a visualization with fields in a different index pattern, open the *Index pattern* dropdown, then select the index pattern. * If the fields list is empty, change the <>. -* To add fields, open the action menu (*...*) next to the *Change index pattern* dropdown, then select *Add field to index pattern*. +* To add fields, open the action menu (*...*) next to the *Index pattern* dropdown, then select *Add field to index pattern*. + [role="screenshot"] image:images/runtime-field-menu.png[Dropdown menu located next to index pattern field with items for adding and managing fields, width=50%] @@ -176,6 +178,29 @@ Compare your real-time data set to the results that are offset by a time increme For a time shift example, refer to <>. +[float] +[[add-reference-lines]] +==== Add reference lines + +With reference lines, you can identify specific values in your visualizations with icons, colors, and other display options. You can add reference lines to any visualization type that displays axes. + +For example, to track the number of bytes in the 75th percentile, add a shaded *Percentile* reference line to your time series visualization. + +[role="screenshot"] +image::images/lens_referenceLine_7.16.png[Lens drag and drop focus state] + +. In the layer pane, click *Add layer > Add reference layer*. + +. Click the reference line value, then specify the reference line you want to use: + +* To add a static reference line, click *Static*, then enter the reference line value you want to use. + +* To add a dynamic reference line, click *Quick functions*, then click and configure the functions you want to use. + +* To calculate the reference line value with math, click *Formula*, then enter the formula. + +. Specify the display options, such as *Display name* and *Icon*, then click *Close*. + [float] [[filter-the-data]] ==== Apply filters @@ -236,9 +261,29 @@ The following component menus are available: * *Left axis*, *Bottom axis*, and *Right axis* — Specify how you want to display the chart axes. For example, add axis labels and change the orientation and bounds. +[float] +[[view-data-and-requests]] +==== View the visualization data and requests + +To view the data included in the visualization and the requests that collected the data, use the *Inspector*. + +. In the toolbar, click *Inspect*. + +. Open the *View* dropdown, then click *Data*. + +.. From the dropdown, select the table that contains the data you want to view. + +.. To download the data, click *Download CSV*, then select the format type. + +. Open the *View* dropdown, then click *Requests*. + +.. From the dropdown, select the requests you want to view. + +.. To view the requests in *Console*, click *Request*, then click *Open in Console*. + [float] [[save-the-lens-panel]] -===== Save and add the panel +==== Save and add the panel Save the panel to the *Visualize Library* and add it to the dashboard, or add it to the dashboard without saving. @@ -408,7 +453,7 @@ To configure the bounds, use the menus in the editor toolbar. Bar and area chart .*Is it possible to display icons in data tables?* [%collapsible] ==== -You can display icons with <> in data tables. +You can display icons with <> in data tables. ==== [discrete] diff --git a/docs/user/dashboard/tutorial-create-a-dashboard-of-lens-panels.asciidoc b/docs/user/dashboard/tutorial-create-a-dashboard-of-lens-panels.asciidoc index c3d76ee88322b..e270c16cf60f6 100644 --- a/docs/user/dashboard/tutorial-create-a-dashboard-of-lens-panels.asciidoc +++ b/docs/user/dashboard/tutorial-create-a-dashboard-of-lens-panels.asciidoc @@ -1,21 +1,24 @@ [[create-a-dashboard-of-panels-with-web-server-data]] -== Build your first dashboard +== Create your first dashboard -Learn the most common ways to build a dashboard from your own data. +Learn the most common ways to create a dashboard from your own data. The tutorial will use sample data from the perspective of an analyst looking at website logs, but this type of dashboard works on any type of data. -Before using this tutorial, you should be familiar with the <>. + +When you're done, you'll have a complete overview of the sample web logs data. [role="screenshot"] -image::images/lens_end_to_end_dashboard.png[Final dashboard vis] +image::images/lens_logsDashboard_7.16.png[Logs dashboard] + +Before you begin, you should be familiar with the <>. [discrete] [[add-the-data-and-create-the-dashboard]] === Add the data and create the dashboard -Add the sample web logs data that you'll use to create the dashboard panels. +Add the sample web logs data, and create and set up the dashboard. -. Go to the {kib} *Home* page, then click *Try our sample data*. +. Go to the *Home* page, then click *Try sample data*. . On the *Sample web logs* card, click *Add data*. @@ -29,56 +32,70 @@ Create the dashboard where you'll display the visualization panels. [float] [[open-and-set-up-lens]] -=== Open Lens and get familiar with the data +=== Open the visualization editor and get familiar with the data + +Open the visualization editor, then make sure the correct fields appear. . On the dashboard, click *Create visualization*. . Make sure the *kibana_sample_data_logs* index appears. + [role="screenshot"] -image::images/lens_end_to_end_1_2.png[Lens index pattern selector, width=50%] +image::images/lens_indexPatternDropDown_7.16.png[Index pattern dropdown] + +To create the visualizations in this tutorial, you'll use the following fields: + +* *Records* -. To create the visualizations in this tutorial, you'll use the *Records*, *timestamp*, *bytes*, *clientip*, and *referer.keyword* fields. To see the most frequent values of a field, hover over the field name, then click *i*. +* *timestamp* + +* *bytes* + +* *clientip* + +* *referer.keyword* + +To see the most frequent values in a field, hover over the field name, then click *i*. [discrete] [[view-the-number-of-website-visitors]] === Create your first visualization -Pick a field you want to analyze, such as *clientip*. If you want -to analyze only this field, you can use the *Metric* visualization to display a big number. -The only number function that you can use with *clientip* is *Unique count*. -*Unique count*, also referred to as cardinality, approximates the number of unique values -of the *clientip* field. +Pick a field you want to analyze, such as *clientip*. To analyze only the *clientip* field, use the *Metric* visualization to display the field as a number. + +The only number function that you can use with *clientip* is *Unique count*, also referred to as cardinality, which approximates the number of unique values. -. To select the visualization type, open the *Chart type* dropdown, then select *Metric*. +. Open the *Visualization type* dropdown, then select *Metric*. + [role="screenshot"] -image::images/lens_end_to_end_1_2_1.png[Chart Type dropdown with Metric selected, width=50%] +image::images/lens_visualizationTypeDropdown_7.16.png[Visualization type dropdown] -. From the *Available fields* list, drag *clientip* to the workspace. +. From the *Available fields* list, drag *clientip* to the workspace or layer pane. + [role="screenshot"] -image::images/lens_end_to_end_1_3.png[Changed type and dropped clientip field] +image::images/lens_metricUniqueCountOfClientip_7.16.png[Metric visualization of the clientip field] + -*Lens* selects the *Unique count* function because it is the only numeric function -that works for IP addresses. You can also drag *clientip* to the layer pane for the same result. +In the layer pane, *Unique count of clientip* appears because the editor automatically applies the *Unique count* function to the *clientip* field. *Unique count* is the only numeric function that works with IP addresses. . In the layer pane, click *Unique count of clientip*. .. In the *Display name* field, enter `Unique visitors`. .. Click *Close*. ++ +[role="screenshot"] +image::images/lens_metricUniqueVisitors_7.16.png[Metric visualization that displays number of unique visitors] . Click *Save and return*. + -The metric visualization has its own label, so you do not need to add a panel title. +*[No Title]* appears in the visualization panel header. Since the visualization has its own `Unique visitors` label, you do not need to add a panel title. [discrete] [[mixed-multiaxis]] === View a metric over time -*Lens* has two shortcuts that simplify viewing metrics over time. -If you drag a numeric field to the workspace, *Lens* adds the default +There are two shortcuts you can use to view metrics over time. +When you drag a numeric field to the workspace, the visualization editor adds the default time field from the index pattern. When you use the *Date histogram* function, you can replace the time field by dragging the field to the workspace. @@ -88,78 +105,76 @@ To visualize the *bytes* field over time: . From the *Available fields* list, drag *bytes* to the workspace. + -*Lens* creates a bar chart with the *timestamp* and *Median of bytes* fields, and automatically chooses a date interval. +The visualization editor creates a bar chart with the *timestamp* and *Median of bytes* fields. -. To zoom in on the data you want to view, click and drag your cursor across the bars. +. To zoom in on the data, click and drag your cursor across the bars. + [role="screenshot"] image::images/lens_end_to_end_3_1_1.gif[Zoom in on the data] -To emphasize the change in *Median of bytes* over time, change to a line chart with one of the following options: - -* From the *Suggestions*, click the line chart. -* Open the *Chart type* dropdown in the editor toolbar, then select *Line*. -* Open the *Chart type* menu in the layer pane, then click the line chart. +To emphasize the change in *Median of bytes* over time, change the visualization type to *Line* with one of the following options: -You can increase and decrease the minimum interval that *Lens* uses, but you are unable to decrease the interval -below the <>. +* In the *Suggestions*, click the line chart. +* In the editor toolbar, open the *Visualization type* dropdown, then select *Line*. +* In the layer pane, open the *Layer visualization type* menu, then click *Line*. -To set the minimum time interval: +To increase the minimum time interval: . In the layer pane, click *timestamp*. . Select *Customize time interval*. . Change the *Minimum interval* to *1 days*, then click *Close*. ++ +You can increase and decrease the minimum interval, but you are unable to decrease the interval below the <>. -To save space on the dashboard, hide the vertical and horizontal axis labels. +To save space on the dashboard, hide the axis labels. . Open the *Left axis* menu, then deselect *Show*. + [role="screenshot"] -image::images/lens_end_to_end_4_3.png[Turn off axis label] +image::images/lens_leftAxisMenu_7.16.png[Left axis menu] . Open the *Bottom axis* menu, then deselect *Show*. ++ +[role="screenshot"] +image::images/lens_lineChartMetricOverTime_7.16.png[Line chart that displays metric data over time] . Click *Save and return* -Add a panel title to explain the panel, which is necessary because you removed the axis labels. +Since you removed the axis labels, add a panel title: -.. Open the panel menu, then select *Edit panel title*. +. Open the panel menu, then select *Edit panel title*. -.. In the *Panel title* field, enter `Median of bytes`, then click *Save*. +. In the *Panel title* field, enter `Median of bytes`, then click *Save*. [discrete] [[view-the-distribution-of-visitors-by-operating-system]] === View the top values of a field +Create a visualization that displays the most frequent values of *request.keyword* on your website, ranked by the unique visitors. +To create the visualization, use *Top values of request.keyword* ranked by *Unique count of clientip*, instead of being ranked by *Count of records*. + The *Top values* function ranks the unique values of a field by another function. The values are the most frequent when ranked by a *Count* function, and the largest when ranked by the *Sum* function. -Create a visualization that displays the most frequent values of *request.keyword* on your website, ranked by the unique visitors. -To create the visualization, use *Top values of request.keyword* ranked by *Unique count of clientip*, instead of -being ranked by *Count of records*. - . On the dashboard, click *Create visualization*. . From the *Available fields* list, drag *clientip* to the *Vertical axis* field in the layer pane. + -*Lens* automatically chooses the *Unique count* function. If you drag *clientip* to the workspace, *Lens* adds the field to the incorrect axis. -+ -When you drag a text or IP address field to the workspace, -*Lens* adds the *Top values* function ranked by *Count of records* to show the most frequent values. +The visualization editor automatically applies the *Unique count* function. If you drag *clientip* to the workspace, the editor adds the field to the incorrect axis. . Drag *request.keyword* to the workspace. + [role="screenshot"] image::images/lens_end_to_end_2_1_1.png[Vertical bar chart with top values of request.keyword by most unique visitors] + -*Lens* adds *Top values of request.keyword* to the *Horizontal axis*. +When you drag a text or IP address field to the workspace, +the editor adds the *Top values* function ranked by *Count of records* to show the most frequent values. -The chart is hard to read because the *request.keyword* field contains long text. You could try -using one of the *Suggestions*, but the suggestions also have issues with long text. Instead, create a *Table* visualization. +The chart labels are unable to display because the *request.keyword* field contains long text fields. You could use one of the *Suggestions*, but the suggestions also have issues with long text. The best way to display long text fields is with the *Table* visualization. -. Open the *Chart type* dropdown, then select *Table*. +. Open the *Visualization type* dropdown, then select *Table*. + [role="screenshot"] image::images/lens_end_to_end_2_1_2.png[Table with top values of request.keyword by most unique visitors] @@ -171,16 +186,19 @@ image::images/lens_end_to_end_2_1_2.png[Table with top values of request.keyword .. In the *Display name* field, enter `Page URL`. .. Click *Close*. ++ +[role="screenshot"] +image::images/lens_tableTopFieldValues_7.16.png[Table that displays the top field values] . Click *Save and return*. + -The table does not need a panel title because the columns are clearly labeled. +Since the table columns are labeled, you do not need to add a panel title. [discrete] [[custom-ranges]] === Compare a subset of documents to all documents -Create a proportional visualization that helps you to determine if your users transfer more bytes from documents under 10KB versus documents over 10 Kb. +Create a proportional visualization that helps you determine if your users transfer more bytes from documents under 10KB versus documents over 10Kb. . On the dashboard, click *Create visualization*. @@ -190,12 +208,14 @@ Create a proportional visualization that helps you to determine if your users tr . From the *Available fields* list, drag *bytes* to the *Break down by* field in the layer pane. -Use the *Intervals* function to select documents based on the number range of a field. -If the ranges were non numeric, or if the query required multiple clauses, you could use the *Filters* function. +To select documents based on the number range of a field, use the *Intervals* function. +When the ranges are non numeric, or the query requires multiple clauses, you could use the *Filters* function. + +Specify the file size ranges: -. To specify the file size ranges, click *bytes* in the layer pane. +. In the layer pane, click *bytes*. -. Click *Create custom ranges*, enter the following, then press Return: +. Click *Create custom ranges*, enter the following in the *Ranges* field, then press Return: * *Ranges* — `0` -> `10240` @@ -214,27 +234,30 @@ image::images/lens_end_to_end_6_1.png[Custom ranges configuration] To display the values as a percentage of the sum of all values, use the *Pie* chart. -. Open the *Chart Type* dropdown, then select *Pie*. +. Open the *Visualization Type* dropdown, then select *Pie*. ++ +[role="screenshot"] +image::images/lens_pieChartCompareSubsetOfDocs_7.16.png[Pie chart that compares a subset of documents to all documents] . Click *Save and return*. -. Add a panel title. +Add a panel title: -.. Open the panel menu, then select *Edit panel title*. +. Open the panel menu, then select *Edit panel title*. -.. In the *Panel title* field, enter `Sum of bytes from large requests`, then click *Save*. +. In the *Panel title* field, enter `Sum of bytes from large requests`, then click *Save*. [discrete] [[histogram]] === View the distribution of a number field -Knowing the distribution of a number helps you find patterns. For example, you can analyze the website traffic per hour to find the best time to do routine maintenance. +The distribution of a number can help you find patterns. For example, you can analyze the website traffic per hour to find the best time for routine maintenance. . On the dashboard, click *Create visualization*. . From the *Available fields* list, drag *bytes* to *Vertical axis* field in the layer pane. -. In the layer pane, click *Median of bytes* +. In the layer pane, click *Median of bytes*. .. Click the *Sum* function. @@ -246,70 +269,80 @@ Knowing the distribution of a number helps you find patterns. For example, you c . In the layer pane, click *hour_of_day*, then slide the *Intervals granularity* slider until the horizontal axis displays hourly intervals. + -The *Intervals* function displays an evenly spaced distribution of the field. +[role="screenshot"] +image::images/lens_barChartDistributionOfNumberField_7.16.png[Bar chart that displays the distribution of a number field] . Click *Save and return*. +Add a panel title: + +. Open the panel menu, then select *Edit panel title*. + +. In the *Panel title* field, enter `Website traffic`, then click *Save*. + [discrete] [[treemap]] === Create a multi-level chart -You can use multiple functions in data tables and proportion charts. For example, -to create a chart that breaks down the traffic sources and user geography, use *Filters* and -*Top values*. +*Table* and *Proportion* visualizations support multiple functions. For example, to create visualizations that break down the data by website traffic sources and user geography, apply the *Filters* and *Top values* functions. . On the dashboard, click *Create visualization*. -. Open the *Chart type* dropdown, then select *Treemap*. +. Open the *Visualization type* dropdown, then select *Treemap*. . From the *Available fields* list, drag *Records* to the *Size by* field in the layer pane. -. In the editor, click the *Drop a field or click to add* field for *Group by*, then create a filter for each website traffic source. +. In the editor, click *Add or drag-and-drop a field* for *Group by*. -.. From *Select a function*, click *Filters*. +Create a filter for each website traffic source: -.. Click *All records*, enter the following, then press Return: +. From *Select a function*, click *Filters*. + +. Click *All records*, enter the following in the query bar, then press Return: * *KQL* — `referer : *facebook.com*` * *Label* — `Facebook` -.. Click *Add a filter*, enter the following, then press Return: +. Click *Add a filter*, enter the following in the query bar, then press Return: * *KQL* — `referer : *twitter.com*` * *Label* — `Twitter` -.. Click *Add a filter*, enter the following, then press Return: +. Click *Add a filter*, enter the following in the query bar, then press Return: * *KQL* — `NOT referer : *twitter.com* OR NOT referer: *facebook.com*` * *Label* — `Other` -.. Click *Close*. +. Click *Close*. -Add a geography grouping: +Add the user geography grouping: -. From the *Available fields* list, drag *geo.src* to the workspace. +. From the *Available fields* list, drag *geo.srcdest* to the workspace. -. To change the *Group by* order, drag *Top values of geo.src* so that it appears first. +. To change the *Group by* order, drag *Top values of geo.srcdest* in the layer pane so that appears first. + [role="screenshot"] image::images/lens_end_to_end_7_2.png[Treemap visualization] -. To view only the Facebook and Twitter data, remove the *Other* category. +Remove the documents that do not match the filter criteria: -.. In the layer pane, click *Top values of geo.src*. +. In the layer pane, click *Top values of geo.srcdest*. -.. Open the *Advanced* dropdown, deselect *Group other values as "Other"*, then click *Close*. +. Click *Advanced*, then deselect *Group other values as "Other"*, the click *Close*. ++ +[role="screenshot"] +image::images/lens_treemapMultiLevelChart_7.16.png[Treemap visualization] . Click *Save and return*. -. Add a panel title. +Add a panel title: -.. Open the panel menu, then select *Edit panel title*. +. Open the panel menu, then select *Edit panel title*. -.. In the *Panel title* field, enter `Page views by location and referrer`, then click *Save*. +. In the *Panel title* field, enter `Page views by location and referrer`, then click *Save*. [float] [[arrange-the-lens-panels]] @@ -317,7 +350,7 @@ image::images/lens_end_to_end_7_2.png[Treemap visualization] Resize and move the panels so they all appear on the dashboard without scrolling. -Decrease the size of the following panels, then move them to the first row: +Decrease the size of the following panels, then move the panels to the first row: * *Unique visitors* @@ -325,7 +358,10 @@ Decrease the size of the following panels, then move them to the first row: * *Sum of bytes from large requests* -* *hour_of_day* +* *Website traffic* ++ +[role="screenshot"] +image::images/lens_logsDashboard_7.16.png[Logs dashboard] [discrete] === Save the dashboard diff --git a/docs/user/graph/configuring-graph.asciidoc b/docs/user/graph/configuring-graph.asciidoc index 968e08db33d49..aa9e6e6db3ee6 100644 --- a/docs/user/graph/configuring-graph.asciidoc +++ b/docs/user/graph/configuring-graph.asciidoc @@ -8,7 +8,7 @@ By default, both the configuration and data are saved for the workspace: [horizontal] *configuration*:: -The selected index pattern, fields, colors, icons, +The selected data view, fields, colors, icons, and settings. *data*:: The visualized content (the vertices and connections displayed in diff --git a/docs/user/management.asciidoc b/docs/user/management.asciidoc index 1f38d50e2d0bd..9d6392c39ba84 100644 --- a/docs/user/management.asciidoc +++ b/docs/user/management.asciidoc @@ -4,7 +4,7 @@ [partintro] -- *Stack Management* is home to UIs for managing all things Elastic Stack— -indices, clusters, licenses, UI settings, index patterns, spaces, and more. +indices, clusters, licenses, UI settings, data views, spaces, and more. Access to individual features is governed by {es} and {kib} privileges. @@ -128,12 +128,12 @@ Kerberos, PKI, OIDC, and SAML. [cols="50, 50"] |=== -a| <> -|Manage the data fields in the index patterns that retrieve your data from {es}. +a| <> +|Manage the fields in the data views that retrieve your data from {es}. | <> | Copy, edit, delete, import, and export your saved objects. -These include dashboards, visualizations, maps, index patterns, Canvas workpads, and more. +These include dashboards, visualizations, maps, data views, Canvas workpads, and more. | <> |Create, manage, and assign tags to your saved objects. @@ -183,7 +183,7 @@ include::{kib-repo-dir}/management/action-types.asciidoc[] include::{kib-repo-dir}/management/managing-licenses.asciidoc[] -include::{kib-repo-dir}/management/manage-index-patterns.asciidoc[] +include::{kib-repo-dir}/management/manage-data-views.asciidoc[] include::{kib-repo-dir}/management/numeral.asciidoc[] diff --git a/docs/user/monitoring/kibana-alerts.asciidoc b/docs/user/monitoring/kibana-alerts.asciidoc index 64ba8bf044e4f..f6deaed7fa3b9 100644 --- a/docs/user/monitoring/kibana-alerts.asciidoc +++ b/docs/user/monitoring/kibana-alerts.asciidoc @@ -5,21 +5,21 @@ The {stack} {monitor-features} provide <> out-of-the box to notify you of potential issues in the {stack}. These rules are preconfigured based on the -best practices recommended by Elastic. However, you can tailor them to meet your +best practices recommended by Elastic. However, you can tailor them to meet your specific needs. [role="screenshot"] image::user/monitoring/images/monitoring-kibana-alerting-notification.png["{kib} alerting notifications in {stack-monitor-app}"] -When you open *{stack-monitor-app}* for the first time, you will be asked to acknowledge the creation of these default rules. They are initially configured to detect and notify on various +When you open *{stack-monitor-app}* for the first time, you will be asked to acknowledge the creation of these default rules. They are initially configured to detect and notify on various conditions across your monitored clusters. You can view notifications for: *Cluster health*, *Resource utilization*, and *Errors and exceptions* for {es} in real time. -NOTE: The default {watcher} based "cluster alerts" for {stack-monitor-app} have -been recreated as rules in {kib} {alert-features}. For this reason, the existing -{watcher} email action +NOTE: The default {watcher} based "cluster alerts" for {stack-monitor-app} have +been recreated as rules in {kib} {alert-features}. For this reason, the existing +{watcher} email action `monitoring.cluster_alerts.email_notifications.email_address` no longer works. -The default action for all {stack-monitor-app} rules is to write to {kib} logs +The default action for all {stack-monitor-app} rules is to write to {kib} logs and display a notification in the UI. To review and modify existing *{stack-monitor-app}* rules, click *Enter setup mode* on the *Cluster overview* page. @@ -47,21 +47,21 @@ checks on a schedule time of 1 minute with a re-notify interval of 1 day. This rule checks for {es} nodes that use a high amount of JVM memory. By default, the condition is set at 85% or more averaged over the last 5 minutes. -The default rule checks on a schedule time of 1 minute with a re-notify interval of 1 day. +The default rule checks on a schedule time of 1 minute with a re-notify interval of 1 day. [discrete] [[kibana-alerts-missing-monitoring-data]] == Missing monitoring data -This rule checks for {es} nodes that stop sending monitoring data. By default, +This rule checks for {es} nodes that stop sending monitoring data. By default, the condition is set to missing for 15 minutes looking back 1 day. The default rule checks on a schedule -time of 1 minute with a re-notify interval of 6 hours. +time of 1 minute with a re-notify interval of 6 hours. [discrete] [[kibana-alerts-thread-pool-rejections]] == Thread pool rejections (search/write) -This rule checks for {es} nodes that experience thread pool rejections. By +This rule checks for {es} nodes that experience thread pool rejections. By default, the condition is set at 300 or more over the last 5 minutes. The default rule checks on a schedule time of 1 minute with a re-notify interval of 1 day. Thresholds can be set independently for `search` and `write` type rejections. @@ -72,14 +72,14 @@ independently for `search` and `write` type rejections. This rule checks for read exceptions on any of the replicated {es} clusters. The condition is met if 1 or more read exceptions are detected in the last hour. The -default rule checks on a schedule time of 1 minute with a re-notify interval of 6 hours. +default rule checks on a schedule time of 1 minute with a re-notify interval of 6 hours. [discrete] [[kibana-alerts-large-shard-size]] == Large shard size This rule checks for a large average shard size (across associated primaries) on -any of the specified index patterns in an {es} cluster. The condition is met if +any of the specified data views in an {es} cluster. The condition is met if an index's average shard size is 55gb or higher in the last 5 minutes. The default rule matches the pattern of `-.*` by running checks on a schedule time of 1 minute with a re-notify interval of 12 hours. @@ -124,8 +124,8 @@ valid for 30 days. == Alerts and rules [discrete] === Create default rules -This option can be used to create default rules in this kibana space. This is -useful for scenarios when you didn't choose to create these default rules initially +This option can be used to create default rules in this Kibana space. This is +useful for scenarios when you didn't choose to create these default rules initially or anytime later if the rules were accidentally deleted. NOTE: Some action types are subscription features, while others are free. diff --git a/docs/user/security/securing-kibana.asciidoc b/docs/user/security/securing-kibana.asciidoc index a4ec2ecadece3..bdb36a6fe117c 100644 --- a/docs/user/security/securing-kibana.asciidoc +++ b/docs/user/security/securing-kibana.asciidoc @@ -108,7 +108,7 @@ TIP: For more information on Basic Authentication and additional methods of auth TIP: You can define as many different roles for your {kib} users as you need. For example, create roles that have `read` and `view_index_metadata` privileges -on specific index patterns. For more information, see +on specific data views. For more information, see {ref}/authorization.html[User authorization]. -- diff --git a/examples/embeddable_examples/public/hello_world/hello_world_embeddable.tsx b/examples/embeddable_examples/public/hello_world/hello_world_embeddable.tsx index 3d0e82b6f99c1..afec509037434 100644 --- a/examples/embeddable_examples/public/hello_world/hello_world_embeddable.tsx +++ b/examples/embeddable_examples/public/hello_world/hello_world_embeddable.tsx @@ -33,7 +33,8 @@ export class HelloWorldEmbeddable extends Embeddable { * @param node */ public render(node: HTMLElement) { - node.innerHTML = '
    HELLO WORLD!
    '; + node.innerHTML = + '
    HELLO WORLD!
    '; } /** diff --git a/examples/embeddable_examples/public/todo/todo_component.tsx b/examples/embeddable_examples/public/todo/todo_component.tsx index 10d48881c72e5..f2ba44de407ee 100644 --- a/examples/embeddable_examples/public/todo/todo_component.tsx +++ b/examples/embeddable_examples/public/todo/todo_component.tsx @@ -41,7 +41,7 @@ function wrapSearchTerms(task: string, search?: string) { export function TodoEmbeddableComponentInner({ input: { icon, title, task, search } }: Props) { return ( - + {icon ? : } diff --git a/examples/embeddable_examples/public/todo/todo_ref_component.tsx b/examples/embeddable_examples/public/todo/todo_ref_component.tsx index 53ed20042dc5b..d70db903d1dac 100644 --- a/examples/embeddable_examples/public/todo/todo_ref_component.tsx +++ b/examples/embeddable_examples/public/todo/todo_ref_component.tsx @@ -45,7 +45,7 @@ export function TodoRefEmbeddableComponentInner({ const title = savedAttributes?.title; const task = savedAttributes?.task; return ( - + {icon ? ( diff --git a/jest.config.js b/jest.config.js index 09532dc28bbb2..ae07034c10781 100644 --- a/jest.config.js +++ b/jest.config.js @@ -17,5 +17,8 @@ module.exports = { '/src/plugins/vis_types/*/jest.config.js', '/test/*/jest.config.js', '/x-pack/plugins/*/jest.config.js', + '/x-pack/plugins/security_solution/*/jest.config.js', + '/x-pack/plugins/security_solution/public/*/jest.config.js', + '/x-pack/plugins/security_solution/server/*/jest.config.js', ], }; diff --git a/logs/.empty b/logs/.empty new file mode 100644 index 0000000000000..e69de29bb2d1d diff --git a/package.json b/package.json index b77c88f7a2ba3..c30294bf86718 100644 --- a/package.json +++ b/package.json @@ -62,7 +62,8 @@ "backport-skip-ci": "backport --prDescription \"[skip-ci]\"", "storybook": "node scripts/storybook", "cover:report": "nyc report --temp-dir target/kibana-coverage/functional --report-dir target/coverage/report --reporter=lcov && open ./target/coverage/report/lcov-report/index.html", - "cover:functional:merge": "nyc report --temp-dir target/kibana-coverage/functional --report-dir target/coverage/report/functional --reporter=json-summary" + "cover:functional:merge": "nyc report --temp-dir target/kibana-coverage/functional --report-dir target/coverage/report/functional --reporter=json-summary", + "postinstall": "node scripts/kbn patch_native_modules" }, "repository": { "type": "git", @@ -95,10 +96,10 @@ "@dnd-kit/core": "^3.1.1", "@dnd-kit/sortable": "^4.0.0", "@dnd-kit/utilities": "^2.0.0", - "@elastic/apm-synthtrace": "link:bazel-bin/packages/elastic-apm-synthtrace", "@elastic/apm-rum": "^5.9.1", "@elastic/apm-rum-react": "^1.3.1", - "@elastic/charts": "38.1.3", + "@elastic/apm-synthtrace": "link:bazel-bin/packages/elastic-apm-synthtrace", + "@elastic/charts": "39.0.0", "@elastic/datemath": "link:bazel-bin/packages/elastic-datemath", "@elastic/elasticsearch": "npm:@elastic/elasticsearch-canary@^8.0.0-canary.35", "@elastic/ems-client": "8.0.0", @@ -111,7 +112,10 @@ "@elastic/request-crypto": "1.1.4", "@elastic/safer-lodash-set": "link:bazel-bin/packages/elastic-safer-lodash-set", "@elastic/search-ui-app-search-connector": "^1.6.0", + "@emotion/cache": "^11.4.0", + "@emotion/css": "^11.4.0", "@emotion/react": "^11.4.0", + "@emotion/serialize": "^1.0.2", "@hapi/accept": "^5.0.2", "@hapi/boom": "^9.1.4", "@hapi/cookie": "^11.0.2", @@ -136,6 +140,7 @@ "@kbn/logging": "link:bazel-bin/packages/kbn-logging", "@kbn/mapbox-gl": "link:bazel-bin/packages/kbn-mapbox-gl", "@kbn/monaco": "link:bazel-bin/packages/kbn-monaco", + "@kbn/react-field": "link:bazel-bin/packages/kbn-react-field", "@kbn/rule-data-utils": "link:bazel-bin/packages/kbn-rule-data-utils", "@kbn/securitysolution-autocomplete": "link:bazel-bin/packages/kbn-securitysolution-autocomplete", "@kbn/securitysolution-es-utils": "link:bazel-bin/packages/kbn-securitysolution-es-utils", @@ -179,8 +184,6 @@ "@turf/distance": "6.0.1", "@turf/helpers": "6.0.1", "@turf/length": "^6.0.2", - "@types/react-router-config": "^5.0.2", - "@types/redux-logger": "^3.0.8", "JSONStream": "1.3.5", "abort-controller": "^3.0.0", "abortcontroller-polyfill": "^1.7.3", @@ -188,7 +191,6 @@ "archiver": "^5.2.0", "axios": "^0.21.1", "base64-js": "^1.3.1", - "bluebird": "3.5.5", "brace": "0.11.1", "broadcast-channel": "^4.2.0", "chalk": "^4.1.0", @@ -218,7 +220,7 @@ "deep-freeze-strict": "^1.1.1", "deepmerge": "^4.2.2", "del": "^5.1.0", - "elastic-apm-node": "^3.23.0", + "elastic-apm-node": "3.24.0", "execa": "^4.0.2", "exit-hook": "^2.2.0", "expiry-js": "0.1.7", @@ -461,8 +463,8 @@ "@kbn/test-subj-selector": "link:bazel-bin/packages/kbn-test-subj-selector", "@loaders.gl/polyfills": "^2.3.5", "@mapbox/vector-tile": "1.3.1", - "@microsoft/api-documenter": "7.7.2", - "@microsoft/api-extractor": "7.7.0", + "@microsoft/api-documenter": "7.13.68", + "@microsoft/api-extractor": "7.18.19", "@octokit/rest": "^16.35.0", "@percy/agent": "^0.28.6", "@storybook/addon-a11y": "^6.1.20", @@ -489,7 +491,6 @@ "@types/archiver": "^5.1.0", "@types/babel__core": "^7.1.16", "@types/base64-js": "^1.2.5", - "@types/bluebird": "^3.1.1", "@types/chance": "^1.0.0", "@types/chroma-js": "^1.4.2", "@types/chromedriver": "^81.0.0", @@ -542,6 +543,7 @@ "@types/jest-when": "^2.7.2", "@types/joi": "^17.2.3", "@types/jquery": "^3.3.31", + "@types/js-levenshtein": "^1.1.0", "@types/js-search": "^1.4.0", "@types/js-yaml": "^3.11.1", "@types/jsdom": "^16.2.3", @@ -592,13 +594,16 @@ "@types/react-redux": "^7.1.9", "@types/react-resize-detector": "^4.0.1", "@types/react-router": "^5.1.7", + "@types/react-router-config": "^5.0.2", "@types/react-router-dom": "^5.1.5", "@types/react-test-renderer": "^16.9.1", "@types/react-virtualized": "^9.18.7", + "@types/react-vis": "^1.11.9", "@types/read-pkg": "^4.0.0", "@types/recompose": "^0.30.6", "@types/reduce-reducers": "^1.0.0", "@types/redux-actions": "^2.6.1", + "@types/redux-logger": "^3.0.8", "@types/seedrandom": ">=2.0.0 <4.0.0", "@types/selenium-webdriver": "^4.0.15", "@types/semver": "^7", @@ -754,7 +759,7 @@ "mocha-junit-reporter": "^2.0.0", "mochawesome": "^6.2.1", "mochawesome-merge": "^4.2.0", - "mock-fs": "^5.1.1", + "mock-fs": "^5.1.2", "mock-http-server": "1.3.0", "ms-chromium-edge-driver": "^0.4.2", "multimatch": "^4.0.0", @@ -766,7 +771,6 @@ "oboe": "^2.1.4", "parse-link-header": "^1.0.1", "pbf": "3.2.1", - "pdf-to-img": "^1.1.1", "pirates": "^4.0.1", "pixelmatch": "^5.1.0", "postcss": "^7.0.32", diff --git a/packages/BUILD.bazel b/packages/BUILD.bazel index bda4f1b79df55..214f5f9abe5cd 100644 --- a/packages/BUILD.bazel +++ b/packages/BUILD.bazel @@ -36,6 +36,7 @@ filegroup( "//packages/kbn-optimizer:build", "//packages/kbn-plugin-generator:build", "//packages/kbn-plugin-helpers:build", + "//packages/kbn-react-field:build", "//packages/kbn-rule-data-utils:build", "//packages/kbn-securitysolution-autocomplete:build", "//packages/kbn-securitysolution-list-constants:build", diff --git a/packages/elastic-apm-synthtrace/src/lib/utils/clean_write_targets.ts b/packages/elastic-apm-synthtrace/src/lib/utils/clean_write_targets.ts index 3c514e1097b31..4a2ab281a2849 100644 --- a/packages/elastic-apm-synthtrace/src/lib/utils/clean_write_targets.ts +++ b/packages/elastic-apm-synthtrace/src/lib/utils/clean_write_targets.ts @@ -27,6 +27,7 @@ export async function cleanWriteTargets({ index: targets, allow_no_indices: true, conflicts: 'proceed', + refresh: true, body: { query: { match_all: {}, diff --git a/packages/elastic-eslint-config-kibana/.eslintrc.js b/packages/elastic-eslint-config-kibana/.eslintrc.js index 6bfefc8e118d4..99377540d38f7 100644 --- a/packages/elastic-eslint-config-kibana/.eslintrc.js +++ b/packages/elastic-eslint-config-kibana/.eslintrc.js @@ -88,6 +88,16 @@ module.exports = { exclude: USES_STYLED_COMPONENTS, disallowedMessage: `Prefer using @emotion/react instead. To use styled-components, ensure you plugin is enabled in @kbn/dev-utils/src/babel.ts.` }, + ...[ + '@elastic/eui/dist/eui_theme_light.json', + '@elastic/eui/dist/eui_theme_dark.json', + '@elastic/eui/dist/eui_theme_amsterdam_light.json', + '@elastic/eui/dist/eui_theme_amsterdam_dark.json', + ].map(from => ({ + from, + to: false, + disallowedMessage: `Use "@kbn/ui-shared-deps-src/theme" to access theme vars.` + })), ], ], diff --git a/packages/kbn-apm-config-loader/src/config.test.ts b/packages/kbn-apm-config-loader/src/config.test.ts index 60d773e3a420b..b0beebfefd6bd 100644 --- a/packages/kbn-apm-config-loader/src/config.test.ts +++ b/packages/kbn-apm-config-loader/src/config.test.ts @@ -21,7 +21,7 @@ describe('ApmConfiguration', () => { beforeEach(() => { // start with an empty env to avoid CI from spoiling snapshots, env is unique for each jest file process.env = {}; - + devConfigMock.raw = {}; packageMock.raw = { version: '8.0.0', build: { @@ -86,10 +86,11 @@ describe('ApmConfiguration', () => { let config = new ApmConfiguration(mockedRootDir, {}, false); expect(config.getConfig('serviceName')).toMatchInlineSnapshot(` Object { - "active": false, + "active": true, "breakdownMetrics": true, "captureSpanStackTraces": false, "centralConfig": false, + "contextPropagationOnly": true, "environment": "development", "globalLabels": Object {}, "logUncaughtExceptions": true, @@ -105,12 +106,13 @@ describe('ApmConfiguration', () => { config = new ApmConfiguration(mockedRootDir, {}, true); expect(config.getConfig('serviceName')).toMatchInlineSnapshot(` Object { - "active": false, + "active": true, "breakdownMetrics": false, "captureBody": "off", "captureHeaders": false, "captureSpanStackTraces": false, "centralConfig": false, + "contextPropagationOnly": true, "environment": "development", "globalLabels": Object { "git_rev": "sha", @@ -162,13 +164,12 @@ describe('ApmConfiguration', () => { it('does not load the configuration from the dev config in distributable', () => { devConfigMock.raw = { - active: true, - serverUrl: 'https://dev-url.co', + active: false, }; const config = new ApmConfiguration(mockedRootDir, {}, true); expect(config.getConfig('serviceName')).toEqual( expect.objectContaining({ - active: false, + active: true, }) ); }); @@ -224,4 +225,130 @@ describe('ApmConfiguration', () => { }) ); }); + + describe('contextPropagationOnly', () => { + it('sets "active: true" and "contextPropagationOnly: true" by default', () => { + expect(new ApmConfiguration(mockedRootDir, {}, false).getConfig('serviceName')).toEqual( + expect.objectContaining({ + active: true, + contextPropagationOnly: true, + }) + ); + + expect(new ApmConfiguration(mockedRootDir, {}, true).getConfig('serviceName')).toEqual( + expect.objectContaining({ + active: true, + contextPropagationOnly: true, + }) + ); + }); + + it('value from config overrides the default', () => { + const kibanaConfig = { + elastic: { + apm: { + active: false, + contextPropagationOnly: false, + }, + }, + }; + + expect( + new ApmConfiguration(mockedRootDir, kibanaConfig, false).getConfig('serviceName') + ).toEqual( + expect.objectContaining({ + active: false, + contextPropagationOnly: false, + }) + ); + + expect( + new ApmConfiguration(mockedRootDir, kibanaConfig, true).getConfig('serviceName') + ).toEqual( + expect.objectContaining({ + active: false, + contextPropagationOnly: false, + }) + ); + }); + + it('is "false" if "active: true" configured and "contextPropagationOnly" is not specified', () => { + const kibanaConfig = { + elastic: { + apm: { + active: true, + }, + }, + }; + + expect( + new ApmConfiguration(mockedRootDir, kibanaConfig, false).getConfig('serviceName') + ).toEqual( + expect.objectContaining({ + active: true, + contextPropagationOnly: false, + }) + ); + + expect( + new ApmConfiguration(mockedRootDir, kibanaConfig, true).getConfig('serviceName') + ).toEqual( + expect.objectContaining({ + active: true, + contextPropagationOnly: false, + }) + ); + }); + + it('throws if "active: false" set without configuring "contextPropagationOnly: false"', () => { + const kibanaConfig = { + elastic: { + apm: { + active: false, + }, + }, + }; + + expect(() => + new ApmConfiguration(mockedRootDir, kibanaConfig, false).getConfig('serviceName') + ).toThrowErrorMatchingInlineSnapshot( + `"APM is disabled, but context propagation is enabled. Please disable context propagation with contextPropagationOnly:false"` + ); + + expect(() => + new ApmConfiguration(mockedRootDir, kibanaConfig, true).getConfig('serviceName') + ).toThrowErrorMatchingInlineSnapshot( + `"APM is disabled, but context propagation is enabled. Please disable context propagation with contextPropagationOnly:false"` + ); + }); + + it('does not throw if "active: false" and "contextPropagationOnly: false" configured', () => { + const kibanaConfig = { + elastic: { + apm: { + active: false, + contextPropagationOnly: false, + }, + }, + }; + + expect( + new ApmConfiguration(mockedRootDir, kibanaConfig, false).getConfig('serviceName') + ).toEqual( + expect.objectContaining({ + active: false, + contextPropagationOnly: false, + }) + ); + + expect( + new ApmConfiguration(mockedRootDir, kibanaConfig, true).getConfig('serviceName') + ).toEqual( + expect.objectContaining({ + active: false, + contextPropagationOnly: false, + }) + ); + }); + }); }); diff --git a/packages/kbn-apm-config-loader/src/config.ts b/packages/kbn-apm-config-loader/src/config.ts index 999e4ce3a6805..ecafcbd7e3261 100644 --- a/packages/kbn-apm-config-loader/src/config.ts +++ b/packages/kbn-apm-config-loader/src/config.ts @@ -16,7 +16,8 @@ import type { AgentConfigOptions } from 'elastic-apm-node'; // https://www.elastic.co/guide/en/apm/agent/nodejs/current/configuration.html const DEFAULT_CONFIG: AgentConfigOptions = { - active: false, + active: true, + contextPropagationOnly: true, environment: 'development', logUncaughtExceptions: true, globalLabels: {}, @@ -71,6 +72,8 @@ export class ApmConfiguration { private getBaseConfig() { if (!this.baseConfig) { + const configFromSources = this.getConfigFromAllSources(); + this.baseConfig = merge( { serviceVersion: this.kibanaVersion, @@ -79,9 +82,7 @@ export class ApmConfiguration { this.getUuidConfig(), this.getGitConfig(), this.getCiConfig(), - this.getConfigFromKibanaConfig(), - this.getDevConfig(), - this.getConfigFromEnv() + configFromSources ); /** @@ -114,6 +115,12 @@ export class ApmConfiguration { config.active = true; } + if (process.env.ELASTIC_APM_CONTEXT_PROPAGATION_ONLY === 'true') { + config.contextPropagationOnly = true; + } else if (process.env.ELASTIC_APM_CONTEXT_PROPAGATION_ONLY === 'false') { + config.contextPropagationOnly = false; + } + if (process.env.ELASTIC_APM_ENVIRONMENT || process.env.NODE_ENV) { config.environment = process.env.ELASTIC_APM_ENVIRONMENT || process.env.NODE_ENV; } @@ -249,4 +256,28 @@ export class ApmConfiguration { return {}; } } + + /** + * Reads APM configuration from different sources and merges them together. + */ + private getConfigFromAllSources(): AgentConfigOptions { + const config = merge( + {}, + this.getConfigFromKibanaConfig(), + this.getDevConfig(), + this.getConfigFromEnv() + ); + + if (config.active === false && config.contextPropagationOnly !== false) { + throw new Error( + 'APM is disabled, but context propagation is enabled. Please disable context propagation with contextPropagationOnly:false' + ); + } + + if (config.active === true) { + config.contextPropagationOnly = config.contextPropagationOnly ?? false; + } + + return config; + } } diff --git a/packages/kbn-cli-dev-mode/src/dev_server.test.ts b/packages/kbn-cli-dev-mode/src/dev_server.test.ts index 92dbe484eb005..772ba097fc31a 100644 --- a/packages/kbn-cli-dev-mode/src/dev_server.test.ts +++ b/packages/kbn-cli-dev-mode/src/dev_server.test.ts @@ -79,6 +79,8 @@ expect.addSnapshotSerializer(extendedEnvSerializer); beforeEach(() => { jest.clearAllMocks(); log.messages.length = 0; + process.execArgv = ['--inheritted', '--exec', '--argv']; + process.env.FORCE_COLOR = process.env.FORCE_COLOR || '1'; currentProc = undefined; }); @@ -120,9 +122,6 @@ describe('#run$', () => { it('starts the dev server with the right options', () => { run(new DevServer(defaultOptions)).unsubscribe(); - // ensure that FORCE_COLOR is in the env for consistency in snapshot - process.env.FORCE_COLOR = process.env.FORCE_COLOR || 'true'; - expect(execa.node.mock.calls).toMatchInlineSnapshot(` Array [ Array [ @@ -135,11 +134,13 @@ describe('#run$', () => { "env": Object { "": true, "ELASTIC_APM_SERVICE_NAME": "kibana", + "FORCE_COLOR": "true", "isDevCliChild": "true", }, "nodeOptions": Array [ - "--preserve-symlinks-main", - "--preserve-symlinks", + "--inheritted", + "--exec", + "--argv", ], "stdio": "pipe", }, diff --git a/packages/kbn-es-archiver/BUILD.bazel b/packages/kbn-es-archiver/BUILD.bazel index 90c63f82b72fa..2dc311ed74406 100644 --- a/packages/kbn-es-archiver/BUILD.bazel +++ b/packages/kbn-es-archiver/BUILD.bazel @@ -34,7 +34,6 @@ RUNTIME_DEPS = [ "//packages/kbn-utils", "@npm//@elastic/elasticsearch", "@npm//aggregate-error", - "@npm//bluebird", "@npm//chance", "@npm//globby", "@npm//json-stable-stringify", @@ -51,7 +50,6 @@ TYPES_DEPS = [ "@npm//aggregate-error", "@npm//globby", "@npm//zlib", - "@npm//@types/bluebird", "@npm//@types/chance", "@npm//@types/jest", "@npm//@types/json-stable-stringify", diff --git a/packages/kbn-es-archiver/src/actions/load.ts b/packages/kbn-es-archiver/src/actions/load.ts index 619c946f0c988..0a7235c566b52 100644 --- a/packages/kbn-es-archiver/src/actions/load.ts +++ b/packages/kbn-es-archiver/src/actions/load.ts @@ -40,6 +40,7 @@ export async function loadAction({ inputDir, skipExisting, useCreate, + docsOnly, client, log, kbnClient, @@ -47,6 +48,7 @@ export async function loadAction({ inputDir: string; skipExisting: boolean; useCreate: boolean; + docsOnly?: boolean; client: Client; log: ToolingLog; kbnClient: KbnClient; @@ -76,7 +78,7 @@ export async function loadAction({ await createPromiseFromStreams([ recordStream, - createCreateIndexStream({ client, stats, skipExisting, log }), + createCreateIndexStream({ client, stats, skipExisting, docsOnly, log }), createIndexDocRecordsStream(client, stats, progress, useCreate), ]); diff --git a/packages/kbn-es-archiver/src/actions/rebuild_all.ts b/packages/kbn-es-archiver/src/actions/rebuild_all.ts index f286f9719bdf1..360fdb438f2db 100644 --- a/packages/kbn-es-archiver/src/actions/rebuild_all.ts +++ b/packages/kbn-es-archiver/src/actions/rebuild_all.ts @@ -7,9 +7,9 @@ */ import { resolve, relative } from 'path'; -import { stat, Stats, rename, createReadStream, createWriteStream } from 'fs'; +import { Stats, createReadStream, createWriteStream } from 'fs'; +import { stat, rename } from 'fs/promises'; import { Readable, Writable } from 'stream'; -import { fromNode } from 'bluebird'; import { ToolingLog, REPO_ROOT } from '@kbn/dev-utils'; import { createPromiseFromStreams } from '@kbn/utils'; import { @@ -21,7 +21,7 @@ import { } from '../lib'; async function isDirectory(path: string): Promise { - const stats: Stats = await fromNode((cb) => stat(path, cb)); + const stats: Stats = await stat(path); return stats.isDirectory(); } @@ -50,7 +50,7 @@ export async function rebuildAllAction({ dataDir, log }: { dataDir: string; log: createWriteStream(tempFile), ] as [Readable, ...Writable[]]); - await fromNode((cb) => rename(tempFile, childPath, cb)); + await rename(tempFile, childPath); log.info('[%s] Rebuilt %j', archiveName, childName); } } diff --git a/packages/kbn-es-archiver/src/actions/save.ts b/packages/kbn-es-archiver/src/actions/save.ts index 07ed2b206c1dd..9cb5be05ac060 100644 --- a/packages/kbn-es-archiver/src/actions/save.ts +++ b/packages/kbn-es-archiver/src/actions/save.ts @@ -27,6 +27,7 @@ export async function saveAction({ client, log, raw, + keepIndexNames, query, }: { outputDir: string; @@ -34,6 +35,7 @@ export async function saveAction({ client: Client; log: ToolingLog; raw: boolean; + keepIndexNames?: boolean; query?: Record; }) { const name = relative(REPO_ROOT, outputDir); @@ -50,7 +52,7 @@ export async function saveAction({ // export and save the matching indices to mappings.json createPromiseFromStreams([ createListStream(indices), - createGenerateIndexRecordsStream(client, stats), + createGenerateIndexRecordsStream({ client, stats, keepIndexNames }), ...createFormatArchiveStreams(), createWriteStream(resolve(outputDir, 'mappings.json')), ] as [Readable, ...Writable[]]), @@ -58,7 +60,7 @@ export async function saveAction({ // export all documents from matching indexes into data.json.gz createPromiseFromStreams([ createListStream(indices), - createGenerateDocRecordsStream({ client, stats, progress, query }), + createGenerateDocRecordsStream({ client, stats, progress, keepIndexNames, query }), ...createFormatArchiveStreams({ gzip: !raw }), createWriteStream(resolve(outputDir, `data.json${raw ? '' : '.gz'}`)), ] as [Readable, ...Writable[]]), diff --git a/packages/kbn-es-archiver/src/cli.ts b/packages/kbn-es-archiver/src/cli.ts index db54a3bade74b..e54b4d5fbdb52 100644 --- a/packages/kbn-es-archiver/src/cli.ts +++ b/packages/kbn-es-archiver/src/cli.ts @@ -143,11 +143,12 @@ export function runCli() { $ node scripts/es_archiver save test/functional/es_archives/my_test_data logstash-* `, flags: { - boolean: ['raw'], + boolean: ['raw', 'keep-index-names'], string: ['query'], help: ` - --raw don't gzip the archives - --query query object to limit the documents being archived, needs to be properly escaped JSON + --raw don't gzip the archives + --keep-index-names don't change the names of Kibana indices to .kibana_1 + --query query object to limit the documents being archived, needs to be properly escaped JSON `, }, async run({ flags, esArchiver, statsMeta }) { @@ -168,6 +169,11 @@ export function runCli() { throw createFlagError('--raw does not take a value'); } + const keepIndexNames = flags['keep-index-names']; + if (typeof keepIndexNames !== 'boolean') { + throw createFlagError('--keep-index-names does not take a value'); + } + const query = flags.query; let parsedQuery; if (typeof query === 'string' && query.length > 0) { @@ -178,7 +184,7 @@ export function runCli() { } } - await esArchiver.save(path, indices, { raw, query: parsedQuery }); + await esArchiver.save(path, indices, { raw, keepIndexNames, query: parsedQuery }); }, }) .command({ @@ -196,9 +202,10 @@ export function runCli() { $ node scripts/es_archiver load my_test_data --config ../config.js `, flags: { - boolean: ['use-create'], + boolean: ['use-create', 'docs-only'], help: ` --use-create use create instead of index for loading documents + --docs-only load only documents, not indices `, }, async run({ flags, esArchiver, statsMeta }) { @@ -217,7 +224,12 @@ export function runCli() { throw createFlagError('--use-create does not take a value'); } - await esArchiver.load(path, { useCreate }); + const docsOnly = flags['docs-only']; + if (typeof docsOnly !== 'boolean') { + throw createFlagError('--docs-only does not take a value'); + } + + await esArchiver.load(path, { useCreate, docsOnly }); }, }) .command({ diff --git a/packages/kbn-es-archiver/src/es_archiver.ts b/packages/kbn-es-archiver/src/es_archiver.ts index ed27bc0afcf34..354197a98fa46 100644 --- a/packages/kbn-es-archiver/src/es_archiver.ts +++ b/packages/kbn-es-archiver/src/es_archiver.ts @@ -50,16 +50,22 @@ export class EsArchiver { * @param {String|Array} indices - the indices to archive * @param {Object} options * @property {Boolean} options.raw - should the archive be raw (unzipped) or not + * @property {Boolean} options.keepIndexNames - should the Kibana index name be kept as-is or renamed */ async save( path: string, indices: string | string[], - { raw = false, query }: { raw?: boolean; query?: Record } = {} + { + raw = false, + keepIndexNames = false, + query, + }: { raw?: boolean; keepIndexNames?: boolean; query?: Record } = {} ) { return await saveAction({ outputDir: Path.resolve(this.baseDir, path), indices, raw, + keepIndexNames, client: this.client, log: this.log, query, @@ -74,18 +80,21 @@ export class EsArchiver { * @property {Boolean} options.skipExisting - should existing indices * be ignored or overwritten * @property {Boolean} options.useCreate - use a create operation instead of index for documents + * @property {Boolean} options.docsOnly - load only documents, not indices */ async load( path: string, { skipExisting = false, useCreate = false, - }: { skipExisting?: boolean; useCreate?: boolean } = {} + docsOnly = false, + }: { skipExisting?: boolean; useCreate?: boolean; docsOnly?: boolean } = {} ) { return await loadAction({ inputDir: this.findArchive(path), skipExisting: !!skipExisting, useCreate: !!useCreate, + docsOnly, client: this.client, log: this.log, kbnClient: this.kbnClient, diff --git a/packages/kbn-es-archiver/src/lib/directory.ts b/packages/kbn-es-archiver/src/lib/directory.ts index f82e59a0ed252..2ff5b7e704edf 100644 --- a/packages/kbn-es-archiver/src/lib/directory.ts +++ b/packages/kbn-es-archiver/src/lib/directory.ts @@ -6,10 +6,9 @@ * Side Public License, v 1. */ -import { readdir } from 'fs'; -import { fromNode } from 'bluebird'; +import { readdir } from 'fs/promises'; export async function readDirectory(path: string) { - const allNames = await fromNode((cb) => readdir(path, cb)); + const allNames = await readdir(path); return allNames.filter((name) => !name.startsWith('.')); } diff --git a/packages/kbn-es-archiver/src/lib/docs/generate_doc_records_stream.test.ts b/packages/kbn-es-archiver/src/lib/docs/generate_doc_records_stream.test.ts index 2902812f51493..3b5f1f777b0e3 100644 --- a/packages/kbn-es-archiver/src/lib/docs/generate_doc_records_stream.test.ts +++ b/packages/kbn-es-archiver/src/lib/docs/generate_doc_records_stream.test.ts @@ -20,48 +20,24 @@ import { createStats } from '../stats'; const log = new ToolingLog(); -it('transforms each input index to a stream of docs using scrollSearch helper', async () => { - const responses: any = { - foo: [ - { - body: { - hits: { - total: 5, - hits: [ - { _index: 'foo', _type: '_doc', _id: '0', _source: {} }, - { _index: 'foo', _type: '_doc', _id: '1', _source: {} }, - { _index: 'foo', _type: '_doc', _id: '2', _source: {} }, - ], - }, - }, - }, - { - body: { - hits: { - total: 5, - hits: [ - { _index: 'foo', _type: '_doc', _id: '3', _source: {} }, - { _index: 'foo', _type: '_doc', _id: '4', _source: {} }, - ], - }, - }, - }, - ], - bar: [ - { - body: { - hits: { - total: 2, - hits: [ - { _index: 'bar', _type: '_doc', _id: '0', _source: {} }, - { _index: 'bar', _type: '_doc', _id: '1', _source: {} }, - ], - }, - }, - }, - ], - }; +interface SearchResponses { + [key: string]: Array<{ + body: { + hits: { + total: number; + hits: Array<{ + _index: string; + _type: string; + _id: string; + _source: Record; + }>; + }; + }; + }>; +} +function createMockClient(responses: SearchResponses) { + // TODO: replace with proper mocked client const client: any = { helpers: { scrollSearch: jest.fn(function* ({ index }) { @@ -71,29 +47,76 @@ it('transforms each input index to a stream of docs using scrollSearch helper', }), }, }; + return client; +} - const stats = createStats('test', log); - const progress = new Progress(); - - const results = await createPromiseFromStreams([ - createListStream(['bar', 'foo']), - createGenerateDocRecordsStream({ - client, - stats, - progress, - }), - createMapStream((record: any) => { - expect(record).toHaveProperty('type', 'doc'); - expect(record.value.source).toEqual({}); - expect(record.value.type).toBe('_doc'); - expect(record.value.index).toMatch(/^(foo|bar)$/); - expect(record.value.id).toMatch(/^\d+$/); - return `${record.value.index}:${record.value.id}`; - }), - createConcatStream([]), - ]); - - expect(client.helpers.scrollSearch).toMatchInlineSnapshot(` +describe('esArchiver: createGenerateDocRecordsStream()', () => { + it('transforms each input index to a stream of docs using scrollSearch helper', async () => { + const responses = { + foo: [ + { + body: { + hits: { + total: 5, + hits: [ + { _index: 'foo', _type: '_doc', _id: '0', _source: {} }, + { _index: 'foo', _type: '_doc', _id: '1', _source: {} }, + { _index: 'foo', _type: '_doc', _id: '2', _source: {} }, + ], + }, + }, + }, + { + body: { + hits: { + total: 5, + hits: [ + { _index: 'foo', _type: '_doc', _id: '3', _source: {} }, + { _index: 'foo', _type: '_doc', _id: '4', _source: {} }, + ], + }, + }, + }, + ], + bar: [ + { + body: { + hits: { + total: 2, + hits: [ + { _index: 'bar', _type: '_doc', _id: '0', _source: {} }, + { _index: 'bar', _type: '_doc', _id: '1', _source: {} }, + ], + }, + }, + }, + ], + }; + + const client = createMockClient(responses); + + const stats = createStats('test', log); + const progress = new Progress(); + + const results = await createPromiseFromStreams([ + createListStream(['bar', 'foo']), + createGenerateDocRecordsStream({ + client, + stats, + progress, + }), + createMapStream((record: any) => { + expect(record).toHaveProperty('type', 'doc'); + expect(record.value.source).toEqual({}); + expect(record.value.type).toBe('_doc'); + expect(record.value.index).toMatch(/^(foo|bar)$/); + expect(record.value.id).toMatch(/^\d+$/); + return `${record.value.index}:${record.value.id}`; + }), + createConcatStream([]), + ]); + + expect(client.helpers.scrollSearch).toMatchInlineSnapshot(` [MockFunction] { "calls": Array [ Array [ @@ -139,7 +162,7 @@ it('transforms each input index to a stream of docs using scrollSearch helper', ], } `); - expect(results).toMatchInlineSnapshot(` + expect(results).toMatchInlineSnapshot(` Array [ "bar:0", "bar:1", @@ -150,14 +173,14 @@ it('transforms each input index to a stream of docs using scrollSearch helper', "foo:4", ] `); - expect(progress).toMatchInlineSnapshot(` + expect(progress).toMatchInlineSnapshot(` Progress { "complete": 7, "loggingInterval": undefined, "total": 7, } `); - expect(stats).toMatchInlineSnapshot(` + expect(stats).toMatchInlineSnapshot(` Object { "bar": Object { "archived": false, @@ -193,4 +216,80 @@ it('transforms each input index to a stream of docs using scrollSearch helper', }, } `); + }); + + describe('keepIndexNames', () => { + it('changes .kibana* index names if keepIndexNames is not enabled', async () => { + const hits = [{ _index: '.kibana_7.16.0_001', _type: '_doc', _id: '0', _source: {} }]; + const responses = { + ['.kibana_7.16.0_001']: [{ body: { hits: { hits, total: hits.length } } }], + }; + const client = createMockClient(responses); + const stats = createStats('test', log); + const progress = new Progress(); + + const results = await createPromiseFromStreams([ + createListStream(['.kibana_7.16.0_001']), + createGenerateDocRecordsStream({ + client, + stats, + progress, + }), + createMapStream((record: { value: { index: string; id: string } }) => { + return `${record.value.index}:${record.value.id}`; + }), + createConcatStream([]), + ]); + expect(results).toEqual(['.kibana_1:0']); + }); + + it('does not change non-.kibana* index names if keepIndexNames is not enabled', async () => { + const hits = [{ _index: '.foo', _type: '_doc', _id: '0', _source: {} }]; + const responses = { + ['.foo']: [{ body: { hits: { hits, total: hits.length } } }], + }; + const client = createMockClient(responses); + const stats = createStats('test', log); + const progress = new Progress(); + + const results = await createPromiseFromStreams([ + createListStream(['.foo']), + createGenerateDocRecordsStream({ + client, + stats, + progress, + }), + createMapStream((record: { value: { index: string; id: string } }) => { + return `${record.value.index}:${record.value.id}`; + }), + createConcatStream([]), + ]); + expect(results).toEqual(['.foo:0']); + }); + + it('does not change .kibana* index names if keepIndexNames is enabled', async () => { + const hits = [{ _index: '.kibana_7.16.0_001', _type: '_doc', _id: '0', _source: {} }]; + const responses = { + ['.kibana_7.16.0_001']: [{ body: { hits: { hits, total: hits.length } } }], + }; + const client = createMockClient(responses); + const stats = createStats('test', log); + const progress = new Progress(); + + const results = await createPromiseFromStreams([ + createListStream(['.kibana_7.16.0_001']), + createGenerateDocRecordsStream({ + client, + stats, + progress, + keepIndexNames: true, + }), + createMapStream((record: { value: { index: string; id: string } }) => { + return `${record.value.index}:${record.value.id}`; + }), + createConcatStream([]), + ]); + expect(results).toEqual(['.kibana_7.16.0_001:0']); + }); + }); }); diff --git a/packages/kbn-es-archiver/src/lib/docs/generate_doc_records_stream.ts b/packages/kbn-es-archiver/src/lib/docs/generate_doc_records_stream.ts index a0636d6a3f76a..4bd44b649afd2 100644 --- a/packages/kbn-es-archiver/src/lib/docs/generate_doc_records_stream.ts +++ b/packages/kbn-es-archiver/src/lib/docs/generate_doc_records_stream.ts @@ -19,11 +19,13 @@ export function createGenerateDocRecordsStream({ client, stats, progress, + keepIndexNames, query, }: { client: Client; stats: Stats; progress: Progress; + keepIndexNames?: boolean; query?: Record; }) { return new Transform({ @@ -59,9 +61,10 @@ export function createGenerateDocRecordsStream({ this.push({ type: 'doc', value: { - // always rewrite the .kibana_* index to .kibana_1 so that + // if keepIndexNames is false, rewrite the .kibana_* index to .kibana_1 so that // when it is loaded it can skip migration, if possible - index: hit._index.startsWith('.kibana') ? '.kibana_1' : hit._index, + index: + hit._index.startsWith('.kibana') && !keepIndexNames ? '.kibana_1' : hit._index, type: hit._type, id: hit._id, source: hit._source, diff --git a/packages/kbn-es-archiver/src/lib/indices/create_index_stream.test.mock.ts b/packages/kbn-es-archiver/src/lib/indices/create_index_stream.test.mock.ts new file mode 100644 index 0000000000000..d17bd33fa07ab --- /dev/null +++ b/packages/kbn-es-archiver/src/lib/indices/create_index_stream.test.mock.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 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 { deleteKibanaIndices } from './kibana_index'; + +export const mockDeleteKibanaIndices = jest.fn() as jest.MockedFunction; + +jest.mock('./kibana_index', () => ({ + deleteKibanaIndices: mockDeleteKibanaIndices, +})); diff --git a/packages/kbn-es-archiver/src/lib/indices/create_index_stream.test.ts b/packages/kbn-es-archiver/src/lib/indices/create_index_stream.test.ts index 3a8180b724e07..615555b405e44 100644 --- a/packages/kbn-es-archiver/src/lib/indices/create_index_stream.test.ts +++ b/packages/kbn-es-archiver/src/lib/indices/create_index_stream.test.ts @@ -6,6 +6,8 @@ * Side Public License, v 1. */ +import { mockDeleteKibanaIndices } from './create_index_stream.test.mock'; + import sinon from 'sinon'; import Chance from 'chance'; import { createPromiseFromStreams, createConcatStream, createListStream } from '@kbn/utils'; @@ -24,6 +26,10 @@ const chance = new Chance(); const log = createStubLogger(); +beforeEach(() => { + mockDeleteKibanaIndices.mockClear(); +}); + describe('esArchiver: createCreateIndexStream()', () => { describe('defaults', () => { it('deletes existing indices, creates all', async () => { @@ -167,6 +173,73 @@ describe('esArchiver: createCreateIndexStream()', () => { }); }); + describe('deleteKibanaIndices', () => { + function doTest(...indices: string[]) { + return createPromiseFromStreams([ + createListStream(indices.map((index) => createStubIndexRecord(index))), + createCreateIndexStream({ client: createStubClient(), stats: createStubStats(), log }), + createConcatStream([]), + ]); + } + + it('does not delete Kibana indices for indexes that do not start with .kibana', async () => { + await doTest('.foo'); + + expect(mockDeleteKibanaIndices).not.toHaveBeenCalled(); + }); + + it('deletes Kibana indices at most once for indices that start with .kibana', async () => { + // If we are loading the main Kibana index, we should delete all Kibana indices for backwards compatibility reasons. + await doTest('.kibana_7.16.0_001', '.kibana_task_manager_7.16.0_001'); + + expect(mockDeleteKibanaIndices).toHaveBeenCalledTimes(1); + expect(mockDeleteKibanaIndices).toHaveBeenCalledWith( + expect.not.objectContaining({ onlyTaskManager: true }) + ); + }); + + it('deletes Kibana task manager index at most once, using onlyTaskManager: true', async () => { + // If we are loading the Kibana task manager index, we should only delete that index, not any other Kibana indices. + await doTest('.kibana_task_manager_7.16.0_001', '.kibana_task_manager_7.16.0_002'); + + expect(mockDeleteKibanaIndices).toHaveBeenCalledTimes(1); + expect(mockDeleteKibanaIndices).toHaveBeenCalledWith( + expect.objectContaining({ onlyTaskManager: true }) + ); + }); + + it('deletes Kibana task manager index AND deletes all Kibana indices', async () => { + // Because we are reading from a stream, we can't look ahead to see if we'll eventually wind up deleting all Kibana indices. + // So, we first delete only the Kibana task manager indices, then we wind up deleting all Kibana indices. + await doTest('.kibana_task_manager_7.16.0_001', '.kibana_7.16.0_001'); + + expect(mockDeleteKibanaIndices).toHaveBeenCalledTimes(2); + expect(mockDeleteKibanaIndices).toHaveBeenNthCalledWith( + 1, + expect.objectContaining({ onlyTaskManager: true }) + ); + expect(mockDeleteKibanaIndices).toHaveBeenNthCalledWith( + 2, + expect.not.objectContaining({ onlyTaskManager: true }) + ); + }); + }); + + describe('docsOnly = true', () => { + it('passes through "hit" records without attempting to create indices', async () => { + const client = createStubClient(); + const stats = createStubStats(); + const output = await createPromiseFromStreams([ + createListStream([createStubIndexRecord('index'), createStubDocRecord('index', 1)]), + createCreateIndexStream({ client, stats, log, docsOnly: true }), + createConcatStream([]), + ]); + + sinon.assert.notCalled(client.indices.create as sinon.SinonSpy); + expect(output).toEqual([createStubDocRecord('index', 1)]); + }); + }); + describe('skipExisting = true', () => { it('ignores preexisting indexes', async () => { const client = createStubClient(['existing-index']); diff --git a/packages/kbn-es-archiver/src/lib/indices/create_index_stream.ts b/packages/kbn-es-archiver/src/lib/indices/create_index_stream.ts index 50d13fc728c79..26472d72bef0f 100644 --- a/packages/kbn-es-archiver/src/lib/indices/create_index_stream.ts +++ b/packages/kbn-es-archiver/src/lib/indices/create_index_stream.ts @@ -29,11 +29,13 @@ export function createCreateIndexStream({ client, stats, skipExisting = false, + docsOnly = false, log, }: { client: Client; stats: Stats; skipExisting?: boolean; + docsOnly?: boolean; log: ToolingLog; }) { const skipDocsFromIndices = new Set(); @@ -42,6 +44,7 @@ export function createCreateIndexStream({ // previous indices are removed so we're starting w/ a clean slate for // migrations. This only needs to be done once per archive load operation. let kibanaIndexAlreadyDeleted = false; + let kibanaTaskManagerIndexAlreadyDeleted = false; async function handleDoc(stream: Readable, record: DocRecord) { if (skipDocsFromIndices.has(record.value.index)) { @@ -53,13 +56,21 @@ export function createCreateIndexStream({ async function handleIndex(record: DocRecord) { const { index, settings, mappings, aliases } = record.value; - const isKibana = index.startsWith('.kibana'); + const isKibanaTaskManager = index.startsWith('.kibana_task_manager'); + const isKibana = index.startsWith('.kibana') && !isKibanaTaskManager; + + if (docsOnly) { + return; + } async function attemptToCreate(attemptNumber = 1) { try { if (isKibana && !kibanaIndexAlreadyDeleted) { - await deleteKibanaIndices({ client, stats, log }); - kibanaIndexAlreadyDeleted = true; + await deleteKibanaIndices({ client, stats, log }); // delete all .kibana* indices + kibanaIndexAlreadyDeleted = kibanaTaskManagerIndexAlreadyDeleted = true; + } else if (isKibanaTaskManager && !kibanaTaskManagerIndexAlreadyDeleted) { + await deleteKibanaIndices({ client, stats, onlyTaskManager: true, log }); // delete only .kibana_task_manager* indices + kibanaTaskManagerIndexAlreadyDeleted = true; } await client.indices.create( diff --git a/packages/kbn-es-archiver/src/lib/indices/generate_index_records_stream.test.ts b/packages/kbn-es-archiver/src/lib/indices/generate_index_records_stream.test.ts index 0e04d6b9ba799..fbd351cea63a9 100644 --- a/packages/kbn-es-archiver/src/lib/indices/generate_index_records_stream.test.ts +++ b/packages/kbn-es-archiver/src/lib/indices/generate_index_records_stream.test.ts @@ -21,7 +21,7 @@ describe('esArchiver: createGenerateIndexRecordsStream()', () => { await createPromiseFromStreams([ createListStream(indices), - createGenerateIndexRecordsStream(client, stats), + createGenerateIndexRecordsStream({ client, stats }), ]); expect(stats.getTestSummary()).toEqual({ @@ -40,7 +40,7 @@ describe('esArchiver: createGenerateIndexRecordsStream()', () => { await createPromiseFromStreams([ createListStream(['index1']), - createGenerateIndexRecordsStream(client, stats), + createGenerateIndexRecordsStream({ client, stats }), ]); const params = (client.indices.get as sinon.SinonSpy).args[0][0]; @@ -58,7 +58,7 @@ describe('esArchiver: createGenerateIndexRecordsStream()', () => { const indexRecords = await createPromiseFromStreams([ createListStream(['index1', 'index2', 'index3']), - createGenerateIndexRecordsStream(client, stats), + createGenerateIndexRecordsStream({ client, stats }), createConcatStream([]), ]); @@ -83,7 +83,7 @@ describe('esArchiver: createGenerateIndexRecordsStream()', () => { const indexRecords = await createPromiseFromStreams([ createListStream(['index1']), - createGenerateIndexRecordsStream(client, stats), + createGenerateIndexRecordsStream({ client, stats }), createConcatStream([]), ]); @@ -99,4 +99,51 @@ describe('esArchiver: createGenerateIndexRecordsStream()', () => { }, ]); }); + + describe('change index names', () => { + it('changes .kibana* index names if keepIndexNames is not enabled', async () => { + const stats = createStubStats(); + const client = createStubClient(['.kibana_7.16.0_001']); + + const indexRecords = await createPromiseFromStreams([ + createListStream(['.kibana_7.16.0_001']), + createGenerateIndexRecordsStream({ client, stats }), + createConcatStream([]), + ]); + + expect(indexRecords).toEqual([ + { type: 'index', value: expect.objectContaining({ index: '.kibana_1' }) }, + ]); + }); + + it('does not change non-.kibana* index names if keepIndexNames is not enabled', async () => { + const stats = createStubStats(); + const client = createStubClient(['.foo']); + + const indexRecords = await createPromiseFromStreams([ + createListStream(['.foo']), + createGenerateIndexRecordsStream({ client, stats }), + createConcatStream([]), + ]); + + expect(indexRecords).toEqual([ + { type: 'index', value: expect.objectContaining({ index: '.foo' }) }, + ]); + }); + + it('does not change .kibana* index names if keepIndexNames is enabled', async () => { + const stats = createStubStats(); + const client = createStubClient(['.kibana_7.16.0_001']); + + const indexRecords = await createPromiseFromStreams([ + createListStream(['.kibana_7.16.0_001']), + createGenerateIndexRecordsStream({ client, stats, keepIndexNames: true }), + createConcatStream([]), + ]); + + expect(indexRecords).toEqual([ + { type: 'index', value: expect.objectContaining({ index: '.kibana_7.16.0_001' }) }, + ]); + }); + }); }); diff --git a/packages/kbn-es-archiver/src/lib/indices/generate_index_records_stream.ts b/packages/kbn-es-archiver/src/lib/indices/generate_index_records_stream.ts index d647a4fe5f501..e3efaa2851609 100644 --- a/packages/kbn-es-archiver/src/lib/indices/generate_index_records_stream.ts +++ b/packages/kbn-es-archiver/src/lib/indices/generate_index_records_stream.ts @@ -11,7 +11,15 @@ import { Transform } from 'stream'; import { Stats } from '../stats'; import { ES_CLIENT_HEADERS } from '../../client_headers'; -export function createGenerateIndexRecordsStream(client: Client, stats: Stats) { +export function createGenerateIndexRecordsStream({ + client, + stats, + keepIndexNames, +}: { + client: Client; + stats: Stats; + keepIndexNames?: boolean; +}) { return new Transform({ writableObjectMode: true, readableObjectMode: true, @@ -59,9 +67,9 @@ export function createGenerateIndexRecordsStream(client: Client, stats: Stats) { this.push({ type: 'index', value: { - // always rewrite the .kibana_* index to .kibana_1 so that + // if keepIndexNames is false, rewrite the .kibana_* index to .kibana_1 so that // when it is loaded it can skip migration, if possible - index: index.startsWith('.kibana') ? '.kibana_1' : index, + index: index.startsWith('.kibana') && !keepIndexNames ? '.kibana_1' : index, settings, mappings, aliases, diff --git a/packages/kbn-es-archiver/src/lib/indices/kibana_index.ts b/packages/kbn-es-archiver/src/lib/indices/kibana_index.ts index 069db636c596b..eaae1de46f1e6 100644 --- a/packages/kbn-es-archiver/src/lib/indices/kibana_index.ts +++ b/packages/kbn-es-archiver/src/lib/indices/kibana_index.ts @@ -16,18 +16,21 @@ import { deleteIndex } from './delete_index'; import { ES_CLIENT_HEADERS } from '../../client_headers'; /** - * Deletes all indices that start with `.kibana` + * Deletes all indices that start with `.kibana`, or if onlyTaskManager==true, all indices that start with `.kibana_task_manager` */ export async function deleteKibanaIndices({ client, stats, + onlyTaskManager = false, log, }: { client: Client; stats: Stats; + onlyTaskManager?: boolean; log: ToolingLog; }) { - const indexNames = await fetchKibanaIndices(client); + const indexPattern = onlyTaskManager ? '.kibana_task_manager*' : '.kibana*'; + const indexNames = await fetchKibanaIndices(client, indexPattern); if (!indexNames.length) { return; } @@ -75,9 +78,9 @@ function isKibanaIndex(index?: string): index is string { ); } -async function fetchKibanaIndices(client: Client) { +async function fetchKibanaIndices(client: Client, indexPattern: string) { const resp = await client.cat.indices( - { index: '.kibana*', format: 'json' }, + { index: indexPattern, format: 'json' }, { headers: ES_CLIENT_HEADERS, } diff --git a/packages/kbn-eslint-plugin-eslint/helpers/find_kibana_root.js b/packages/kbn-eslint-plugin-eslint/helpers/find_kibana_root.js new file mode 100644 index 0000000000000..5915b10b443bb --- /dev/null +++ b/packages/kbn-eslint-plugin-eslint/helpers/find_kibana_root.js @@ -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. + */ + +const path = require('path'); +const fs = require('fs'); + +function isKibanaRoot(maybeKibanaRoot) { + try { + const packageJsonPath = path.join(maybeKibanaRoot, 'package.json'); + fs.accessSync(packageJsonPath, fs.constants.R_OK); + const packageJsonContent = fs.readFileSync(packageJsonPath); + return JSON.parse(packageJsonContent).name === 'kibana'; + } catch (e) { + return false; + } +} + +module.exports = function findKibanaRoot() { + let maybeKibanaRoot = path.resolve(__dirname, '../../..'); + + // when using syslinks, __dirname reports outside of the repo + // if that's the case, the path will contain .cache/bazel + if (!maybeKibanaRoot.includes('.cache/bazel')) { + return maybeKibanaRoot; + } + + // process.argv[1] would be the eslint binary, a correctly-set editor + // will use a local eslint inside the repo node_modules and its value + // should be `ACTUAL_KIBANA_ROOT/node_modules/.bin/eslint` + maybeKibanaRoot = path.resolve(process.argv[1], '../../../'); + if (isKibanaRoot(maybeKibanaRoot)) { + return maybeKibanaRoot; + } + + // eslint should run on the repo root level + // try to use process.cwd as the kibana root + maybeKibanaRoot = process.cwd(); + if (isKibanaRoot(maybeKibanaRoot)) { + return maybeKibanaRoot; + } + + // fallback to the first predicted path (original script) + return maybeKibanaRoot; +}; diff --git a/packages/kbn-eslint-plugin-eslint/rules/module_migration.js b/packages/kbn-eslint-plugin-eslint/rules/module_migration.js index 3175210eccb10..04fbbfd35a565 100644 --- a/packages/kbn-eslint-plugin-eslint/rules/module_migration.js +++ b/packages/kbn-eslint-plugin-eslint/rules/module_migration.js @@ -7,7 +7,8 @@ */ const path = require('path'); -const KIBANA_ROOT = path.resolve(__dirname, '../../..'); +const findKibanaRoot = require('../helpers/find_kibana_root'); +const KIBANA_ROOT = findKibanaRoot(); function checkModuleNameNode(context, mappings, node, desc = 'Imported') { const mapping = mappings.find( diff --git a/packages/kbn-eslint-plugin-eslint/rules/module_migration.test.js b/packages/kbn-eslint-plugin-eslint/rules/module_migration.test.js index 1ff65fc19a966..2ecaf283133e7 100644 --- a/packages/kbn-eslint-plugin-eslint/rules/module_migration.test.js +++ b/packages/kbn-eslint-plugin-eslint/rules/module_migration.test.js @@ -77,5 +77,76 @@ ruleTester.run('@kbn/eslint/module-migration', rule, { export const foo2 = 'bar' `, }, + /** + * Given this tree: + * x-pack/ + * - common/ + * - foo.ts <-- the target import + * - other/ + * - folder/ + * - bar.ts <-- the linted fle + * import "x-pack/common/foo" should be + * import ../../foo + */ + { + code: dedent` + import "x-pack/common/foo" + `, + filename: 'x-pack/common/other/folder/bar.ts', + options: [ + [ + { + from: 'x-pack', + to: 'foo', + toRelative: 'x-pack', + }, + ], + ], + errors: [ + { + line: 1, + message: 'Imported module "x-pack/common/foo" should be "../../foo"', + }, + ], + output: dedent` + import '../../foo' + `, + }, + /** + * Given this tree: + * x-pack/ + * - common/ + * - foo.ts <-- the target import + * - another/ + * - posible + * - example <-- the linted file + * + * import "x-pack/common/foo" should be + * import ../../common/foo + */ + { + code: dedent` + import "x-pack/common/foo" + `, + filename: 'x-pack/another/possible/example.ts', + options: [ + [ + { + from: 'x-pack', + to: 'foo', + toRelative: 'x-pack', + }, + ], + ], + errors: [ + { + line: 1, + message: 'Imported module "x-pack/common/foo" should be "../../common/foo"', + }, + ], + output: dedent` + import '../../common/foo' + `, + }, ], }); diff --git a/packages/kbn-io-ts-utils/BUILD.bazel b/packages/kbn-io-ts-utils/BUILD.bazel index 474fa2c2bb121..e5f1de4d07f63 100644 --- a/packages/kbn-io-ts-utils/BUILD.bazel +++ b/packages/kbn-io-ts-utils/BUILD.bazel @@ -23,6 +23,17 @@ filegroup( NPM_MODULE_EXTRA_FILES = [ "package.json", + "deep_exact_rt/package.json", + "iso_to_epoch_rt/package.json", + "json_rt/package.json", + "merge_rt/package.json", + "non_empty_string_rt/package.json", + "parseable_types/package.json", + "props_to_schema/package.json", + "strict_keys_rt/package.json", + "to_boolean_rt/package.json", + "to_json_schema/package.json", + "to_number_rt/package.json", ] RUNTIME_DEPS = [ diff --git a/packages/kbn-io-ts-utils/deep_exact_rt/package.json b/packages/kbn-io-ts-utils/deep_exact_rt/package.json new file mode 100644 index 0000000000000..b42591a2e82d0 --- /dev/null +++ b/packages/kbn-io-ts-utils/deep_exact_rt/package.json @@ -0,0 +1,4 @@ +{ + "main": "../target_node/deep_exact_rt", + "types": "../target_types/deep_exact_rt" +} \ No newline at end of file diff --git a/packages/kbn-io-ts-utils/iso_to_epoch_rt/package.json b/packages/kbn-io-ts-utils/iso_to_epoch_rt/package.json new file mode 100644 index 0000000000000..e96c50b9fbf4e --- /dev/null +++ b/packages/kbn-io-ts-utils/iso_to_epoch_rt/package.json @@ -0,0 +1,4 @@ +{ + "main": "../target_node/iso_to_epoch_rt", + "types": "../target_types/iso_to_epoch_rt" +} \ No newline at end of file diff --git a/packages/kbn-io-ts-utils/json_rt/package.json b/packages/kbn-io-ts-utils/json_rt/package.json new file mode 100644 index 0000000000000..f896827cf99a4 --- /dev/null +++ b/packages/kbn-io-ts-utils/json_rt/package.json @@ -0,0 +1,4 @@ +{ + "main": "../target_node/json_rt", + "types": "../target_types/json_rt" +} \ No newline at end of file diff --git a/packages/kbn-io-ts-utils/merge_rt/package.json b/packages/kbn-io-ts-utils/merge_rt/package.json new file mode 100644 index 0000000000000..f7773688068e0 --- /dev/null +++ b/packages/kbn-io-ts-utils/merge_rt/package.json @@ -0,0 +1,4 @@ +{ + "main": "../target_node/merge_rt", + "types": "../target_types/merge_rt" +} \ No newline at end of file diff --git a/packages/kbn-io-ts-utils/non_empty_string_rt/package.json b/packages/kbn-io-ts-utils/non_empty_string_rt/package.json new file mode 100644 index 0000000000000..6348f6d728059 --- /dev/null +++ b/packages/kbn-io-ts-utils/non_empty_string_rt/package.json @@ -0,0 +1,4 @@ +{ + "main": "../target_node/non_empty_string_rt", + "types": "../target_types/non_empty_string_rt" +} \ No newline at end of file diff --git a/packages/kbn-io-ts-utils/parseable_types/package.json b/packages/kbn-io-ts-utils/parseable_types/package.json new file mode 100644 index 0000000000000..6dab2a5ee156e --- /dev/null +++ b/packages/kbn-io-ts-utils/parseable_types/package.json @@ -0,0 +1,4 @@ +{ + "main": "../target_node/parseable_types", + "types": "../target_types/parseable_types" +} \ No newline at end of file diff --git a/packages/kbn-io-ts-utils/props_to_schema/package.json b/packages/kbn-io-ts-utils/props_to_schema/package.json new file mode 100644 index 0000000000000..478de84d17f81 --- /dev/null +++ b/packages/kbn-io-ts-utils/props_to_schema/package.json @@ -0,0 +1,4 @@ +{ + "main": "../target_node/props_to_schema", + "types": "../target_types/props_to_schema" +} \ No newline at end of file diff --git a/packages/kbn-io-ts-utils/src/strict_keys_rt/index.ts b/packages/kbn-io-ts-utils/src/strict_keys_rt/index.ts index cb3d9bb2100d0..28fdc89751fd4 100644 --- a/packages/kbn-io-ts-utils/src/strict_keys_rt/index.ts +++ b/packages/kbn-io-ts-utils/src/strict_keys_rt/index.ts @@ -113,7 +113,7 @@ export function strictKeysRt(type: T) { const excessKeys = difference([...keys.all], [...keys.handled]); if (excessKeys.length) { - return t.failure(i, context, `Excess keys are not allowed: \n${excessKeys.join('\n')}`); + return t.failure(i, context, `Excess keys are not allowed:\n${excessKeys.join('\n')}`); } return t.success(i); diff --git a/packages/kbn-io-ts-utils/strict_keys_rt/package.json b/packages/kbn-io-ts-utils/strict_keys_rt/package.json new file mode 100644 index 0000000000000..68823d97a5d00 --- /dev/null +++ b/packages/kbn-io-ts-utils/strict_keys_rt/package.json @@ -0,0 +1,4 @@ +{ + "main": "../target_node/strict_keys_rt", + "types": "../target_types/strict_keys_rt" +} \ No newline at end of file diff --git a/packages/kbn-io-ts-utils/to_boolean_rt/package.json b/packages/kbn-io-ts-utils/to_boolean_rt/package.json new file mode 100644 index 0000000000000..5e801a6529153 --- /dev/null +++ b/packages/kbn-io-ts-utils/to_boolean_rt/package.json @@ -0,0 +1,4 @@ +{ + "main": "../target_node/to_boolean_rt", + "types": "../target_types/to_boolean_rt" +} \ No newline at end of file diff --git a/packages/kbn-io-ts-utils/to_json_schema/package.json b/packages/kbn-io-ts-utils/to_json_schema/package.json new file mode 100644 index 0000000000000..366f3243b1156 --- /dev/null +++ b/packages/kbn-io-ts-utils/to_json_schema/package.json @@ -0,0 +1,4 @@ +{ + "main": "../target_node/to_json_schema", + "types": "../target_types/to_json_schema" +} \ No newline at end of file diff --git a/packages/kbn-io-ts-utils/to_number_rt/package.json b/packages/kbn-io-ts-utils/to_number_rt/package.json new file mode 100644 index 0000000000000..f5da955cb9775 --- /dev/null +++ b/packages/kbn-io-ts-utils/to_number_rt/package.json @@ -0,0 +1,4 @@ +{ + "main": "../target_node/to_number_rt", + "types": "../target_types/to_number_rt" +} \ No newline at end of file diff --git a/packages/kbn-logging/src/log_record.ts b/packages/kbn-logging/src/log_record.ts index 22931a67a823d..a212a50b8c98a 100644 --- a/packages/kbn-logging/src/log_record.ts +++ b/packages/kbn-logging/src/log_record.ts @@ -6,7 +6,8 @@ * Side Public License, v 1. */ -import { LogLevel } from './log_level'; +import type { LogLevel } from './log_level'; +import type { LogMeta } from './log_meta'; /** * Essential parts of every log message. @@ -18,6 +19,9 @@ export interface LogRecord { context: string; message: string; error?: Error; - meta?: { [name: string]: any }; + meta?: LogMeta; pid: number; + spanId?: string; + traceId?: string; + transactionId?: string; } diff --git a/packages/kbn-monaco/BUILD.bazel b/packages/kbn-monaco/BUILD.bazel index d2d9bf3f9a00c..b2efa79f7fb34 100644 --- a/packages/kbn-monaco/BUILD.bazel +++ b/packages/kbn-monaco/BUILD.bazel @@ -11,6 +11,7 @@ SOURCE_FILES = glob( "src/**/*", ], exclude = [ + "**/__jest__", "**/*.test.*", "**/README.md", ], @@ -36,12 +37,14 @@ RUNTIME_DEPS = [ "@npm//monaco-editor", "@npm//raw-loader", "@npm//regenerator-runtime", + "@npm//rxjs", ] TYPES_DEPS = [ "//packages/kbn-i18n", "@npm//antlr4ts", "@npm//monaco-editor", + "@npm//rxjs", "@npm//@types/jest", "@npm//@types/node", ] diff --git a/packages/kbn-monaco/src/__jest__/jest.mocks.ts b/packages/kbn-monaco/src/__jest__/jest.mocks.ts new file mode 100644 index 0000000000000..1df4f9b115002 --- /dev/null +++ b/packages/kbn-monaco/src/__jest__/jest.mocks.ts @@ -0,0 +1,68 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 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 { MockIModel } from './types'; + +const createMockModel = (ID: string) => { + const model: MockIModel = { + uri: '', + id: 'mockModel', + value: '', + getModeId: () => ID, + changeContentListeners: [], + setValue(newValue) { + this.value = newValue; + this.changeContentListeners.forEach((listener) => listener()); + }, + getValue() { + return this.value; + }, + onDidChangeContent(handler) { + this.changeContentListeners.push(handler); + }, + onDidChangeLanguage: (handler) => { + handler({ newLanguage: ID }); + }, + }; + + return model; +}; + +jest.mock('../monaco_imports', () => { + const original = jest.requireActual('../monaco_imports'); + const originalMonaco = original.monaco; + const originalEditor = original.monaco.editor; + + return { + ...original, + monaco: { + ...originalMonaco, + editor: { + ...originalEditor, + model: null, + createModel(ID: string) { + this.model = createMockModel(ID); + return this.model; + }, + onDidCreateModel(handler: (model: MockIModel) => void) { + if (!this.model) { + throw new Error( + `Model needs to be created by calling monaco.editor.createModel(ID) first.` + ); + } + handler(this.model); + }, + getModel() { + return this.model; + }, + getModels: () => [], + setModelMarkers: () => undefined, + }, + }, + }; +}); diff --git a/packages/kbn-monaco/src/__jest__/types.ts b/packages/kbn-monaco/src/__jest__/types.ts new file mode 100644 index 0000000000000..929964c5300fc --- /dev/null +++ b/packages/kbn-monaco/src/__jest__/types.ts @@ -0,0 +1,19 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +export interface MockIModel { + uri: string; + id: string; + value: string; + changeContentListeners: Array<() => void>; + getModeId: () => string; + setValue: (value: string) => void; + getValue: () => string; + onDidChangeContent: (handler: () => void) => void; + onDidChangeLanguage: (handler: (options: { newLanguage: string }) => void) => void; +} diff --git a/packages/kbn-monaco/src/painless/diagnostics_adapter.test.ts b/packages/kbn-monaco/src/painless/diagnostics_adapter.test.ts new file mode 100644 index 0000000000000..5d00ad726d031 --- /dev/null +++ b/packages/kbn-monaco/src/painless/diagnostics_adapter.test.ts @@ -0,0 +1,147 @@ +/* + * Copyright 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 '../__jest__/jest.mocks'; // Make sure this is the first import + +import { Subscription } from 'rxjs'; + +import { MockIModel } from '../__jest__/types'; +import { LangValidation } from '../types'; +import { monaco } from '../monaco_imports'; +import { ID } from './constants'; + +import { DiagnosticsAdapter } from './diagnostics_adapter'; + +const getSyntaxErrors = jest.fn(async (): Promise => undefined); + +const getMockWorker = async () => { + return { + getSyntaxErrors, + } as any; +}; + +function flushPromises() { + return new Promise((resolve) => setImmediate(resolve)); +} + +describe('Painless DiagnosticAdapter', () => { + let diagnosticAdapter: DiagnosticsAdapter; + let subscription: Subscription; + let model: MockIModel; + let validation: LangValidation; + + beforeAll(() => { + jest.useFakeTimers(); + }); + + afterAll(() => { + jest.useRealTimers(); + }); + + beforeEach(async () => { + model = monaco.editor.createModel(ID) as unknown as MockIModel; + diagnosticAdapter = new DiagnosticsAdapter(getMockWorker); + + // validate() has a promise we need to wait for + // --> await worker.getSyntaxErrors() + await flushPromises(); + + subscription = diagnosticAdapter.validation$.subscribe((newValidation) => { + validation = newValidation; + }); + }); + + afterEach(() => { + if (subscription) { + subscription.unsubscribe(); + } + }); + + test('should validate when the content changes', async () => { + expect(validation!.isValidating).toBe(false); + + model.setValue('new content'); + await flushPromises(); + expect(validation!.isValidating).toBe(true); + + jest.advanceTimersByTime(500); // there is a 500ms debounce for the validate() to trigger + await flushPromises(); + + expect(validation!.isValidating).toBe(false); + + model.setValue('changed'); + // Flushing promise here is not actually required but adding it to make sure the test + // works as expected even when doing so. + await flushPromises(); + expect(validation!.isValidating).toBe(true); + + // when we clear the content we immediately set the + // "isValidating" to false and mark the content as valid. + // No need to wait for the setTimeout + model.setValue(''); + await flushPromises(); + expect(validation!.isValidating).toBe(false); + expect(validation!.isValid).toBe(true); + }); + + test('should prevent race condition of multiple content change and validation triggered', async () => { + const errors = ['Syntax error returned']; + + getSyntaxErrors.mockResolvedValueOnce(errors); + + expect(validation!.isValidating).toBe(false); + + model.setValue('foo'); + jest.advanceTimersByTime(300); // only 300ms out of the 500ms + + model.setValue('bar'); // This will cancel the first setTimeout + + jest.advanceTimersByTime(300); // Again, only 300ms out of the 500ms. + await flushPromises(); + + expect(validation!.isValidating).toBe(true); // we are still validating + + jest.advanceTimersByTime(200); // rest of the 500ms + await flushPromises(); + + expect(validation!.isValidating).toBe(false); + expect(validation!.isValid).toBe(false); + expect(validation!.errors).toBe(errors); + }); + + test('should prevent race condition (2) of multiple content change and validation triggered', async () => { + const errors1 = ['First error returned']; + const errors2 = ['Second error returned']; + + getSyntaxErrors + .mockResolvedValueOnce(errors1) // first call + .mockResolvedValueOnce(errors2); // second call + + model.setValue('foo'); + // By now we are waiting on the worker to await getSyntaxErrors() + // we won't flush the promise to not pass this point in time just yet + jest.advanceTimersByTime(700); + + // We change the value at the same moment + model.setValue('bar'); + // now we pass the await getSyntaxErrors() point but its result (errors1) should be stale and discarted + await flushPromises(); + + jest.advanceTimersByTime(300); + await flushPromises(); + + expect(validation!.isValidating).toBe(true); // we are still validating value "bar" + + jest.advanceTimersByTime(200); // rest of the 500ms + await flushPromises(); + + expect(validation!.isValidating).toBe(false); + expect(validation!.isValid).toBe(false); + // We have the second error response, the first one has been discarted + expect(validation!.errors).toBe(errors2); + }); +}); diff --git a/packages/kbn-monaco/src/painless/diagnostics_adapter.ts b/packages/kbn-monaco/src/painless/diagnostics_adapter.ts index 3d13d76743dbc..a113adb74f22d 100644 --- a/packages/kbn-monaco/src/painless/diagnostics_adapter.ts +++ b/packages/kbn-monaco/src/painless/diagnostics_adapter.ts @@ -6,7 +6,10 @@ * Side Public License, v 1. */ +import { BehaviorSubject } from 'rxjs'; + import { monaco } from '../monaco_imports'; +import { SyntaxErrors, LangValidation } from '../types'; import { ID } from './constants'; import { WorkerAccessor } from './language'; import { PainlessError } from './worker'; @@ -18,11 +21,17 @@ const toDiagnostics = (error: PainlessError): monaco.editor.IMarkerData => { }; }; -export interface SyntaxErrors { - [modelId: string]: PainlessError[]; -} export class DiagnosticsAdapter { private errors: SyntaxErrors = {}; + private validation = new BehaviorSubject({ + isValid: true, + isValidating: false, + errors: [], + }); + // To avoid stale validation data we keep track of the latest call to validate(). + private validateIdx = 0; + + public validation$ = this.validation.asObservable(); constructor(private worker: WorkerAccessor) { const onModelAdd = (model: monaco.editor.IModel): void => { @@ -35,14 +44,27 @@ export class DiagnosticsAdapter { return; } + const idx = ++this.validateIdx; // Disable any possible inflight validation + clearTimeout(handle); + // Reset the model markers if an empty string is provided on change if (model.getValue().trim() === '') { + this.validation.next({ + isValid: true, + isValidating: false, + errors: [], + }); return monaco.editor.setModelMarkers(model, ID, []); } + this.validation.next({ + ...this.validation.value, + isValidating: true, + }); // Every time a new change is made, wait 500ms before validating - clearTimeout(handle); - handle = setTimeout(() => this.validate(model.uri), 500); + handle = setTimeout(() => { + this.validate(model.uri, idx); + }, 500); }); model.onDidChangeLanguage(({ newLanguage }) => { @@ -51,21 +73,33 @@ export class DiagnosticsAdapter { if (newLanguage !== ID) { return monaco.editor.setModelMarkers(model, ID, []); } else { - this.validate(model.uri); + this.validate(model.uri, ++this.validateIdx); } }); - this.validate(model.uri); + this.validation.next({ + ...this.validation.value, + isValidating: true, + }); + this.validate(model.uri, ++this.validateIdx); } }; monaco.editor.onDidCreateModel(onModelAdd); monaco.editor.getModels().forEach(onModelAdd); } - private async validate(resource: monaco.Uri): Promise { + private async validate(resource: monaco.Uri, idx: number): Promise { + if (idx !== this.validateIdx) { + return; + } + const worker = await this.worker(resource); const errorMarkers = await worker.getSyntaxErrors(resource.toString()); + if (idx !== this.validateIdx) { + return; + } + if (errorMarkers) { const model = monaco.editor.getModel(resource); this.errors = { @@ -75,6 +109,9 @@ export class DiagnosticsAdapter { // Set the error markers and underline them with "Error" severity monaco.editor.setModelMarkers(model!, ID, errorMarkers.map(toDiagnostics)); } + + const isValid = errorMarkers === undefined || errorMarkers.length === 0; + this.validation.next({ isValidating: false, isValid, errors: errorMarkers ?? [] }); } public getSyntaxErrors() { diff --git a/packages/kbn-monaco/src/painless/index.ts b/packages/kbn-monaco/src/painless/index.ts index 3bba7643e28b6..793dc5142a41e 100644 --- a/packages/kbn-monaco/src/painless/index.ts +++ b/packages/kbn-monaco/src/painless/index.ts @@ -8,7 +8,7 @@ import { ID } from './constants'; import { lexerRules, languageConfiguration } from './lexer_rules'; -import { getSuggestionProvider, getSyntaxErrors } from './language'; +import { getSuggestionProvider, getSyntaxErrors, validation$ } from './language'; import { CompleteLangModuleType } from '../types'; export const PainlessLang: CompleteLangModuleType = { @@ -17,6 +17,7 @@ export const PainlessLang: CompleteLangModuleType = { lexerRules, languageConfiguration, getSyntaxErrors, + validation$, }; export * from './types'; diff --git a/packages/kbn-monaco/src/painless/language.ts b/packages/kbn-monaco/src/painless/language.ts index 3cb26d970fc7d..abeee8d501f31 100644 --- a/packages/kbn-monaco/src/painless/language.ts +++ b/packages/kbn-monaco/src/painless/language.ts @@ -5,15 +5,16 @@ * in compliance with, at your election, the Elastic License 2.0 or the Server * Side Public License, v 1. */ - +import { Observable, of } from 'rxjs'; import { monaco } from '../monaco_imports'; import { WorkerProxyService, EditorStateService } from './lib'; +import { LangValidation, SyntaxErrors } from '../types'; import { ID } from './constants'; import { PainlessContext, PainlessAutocompleteField } from './types'; import { PainlessWorker } from './worker'; import { PainlessCompletionAdapter } from './completion_adapter'; -import { DiagnosticsAdapter, SyntaxErrors } from './diagnostics_adapter'; +import { DiagnosticsAdapter } from './diagnostics_adapter'; const workerProxyService = new WorkerProxyService(); const editorStateService = new EditorStateService(); @@ -37,9 +38,13 @@ let diagnosticsAdapter: DiagnosticsAdapter; // Returns syntax errors for all models by model id export const getSyntaxErrors = (): SyntaxErrors => { - return diagnosticsAdapter.getSyntaxErrors(); + return diagnosticsAdapter?.getSyntaxErrors() ?? {}; }; +export const validation$: () => Observable = () => + diagnosticsAdapter?.validation$ || + of({ isValid: true, isValidating: false, errors: [] }); + monaco.languages.onLanguage(ID, async () => { workerProxyService.setup(); diff --git a/packages/kbn-monaco/src/types.ts b/packages/kbn-monaco/src/types.ts index 0e20021bf69eb..8512ef1ac58c0 100644 --- a/packages/kbn-monaco/src/types.ts +++ b/packages/kbn-monaco/src/types.ts @@ -5,6 +5,8 @@ * in compliance with, at your election, the Elastic License 2.0 or the Server * Side Public License, v 1. */ +import type { Observable } from 'rxjs'; + import { monaco } from './monaco_imports'; export interface LangModuleType { @@ -19,4 +21,23 @@ export interface CompleteLangModuleType extends LangModuleType { languageConfiguration: monaco.languages.LanguageConfiguration; getSuggestionProvider: Function; getSyntaxErrors: Function; + validation$: () => Observable; +} + +export interface EditorError { + startLineNumber: number; + startColumn: number; + endLineNumber: number; + endColumn: number; + message: string; +} + +export interface LangValidation { + isValidating: boolean; + isValid: boolean; + errors: EditorError[]; +} + +export interface SyntaxErrors { + [modelId: string]: EditorError[]; } diff --git a/packages/kbn-optimizer/limits.yml b/packages/kbn-optimizer/limits.yml index 99f9c04069b72..b39968f4889f5 100644 --- a/packages/kbn-optimizer/limits.yml +++ b/packages/kbn-optimizer/limits.yml @@ -104,7 +104,7 @@ pageLoadAssetSize: dataViews: 41532 expressions: 140958 fieldFormats: 65209 - kibanaReact: 84422 + kibanaReact: 74422 share: 71239 uiActions: 35121 dataEnhanced: 24980 diff --git a/packages/kbn-optimizer/src/worker/webpack.config.ts b/packages/kbn-optimizer/src/worker/webpack.config.ts index 3e46f5a768d87..58201152a2459 100644 --- a/packages/kbn-optimizer/src/worker/webpack.config.ts +++ b/packages/kbn-optimizer/src/worker/webpack.config.ts @@ -28,6 +28,14 @@ const IS_CODE_COVERAGE = !!process.env.CODE_COVERAGE; const ISTANBUL_PRESET_PATH = require.resolve('@kbn/babel-preset/istanbul_preset'); const BABEL_PRESET_PATH = require.resolve('@kbn/babel-preset/webpack_preset'); +const nodeModulesButNotKbnPackages = (path: string) => { + if (!path.includes('node_modules')) { + return false; + } + + return !path.includes(`node_modules${Path.sep}@kbn${Path.sep}`); +}; + export function getWebpackConfig(bundle: Bundle, bundleRefs: BundleRefs, worker: WorkerConfig) { const ENTRY_CREATOR = require.resolve('./entry_point_creator'); @@ -138,7 +146,7 @@ export function getWebpackConfig(bundle: Bundle, bundleRefs: BundleRefs, worker: }, { test: /\.scss$/, - exclude: /node_modules/, + exclude: nodeModulesButNotKbnPackages, oneOf: [ ...worker.themeTags.map((theme) => ({ resourceQuery: `?${theme}`, diff --git a/packages/kbn-pm/dist/index.js b/packages/kbn-pm/dist/index.js index d2c067759e25f..49729eee0aa1e 100644 --- a/packages/kbn-pm/dist/index.js +++ b/packages/kbn-pm/dist/index.js @@ -94,7 +94,7 @@ __webpack_require__.r(__webpack_exports__); /* harmony import */ var _cli__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(1); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "run", function() { return _cli__WEBPACK_IMPORTED_MODULE_0__["run"]; }); -/* harmony import */ var _production__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(560); +/* harmony import */ var _production__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(561); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "buildBazelProductionProjects", function() { return _production__WEBPACK_IMPORTED_MODULE_1__["buildBazelProductionProjects"]; }); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "buildNonBazelProductionProjects", function() { return _production__WEBPACK_IMPORTED_MODULE_1__["buildNonBazelProductionProjects"]; }); @@ -108,7 +108,7 @@ __webpack_require__.r(__webpack_exports__); /* harmony import */ var _utils_package_json__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(349); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "transformDependencies", function() { return _utils_package_json__WEBPACK_IMPORTED_MODULE_4__["transformDependencies"]; }); -/* harmony import */ var _config__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(559); +/* harmony import */ var _config__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(560); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "getProjectPaths", function() { return _config__WEBPACK_IMPORTED_MODULE_5__["getProjectPaths"]; }); /* @@ -141,7 +141,7 @@ __webpack_require__.r(__webpack_exports__); /* harmony import */ var _kbn_dev_utils_tooling_log__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(5); /* harmony import */ var _kbn_dev_utils_tooling_log__WEBPACK_IMPORTED_MODULE_3___default = /*#__PURE__*/__webpack_require__.n(_kbn_dev_utils_tooling_log__WEBPACK_IMPORTED_MODULE_3__); /* harmony import */ var _commands__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(129); -/* harmony import */ var _run__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(554); +/* harmony import */ var _run__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(555); /* harmony import */ var _utils_log__WEBPACK_IMPORTED_MODULE_6__ = __webpack_require__(220); /* * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one @@ -8816,6 +8816,7 @@ __webpack_require__.r(__webpack_exports__); /* harmony import */ var _reset__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(550); /* harmony import */ var _run__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(551); /* harmony import */ var _watch__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(553); +/* harmony import */ var _patch_native_modules__WEBPACK_IMPORTED_MODULE_6__ = __webpack_require__(554); /* * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one * or more contributor license agreements. Licensed under the Elastic License @@ -8829,13 +8830,15 @@ __webpack_require__.r(__webpack_exports__); + const commands = { bootstrap: _bootstrap__WEBPACK_IMPORTED_MODULE_0__["BootstrapCommand"], build: _build__WEBPACK_IMPORTED_MODULE_1__["BuildCommand"], clean: _clean__WEBPACK_IMPORTED_MODULE_2__["CleanCommand"], reset: _reset__WEBPACK_IMPORTED_MODULE_3__["ResetCommand"], run: _run__WEBPACK_IMPORTED_MODULE_4__["RunCommand"], - watch: _watch__WEBPACK_IMPORTED_MODULE_5__["WatchCommand"] + watch: _watch__WEBPACK_IMPORTED_MODULE_5__["WatchCommand"], + patch_native_modules: _patch_native_modules__WEBPACK_IMPORTED_MODULE_6__["PatchNativeModulesCommand"] }; /***/ }), @@ -62929,6 +62932,86 @@ const WatchCommand = { /* 554 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { +"use strict"; +__webpack_require__.r(__webpack_exports__); +/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "PatchNativeModulesCommand", function() { return PatchNativeModulesCommand; }); +/* 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__); +/* harmony import */ var fs__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(132); +/* harmony import */ var fs__WEBPACK_IMPORTED_MODULE_1___default = /*#__PURE__*/__webpack_require__.n(fs__WEBPACK_IMPORTED_MODULE_1__); +/* harmony import */ var _kbn_dev_utils_ci_stats_reporter__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(131); +/* harmony import */ var _kbn_dev_utils_ci_stats_reporter__WEBPACK_IMPORTED_MODULE_2___default = /*#__PURE__*/__webpack_require__.n(_kbn_dev_utils_ci_stats_reporter__WEBPACK_IMPORTED_MODULE_2__); +/* harmony import */ var _utils_log__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(220); +/* harmony import */ var _utils_child_process__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(221); +/* + * Copyright 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 PatchNativeModulesCommand = { + description: 'Patch native modules by running build commands on M1 Macs', + name: 'patch_native_modules', + + async run(projects, _, { + kbn + }) { + var _projects$get; + + const kibanaProjectPath = ((_projects$get = projects.get('kibana')) === null || _projects$get === void 0 ? void 0 : _projects$get.path) || ''; + const reporter = _kbn_dev_utils_ci_stats_reporter__WEBPACK_IMPORTED_MODULE_2__["CiStatsReporter"].fromEnv(_utils_log__WEBPACK_IMPORTED_MODULE_3__["log"]); + + if (process.platform !== 'darwin' || process.arch !== 'arm64') { + return; + } + + const startTime = Date.now(); + const nodeSassDir = path__WEBPACK_IMPORTED_MODULE_0___default.a.resolve(kibanaProjectPath, 'node_modules/node-sass'); + const nodeSassNativeDist = path__WEBPACK_IMPORTED_MODULE_0___default.a.resolve(nodeSassDir, `vendor/darwin-arm64-${process.versions.modules}/binding.node`); + + if (!fs__WEBPACK_IMPORTED_MODULE_1___default.a.existsSync(nodeSassNativeDist)) { + _utils_log__WEBPACK_IMPORTED_MODULE_3__["log"].info('Running build script for node-sass'); + await Object(_utils_child_process__WEBPACK_IMPORTED_MODULE_4__["spawn"])('npm', ['run', 'build'], { + cwd: nodeSassDir + }); + } + + const re2Dir = path__WEBPACK_IMPORTED_MODULE_0___default.a.resolve(kibanaProjectPath, 'node_modules/re2'); + const re2NativeDist = path__WEBPACK_IMPORTED_MODULE_0___default.a.resolve(re2Dir, 'build/Release/re2.node'); + + if (!fs__WEBPACK_IMPORTED_MODULE_1___default.a.existsSync(re2NativeDist)) { + _utils_log__WEBPACK_IMPORTED_MODULE_3__["log"].info('Running build script for re2'); + await Object(_utils_child_process__WEBPACK_IMPORTED_MODULE_4__["spawn"])('npm', ['run', 'rebuild'], { + cwd: re2Dir + }); + } + + _utils_log__WEBPACK_IMPORTED_MODULE_3__["log"].success('native modules should be setup for native ARM Mac development'); // send timings + + await reporter.timings({ + upstreamBranch: kbn.kibanaProject.json.branch, + // prevent loading @kbn/utils by passing null + kibanaUuid: kbn.getUuid() || null, + timings: [{ + group: 'scripts/kbn bootstrap', + id: 'patch native modudles for arm macs', + ms: Date.now() - startTime + }] + }); + } + +}; + +/***/ }), +/* 555 */ +/***/ (function(module, __webpack_exports__, __webpack_require__) { + "use strict"; __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "runCommand", function() { return runCommand; }); @@ -62938,7 +63021,7 @@ __webpack_require__.r(__webpack_exports__); /* harmony import */ var _utils_log__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(220); /* harmony import */ var _utils_projects__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(346); /* harmony import */ var _utils_projects_tree__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(418); -/* harmony import */ var _utils_kibana__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(555); +/* harmony import */ var _utils_kibana__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(556); function ownKeys(object, enumerableOnly) { var keys = Object.keys(object); if (Object.getOwnPropertySymbols) { var symbols = Object.getOwnPropertySymbols(object); if (enumerableOnly) { symbols = symbols.filter(function (sym) { return Object.getOwnPropertyDescriptor(object, sym).enumerable; }); } keys.push.apply(keys, symbols); } return keys; } function _objectSpread(target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i] != null ? arguments[i] : {}; if (i % 2) { ownKeys(Object(source), true).forEach(function (key) { _defineProperty(target, key, source[key]); }); } else if (Object.getOwnPropertyDescriptors) { Object.defineProperties(target, Object.getOwnPropertyDescriptors(source)); } else { ownKeys(Object(source)).forEach(function (key) { Object.defineProperty(target, key, Object.getOwnPropertyDescriptor(source, key)); }); } } return target; } @@ -63057,7 +63140,7 @@ function toArray(value) { } /***/ }), -/* 555 */ +/* 556 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -63067,13 +63150,13 @@ __webpack_require__.r(__webpack_exports__); /* harmony import */ var path__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(path__WEBPACK_IMPORTED_MODULE_0__); /* harmony import */ var fs__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(132); /* harmony import */ var fs__WEBPACK_IMPORTED_MODULE_1___default = /*#__PURE__*/__webpack_require__.n(fs__WEBPACK_IMPORTED_MODULE_1__); -/* harmony import */ var multimatch__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(556); +/* harmony import */ var multimatch__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(557); /* harmony import */ var multimatch__WEBPACK_IMPORTED_MODULE_2___default = /*#__PURE__*/__webpack_require__.n(multimatch__WEBPACK_IMPORTED_MODULE_2__); /* harmony import */ var is_path_inside__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(339); /* harmony import */ var is_path_inside__WEBPACK_IMPORTED_MODULE_3___default = /*#__PURE__*/__webpack_require__.n(is_path_inside__WEBPACK_IMPORTED_MODULE_3__); /* harmony import */ var _yarn_lock__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(414); /* harmony import */ var _projects__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(346); -/* harmony import */ var _config__WEBPACK_IMPORTED_MODULE_6__ = __webpack_require__(559); +/* harmony import */ var _config__WEBPACK_IMPORTED_MODULE_6__ = __webpack_require__(560); function ownKeys(object, enumerableOnly) { var keys = Object.keys(object); if (Object.getOwnPropertySymbols) { var symbols = Object.getOwnPropertySymbols(object); if (enumerableOnly) { symbols = symbols.filter(function (sym) { return Object.getOwnPropertyDescriptor(object, sym).enumerable; }); } keys.push.apply(keys, symbols); } return keys; } function _objectSpread(target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i] != null ? arguments[i] : {}; if (i % 2) { ownKeys(Object(source), true).forEach(function (key) { _defineProperty(target, key, source[key]); }); } else if (Object.getOwnPropertyDescriptors) { Object.defineProperties(target, Object.getOwnPropertyDescriptors(source)); } else { ownKeys(Object(source)).forEach(function (key) { Object.defineProperty(target, key, Object.getOwnPropertyDescriptor(source, key)); }); } } return target; } @@ -63237,15 +63320,15 @@ class Kibana { } /***/ }), -/* 556 */ +/* 557 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; const minimatch = __webpack_require__(247); const arrayUnion = __webpack_require__(242); -const arrayDiffer = __webpack_require__(557); -const arrify = __webpack_require__(558); +const arrayDiffer = __webpack_require__(558); +const arrify = __webpack_require__(559); module.exports = (list, patterns, options = {}) => { list = arrify(list); @@ -63269,7 +63352,7 @@ module.exports = (list, patterns, options = {}) => { /***/ }), -/* 557 */ +/* 558 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -63284,7 +63367,7 @@ module.exports = arrayDiffer; /***/ }), -/* 558 */ +/* 559 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -63314,7 +63397,7 @@ module.exports = arrify; /***/ }), -/* 559 */ +/* 560 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -63374,15 +63457,15 @@ function getProjectPaths({ } /***/ }), -/* 560 */ +/* 561 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); -/* harmony import */ var _build_bazel_production_projects__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(561); +/* harmony import */ var _build_bazel_production_projects__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(562); /* 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__(808); +/* harmony import */ var _build_non_bazel_production_projects__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(809); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "buildNonBazelProductionProjects", function() { return _build_non_bazel_production_projects__WEBPACK_IMPORTED_MODULE_1__["buildNonBazelProductionProjects"]; }); /* @@ -63396,19 +63479,19 @@ __webpack_require__.r(__webpack_exports__); /***/ }), -/* 561 */ +/* 562 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "buildBazelProductionProjects", function() { return buildBazelProductionProjects; }); -/* harmony import */ var cpy__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(562); +/* harmony import */ var cpy__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(563); /* harmony import */ var cpy__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(cpy__WEBPACK_IMPORTED_MODULE_0__); -/* harmony import */ var globby__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(774); +/* harmony import */ var globby__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(775); /* 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__(808); +/* harmony import */ var _build_non_bazel_production_projects__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(809); /* harmony import */ var _utils_bazel__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(419); /* harmony import */ var _utils_fs__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(231); /* harmony import */ var _utils_log__WEBPACK_IMPORTED_MODULE_6__ = __webpack_require__(220); @@ -63503,7 +63586,7 @@ async function applyCorrectPermissions(project, kibanaRoot, buildRoot) { } /***/ }), -/* 562 */ +/* 563 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -63511,14 +63594,14 @@ async function applyCorrectPermissions(project, kibanaRoot, buildRoot) { const EventEmitter = __webpack_require__(164); const path = __webpack_require__(4); const os = __webpack_require__(122); -const pMap = __webpack_require__(563); -const arrify = __webpack_require__(558); -const globby = __webpack_require__(566); -const hasGlob = __webpack_require__(758); -const cpFile = __webpack_require__(760); -const junk = __webpack_require__(770); -const pFilter = __webpack_require__(771); -const CpyError = __webpack_require__(773); +const pMap = __webpack_require__(564); +const arrify = __webpack_require__(559); +const globby = __webpack_require__(567); +const hasGlob = __webpack_require__(759); +const cpFile = __webpack_require__(761); +const junk = __webpack_require__(771); +const pFilter = __webpack_require__(772); +const CpyError = __webpack_require__(774); const defaultOptions = { ignoreJunk: true @@ -63669,12 +63752,12 @@ module.exports = (source, destination, { /***/ }), -/* 563 */ +/* 564 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -const AggregateError = __webpack_require__(564); +const AggregateError = __webpack_require__(565); module.exports = async ( iterable, @@ -63757,12 +63840,12 @@ module.exports = async ( /***/ }), -/* 564 */ +/* 565 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -const indentString = __webpack_require__(565); +const indentString = __webpack_require__(566); const cleanStack = __webpack_require__(344); const cleanInternalStack = stack => stack.replace(/\s+at .*aggregate-error\/index.js:\d+:\d+\)?/g, ''); @@ -63811,7 +63894,7 @@ module.exports = AggregateError; /***/ }), -/* 565 */ +/* 566 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -63853,17 +63936,17 @@ module.exports = (string, count = 1, options) => { /***/ }), -/* 566 */ +/* 567 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; const fs = __webpack_require__(132); -const arrayUnion = __webpack_require__(567); +const arrayUnion = __webpack_require__(568); const glob = __webpack_require__(244); -const fastGlob = __webpack_require__(569); -const dirGlob = __webpack_require__(752); -const gitignore = __webpack_require__(755); +const fastGlob = __webpack_require__(570); +const dirGlob = __webpack_require__(753); +const gitignore = __webpack_require__(756); const DEFAULT_FILTER = () => false; @@ -64008,12 +64091,12 @@ module.exports.gitignore = gitignore; /***/ }), -/* 567 */ +/* 568 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var arrayUniq = __webpack_require__(568); +var arrayUniq = __webpack_require__(569); module.exports = function () { return arrayUniq([].concat.apply([], arguments)); @@ -64021,7 +64104,7 @@ module.exports = function () { /***/ }), -/* 568 */ +/* 569 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -64090,10 +64173,10 @@ if ('Set' in global) { /***/ }), -/* 569 */ +/* 570 */ /***/ (function(module, exports, __webpack_require__) { -const pkg = __webpack_require__(570); +const pkg = __webpack_require__(571); module.exports = pkg.async; module.exports.default = pkg.async; @@ -64106,19 +64189,19 @@ module.exports.generateTasks = pkg.generateTasks; /***/ }), -/* 570 */ +/* 571 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -var optionsManager = __webpack_require__(571); -var taskManager = __webpack_require__(572); -var reader_async_1 = __webpack_require__(723); -var reader_stream_1 = __webpack_require__(747); -var reader_sync_1 = __webpack_require__(748); -var arrayUtils = __webpack_require__(750); -var streamUtils = __webpack_require__(751); +var optionsManager = __webpack_require__(572); +var taskManager = __webpack_require__(573); +var reader_async_1 = __webpack_require__(724); +var reader_stream_1 = __webpack_require__(748); +var reader_sync_1 = __webpack_require__(749); +var arrayUtils = __webpack_require__(751); +var streamUtils = __webpack_require__(752); /** * Synchronous API. */ @@ -64184,7 +64267,7 @@ function isString(source) { /***/ }), -/* 571 */ +/* 572 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -64222,13 +64305,13 @@ exports.prepare = prepare; /***/ }), -/* 572 */ +/* 573 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -var patternUtils = __webpack_require__(573); +var patternUtils = __webpack_require__(574); /** * Generate tasks based on parent directory of each pattern. */ @@ -64319,16 +64402,16 @@ exports.convertPatternGroupToTask = convertPatternGroupToTask; /***/ }), -/* 573 */ +/* 574 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); var path = __webpack_require__(4); -var globParent = __webpack_require__(574); +var globParent = __webpack_require__(575); var isGlob = __webpack_require__(266); -var micromatch = __webpack_require__(577); +var micromatch = __webpack_require__(578); var GLOBSTAR = '**'; /** * Return true for static pattern. @@ -64474,15 +64557,15 @@ exports.matchAny = matchAny; /***/ }), -/* 574 */ +/* 575 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; var path = __webpack_require__(4); -var isglob = __webpack_require__(575); -var pathDirname = __webpack_require__(576); +var isglob = __webpack_require__(576); +var pathDirname = __webpack_require__(577); var isWin32 = __webpack_require__(122).platform() === 'win32'; module.exports = function globParent(str) { @@ -64505,7 +64588,7 @@ module.exports = function globParent(str) { /***/ }), -/* 575 */ +/* 576 */ /***/ (function(module, exports, __webpack_require__) { /*! @@ -64536,7 +64619,7 @@ module.exports = function isGlob(str) { /***/ }), -/* 576 */ +/* 577 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -64686,7 +64769,7 @@ module.exports.win32 = win32; /***/ }), -/* 577 */ +/* 578 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -64697,18 +64780,18 @@ module.exports.win32 = win32; */ var util = __webpack_require__(113); -var braces = __webpack_require__(578); -var toRegex = __webpack_require__(579); -var extend = __webpack_require__(691); +var braces = __webpack_require__(579); +var toRegex = __webpack_require__(580); +var extend = __webpack_require__(692); /** * Local dependencies */ -var compilers = __webpack_require__(693); -var parsers = __webpack_require__(719); -var cache = __webpack_require__(720); -var utils = __webpack_require__(721); +var compilers = __webpack_require__(694); +var parsers = __webpack_require__(720); +var cache = __webpack_require__(721); +var utils = __webpack_require__(722); var MAX_LENGTH = 1024 * 64; /** @@ -65570,7 +65653,7 @@ module.exports = micromatch; /***/ }), -/* 578 */ +/* 579 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -65580,18 +65663,18 @@ module.exports = micromatch; * Module dependencies */ -var toRegex = __webpack_require__(579); -var unique = __webpack_require__(599); -var extend = __webpack_require__(600); +var toRegex = __webpack_require__(580); +var unique = __webpack_require__(600); +var extend = __webpack_require__(601); /** * Local dependencies */ -var compilers = __webpack_require__(602); -var parsers = __webpack_require__(617); -var Braces = __webpack_require__(622); -var utils = __webpack_require__(603); +var compilers = __webpack_require__(603); +var parsers = __webpack_require__(618); +var Braces = __webpack_require__(623); +var utils = __webpack_require__(604); var MAX_LENGTH = 1024 * 64; var cache = {}; @@ -65895,16 +65978,16 @@ module.exports = braces; /***/ }), -/* 579 */ +/* 580 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var safe = __webpack_require__(580); -var define = __webpack_require__(586); -var extend = __webpack_require__(592); -var not = __webpack_require__(596); +var safe = __webpack_require__(581); +var define = __webpack_require__(587); +var extend = __webpack_require__(593); +var not = __webpack_require__(597); var MAX_LENGTH = 1024 * 64; /** @@ -66057,10 +66140,10 @@ module.exports.makeRe = makeRe; /***/ }), -/* 580 */ +/* 581 */ /***/ (function(module, exports, __webpack_require__) { -var parse = __webpack_require__(581); +var parse = __webpack_require__(582); var types = parse.types; module.exports = function (re, opts) { @@ -66106,13 +66189,13 @@ function isRegExp (x) { /***/ }), -/* 581 */ +/* 582 */ /***/ (function(module, exports, __webpack_require__) { -var util = __webpack_require__(582); -var types = __webpack_require__(583); -var sets = __webpack_require__(584); -var positions = __webpack_require__(585); +var util = __webpack_require__(583); +var types = __webpack_require__(584); +var sets = __webpack_require__(585); +var positions = __webpack_require__(586); module.exports = function(regexpStr) { @@ -66394,11 +66477,11 @@ module.exports.types = types; /***/ }), -/* 582 */ +/* 583 */ /***/ (function(module, exports, __webpack_require__) { -var types = __webpack_require__(583); -var sets = __webpack_require__(584); +var types = __webpack_require__(584); +var sets = __webpack_require__(585); // All of these are private and only used by randexp. @@ -66511,7 +66594,7 @@ exports.error = function(regexp, msg) { /***/ }), -/* 583 */ +/* 584 */ /***/ (function(module, exports) { module.exports = { @@ -66527,10 +66610,10 @@ module.exports = { /***/ }), -/* 584 */ +/* 585 */ /***/ (function(module, exports, __webpack_require__) { -var types = __webpack_require__(583); +var types = __webpack_require__(584); var INTS = function() { return [{ type: types.RANGE , from: 48, to: 57 }]; @@ -66615,10 +66698,10 @@ exports.anyChar = function() { /***/ }), -/* 585 */ +/* 586 */ /***/ (function(module, exports, __webpack_require__) { -var types = __webpack_require__(583); +var types = __webpack_require__(584); exports.wordBoundary = function() { return { type: types.POSITION, value: 'b' }; @@ -66638,7 +66721,7 @@ exports.end = function() { /***/ }), -/* 586 */ +/* 587 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -66651,8 +66734,8 @@ exports.end = function() { -var isobject = __webpack_require__(587); -var isDescriptor = __webpack_require__(588); +var isobject = __webpack_require__(588); +var isDescriptor = __webpack_require__(589); var define = (typeof Reflect !== 'undefined' && Reflect.defineProperty) ? Reflect.defineProperty : Object.defineProperty; @@ -66683,7 +66766,7 @@ module.exports = function defineProperty(obj, key, val) { /***/ }), -/* 587 */ +/* 588 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -66702,7 +66785,7 @@ module.exports = function isObject(val) { /***/ }), -/* 588 */ +/* 589 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -66715,9 +66798,9 @@ module.exports = function isObject(val) { -var typeOf = __webpack_require__(589); -var isAccessor = __webpack_require__(590); -var isData = __webpack_require__(591); +var typeOf = __webpack_require__(590); +var isAccessor = __webpack_require__(591); +var isData = __webpack_require__(592); module.exports = function isDescriptor(obj, key) { if (typeOf(obj) !== 'object') { @@ -66731,7 +66814,7 @@ module.exports = function isDescriptor(obj, key) { /***/ }), -/* 589 */ +/* 590 */ /***/ (function(module, exports) { var toString = Object.prototype.toString; @@ -66866,7 +66949,7 @@ function isBuffer(val) { /***/ }), -/* 590 */ +/* 591 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -66879,7 +66962,7 @@ function isBuffer(val) { -var typeOf = __webpack_require__(589); +var typeOf = __webpack_require__(590); // accessor descriptor properties var accessor = { @@ -66942,7 +67025,7 @@ module.exports = isAccessorDescriptor; /***/ }), -/* 591 */ +/* 592 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -66955,7 +67038,7 @@ module.exports = isAccessorDescriptor; -var typeOf = __webpack_require__(589); +var typeOf = __webpack_require__(590); module.exports = function isDataDescriptor(obj, prop) { // data descriptor properties @@ -66998,14 +67081,14 @@ module.exports = function isDataDescriptor(obj, prop) { /***/ }), -/* 592 */ +/* 593 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var isExtendable = __webpack_require__(593); -var assignSymbols = __webpack_require__(595); +var isExtendable = __webpack_require__(594); +var assignSymbols = __webpack_require__(596); module.exports = Object.assign || function(obj/*, objects*/) { if (obj === null || typeof obj === 'undefined') { @@ -67065,7 +67148,7 @@ function isEnum(obj, key) { /***/ }), -/* 593 */ +/* 594 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -67078,7 +67161,7 @@ function isEnum(obj, key) { -var isPlainObject = __webpack_require__(594); +var isPlainObject = __webpack_require__(595); module.exports = function isExtendable(val) { return isPlainObject(val) || typeof val === 'function' || Array.isArray(val); @@ -67086,7 +67169,7 @@ module.exports = function isExtendable(val) { /***/ }), -/* 594 */ +/* 595 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -67099,7 +67182,7 @@ module.exports = function isExtendable(val) { -var isObject = __webpack_require__(587); +var isObject = __webpack_require__(588); function isObjectObject(o) { return isObject(o) === true @@ -67130,7 +67213,7 @@ module.exports = function isPlainObject(o) { /***/ }), -/* 595 */ +/* 596 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -67177,14 +67260,14 @@ module.exports = function(receiver, objects) { /***/ }), -/* 596 */ +/* 597 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var extend = __webpack_require__(597); -var safe = __webpack_require__(580); +var extend = __webpack_require__(598); +var safe = __webpack_require__(581); /** * The main export is a function that takes a `pattern` string and an `options` object. @@ -67256,14 +67339,14 @@ module.exports = toRegex; /***/ }), -/* 597 */ +/* 598 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var isExtendable = __webpack_require__(598); -var assignSymbols = __webpack_require__(595); +var isExtendable = __webpack_require__(599); +var assignSymbols = __webpack_require__(596); module.exports = Object.assign || function(obj/*, objects*/) { if (obj === null || typeof obj === 'undefined') { @@ -67323,7 +67406,7 @@ function isEnum(obj, key) { /***/ }), -/* 598 */ +/* 599 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -67336,7 +67419,7 @@ function isEnum(obj, key) { -var isPlainObject = __webpack_require__(594); +var isPlainObject = __webpack_require__(595); module.exports = function isExtendable(val) { return isPlainObject(val) || typeof val === 'function' || Array.isArray(val); @@ -67344,7 +67427,7 @@ module.exports = function isExtendable(val) { /***/ }), -/* 599 */ +/* 600 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -67394,13 +67477,13 @@ module.exports.immutable = function uniqueImmutable(arr) { /***/ }), -/* 600 */ +/* 601 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var isObject = __webpack_require__(601); +var isObject = __webpack_require__(602); module.exports = function extend(o/*, objects*/) { if (!isObject(o)) { o = {}; } @@ -67434,7 +67517,7 @@ function hasOwn(obj, key) { /***/ }), -/* 601 */ +/* 602 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -67454,13 +67537,13 @@ module.exports = function isExtendable(val) { /***/ }), -/* 602 */ +/* 603 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var utils = __webpack_require__(603); +var utils = __webpack_require__(604); module.exports = function(braces, options) { braces.compiler @@ -67743,25 +67826,25 @@ function hasQueue(node) { /***/ }), -/* 603 */ +/* 604 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var splitString = __webpack_require__(604); +var splitString = __webpack_require__(605); var utils = module.exports; /** * Module dependencies */ -utils.extend = __webpack_require__(600); -utils.flatten = __webpack_require__(607); -utils.isObject = __webpack_require__(587); -utils.fillRange = __webpack_require__(608); -utils.repeat = __webpack_require__(616); -utils.unique = __webpack_require__(599); +utils.extend = __webpack_require__(601); +utils.flatten = __webpack_require__(608); +utils.isObject = __webpack_require__(588); +utils.fillRange = __webpack_require__(609); +utils.repeat = __webpack_require__(617); +utils.unique = __webpack_require__(600); utils.define = function(obj, key, val) { Object.defineProperty(obj, key, { @@ -68093,7 +68176,7 @@ utils.escapeRegex = function(str) { /***/ }), -/* 604 */ +/* 605 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -68106,7 +68189,7 @@ utils.escapeRegex = function(str) { -var extend = __webpack_require__(605); +var extend = __webpack_require__(606); module.exports = function(str, options, fn) { if (typeof str !== 'string') { @@ -68271,14 +68354,14 @@ function keepEscaping(opts, str, idx) { /***/ }), -/* 605 */ +/* 606 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var isExtendable = __webpack_require__(606); -var assignSymbols = __webpack_require__(595); +var isExtendable = __webpack_require__(607); +var assignSymbols = __webpack_require__(596); module.exports = Object.assign || function(obj/*, objects*/) { if (obj === null || typeof obj === 'undefined') { @@ -68338,7 +68421,7 @@ function isEnum(obj, key) { /***/ }), -/* 606 */ +/* 607 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -68351,7 +68434,7 @@ function isEnum(obj, key) { -var isPlainObject = __webpack_require__(594); +var isPlainObject = __webpack_require__(595); module.exports = function isExtendable(val) { return isPlainObject(val) || typeof val === 'function' || Array.isArray(val); @@ -68359,7 +68442,7 @@ module.exports = function isExtendable(val) { /***/ }), -/* 607 */ +/* 608 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -68388,7 +68471,7 @@ function flat(arr, res) { /***/ }), -/* 608 */ +/* 609 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -68402,10 +68485,10 @@ function flat(arr, res) { var util = __webpack_require__(113); -var isNumber = __webpack_require__(609); -var extend = __webpack_require__(612); -var repeat = __webpack_require__(614); -var toRegex = __webpack_require__(615); +var isNumber = __webpack_require__(610); +var extend = __webpack_require__(613); +var repeat = __webpack_require__(615); +var toRegex = __webpack_require__(616); /** * Return a range of numbers or letters. @@ -68603,7 +68686,7 @@ module.exports = fillRange; /***/ }), -/* 609 */ +/* 610 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -68616,7 +68699,7 @@ module.exports = fillRange; -var typeOf = __webpack_require__(610); +var typeOf = __webpack_require__(611); module.exports = function isNumber(num) { var type = typeOf(num); @@ -68632,10 +68715,10 @@ module.exports = function isNumber(num) { /***/ }), -/* 610 */ +/* 611 */ /***/ (function(module, exports, __webpack_require__) { -var isBuffer = __webpack_require__(611); +var isBuffer = __webpack_require__(612); var toString = Object.prototype.toString; /** @@ -68754,7 +68837,7 @@ module.exports = function kindOf(val) { /***/ }), -/* 611 */ +/* 612 */ /***/ (function(module, exports) { /*! @@ -68781,13 +68864,13 @@ function isSlowBuffer (obj) { /***/ }), -/* 612 */ +/* 613 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var isObject = __webpack_require__(613); +var isObject = __webpack_require__(614); module.exports = function extend(o/*, objects*/) { if (!isObject(o)) { o = {}; } @@ -68821,7 +68904,7 @@ function hasOwn(obj, key) { /***/ }), -/* 613 */ +/* 614 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -68841,7 +68924,7 @@ module.exports = function isExtendable(val) { /***/ }), -/* 614 */ +/* 615 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -68918,7 +69001,7 @@ function repeat(str, num) { /***/ }), -/* 615 */ +/* 616 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -68931,8 +69014,8 @@ function repeat(str, num) { -var repeat = __webpack_require__(614); -var isNumber = __webpack_require__(609); +var repeat = __webpack_require__(615); +var isNumber = __webpack_require__(610); var cache = {}; function toRegexRange(min, max, options) { @@ -69219,7 +69302,7 @@ module.exports = toRegexRange; /***/ }), -/* 616 */ +/* 617 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -69244,14 +69327,14 @@ module.exports = function repeat(ele, num) { /***/ }), -/* 617 */ +/* 618 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var Node = __webpack_require__(618); -var utils = __webpack_require__(603); +var Node = __webpack_require__(619); +var utils = __webpack_require__(604); /** * Braces parsers @@ -69611,15 +69694,15 @@ function concatNodes(pos, node, parent, options) { /***/ }), -/* 618 */ +/* 619 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var isObject = __webpack_require__(587); -var define = __webpack_require__(619); -var utils = __webpack_require__(620); +var isObject = __webpack_require__(588); +var define = __webpack_require__(620); +var utils = __webpack_require__(621); var ownNames; /** @@ -70110,7 +70193,7 @@ exports = module.exports = Node; /***/ }), -/* 619 */ +/* 620 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -70123,7 +70206,7 @@ exports = module.exports = Node; -var isDescriptor = __webpack_require__(588); +var isDescriptor = __webpack_require__(589); module.exports = function defineProperty(obj, prop, val) { if (typeof obj !== 'object' && typeof obj !== 'function') { @@ -70148,13 +70231,13 @@ module.exports = function defineProperty(obj, prop, val) { /***/ }), -/* 620 */ +/* 621 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var typeOf = __webpack_require__(621); +var typeOf = __webpack_require__(622); var utils = module.exports; /** @@ -71174,10 +71257,10 @@ function assert(val, message) { /***/ }), -/* 621 */ +/* 622 */ /***/ (function(module, exports, __webpack_require__) { -var isBuffer = __webpack_require__(611); +var isBuffer = __webpack_require__(612); var toString = Object.prototype.toString; /** @@ -71296,17 +71379,17 @@ module.exports = function kindOf(val) { /***/ }), -/* 622 */ +/* 623 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var extend = __webpack_require__(600); -var Snapdragon = __webpack_require__(623); -var compilers = __webpack_require__(602); -var parsers = __webpack_require__(617); -var utils = __webpack_require__(603); +var extend = __webpack_require__(601); +var Snapdragon = __webpack_require__(624); +var compilers = __webpack_require__(603); +var parsers = __webpack_require__(618); +var utils = __webpack_require__(604); /** * Customize Snapdragon parser and renderer @@ -71407,17 +71490,17 @@ module.exports = Braces; /***/ }), -/* 623 */ +/* 624 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var Base = __webpack_require__(624); -var define = __webpack_require__(654); -var Compiler = __webpack_require__(665); -var Parser = __webpack_require__(688); -var utils = __webpack_require__(668); +var Base = __webpack_require__(625); +var define = __webpack_require__(655); +var Compiler = __webpack_require__(666); +var Parser = __webpack_require__(689); +var utils = __webpack_require__(669); var regexCache = {}; var cache = {}; @@ -71588,20 +71671,20 @@ module.exports.Parser = Parser; /***/ }), -/* 624 */ +/* 625 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; var util = __webpack_require__(113); -var define = __webpack_require__(625); -var CacheBase = __webpack_require__(626); -var Emitter = __webpack_require__(627); -var isObject = __webpack_require__(587); -var merge = __webpack_require__(648); -var pascal = __webpack_require__(651); -var cu = __webpack_require__(652); +var define = __webpack_require__(626); +var CacheBase = __webpack_require__(627); +var Emitter = __webpack_require__(628); +var isObject = __webpack_require__(588); +var merge = __webpack_require__(649); +var pascal = __webpack_require__(652); +var cu = __webpack_require__(653); /** * Optionally define a custom `cache` namespace to use. @@ -72030,7 +72113,7 @@ module.exports.namespace = namespace; /***/ }), -/* 625 */ +/* 626 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -72043,7 +72126,7 @@ module.exports.namespace = namespace; -var isDescriptor = __webpack_require__(588); +var isDescriptor = __webpack_require__(589); module.exports = function defineProperty(obj, prop, val) { if (typeof obj !== 'object' && typeof obj !== 'function') { @@ -72068,21 +72151,21 @@ module.exports = function defineProperty(obj, prop, val) { /***/ }), -/* 626 */ +/* 627 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var isObject = __webpack_require__(587); -var Emitter = __webpack_require__(627); -var visit = __webpack_require__(628); -var toPath = __webpack_require__(631); -var union = __webpack_require__(633); -var del = __webpack_require__(639); -var get = __webpack_require__(636); -var has = __webpack_require__(644); -var set = __webpack_require__(647); +var isObject = __webpack_require__(588); +var Emitter = __webpack_require__(628); +var visit = __webpack_require__(629); +var toPath = __webpack_require__(632); +var union = __webpack_require__(634); +var del = __webpack_require__(640); +var get = __webpack_require__(637); +var has = __webpack_require__(645); +var set = __webpack_require__(648); /** * Create a `Cache` constructor that when instantiated will @@ -72336,7 +72419,7 @@ module.exports.namespace = namespace; /***/ }), -/* 627 */ +/* 628 */ /***/ (function(module, exports, __webpack_require__) { @@ -72505,7 +72588,7 @@ Emitter.prototype.hasListeners = function(event){ /***/ }), -/* 628 */ +/* 629 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -72518,8 +72601,8 @@ Emitter.prototype.hasListeners = function(event){ -var visit = __webpack_require__(629); -var mapVisit = __webpack_require__(630); +var visit = __webpack_require__(630); +var mapVisit = __webpack_require__(631); module.exports = function(collection, method, val) { var result; @@ -72542,7 +72625,7 @@ module.exports = function(collection, method, val) { /***/ }), -/* 629 */ +/* 630 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -72555,7 +72638,7 @@ module.exports = function(collection, method, val) { -var isObject = __webpack_require__(587); +var isObject = __webpack_require__(588); module.exports = function visit(thisArg, method, target, val) { if (!isObject(thisArg) && typeof thisArg !== 'function') { @@ -72582,14 +72665,14 @@ module.exports = function visit(thisArg, method, target, val) { /***/ }), -/* 630 */ +/* 631 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; var util = __webpack_require__(113); -var visit = __webpack_require__(629); +var visit = __webpack_require__(630); /** * Map `visit` over an array of objects. @@ -72626,7 +72709,7 @@ function isObject(val) { /***/ }), -/* 631 */ +/* 632 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -72639,7 +72722,7 @@ function isObject(val) { -var typeOf = __webpack_require__(632); +var typeOf = __webpack_require__(633); module.exports = function toPath(args) { if (typeOf(args) !== 'arguments') { @@ -72666,10 +72749,10 @@ function filter(arr) { /***/ }), -/* 632 */ +/* 633 */ /***/ (function(module, exports, __webpack_require__) { -var isBuffer = __webpack_require__(611); +var isBuffer = __webpack_require__(612); var toString = Object.prototype.toString; /** @@ -72788,16 +72871,16 @@ module.exports = function kindOf(val) { /***/ }), -/* 633 */ +/* 634 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var isObject = __webpack_require__(634); -var union = __webpack_require__(635); -var get = __webpack_require__(636); -var set = __webpack_require__(637); +var isObject = __webpack_require__(635); +var union = __webpack_require__(636); +var get = __webpack_require__(637); +var set = __webpack_require__(638); module.exports = function unionValue(obj, prop, value) { if (!isObject(obj)) { @@ -72825,7 +72908,7 @@ function arrayify(val) { /***/ }), -/* 634 */ +/* 635 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -72845,7 +72928,7 @@ module.exports = function isExtendable(val) { /***/ }), -/* 635 */ +/* 636 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -72881,7 +72964,7 @@ module.exports = function union(init) { /***/ }), -/* 636 */ +/* 637 */ /***/ (function(module, exports) { /*! @@ -72937,7 +73020,7 @@ function toString(val) { /***/ }), -/* 637 */ +/* 638 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -72950,10 +73033,10 @@ function toString(val) { -var split = __webpack_require__(604); -var extend = __webpack_require__(638); -var isPlainObject = __webpack_require__(594); -var isObject = __webpack_require__(634); +var split = __webpack_require__(605); +var extend = __webpack_require__(639); +var isPlainObject = __webpack_require__(595); +var isObject = __webpack_require__(635); module.exports = function(obj, prop, val) { if (!isObject(obj)) { @@ -72999,13 +73082,13 @@ function isValidKey(key) { /***/ }), -/* 638 */ +/* 639 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var isObject = __webpack_require__(634); +var isObject = __webpack_require__(635); module.exports = function extend(o/*, objects*/) { if (!isObject(o)) { o = {}; } @@ -73039,7 +73122,7 @@ function hasOwn(obj, key) { /***/ }), -/* 639 */ +/* 640 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -73052,8 +73135,8 @@ function hasOwn(obj, key) { -var isObject = __webpack_require__(587); -var has = __webpack_require__(640); +var isObject = __webpack_require__(588); +var has = __webpack_require__(641); module.exports = function unset(obj, prop) { if (!isObject(obj)) { @@ -73078,7 +73161,7 @@ module.exports = function unset(obj, prop) { /***/ }), -/* 640 */ +/* 641 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -73091,9 +73174,9 @@ module.exports = function unset(obj, prop) { -var isObject = __webpack_require__(641); -var hasValues = __webpack_require__(643); -var get = __webpack_require__(636); +var isObject = __webpack_require__(642); +var hasValues = __webpack_require__(644); +var get = __webpack_require__(637); module.exports = function(obj, prop, noZero) { if (isObject(obj)) { @@ -73104,7 +73187,7 @@ module.exports = function(obj, prop, noZero) { /***/ }), -/* 641 */ +/* 642 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -73117,7 +73200,7 @@ module.exports = function(obj, prop, noZero) { -var isArray = __webpack_require__(642); +var isArray = __webpack_require__(643); module.exports = function isObject(val) { return val != null && typeof val === 'object' && isArray(val) === false; @@ -73125,7 +73208,7 @@ module.exports = function isObject(val) { /***/ }), -/* 642 */ +/* 643 */ /***/ (function(module, exports) { var toString = {}.toString; @@ -73136,7 +73219,7 @@ module.exports = Array.isArray || function (arr) { /***/ }), -/* 643 */ +/* 644 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -73179,7 +73262,7 @@ module.exports = function hasValue(o, noZero) { /***/ }), -/* 644 */ +/* 645 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -73192,9 +73275,9 @@ module.exports = function hasValue(o, noZero) { -var isObject = __webpack_require__(587); -var hasValues = __webpack_require__(645); -var get = __webpack_require__(636); +var isObject = __webpack_require__(588); +var hasValues = __webpack_require__(646); +var get = __webpack_require__(637); module.exports = function(val, prop) { return hasValues(isObject(val) && prop ? get(val, prop) : val); @@ -73202,7 +73285,7 @@ module.exports = function(val, prop) { /***/ }), -/* 645 */ +/* 646 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -73215,8 +73298,8 @@ module.exports = function(val, prop) { -var typeOf = __webpack_require__(646); -var isNumber = __webpack_require__(609); +var typeOf = __webpack_require__(647); +var isNumber = __webpack_require__(610); module.exports = function hasValue(val) { // is-number checks for NaN and other edge cases @@ -73269,10 +73352,10 @@ module.exports = function hasValue(val) { /***/ }), -/* 646 */ +/* 647 */ /***/ (function(module, exports, __webpack_require__) { -var isBuffer = __webpack_require__(611); +var isBuffer = __webpack_require__(612); var toString = Object.prototype.toString; /** @@ -73394,7 +73477,7 @@ module.exports = function kindOf(val) { /***/ }), -/* 647 */ +/* 648 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -73407,10 +73490,10 @@ module.exports = function kindOf(val) { -var split = __webpack_require__(604); -var extend = __webpack_require__(638); -var isPlainObject = __webpack_require__(594); -var isObject = __webpack_require__(634); +var split = __webpack_require__(605); +var extend = __webpack_require__(639); +var isPlainObject = __webpack_require__(595); +var isObject = __webpack_require__(635); module.exports = function(obj, prop, val) { if (!isObject(obj)) { @@ -73456,14 +73539,14 @@ function isValidKey(key) { /***/ }), -/* 648 */ +/* 649 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var isExtendable = __webpack_require__(649); -var forIn = __webpack_require__(650); +var isExtendable = __webpack_require__(650); +var forIn = __webpack_require__(651); function mixinDeep(target, objects) { var len = arguments.length, i = 0; @@ -73527,7 +73610,7 @@ module.exports = mixinDeep; /***/ }), -/* 649 */ +/* 650 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -73540,7 +73623,7 @@ module.exports = mixinDeep; -var isPlainObject = __webpack_require__(594); +var isPlainObject = __webpack_require__(595); module.exports = function isExtendable(val) { return isPlainObject(val) || typeof val === 'function' || Array.isArray(val); @@ -73548,7 +73631,7 @@ module.exports = function isExtendable(val) { /***/ }), -/* 650 */ +/* 651 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -73571,7 +73654,7 @@ module.exports = function forIn(obj, fn, thisArg) { /***/ }), -/* 651 */ +/* 652 */ /***/ (function(module, exports) { /*! @@ -73598,14 +73681,14 @@ module.exports = pascalcase; /***/ }), -/* 652 */ +/* 653 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; var util = __webpack_require__(113); -var utils = __webpack_require__(653); +var utils = __webpack_require__(654); /** * Expose class utils @@ -73970,7 +74053,7 @@ cu.bubble = function(Parent, events) { /***/ }), -/* 653 */ +/* 654 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -73984,10 +74067,10 @@ var utils = {}; * Lazily required module dependencies */ -utils.union = __webpack_require__(635); -utils.define = __webpack_require__(654); -utils.isObj = __webpack_require__(587); -utils.staticExtend = __webpack_require__(661); +utils.union = __webpack_require__(636); +utils.define = __webpack_require__(655); +utils.isObj = __webpack_require__(588); +utils.staticExtend = __webpack_require__(662); /** @@ -73998,7 +74081,7 @@ module.exports = utils; /***/ }), -/* 654 */ +/* 655 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -74011,7 +74094,7 @@ module.exports = utils; -var isDescriptor = __webpack_require__(655); +var isDescriptor = __webpack_require__(656); module.exports = function defineProperty(obj, prop, val) { if (typeof obj !== 'object' && typeof obj !== 'function') { @@ -74036,7 +74119,7 @@ module.exports = function defineProperty(obj, prop, val) { /***/ }), -/* 655 */ +/* 656 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -74049,9 +74132,9 @@ module.exports = function defineProperty(obj, prop, val) { -var typeOf = __webpack_require__(656); -var isAccessor = __webpack_require__(657); -var isData = __webpack_require__(659); +var typeOf = __webpack_require__(657); +var isAccessor = __webpack_require__(658); +var isData = __webpack_require__(660); module.exports = function isDescriptor(obj, key) { if (typeOf(obj) !== 'object') { @@ -74065,7 +74148,7 @@ module.exports = function isDescriptor(obj, key) { /***/ }), -/* 656 */ +/* 657 */ /***/ (function(module, exports) { var toString = Object.prototype.toString; @@ -74218,7 +74301,7 @@ function isBuffer(val) { /***/ }), -/* 657 */ +/* 658 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -74231,7 +74314,7 @@ function isBuffer(val) { -var typeOf = __webpack_require__(658); +var typeOf = __webpack_require__(659); // accessor descriptor properties var accessor = { @@ -74294,10 +74377,10 @@ module.exports = isAccessorDescriptor; /***/ }), -/* 658 */ +/* 659 */ /***/ (function(module, exports, __webpack_require__) { -var isBuffer = __webpack_require__(611); +var isBuffer = __webpack_require__(612); var toString = Object.prototype.toString; /** @@ -74416,7 +74499,7 @@ module.exports = function kindOf(val) { /***/ }), -/* 659 */ +/* 660 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -74429,7 +74512,7 @@ module.exports = function kindOf(val) { -var typeOf = __webpack_require__(660); +var typeOf = __webpack_require__(661); // data descriptor properties var data = { @@ -74478,10 +74561,10 @@ module.exports = isDataDescriptor; /***/ }), -/* 660 */ +/* 661 */ /***/ (function(module, exports, __webpack_require__) { -var isBuffer = __webpack_require__(611); +var isBuffer = __webpack_require__(612); var toString = Object.prototype.toString; /** @@ -74600,7 +74683,7 @@ module.exports = function kindOf(val) { /***/ }), -/* 661 */ +/* 662 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -74613,8 +74696,8 @@ module.exports = function kindOf(val) { -var copy = __webpack_require__(662); -var define = __webpack_require__(654); +var copy = __webpack_require__(663); +var define = __webpack_require__(655); var util = __webpack_require__(113); /** @@ -74697,15 +74780,15 @@ module.exports = extend; /***/ }), -/* 662 */ +/* 663 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var typeOf = __webpack_require__(663); -var copyDescriptor = __webpack_require__(664); -var define = __webpack_require__(654); +var typeOf = __webpack_require__(664); +var copyDescriptor = __webpack_require__(665); +var define = __webpack_require__(655); /** * Copy static properties, prototype properties, and descriptors from one object to another. @@ -74878,10 +74961,10 @@ module.exports.has = has; /***/ }), -/* 663 */ +/* 664 */ /***/ (function(module, exports, __webpack_require__) { -var isBuffer = __webpack_require__(611); +var isBuffer = __webpack_require__(612); var toString = Object.prototype.toString; /** @@ -75000,7 +75083,7 @@ module.exports = function kindOf(val) { /***/ }), -/* 664 */ +/* 665 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -75088,16 +75171,16 @@ function isObject(val) { /***/ }), -/* 665 */ +/* 666 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var use = __webpack_require__(666); -var define = __webpack_require__(654); +var use = __webpack_require__(667); +var define = __webpack_require__(655); var debug = __webpack_require__(205)('snapdragon:compiler'); -var utils = __webpack_require__(668); +var utils = __webpack_require__(669); /** * Create a new `Compiler` with the given `options`. @@ -75251,7 +75334,7 @@ Compiler.prototype = { // source map support if (opts.sourcemap) { - var sourcemaps = __webpack_require__(687); + var sourcemaps = __webpack_require__(688); sourcemaps(this); this.mapVisit(this.ast.nodes); this.applySourceMaps(); @@ -75272,7 +75355,7 @@ module.exports = Compiler; /***/ }), -/* 666 */ +/* 667 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -75285,7 +75368,7 @@ module.exports = Compiler; -var utils = __webpack_require__(667); +var utils = __webpack_require__(668); module.exports = function base(app, opts) { if (!utils.isObject(app) && typeof app !== 'function') { @@ -75400,7 +75483,7 @@ module.exports = function base(app, opts) { /***/ }), -/* 667 */ +/* 668 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -75414,8 +75497,8 @@ var utils = {}; * Lazily required module dependencies */ -utils.define = __webpack_require__(654); -utils.isObject = __webpack_require__(587); +utils.define = __webpack_require__(655); +utils.isObject = __webpack_require__(588); utils.isString = function(val) { @@ -75430,7 +75513,7 @@ module.exports = utils; /***/ }), -/* 668 */ +/* 669 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -75440,9 +75523,9 @@ module.exports = utils; * Module dependencies */ -exports.extend = __webpack_require__(638); -exports.SourceMap = __webpack_require__(669); -exports.sourceMapResolve = __webpack_require__(680); +exports.extend = __webpack_require__(639); +exports.SourceMap = __webpack_require__(670); +exports.sourceMapResolve = __webpack_require__(681); /** * Convert backslash in the given string to forward slashes @@ -75485,7 +75568,7 @@ exports.last = function(arr, n) { /***/ }), -/* 669 */ +/* 670 */ /***/ (function(module, exports, __webpack_require__) { /* @@ -75493,13 +75576,13 @@ exports.last = function(arr, n) { * Licensed under the New BSD license. See LICENSE.txt or: * http://opensource.org/licenses/BSD-3-Clause */ -exports.SourceMapGenerator = __webpack_require__(670).SourceMapGenerator; -exports.SourceMapConsumer = __webpack_require__(676).SourceMapConsumer; -exports.SourceNode = __webpack_require__(679).SourceNode; +exports.SourceMapGenerator = __webpack_require__(671).SourceMapGenerator; +exports.SourceMapConsumer = __webpack_require__(677).SourceMapConsumer; +exports.SourceNode = __webpack_require__(680).SourceNode; /***/ }), -/* 670 */ +/* 671 */ /***/ (function(module, exports, __webpack_require__) { /* -*- Mode: js; js-indent-level: 2; -*- */ @@ -75509,10 +75592,10 @@ exports.SourceNode = __webpack_require__(679).SourceNode; * http://opensource.org/licenses/BSD-3-Clause */ -var base64VLQ = __webpack_require__(671); -var util = __webpack_require__(673); -var ArraySet = __webpack_require__(674).ArraySet; -var MappingList = __webpack_require__(675).MappingList; +var base64VLQ = __webpack_require__(672); +var util = __webpack_require__(674); +var ArraySet = __webpack_require__(675).ArraySet; +var MappingList = __webpack_require__(676).MappingList; /** * An instance of the SourceMapGenerator represents a source map which is @@ -75921,7 +76004,7 @@ exports.SourceMapGenerator = SourceMapGenerator; /***/ }), -/* 671 */ +/* 672 */ /***/ (function(module, exports, __webpack_require__) { /* -*- Mode: js; js-indent-level: 2; -*- */ @@ -75961,7 +76044,7 @@ exports.SourceMapGenerator = SourceMapGenerator; * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ -var base64 = __webpack_require__(672); +var base64 = __webpack_require__(673); // A single base 64 digit can contain 6 bits of data. For the base 64 variable // length quantities we use in the source map spec, the first bit is the sign, @@ -76067,7 +76150,7 @@ exports.decode = function base64VLQ_decode(aStr, aIndex, aOutParam) { /***/ }), -/* 672 */ +/* 673 */ /***/ (function(module, exports) { /* -*- Mode: js; js-indent-level: 2; -*- */ @@ -76140,7 +76223,7 @@ exports.decode = function (charCode) { /***/ }), -/* 673 */ +/* 674 */ /***/ (function(module, exports) { /* -*- Mode: js; js-indent-level: 2; -*- */ @@ -76563,7 +76646,7 @@ exports.compareByGeneratedPositionsInflated = compareByGeneratedPositionsInflate /***/ }), -/* 674 */ +/* 675 */ /***/ (function(module, exports, __webpack_require__) { /* -*- Mode: js; js-indent-level: 2; -*- */ @@ -76573,7 +76656,7 @@ exports.compareByGeneratedPositionsInflated = compareByGeneratedPositionsInflate * http://opensource.org/licenses/BSD-3-Clause */ -var util = __webpack_require__(673); +var util = __webpack_require__(674); var has = Object.prototype.hasOwnProperty; var hasNativeMap = typeof Map !== "undefined"; @@ -76690,7 +76773,7 @@ exports.ArraySet = ArraySet; /***/ }), -/* 675 */ +/* 676 */ /***/ (function(module, exports, __webpack_require__) { /* -*- Mode: js; js-indent-level: 2; -*- */ @@ -76700,7 +76783,7 @@ exports.ArraySet = ArraySet; * http://opensource.org/licenses/BSD-3-Clause */ -var util = __webpack_require__(673); +var util = __webpack_require__(674); /** * Determine whether mappingB is after mappingA with respect to generated @@ -76775,7 +76858,7 @@ exports.MappingList = MappingList; /***/ }), -/* 676 */ +/* 677 */ /***/ (function(module, exports, __webpack_require__) { /* -*- Mode: js; js-indent-level: 2; -*- */ @@ -76785,11 +76868,11 @@ exports.MappingList = MappingList; * http://opensource.org/licenses/BSD-3-Clause */ -var util = __webpack_require__(673); -var binarySearch = __webpack_require__(677); -var ArraySet = __webpack_require__(674).ArraySet; -var base64VLQ = __webpack_require__(671); -var quickSort = __webpack_require__(678).quickSort; +var util = __webpack_require__(674); +var binarySearch = __webpack_require__(678); +var ArraySet = __webpack_require__(675).ArraySet; +var base64VLQ = __webpack_require__(672); +var quickSort = __webpack_require__(679).quickSort; function SourceMapConsumer(aSourceMap) { var sourceMap = aSourceMap; @@ -77863,7 +77946,7 @@ exports.IndexedSourceMapConsumer = IndexedSourceMapConsumer; /***/ }), -/* 677 */ +/* 678 */ /***/ (function(module, exports) { /* -*- Mode: js; js-indent-level: 2; -*- */ @@ -77980,7 +78063,7 @@ exports.search = function search(aNeedle, aHaystack, aCompare, aBias) { /***/ }), -/* 678 */ +/* 679 */ /***/ (function(module, exports) { /* -*- Mode: js; js-indent-level: 2; -*- */ @@ -78100,7 +78183,7 @@ exports.quickSort = function (ary, comparator) { /***/ }), -/* 679 */ +/* 680 */ /***/ (function(module, exports, __webpack_require__) { /* -*- Mode: js; js-indent-level: 2; -*- */ @@ -78110,8 +78193,8 @@ exports.quickSort = function (ary, comparator) { * http://opensource.org/licenses/BSD-3-Clause */ -var SourceMapGenerator = __webpack_require__(670).SourceMapGenerator; -var util = __webpack_require__(673); +var SourceMapGenerator = __webpack_require__(671).SourceMapGenerator; +var util = __webpack_require__(674); // Matches a Windows-style `\r\n` newline or a `\n` newline used by all other // operating systems these days (capturing the result). @@ -78519,17 +78602,17 @@ exports.SourceNode = SourceNode; /***/ }), -/* 680 */ +/* 681 */ /***/ (function(module, exports, __webpack_require__) { // Copyright 2014, 2015, 2016, 2017 Simon Lydell // X11 (“MIT”) Licensed. (See LICENSE.) -var sourceMappingURL = __webpack_require__(681) -var resolveUrl = __webpack_require__(682) -var decodeUriComponent = __webpack_require__(683) -var urix = __webpack_require__(685) -var atob = __webpack_require__(686) +var sourceMappingURL = __webpack_require__(682) +var resolveUrl = __webpack_require__(683) +var decodeUriComponent = __webpack_require__(684) +var urix = __webpack_require__(686) +var atob = __webpack_require__(687) @@ -78827,7 +78910,7 @@ module.exports = { /***/ }), -/* 681 */ +/* 682 */ /***/ (function(module, exports, __webpack_require__) { var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_RESULT__;// Copyright 2014 Simon Lydell @@ -78890,7 +78973,7 @@ void (function(root, factory) { /***/ }), -/* 682 */ +/* 683 */ /***/ (function(module, exports, __webpack_require__) { // Copyright 2014 Simon Lydell @@ -78908,13 +78991,13 @@ module.exports = resolveUrl /***/ }), -/* 683 */ +/* 684 */ /***/ (function(module, exports, __webpack_require__) { // Copyright 2017 Simon Lydell // X11 (“MIT”) Licensed. (See LICENSE.) -var decodeUriComponent = __webpack_require__(684) +var decodeUriComponent = __webpack_require__(685) function customDecodeUriComponent(string) { // `decodeUriComponent` turns `+` into ` `, but that's not wanted. @@ -78925,7 +79008,7 @@ module.exports = customDecodeUriComponent /***/ }), -/* 684 */ +/* 685 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -79026,7 +79109,7 @@ module.exports = function (encodedURI) { /***/ }), -/* 685 */ +/* 686 */ /***/ (function(module, exports, __webpack_require__) { // Copyright 2014 Simon Lydell @@ -79049,7 +79132,7 @@ module.exports = urix /***/ }), -/* 686 */ +/* 687 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -79063,7 +79146,7 @@ module.exports = atob.atob = atob; /***/ }), -/* 687 */ +/* 688 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -79071,8 +79154,8 @@ module.exports = atob.atob = atob; var fs = __webpack_require__(132); var path = __webpack_require__(4); -var define = __webpack_require__(654); -var utils = __webpack_require__(668); +var define = __webpack_require__(655); +var utils = __webpack_require__(669); /** * Expose `mixin()`. @@ -79215,19 +79298,19 @@ exports.comment = function(node) { /***/ }), -/* 688 */ +/* 689 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var use = __webpack_require__(666); +var use = __webpack_require__(667); var util = __webpack_require__(113); -var Cache = __webpack_require__(689); -var define = __webpack_require__(654); +var Cache = __webpack_require__(690); +var define = __webpack_require__(655); var debug = __webpack_require__(205)('snapdragon:parser'); -var Position = __webpack_require__(690); -var utils = __webpack_require__(668); +var Position = __webpack_require__(691); +var utils = __webpack_require__(669); /** * Create a new `Parser` with the given `input` and `options`. @@ -79755,7 +79838,7 @@ module.exports = Parser; /***/ }), -/* 689 */ +/* 690 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -79862,13 +79945,13 @@ MapCache.prototype.del = function mapDelete(key) { /***/ }), -/* 690 */ +/* 691 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var define = __webpack_require__(654); +var define = __webpack_require__(655); /** * Store position for a node @@ -79883,14 +79966,14 @@ module.exports = function Position(start, parser) { /***/ }), -/* 691 */ +/* 692 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var isExtendable = __webpack_require__(692); -var assignSymbols = __webpack_require__(595); +var isExtendable = __webpack_require__(693); +var assignSymbols = __webpack_require__(596); module.exports = Object.assign || function(obj/*, objects*/) { if (obj === null || typeof obj === 'undefined') { @@ -79950,7 +80033,7 @@ function isEnum(obj, key) { /***/ }), -/* 692 */ +/* 693 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -79963,7 +80046,7 @@ function isEnum(obj, key) { -var isPlainObject = __webpack_require__(594); +var isPlainObject = __webpack_require__(595); module.exports = function isExtendable(val) { return isPlainObject(val) || typeof val === 'function' || Array.isArray(val); @@ -79971,14 +80054,14 @@ module.exports = function isExtendable(val) { /***/ }), -/* 693 */ +/* 694 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var nanomatch = __webpack_require__(694); -var extglob = __webpack_require__(708); +var nanomatch = __webpack_require__(695); +var extglob = __webpack_require__(709); module.exports = function(snapdragon) { var compilers = snapdragon.compiler.compilers; @@ -80055,7 +80138,7 @@ function escapeExtglobs(compiler) { /***/ }), -/* 694 */ +/* 695 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -80066,17 +80149,17 @@ function escapeExtglobs(compiler) { */ var util = __webpack_require__(113); -var toRegex = __webpack_require__(579); -var extend = __webpack_require__(695); +var toRegex = __webpack_require__(580); +var extend = __webpack_require__(696); /** * Local dependencies */ -var compilers = __webpack_require__(697); -var parsers = __webpack_require__(698); -var cache = __webpack_require__(701); -var utils = __webpack_require__(703); +var compilers = __webpack_require__(698); +var parsers = __webpack_require__(699); +var cache = __webpack_require__(702); +var utils = __webpack_require__(704); var MAX_LENGTH = 1024 * 64; /** @@ -80900,14 +80983,14 @@ module.exports = nanomatch; /***/ }), -/* 695 */ +/* 696 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var isExtendable = __webpack_require__(696); -var assignSymbols = __webpack_require__(595); +var isExtendable = __webpack_require__(697); +var assignSymbols = __webpack_require__(596); module.exports = Object.assign || function(obj/*, objects*/) { if (obj === null || typeof obj === 'undefined') { @@ -80967,7 +81050,7 @@ function isEnum(obj, key) { /***/ }), -/* 696 */ +/* 697 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -80980,7 +81063,7 @@ function isEnum(obj, key) { -var isPlainObject = __webpack_require__(594); +var isPlainObject = __webpack_require__(595); module.exports = function isExtendable(val) { return isPlainObject(val) || typeof val === 'function' || Array.isArray(val); @@ -80988,7 +81071,7 @@ module.exports = function isExtendable(val) { /***/ }), -/* 697 */ +/* 698 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -81334,15 +81417,15 @@ module.exports = function(nanomatch, options) { /***/ }), -/* 698 */ +/* 699 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var regexNot = __webpack_require__(596); -var toRegex = __webpack_require__(579); -var isOdd = __webpack_require__(699); +var regexNot = __webpack_require__(597); +var toRegex = __webpack_require__(580); +var isOdd = __webpack_require__(700); /** * Characters to use in negation regex (we want to "not" match @@ -81728,7 +81811,7 @@ module.exports.not = NOT_REGEX; /***/ }), -/* 699 */ +/* 700 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -81741,7 +81824,7 @@ module.exports.not = NOT_REGEX; -var isNumber = __webpack_require__(700); +var isNumber = __webpack_require__(701); module.exports = function isOdd(i) { if (!isNumber(i)) { @@ -81755,7 +81838,7 @@ module.exports = function isOdd(i) { /***/ }), -/* 700 */ +/* 701 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -81783,14 +81866,14 @@ module.exports = function isNumber(num) { /***/ }), -/* 701 */ +/* 702 */ /***/ (function(module, exports, __webpack_require__) { -module.exports = new (__webpack_require__(702))(); +module.exports = new (__webpack_require__(703))(); /***/ }), -/* 702 */ +/* 703 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -81803,7 +81886,7 @@ module.exports = new (__webpack_require__(702))(); -var MapCache = __webpack_require__(689); +var MapCache = __webpack_require__(690); /** * Create a new `FragmentCache` with an optional object to use for `caches`. @@ -81925,7 +82008,7 @@ exports = module.exports = FragmentCache; /***/ }), -/* 703 */ +/* 704 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -81938,14 +82021,14 @@ var path = __webpack_require__(4); * Module dependencies */ -var isWindows = __webpack_require__(704)(); -var Snapdragon = __webpack_require__(623); -utils.define = __webpack_require__(705); -utils.diff = __webpack_require__(706); -utils.extend = __webpack_require__(695); -utils.pick = __webpack_require__(707); -utils.typeOf = __webpack_require__(589); -utils.unique = __webpack_require__(599); +var isWindows = __webpack_require__(705)(); +var Snapdragon = __webpack_require__(624); +utils.define = __webpack_require__(706); +utils.diff = __webpack_require__(707); +utils.extend = __webpack_require__(696); +utils.pick = __webpack_require__(708); +utils.typeOf = __webpack_require__(590); +utils.unique = __webpack_require__(600); /** * Returns true if the given value is effectively an empty string @@ -82311,7 +82394,7 @@ utils.unixify = function(options) { /***/ }), -/* 704 */ +/* 705 */ /***/ (function(module, exports, __webpack_require__) { var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_DEFINE_RESULT__;/*! @@ -82339,7 +82422,7 @@ var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_ /***/ }), -/* 705 */ +/* 706 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -82352,8 +82435,8 @@ var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_ -var isobject = __webpack_require__(587); -var isDescriptor = __webpack_require__(588); +var isobject = __webpack_require__(588); +var isDescriptor = __webpack_require__(589); var define = (typeof Reflect !== 'undefined' && Reflect.defineProperty) ? Reflect.defineProperty : Object.defineProperty; @@ -82384,7 +82467,7 @@ module.exports = function defineProperty(obj, key, val) { /***/ }), -/* 706 */ +/* 707 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -82438,7 +82521,7 @@ function diffArray(one, two) { /***/ }), -/* 707 */ +/* 708 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -82451,7 +82534,7 @@ function diffArray(one, two) { -var isObject = __webpack_require__(587); +var isObject = __webpack_require__(588); module.exports = function pick(obj, keys) { if (!isObject(obj) && typeof obj !== 'function') { @@ -82480,7 +82563,7 @@ module.exports = function pick(obj, keys) { /***/ }), -/* 708 */ +/* 709 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -82490,18 +82573,18 @@ module.exports = function pick(obj, keys) { * Module dependencies */ -var extend = __webpack_require__(638); -var unique = __webpack_require__(599); -var toRegex = __webpack_require__(579); +var extend = __webpack_require__(639); +var unique = __webpack_require__(600); +var toRegex = __webpack_require__(580); /** * Local dependencies */ -var compilers = __webpack_require__(709); -var parsers = __webpack_require__(715); -var Extglob = __webpack_require__(718); -var utils = __webpack_require__(717); +var compilers = __webpack_require__(710); +var parsers = __webpack_require__(716); +var Extglob = __webpack_require__(719); +var utils = __webpack_require__(718); var MAX_LENGTH = 1024 * 64; /** @@ -82818,13 +82901,13 @@ module.exports = extglob; /***/ }), -/* 709 */ +/* 710 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var brackets = __webpack_require__(710); +var brackets = __webpack_require__(711); /** * Extglob compilers @@ -82994,7 +83077,7 @@ module.exports = function(extglob) { /***/ }), -/* 710 */ +/* 711 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -83004,17 +83087,17 @@ module.exports = function(extglob) { * Local dependencies */ -var compilers = __webpack_require__(711); -var parsers = __webpack_require__(713); +var compilers = __webpack_require__(712); +var parsers = __webpack_require__(714); /** * Module dependencies */ var debug = __webpack_require__(205)('expand-brackets'); -var extend = __webpack_require__(638); -var Snapdragon = __webpack_require__(623); -var toRegex = __webpack_require__(579); +var extend = __webpack_require__(639); +var Snapdragon = __webpack_require__(624); +var toRegex = __webpack_require__(580); /** * Parses the given POSIX character class `pattern` and returns a @@ -83212,13 +83295,13 @@ module.exports = brackets; /***/ }), -/* 711 */ +/* 712 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var posix = __webpack_require__(712); +var posix = __webpack_require__(713); module.exports = function(brackets) { brackets.compiler @@ -83306,7 +83389,7 @@ module.exports = function(brackets) { /***/ }), -/* 712 */ +/* 713 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -83335,14 +83418,14 @@ module.exports = { /***/ }), -/* 713 */ +/* 714 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var utils = __webpack_require__(714); -var define = __webpack_require__(654); +var utils = __webpack_require__(715); +var define = __webpack_require__(655); /** * Text regex @@ -83561,14 +83644,14 @@ module.exports.TEXT_REGEX = TEXT_REGEX; /***/ }), -/* 714 */ +/* 715 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var toRegex = __webpack_require__(579); -var regexNot = __webpack_require__(596); +var toRegex = __webpack_require__(580); +var regexNot = __webpack_require__(597); var cached; /** @@ -83602,15 +83685,15 @@ exports.createRegex = function(pattern, include) { /***/ }), -/* 715 */ +/* 716 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var brackets = __webpack_require__(710); -var define = __webpack_require__(716); -var utils = __webpack_require__(717); +var brackets = __webpack_require__(711); +var define = __webpack_require__(717); +var utils = __webpack_require__(718); /** * Characters to use in text regex (we want to "not" match @@ -83765,7 +83848,7 @@ module.exports = parsers; /***/ }), -/* 716 */ +/* 717 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -83778,7 +83861,7 @@ module.exports = parsers; -var isDescriptor = __webpack_require__(588); +var isDescriptor = __webpack_require__(589); module.exports = function defineProperty(obj, prop, val) { if (typeof obj !== 'object' && typeof obj !== 'function') { @@ -83803,14 +83886,14 @@ module.exports = function defineProperty(obj, prop, val) { /***/ }), -/* 717 */ +/* 718 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var regex = __webpack_require__(596); -var Cache = __webpack_require__(702); +var regex = __webpack_require__(597); +var Cache = __webpack_require__(703); /** * Utils @@ -83879,7 +83962,7 @@ utils.createRegex = function(str) { /***/ }), -/* 718 */ +/* 719 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -83889,16 +83972,16 @@ utils.createRegex = function(str) { * Module dependencies */ -var Snapdragon = __webpack_require__(623); -var define = __webpack_require__(716); -var extend = __webpack_require__(638); +var Snapdragon = __webpack_require__(624); +var define = __webpack_require__(717); +var extend = __webpack_require__(639); /** * Local dependencies */ -var compilers = __webpack_require__(709); -var parsers = __webpack_require__(715); +var compilers = __webpack_require__(710); +var parsers = __webpack_require__(716); /** * Customize Snapdragon parser and renderer @@ -83964,16 +84047,16 @@ module.exports = Extglob; /***/ }), -/* 719 */ +/* 720 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var extglob = __webpack_require__(708); -var nanomatch = __webpack_require__(694); -var regexNot = __webpack_require__(596); -var toRegex = __webpack_require__(579); +var extglob = __webpack_require__(709); +var nanomatch = __webpack_require__(695); +var regexNot = __webpack_require__(597); +var toRegex = __webpack_require__(580); var not; /** @@ -84054,14 +84137,14 @@ function textRegex(pattern) { /***/ }), -/* 720 */ +/* 721 */ /***/ (function(module, exports, __webpack_require__) { -module.exports = new (__webpack_require__(702))(); +module.exports = new (__webpack_require__(703))(); /***/ }), -/* 721 */ +/* 722 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -84074,13 +84157,13 @@ var path = __webpack_require__(4); * Module dependencies */ -var Snapdragon = __webpack_require__(623); -utils.define = __webpack_require__(722); -utils.diff = __webpack_require__(706); -utils.extend = __webpack_require__(691); -utils.pick = __webpack_require__(707); -utils.typeOf = __webpack_require__(589); -utils.unique = __webpack_require__(599); +var Snapdragon = __webpack_require__(624); +utils.define = __webpack_require__(723); +utils.diff = __webpack_require__(707); +utils.extend = __webpack_require__(692); +utils.pick = __webpack_require__(708); +utils.typeOf = __webpack_require__(590); +utils.unique = __webpack_require__(600); /** * Returns true if the platform is windows, or `path.sep` is `\\`. @@ -84377,7 +84460,7 @@ utils.unixify = function(options) { /***/ }), -/* 722 */ +/* 723 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -84390,8 +84473,8 @@ utils.unixify = function(options) { -var isobject = __webpack_require__(587); -var isDescriptor = __webpack_require__(588); +var isobject = __webpack_require__(588); +var isDescriptor = __webpack_require__(589); var define = (typeof Reflect !== 'undefined' && Reflect.defineProperty) ? Reflect.defineProperty : Object.defineProperty; @@ -84422,7 +84505,7 @@ module.exports = function defineProperty(obj, key, val) { /***/ }), -/* 723 */ +/* 724 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -84441,9 +84524,9 @@ var __extends = (this && this.__extends) || (function () { }; })(); Object.defineProperty(exports, "__esModule", { value: true }); -var readdir = __webpack_require__(724); -var reader_1 = __webpack_require__(737); -var fs_stream_1 = __webpack_require__(741); +var readdir = __webpack_require__(725); +var reader_1 = __webpack_require__(738); +var fs_stream_1 = __webpack_require__(742); var ReaderAsync = /** @class */ (function (_super) { __extends(ReaderAsync, _super); function ReaderAsync() { @@ -84504,15 +84587,15 @@ exports.default = ReaderAsync; /***/ }), -/* 724 */ +/* 725 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -const readdirSync = __webpack_require__(725); -const readdirAsync = __webpack_require__(733); -const readdirStream = __webpack_require__(736); +const readdirSync = __webpack_require__(726); +const readdirAsync = __webpack_require__(734); +const readdirStream = __webpack_require__(737); module.exports = exports = readdirAsyncPath; exports.readdir = exports.readdirAsync = exports.async = readdirAsyncPath; @@ -84596,7 +84679,7 @@ function readdirStreamStat (dir, options) { /***/ }), -/* 725 */ +/* 726 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -84604,11 +84687,11 @@ function readdirStreamStat (dir, options) { module.exports = readdirSync; -const DirectoryReader = __webpack_require__(726); +const DirectoryReader = __webpack_require__(727); let syncFacade = { - fs: __webpack_require__(731), - forEach: __webpack_require__(732), + fs: __webpack_require__(732), + forEach: __webpack_require__(733), sync: true }; @@ -84637,7 +84720,7 @@ function readdirSync (dir, options, internalOptions) { /***/ }), -/* 726 */ +/* 727 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -84646,9 +84729,9 @@ function readdirSync (dir, options, internalOptions) { const Readable = __webpack_require__(173).Readable; const EventEmitter = __webpack_require__(164).EventEmitter; const path = __webpack_require__(4); -const normalizeOptions = __webpack_require__(727); -const stat = __webpack_require__(729); -const call = __webpack_require__(730); +const normalizeOptions = __webpack_require__(728); +const stat = __webpack_require__(730); +const call = __webpack_require__(731); /** * Asynchronously reads the contents of a directory and streams the results @@ -85024,14 +85107,14 @@ module.exports = DirectoryReader; /***/ }), -/* 727 */ +/* 728 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; const path = __webpack_require__(4); -const globToRegExp = __webpack_require__(728); +const globToRegExp = __webpack_require__(729); module.exports = normalizeOptions; @@ -85208,7 +85291,7 @@ function normalizeOptions (options, internalOptions) { /***/ }), -/* 728 */ +/* 729 */ /***/ (function(module, exports) { module.exports = function (glob, opts) { @@ -85345,13 +85428,13 @@ module.exports = function (glob, opts) { /***/ }), -/* 729 */ +/* 730 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -const call = __webpack_require__(730); +const call = __webpack_require__(731); module.exports = stat; @@ -85426,7 +85509,7 @@ function symlinkStat (fs, path, lstats, callback) { /***/ }), -/* 730 */ +/* 731 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -85487,14 +85570,14 @@ function callOnce (fn) { /***/ }), -/* 731 */ +/* 732 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; const fs = __webpack_require__(132); -const call = __webpack_require__(730); +const call = __webpack_require__(731); /** * A facade around {@link fs.readdirSync} that allows it to be called @@ -85558,7 +85641,7 @@ exports.lstat = function (path, callback) { /***/ }), -/* 732 */ +/* 733 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -85587,7 +85670,7 @@ function syncForEach (array, iterator, done) { /***/ }), -/* 733 */ +/* 734 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -85595,12 +85678,12 @@ function syncForEach (array, iterator, done) { module.exports = readdirAsync; -const maybe = __webpack_require__(734); -const DirectoryReader = __webpack_require__(726); +const maybe = __webpack_require__(735); +const DirectoryReader = __webpack_require__(727); let asyncFacade = { fs: __webpack_require__(132), - forEach: __webpack_require__(735), + forEach: __webpack_require__(736), async: true }; @@ -85642,7 +85725,7 @@ function readdirAsync (dir, options, callback, internalOptions) { /***/ }), -/* 734 */ +/* 735 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -85669,7 +85752,7 @@ module.exports = function maybe (cb, promise) { /***/ }), -/* 735 */ +/* 736 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -85705,7 +85788,7 @@ function asyncForEach (array, iterator, done) { /***/ }), -/* 736 */ +/* 737 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -85713,11 +85796,11 @@ function asyncForEach (array, iterator, done) { module.exports = readdirStream; -const DirectoryReader = __webpack_require__(726); +const DirectoryReader = __webpack_require__(727); let streamFacade = { fs: __webpack_require__(132), - forEach: __webpack_require__(735), + forEach: __webpack_require__(736), async: true }; @@ -85737,16 +85820,16 @@ function readdirStream (dir, options, internalOptions) { /***/ }), -/* 737 */ +/* 738 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); var path = __webpack_require__(4); -var deep_1 = __webpack_require__(738); -var entry_1 = __webpack_require__(740); -var pathUtil = __webpack_require__(739); +var deep_1 = __webpack_require__(739); +var entry_1 = __webpack_require__(741); +var pathUtil = __webpack_require__(740); var Reader = /** @class */ (function () { function Reader(options) { this.options = options; @@ -85812,14 +85895,14 @@ exports.default = Reader; /***/ }), -/* 738 */ +/* 739 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -var pathUtils = __webpack_require__(739); -var patternUtils = __webpack_require__(573); +var pathUtils = __webpack_require__(740); +var patternUtils = __webpack_require__(574); var DeepFilter = /** @class */ (function () { function DeepFilter(options, micromatchOptions) { this.options = options; @@ -85902,7 +85985,7 @@ exports.default = DeepFilter; /***/ }), -/* 739 */ +/* 740 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -85933,14 +86016,14 @@ exports.makeAbsolute = makeAbsolute; /***/ }), -/* 740 */ +/* 741 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -var pathUtils = __webpack_require__(739); -var patternUtils = __webpack_require__(573); +var pathUtils = __webpack_require__(740); +var patternUtils = __webpack_require__(574); var EntryFilter = /** @class */ (function () { function EntryFilter(options, micromatchOptions) { this.options = options; @@ -86025,7 +86108,7 @@ exports.default = EntryFilter; /***/ }), -/* 741 */ +/* 742 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -86045,8 +86128,8 @@ var __extends = (this && this.__extends) || (function () { })(); Object.defineProperty(exports, "__esModule", { value: true }); var stream = __webpack_require__(173); -var fsStat = __webpack_require__(742); -var fs_1 = __webpack_require__(746); +var fsStat = __webpack_require__(743); +var fs_1 = __webpack_require__(747); var FileSystemStream = /** @class */ (function (_super) { __extends(FileSystemStream, _super); function FileSystemStream() { @@ -86096,14 +86179,14 @@ exports.default = FileSystemStream; /***/ }), -/* 742 */ +/* 743 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -const optionsManager = __webpack_require__(743); -const statProvider = __webpack_require__(745); +const optionsManager = __webpack_require__(744); +const statProvider = __webpack_require__(746); /** * Asynchronous API. */ @@ -86134,13 +86217,13 @@ exports.statSync = statSync; /***/ }), -/* 743 */ +/* 744 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -const fsAdapter = __webpack_require__(744); +const fsAdapter = __webpack_require__(745); function prepare(opts) { const options = Object.assign({ fs: fsAdapter.getFileSystemAdapter(opts ? opts.fs : undefined), @@ -86153,7 +86236,7 @@ exports.prepare = prepare; /***/ }), -/* 744 */ +/* 745 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -86176,7 +86259,7 @@ exports.getFileSystemAdapter = getFileSystemAdapter; /***/ }), -/* 745 */ +/* 746 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -86228,7 +86311,7 @@ exports.isFollowedSymlink = isFollowedSymlink; /***/ }), -/* 746 */ +/* 747 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -86259,7 +86342,7 @@ exports.default = FileSystem; /***/ }), -/* 747 */ +/* 748 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -86279,9 +86362,9 @@ var __extends = (this && this.__extends) || (function () { })(); Object.defineProperty(exports, "__esModule", { value: true }); var stream = __webpack_require__(173); -var readdir = __webpack_require__(724); -var reader_1 = __webpack_require__(737); -var fs_stream_1 = __webpack_require__(741); +var readdir = __webpack_require__(725); +var reader_1 = __webpack_require__(738); +var fs_stream_1 = __webpack_require__(742); var TransformStream = /** @class */ (function (_super) { __extends(TransformStream, _super); function TransformStream(reader) { @@ -86349,7 +86432,7 @@ exports.default = ReaderStream; /***/ }), -/* 748 */ +/* 749 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -86368,9 +86451,9 @@ var __extends = (this && this.__extends) || (function () { }; })(); Object.defineProperty(exports, "__esModule", { value: true }); -var readdir = __webpack_require__(724); -var reader_1 = __webpack_require__(737); -var fs_sync_1 = __webpack_require__(749); +var readdir = __webpack_require__(725); +var reader_1 = __webpack_require__(738); +var fs_sync_1 = __webpack_require__(750); var ReaderSync = /** @class */ (function (_super) { __extends(ReaderSync, _super); function ReaderSync() { @@ -86430,7 +86513,7 @@ exports.default = ReaderSync; /***/ }), -/* 749 */ +/* 750 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -86449,8 +86532,8 @@ var __extends = (this && this.__extends) || (function () { }; })(); Object.defineProperty(exports, "__esModule", { value: true }); -var fsStat = __webpack_require__(742); -var fs_1 = __webpack_require__(746); +var fsStat = __webpack_require__(743); +var fs_1 = __webpack_require__(747); var FileSystemSync = /** @class */ (function (_super) { __extends(FileSystemSync, _super); function FileSystemSync() { @@ -86496,7 +86579,7 @@ exports.default = FileSystemSync; /***/ }), -/* 750 */ +/* 751 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -86512,7 +86595,7 @@ exports.flatten = flatten; /***/ }), -/* 751 */ +/* 752 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -86533,13 +86616,13 @@ exports.merge = merge; /***/ }), -/* 752 */ +/* 753 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; const path = __webpack_require__(4); -const pathType = __webpack_require__(753); +const pathType = __webpack_require__(754); const getExtensions = extensions => extensions.length > 1 ? `{${extensions.join(',')}}` : extensions[0]; @@ -86605,13 +86688,13 @@ module.exports.sync = (input, opts) => { /***/ }), -/* 753 */ +/* 754 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; const fs = __webpack_require__(132); -const pify = __webpack_require__(754); +const pify = __webpack_require__(755); function type(fn, fn2, fp) { if (typeof fp !== 'string') { @@ -86654,7 +86737,7 @@ exports.symlinkSync = typeSync.bind(null, 'lstatSync', 'isSymbolicLink'); /***/ }), -/* 754 */ +/* 755 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -86745,17 +86828,17 @@ module.exports = (obj, opts) => { /***/ }), -/* 755 */ +/* 756 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; const fs = __webpack_require__(132); const path = __webpack_require__(4); -const fastGlob = __webpack_require__(569); -const gitIgnore = __webpack_require__(756); +const fastGlob = __webpack_require__(570); +const gitIgnore = __webpack_require__(757); const pify = __webpack_require__(410); -const slash = __webpack_require__(757); +const slash = __webpack_require__(758); const DEFAULT_IGNORE = [ '**/node_modules/**', @@ -86853,7 +86936,7 @@ module.exports.sync = options => { /***/ }), -/* 756 */ +/* 757 */ /***/ (function(module, exports) { // A simple implementation of make-array @@ -87322,7 +87405,7 @@ module.exports = options => new IgnoreBase(options) /***/ }), -/* 757 */ +/* 758 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -87340,7 +87423,7 @@ module.exports = input => { /***/ }), -/* 758 */ +/* 759 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -87353,7 +87436,7 @@ module.exports = input => { -var isGlob = __webpack_require__(759); +var isGlob = __webpack_require__(760); module.exports = function hasGlob(val) { if (val == null) return false; @@ -87373,7 +87456,7 @@ module.exports = function hasGlob(val) { /***/ }), -/* 759 */ +/* 760 */ /***/ (function(module, exports, __webpack_require__) { /*! @@ -87404,17 +87487,17 @@ module.exports = function isGlob(str) { /***/ }), -/* 760 */ +/* 761 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; const path = __webpack_require__(4); const {constants: fsConstants} = __webpack_require__(132); -const pEvent = __webpack_require__(761); -const CpFileError = __webpack_require__(764); -const fs = __webpack_require__(766); -const ProgressEmitter = __webpack_require__(769); +const pEvent = __webpack_require__(762); +const CpFileError = __webpack_require__(765); +const fs = __webpack_require__(767); +const ProgressEmitter = __webpack_require__(770); const cpFileAsync = async (source, destination, options, progressEmitter) => { let readError; @@ -87528,12 +87611,12 @@ module.exports.sync = (source, destination, options) => { /***/ }), -/* 761 */ +/* 762 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -const pTimeout = __webpack_require__(762); +const pTimeout = __webpack_require__(763); const symbolAsyncIterator = Symbol.asyncIterator || '@@asyncIterator'; @@ -87824,12 +87907,12 @@ module.exports.iterator = (emitter, event, options) => { /***/ }), -/* 762 */ +/* 763 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -const pFinally = __webpack_require__(763); +const pFinally = __webpack_require__(764); class TimeoutError extends Error { constructor(message) { @@ -87875,7 +87958,7 @@ module.exports.TimeoutError = TimeoutError; /***/ }), -/* 763 */ +/* 764 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -87897,12 +87980,12 @@ module.exports = (promise, onFinally) => { /***/ }), -/* 764 */ +/* 765 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -const NestedError = __webpack_require__(765); +const NestedError = __webpack_require__(766); class CpFileError extends NestedError { constructor(message, nested) { @@ -87916,7 +87999,7 @@ module.exports = CpFileError; /***/ }), -/* 765 */ +/* 766 */ /***/ (function(module, exports, __webpack_require__) { var inherits = __webpack_require__(113).inherits; @@ -87972,16 +88055,16 @@ module.exports = NestedError; /***/ }), -/* 766 */ +/* 767 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; const {promisify} = __webpack_require__(113); const fs = __webpack_require__(233); -const makeDir = __webpack_require__(767); -const pEvent = __webpack_require__(761); -const CpFileError = __webpack_require__(764); +const makeDir = __webpack_require__(768); +const pEvent = __webpack_require__(762); +const CpFileError = __webpack_require__(765); const stat = promisify(fs.stat); const lstat = promisify(fs.lstat); @@ -88078,7 +88161,7 @@ exports.copyFileSync = (source, destination, flags) => { /***/ }), -/* 767 */ +/* 768 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -88086,7 +88169,7 @@ exports.copyFileSync = (source, destination, flags) => { const fs = __webpack_require__(132); const path = __webpack_require__(4); const {promisify} = __webpack_require__(113); -const semver = __webpack_require__(768); +const semver = __webpack_require__(769); const useNativeRecursiveOption = semver.satisfies(process.version, '>=10.12.0'); @@ -88241,7 +88324,7 @@ module.exports.sync = (input, options) => { /***/ }), -/* 768 */ +/* 769 */ /***/ (function(module, exports) { exports = module.exports = SemVer @@ -89843,7 +89926,7 @@ function coerce (version, options) { /***/ }), -/* 769 */ +/* 770 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -89884,7 +89967,7 @@ module.exports = ProgressEmitter; /***/ }), -/* 770 */ +/* 771 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -89930,12 +90013,12 @@ exports.default = module.exports; /***/ }), -/* 771 */ +/* 772 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -const pMap = __webpack_require__(772); +const pMap = __webpack_require__(773); const pFilter = async (iterable, filterer, options) => { const values = await pMap( @@ -89952,7 +90035,7 @@ module.exports.default = pFilter; /***/ }), -/* 772 */ +/* 773 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -90031,12 +90114,12 @@ module.exports.default = pMap; /***/ }), -/* 773 */ +/* 774 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -const NestedError = __webpack_require__(765); +const NestedError = __webpack_require__(766); class CpyError extends NestedError { constructor(message, nested) { @@ -90050,7 +90133,7 @@ module.exports = CpyError; /***/ }), -/* 774 */ +/* 775 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -90058,10 +90141,10 @@ module.exports = CpyError; const fs = __webpack_require__(132); const arrayUnion = __webpack_require__(242); const merge2 = __webpack_require__(243); -const fastGlob = __webpack_require__(775); +const fastGlob = __webpack_require__(776); const dirGlob = __webpack_require__(332); -const gitignore = __webpack_require__(806); -const {FilterStream, UniqueStream} = __webpack_require__(807); +const gitignore = __webpack_require__(807); +const {FilterStream, UniqueStream} = __webpack_require__(808); const DEFAULT_FILTER = () => false; @@ -90238,17 +90321,17 @@ module.exports.gitignore = gitignore; /***/ }), -/* 775 */ +/* 776 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -const taskManager = __webpack_require__(776); -const async_1 = __webpack_require__(792); -const stream_1 = __webpack_require__(802); -const sync_1 = __webpack_require__(803); -const settings_1 = __webpack_require__(805); -const utils = __webpack_require__(777); +const taskManager = __webpack_require__(777); +const async_1 = __webpack_require__(793); +const stream_1 = __webpack_require__(803); +const sync_1 = __webpack_require__(804); +const settings_1 = __webpack_require__(806); +const utils = __webpack_require__(778); async function FastGlob(source, options) { assertPatternsInput(source); const works = getWorks(source, async_1.default, options); @@ -90312,14 +90395,14 @@ module.exports = FastGlob; /***/ }), -/* 776 */ +/* 777 */ /***/ (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); +const utils = __webpack_require__(778); function generate(patterns, settings) { const positivePatterns = getPositivePatterns(patterns); const negativePatterns = getNegativePatternsAsPositive(patterns, settings.ignore); @@ -90384,31 +90467,31 @@ exports.convertPatternGroupToTask = convertPatternGroupToTask; /***/ }), -/* 777 */ +/* 778 */ /***/ (function(module, exports, __webpack_require__) { "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); +const array = __webpack_require__(779); exports.array = array; -const errno = __webpack_require__(779); +const errno = __webpack_require__(780); exports.errno = errno; -const fs = __webpack_require__(780); +const fs = __webpack_require__(781); exports.fs = fs; -const path = __webpack_require__(781); +const path = __webpack_require__(782); exports.path = path; -const pattern = __webpack_require__(782); +const pattern = __webpack_require__(783); exports.pattern = pattern; -const stream = __webpack_require__(790); +const stream = __webpack_require__(791); exports.stream = stream; -const string = __webpack_require__(791); +const string = __webpack_require__(792); exports.string = string; /***/ }), -/* 778 */ +/* 779 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -90437,7 +90520,7 @@ exports.splitWhen = splitWhen; /***/ }), -/* 779 */ +/* 780 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -90451,7 +90534,7 @@ exports.isEnoentCodeError = isEnoentCodeError; /***/ }), -/* 780 */ +/* 781 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -90477,7 +90560,7 @@ exports.createDirentFromStats = createDirentFromStats; /***/ }), -/* 781 */ +/* 782 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -90517,7 +90600,7 @@ exports.removeLeadingDotSegment = removeLeadingDotSegment; /***/ }), -/* 782 */ +/* 783 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -90526,7 +90609,7 @@ 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__(265); -const micromatch = __webpack_require__(783); +const micromatch = __webpack_require__(784); const picomatch = __webpack_require__(285); const GLOBSTAR = '**'; const ESCAPE_SYMBOL = '\\'; @@ -90656,7 +90739,7 @@ exports.matchAny = matchAny; /***/ }), -/* 783 */ +/* 784 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -90664,8 +90747,8 @@ exports.matchAny = matchAny; const util = __webpack_require__(113); const braces = __webpack_require__(269); -const picomatch = __webpack_require__(784); -const utils = __webpack_require__(787); +const picomatch = __webpack_require__(785); +const utils = __webpack_require__(788); const isEmptyString = val => val === '' || val === './'; /** @@ -91130,27 +91213,27 @@ module.exports = micromatch; /***/ }), -/* 784 */ +/* 785 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -module.exports = __webpack_require__(785); +module.exports = __webpack_require__(786); /***/ }), -/* 785 */ +/* 786 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; const path = __webpack_require__(4); -const scan = __webpack_require__(786); -const parse = __webpack_require__(789); -const utils = __webpack_require__(787); -const constants = __webpack_require__(788); +const scan = __webpack_require__(787); +const parse = __webpack_require__(790); +const utils = __webpack_require__(788); +const constants = __webpack_require__(789); const isObject = val => val && typeof val === 'object' && !Array.isArray(val); /** @@ -91489,13 +91572,13 @@ module.exports = picomatch; /***/ }), -/* 786 */ +/* 787 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -const utils = __webpack_require__(787); +const utils = __webpack_require__(788); const { CHAR_ASTERISK, /* * */ CHAR_AT, /* @ */ @@ -91512,7 +91595,7 @@ const { CHAR_RIGHT_CURLY_BRACE, /* } */ CHAR_RIGHT_PARENTHESES, /* ) */ CHAR_RIGHT_SQUARE_BRACKET /* ] */ -} = __webpack_require__(788); +} = __webpack_require__(789); const isPathSeparator = code => { return code === CHAR_FORWARD_SLASH || code === CHAR_BACKWARD_SLASH; @@ -91887,7 +91970,7 @@ module.exports = scan; /***/ }), -/* 787 */ +/* 788 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -91900,7 +91983,7 @@ const { REGEX_REMOVE_BACKSLASH, REGEX_SPECIAL_CHARS, REGEX_SPECIAL_CHARS_GLOBAL -} = __webpack_require__(788); +} = __webpack_require__(789); exports.isObject = val => val !== null && typeof val === 'object' && !Array.isArray(val); exports.hasRegexChars = str => REGEX_SPECIAL_CHARS.test(str); @@ -91958,7 +92041,7 @@ exports.wrapOutput = (input, state = {}, options = {}) => { /***/ }), -/* 788 */ +/* 789 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -92144,14 +92227,14 @@ module.exports = { /***/ }), -/* 789 */ +/* 790 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -const constants = __webpack_require__(788); -const utils = __webpack_require__(787); +const constants = __webpack_require__(789); +const utils = __webpack_require__(788); /** * Constants @@ -93235,7 +93318,7 @@ module.exports = parse; /***/ }), -/* 790 */ +/* 791 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -93259,7 +93342,7 @@ function propagateCloseEventToSources(streams) { /***/ }), -/* 791 */ +/* 792 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -93277,14 +93360,14 @@ exports.isEmpty = isEmpty; /***/ }), -/* 792 */ +/* 793 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -const stream_1 = __webpack_require__(793); -const provider_1 = __webpack_require__(795); +const stream_1 = __webpack_require__(794); +const provider_1 = __webpack_require__(796); class ProviderAsync extends provider_1.default { constructor() { super(...arguments); @@ -93312,7 +93395,7 @@ exports.default = ProviderAsync; /***/ }), -/* 793 */ +/* 794 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -93321,7 +93404,7 @@ Object.defineProperty(exports, "__esModule", { value: true }); const stream_1 = __webpack_require__(173); const fsStat = __webpack_require__(295); const fsWalk = __webpack_require__(300); -const reader_1 = __webpack_require__(794); +const reader_1 = __webpack_require__(795); class ReaderStream extends reader_1.default { constructor() { super(...arguments); @@ -93374,7 +93457,7 @@ exports.default = ReaderStream; /***/ }), -/* 794 */ +/* 795 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -93382,7 +93465,7 @@ exports.default = ReaderStream; Object.defineProperty(exports, "__esModule", { value: true }); const path = __webpack_require__(4); const fsStat = __webpack_require__(295); -const utils = __webpack_require__(777); +const utils = __webpack_require__(778); class Reader { constructor(_settings) { this._settings = _settings; @@ -93414,17 +93497,17 @@ exports.default = Reader; /***/ }), -/* 795 */ +/* 796 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); const path = __webpack_require__(4); -const deep_1 = __webpack_require__(796); -const entry_1 = __webpack_require__(799); -const error_1 = __webpack_require__(800); -const entry_2 = __webpack_require__(801); +const deep_1 = __webpack_require__(797); +const entry_1 = __webpack_require__(800); +const error_1 = __webpack_require__(801); +const entry_2 = __webpack_require__(802); class Provider { constructor(_settings) { this._settings = _settings; @@ -93469,14 +93552,14 @@ exports.default = Provider; /***/ }), -/* 796 */ +/* 797 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -const utils = __webpack_require__(777); -const partial_1 = __webpack_require__(797); +const utils = __webpack_require__(778); +const partial_1 = __webpack_require__(798); class DeepFilter { constructor(_settings, _micromatchOptions) { this._settings = _settings; @@ -93538,13 +93621,13 @@ exports.default = DeepFilter; /***/ }), -/* 797 */ +/* 798 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -const matcher_1 = __webpack_require__(798); +const matcher_1 = __webpack_require__(799); class PartialMatcher extends matcher_1.default { match(filepath) { const parts = filepath.split('/'); @@ -93583,13 +93666,13 @@ exports.default = PartialMatcher; /***/ }), -/* 798 */ +/* 799 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -const utils = __webpack_require__(777); +const utils = __webpack_require__(778); class Matcher { constructor(_patterns, _settings, _micromatchOptions) { this._patterns = _patterns; @@ -93640,13 +93723,13 @@ exports.default = Matcher; /***/ }), -/* 799 */ +/* 800 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -const utils = __webpack_require__(777); +const utils = __webpack_require__(778); class EntryFilter { constructor(_settings, _micromatchOptions) { this._settings = _settings; @@ -93703,13 +93786,13 @@ exports.default = EntryFilter; /***/ }), -/* 800 */ +/* 801 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -const utils = __webpack_require__(777); +const utils = __webpack_require__(778); class ErrorFilter { constructor(_settings) { this._settings = _settings; @@ -93725,13 +93808,13 @@ exports.default = ErrorFilter; /***/ }), -/* 801 */ +/* 802 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -const utils = __webpack_require__(777); +const utils = __webpack_require__(778); class EntryTransformer { constructor(_settings) { this._settings = _settings; @@ -93758,15 +93841,15 @@ exports.default = EntryTransformer; /***/ }), -/* 802 */ +/* 803 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); const stream_1 = __webpack_require__(173); -const stream_2 = __webpack_require__(793); -const provider_1 = __webpack_require__(795); +const stream_2 = __webpack_require__(794); +const provider_1 = __webpack_require__(796); class ProviderStream extends provider_1.default { constructor() { super(...arguments); @@ -93796,14 +93879,14 @@ exports.default = ProviderStream; /***/ }), -/* 803 */ +/* 804 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -const sync_1 = __webpack_require__(804); -const provider_1 = __webpack_require__(795); +const sync_1 = __webpack_require__(805); +const provider_1 = __webpack_require__(796); class ProviderSync extends provider_1.default { constructor() { super(...arguments); @@ -93826,7 +93909,7 @@ exports.default = ProviderSync; /***/ }), -/* 804 */ +/* 805 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -93834,7 +93917,7 @@ exports.default = ProviderSync; Object.defineProperty(exports, "__esModule", { value: true }); const fsStat = __webpack_require__(295); const fsWalk = __webpack_require__(300); -const reader_1 = __webpack_require__(794); +const reader_1 = __webpack_require__(795); class ReaderSync extends reader_1.default { constructor() { super(...arguments); @@ -93876,7 +93959,7 @@ exports.default = ReaderSync; /***/ }), -/* 805 */ +/* 806 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -93940,7 +94023,7 @@ exports.default = Settings; /***/ }), -/* 806 */ +/* 807 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -93948,7 +94031,7 @@ exports.default = Settings; const {promisify} = __webpack_require__(113); const fs = __webpack_require__(132); const path = __webpack_require__(4); -const fastGlob = __webpack_require__(775); +const fastGlob = __webpack_require__(776); const gitIgnore = __webpack_require__(335); const slash = __webpack_require__(336); @@ -94067,7 +94150,7 @@ module.exports.sync = options => { /***/ }), -/* 807 */ +/* 808 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -94120,7 +94203,7 @@ module.exports = { /***/ }), -/* 808 */ +/* 809 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -94128,13 +94211,13 @@ __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "buildNonBazelProductionProjects", function() { return buildNonBazelProductionProjects; }); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "getProductionProjects", function() { return getProductionProjects; }); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "buildProject", function() { return buildProject; }); -/* harmony import */ var cpy__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(562); +/* harmony import */ var cpy__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(563); /* harmony import */ var cpy__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(cpy__WEBPACK_IMPORTED_MODULE_0__); /* harmony import */ var del__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(240); /* harmony import */ var del__WEBPACK_IMPORTED_MODULE_1___default = /*#__PURE__*/__webpack_require__.n(del__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 _config__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(559); +/* harmony import */ var _config__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(560); /* harmony import */ var _utils_fs__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(231); /* harmony import */ var _utils_log__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(220); /* harmony import */ var _utils_package_json__WEBPACK_IMPORTED_MODULE_6__ = __webpack_require__(349); diff --git a/packages/kbn-pm/src/commands/index.ts b/packages/kbn-pm/src/commands/index.ts index 4c7992859ebdd..70e641f1e9351 100644 --- a/packages/kbn-pm/src/commands/index.ts +++ b/packages/kbn-pm/src/commands/index.ts @@ -32,6 +32,7 @@ import { CleanCommand } from './clean'; import { ResetCommand } from './reset'; import { RunCommand } from './run'; import { WatchCommand } from './watch'; +import { PatchNativeModulesCommand } from './patch_native_modules'; import { Kibana } from '../utils/kibana'; export const commands: { [key: string]: ICommand } = { @@ -41,4 +42,5 @@ export const commands: { [key: string]: ICommand } = { reset: ResetCommand, run: RunCommand, watch: WatchCommand, + patch_native_modules: PatchNativeModulesCommand, }; diff --git a/packages/kbn-pm/src/commands/patch_native_modules.ts b/packages/kbn-pm/src/commands/patch_native_modules.ts new file mode 100644 index 0000000000000..30fd599b83be3 --- /dev/null +++ b/packages/kbn-pm/src/commands/patch_native_modules.ts @@ -0,0 +1,68 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 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 Path from 'path'; +import Fs from 'fs'; + +import { CiStatsReporter } from '@kbn/dev-utils/ci_stats_reporter'; + +import { log } from '../utils/log'; +import { spawn } from '../utils/child_process'; +import { ICommand } from './index'; + +export const PatchNativeModulesCommand: ICommand = { + description: 'Patch native modules by running build commands on M1 Macs', + name: 'patch_native_modules', + + async run(projects, _, { kbn }) { + const kibanaProjectPath = projects.get('kibana')?.path || ''; + const reporter = CiStatsReporter.fromEnv(log); + + if (process.platform !== 'darwin' || process.arch !== 'arm64') { + return; + } + + const startTime = Date.now(); + const nodeSassDir = Path.resolve(kibanaProjectPath, 'node_modules/node-sass'); + const nodeSassNativeDist = Path.resolve( + nodeSassDir, + `vendor/darwin-arm64-${process.versions.modules}/binding.node` + ); + if (!Fs.existsSync(nodeSassNativeDist)) { + log.info('Running build script for node-sass'); + await spawn('npm', ['run', 'build'], { + cwd: nodeSassDir, + }); + } + + const re2Dir = Path.resolve(kibanaProjectPath, 'node_modules/re2'); + const re2NativeDist = Path.resolve(re2Dir, 'build/Release/re2.node'); + if (!Fs.existsSync(re2NativeDist)) { + log.info('Running build script for re2'); + await spawn('npm', ['run', 'rebuild'], { + cwd: re2Dir, + }); + } + + log.success('native modules should be setup for native ARM Mac development'); + + // send timings + await reporter.timings({ + upstreamBranch: kbn.kibanaProject.json.branch, + // prevent loading @kbn/utils by passing null + kibanaUuid: kbn.getUuid() || null, + timings: [ + { + group: 'scripts/kbn bootstrap', + id: 'patch native modudles for arm macs', + ms: Date.now() - startTime, + }, + ], + }); + }, +}; diff --git a/packages/kbn-react-field/BUILD.bazel b/packages/kbn-react-field/BUILD.bazel new file mode 100644 index 0000000000000..9cb2df76bd6c9 --- /dev/null +++ b/packages/kbn-react-field/BUILD.bazel @@ -0,0 +1,121 @@ +load("@npm//@bazel/typescript:index.bzl", "ts_config", "ts_project") +load("@build_bazel_rules_nodejs//:index.bzl", "js_library", "pkg_npm") +load("//src/dev/bazel:index.bzl", "jsts_transpiler") + +PKG_BASE_NAME = "kbn-react-field" +PKG_REQUIRE_NAME = "@kbn/react-field" + +SOURCE_FILES = glob( + [ + "src/**/*.ts", + "src/**/*.tsx", + "src/**/*.scss", + "src/**/*.svg", + ], + exclude = [ + "**/*.test.*", + "**/__fixtures__/**", + "**/__snapshots__/**", + ], +) + +SRCS = SOURCE_FILES + +filegroup( + name = "srcs", + srcs = SRCS, +) + +NPM_MODULE_EXTRA_FILES = [ + "package.json", + "README.md", + "field_button/package.json", + "field_icon/package.json", +] + +RUNTIME_DEPS = [ + "@npm//prop-types", + "@npm//react", + "@npm//classnames", + "@npm//@elastic/eui", + "//packages/kbn-i18n", +] + +TYPES_DEPS = [ + "//packages/kbn-babel-preset", + "//packages/kbn-i18n", + "@npm//tslib", + "@npm//@types/jest", + "@npm//@types/prop-types", + "@npm//@types/classnames", + "@npm//@types/react", + "@npm//@elastic/eui", + "@npm//resize-observer-polyfill", +] + +jsts_transpiler( + name = "target_webpack", + srcs = SRCS, + build_pkg_name = package_name(), + web = True, + additional_args = [ + "--copy-files", + "--quiet" + ], +) + +jsts_transpiler( + name = "target_node", + srcs = SRCS, + build_pkg_name = package_name(), + additional_args = [ + "--copy-files", + "--quiet" + ], +) + +ts_config( + name = "tsconfig", + src = "tsconfig.json", + deps = [ + "//:tsconfig.base.json", + "//:tsconfig.bazel.json", + ], +) + +ts_project( + name = "tsc_types", + args = ['--pretty'], + srcs = SRCS, + deps = TYPES_DEPS, + declaration = True, + declaration_map = True, + emit_declaration_only = True, + out_dir = "target_types", + source_map = True, + root_dir = "src", + tsconfig = ":tsconfig", +) + +js_library( + name = PKG_BASE_NAME, + srcs = NPM_MODULE_EXTRA_FILES, + deps = RUNTIME_DEPS + [":target_node", ":target_webpack", ":tsc_types"], + package_name = PKG_REQUIRE_NAME, + visibility = ["//visibility:public"], +) + +pkg_npm( + name = "npm_module", + deps = [ + ":%s" % PKG_BASE_NAME, + ] +) + +filegroup( + name = "build", + srcs = [ + ":npm_module", + ], + visibility = ["//visibility:public"], +) diff --git a/packages/kbn-react-field/README.md b/packages/kbn-react-field/README.md new file mode 100644 index 0000000000000..12e118cadd09d --- /dev/null +++ b/packages/kbn-react-field/README.md @@ -0,0 +1 @@ +Shareable field type related React components diff --git a/packages/kbn-react-field/field_button/package.json b/packages/kbn-react-field/field_button/package.json new file mode 100644 index 0000000000000..dd708dd5cd32d --- /dev/null +++ b/packages/kbn-react-field/field_button/package.json @@ -0,0 +1,5 @@ +{ + "main": "../target_node/field_button/index.js", + "browser": "../target_webpack/field_button/index.js", + "types": "../target_types/field_button/index.d.ts" +} \ No newline at end of file diff --git a/packages/kbn-react-field/field_icon/package.json b/packages/kbn-react-field/field_icon/package.json new file mode 100644 index 0000000000000..e7220f60e5d29 --- /dev/null +++ b/packages/kbn-react-field/field_icon/package.json @@ -0,0 +1,5 @@ +{ + "main": "../target_node/field_icon/index.js", + "browser": "../target_webpack/field_icon/index.js", + "types": "../target_types/field_icon/index.d.ts" +} \ No newline at end of file diff --git a/packages/kbn-react-field/jest.config.js b/packages/kbn-react-field/jest.config.js new file mode 100644 index 0000000000000..1549cd0071356 --- /dev/null +++ b/packages/kbn-react-field/jest.config.js @@ -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 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: ['/packages/kbn-react-field'], +}; diff --git a/packages/kbn-react-field/package.json b/packages/kbn-react-field/package.json new file mode 100644 index 0000000000000..3cbfdfa010ba0 --- /dev/null +++ b/packages/kbn-react-field/package.json @@ -0,0 +1,9 @@ +{ + "name": "@kbn/react-field", + "main": "./target_node/index.js", + "browser": "./target_webpack/index.js", + "types": "./target_types/index.d.ts", + "version": "1.0.0", + "license": "SSPL-1.0 OR Elastic License 2.0", + "private": true +} \ No newline at end of file diff --git a/src/plugins/kibana_react/public/field_button/__snapshots__/field_button.test.tsx.snap b/packages/kbn-react-field/src/field_button/__snapshots__/field_button.test.tsx.snap similarity index 100% rename from src/plugins/kibana_react/public/field_button/__snapshots__/field_button.test.tsx.snap rename to packages/kbn-react-field/src/field_button/__snapshots__/field_button.test.tsx.snap diff --git a/src/plugins/kibana_react/public/field_button/field_button.scss b/packages/kbn-react-field/src/field_button/field_button.scss similarity index 100% rename from src/plugins/kibana_react/public/field_button/field_button.scss rename to packages/kbn-react-field/src/field_button/field_button.scss diff --git a/src/plugins/kibana_react/public/field_button/field_button.test.tsx b/packages/kbn-react-field/src/field_button/field_button.test.tsx similarity index 100% rename from src/plugins/kibana_react/public/field_button/field_button.test.tsx rename to packages/kbn-react-field/src/field_button/field_button.test.tsx diff --git a/src/plugins/kibana_react/public/field_button/field_button.tsx b/packages/kbn-react-field/src/field_button/field_button.tsx similarity index 100% rename from src/plugins/kibana_react/public/field_button/field_button.tsx rename to packages/kbn-react-field/src/field_button/field_button.tsx diff --git a/packages/kbn-react-field/src/field_button/index.ts b/packages/kbn-react-field/src/field_button/index.ts new file mode 100644 index 0000000000000..15857540baefc --- /dev/null +++ b/packages/kbn-react-field/src/field_button/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 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 { FieldButton } from './field_button'; +export type { FieldButtonProps, ButtonSize } from './field_button'; diff --git a/src/plugins/kibana_react/public/field_icon/__snapshots__/field_icon.test.tsx.snap b/packages/kbn-react-field/src/field_icon/__snapshots__/field_icon.test.tsx.snap similarity index 86% rename from src/plugins/kibana_react/public/field_icon/__snapshots__/field_icon.test.tsx.snap rename to packages/kbn-react-field/src/field_icon/__snapshots__/field_icon.test.tsx.snap index f6870a5209c1e..0e9ae4ee2aaaa 100644 --- a/src/plugins/kibana_react/public/field_icon/__snapshots__/field_icon.test.tsx.snap +++ b/packages/kbn-react-field/src/field_icon/__snapshots__/field_icon.test.tsx.snap @@ -95,6 +95,16 @@ exports[`FieldIcon renders known field types geo_shape is rendered 1`] = ` /> `; +exports[`FieldIcon renders known field types histogram is rendered 1`] = ` + +`; + exports[`FieldIcon renders known field types ip is rendered 1`] = ` `; +exports[`FieldIcon renders known field types keyword is rendered 1`] = ` + +`; + exports[`FieldIcon renders known field types murmur3 is rendered 1`] = ` `; +exports[`FieldIcon renders known field types text is rendered 1`] = ` + +`; + exports[`FieldIcon renders with className if provided 1`] = ` > = { murmur3: { iconType: 'tokenFile' }, number: { iconType: 'tokenNumber' }, number_range: { iconType: 'tokenNumber' }, + histogram: { iconType: 'tokenHistogram' }, _source: { iconType: 'editorCodeBlock', color: 'gray' }, string: { iconType: 'tokenString' }, + text: { iconType: 'tokenString' }, + keyword: { iconType: 'tokenKeyword' }, nested: { iconType: 'tokenNested' }, }; diff --git a/packages/kbn-react-field/src/field_icon/index.ts b/packages/kbn-react-field/src/field_icon/index.ts new file mode 100644 index 0000000000000..fa70a445cdb24 --- /dev/null +++ b/packages/kbn-react-field/src/field_icon/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 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 { FieldIcon } from './field_icon'; +export type { FieldIconProps } from './field_icon'; diff --git a/packages/kbn-react-field/src/index.ts b/packages/kbn-react-field/src/index.ts new file mode 100644 index 0000000000000..fa7a34db3e879 --- /dev/null +++ b/packages/kbn-react-field/src/index.ts @@ -0,0 +1,12 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 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 { FieldIcon } from './field_icon'; +export type { FieldIconProps } from './field_icon'; +export { FieldButton } from './field_button'; +export type { FieldButtonProps, ButtonSize } from './field_button'; diff --git a/packages/kbn-react-field/tsconfig.json b/packages/kbn-react-field/tsconfig.json new file mode 100644 index 0000000000000..90c8a63c746f1 --- /dev/null +++ b/packages/kbn-react-field/tsconfig.json @@ -0,0 +1,23 @@ +{ + "extends": "../../tsconfig.bazel.json", + "compilerOptions": { + "declaration": true, + "declarationMap": true, + "emitDeclarationOnly": true, + "outDir": "./target_types", + "sourceMap": true, + "sourceRoot": "../../../../../packages/kbn-react-field/src", + "types": [ + "jest", + "node", + "resize-observer-polyfill" + ] + }, + "include": [ + "src/**/*.ts", + "src/**/*.tsx", + ], + "exclude": [ + "**/__fixtures__/**/*" + ] +} diff --git a/packages/kbn-rule-data-utils/jest.config.js b/packages/kbn-rule-data-utils/jest.config.js deleted file mode 100644 index 26cb39fe8b55a..0000000000000 --- a/packages/kbn-rule-data-utils/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: ['/packages/kbn-rule-data-utils'], -}; diff --git a/packages/kbn-securitysolution-autocomplete/src/field/index.tsx b/packages/kbn-securitysolution-autocomplete/src/field/index.tsx index 69408e919bb1e..a89e0a096b673 100644 --- a/packages/kbn-securitysolution-autocomplete/src/field/index.tsx +++ b/packages/kbn-securitysolution-autocomplete/src/field/index.tsx @@ -8,7 +8,7 @@ import React, { useCallback, useMemo, useState } from 'react'; import { EuiComboBox, EuiComboBoxOptionOption } from '@elastic/eui'; -import { IndexPatternBase, IndexPatternFieldBase } from '@kbn/es-query'; +import { DataViewBase, DataViewFieldBase } from '@kbn/es-query'; import { getGenericComboBoxProps, @@ -20,14 +20,14 @@ const AS_PLAIN_TEXT = { asPlainText: true }; interface OperatorProps { fieldInputWidth?: number; fieldTypeFilter?: string[]; - indexPattern: IndexPatternBase | undefined; + indexPattern: DataViewBase | undefined; isClearable: boolean; isDisabled: boolean; isLoading: boolean; isRequired?: boolean; - onChange: (a: IndexPatternFieldBase[]) => void; + onChange: (a: DataViewFieldBase[]) => void; placeholder: string; - selectedField: IndexPatternFieldBase | undefined; + selectedField: DataViewFieldBase | undefined; } export const FieldComponent: React.FC = ({ @@ -56,7 +56,7 @@ export const FieldComponent: React.FC = ({ const handleValuesChange = useCallback( (newOptions: EuiComboBoxOptionOption[]): void => { - const newValues: IndexPatternFieldBase[] = newOptions.map( + const newValues: DataViewFieldBase[] = newOptions.map( ({ label }) => availableFields[labels.indexOf(label)] ); onChange(newValues); @@ -94,13 +94,13 @@ export const FieldComponent: React.FC = ({ FieldComponent.displayName = 'Field'; interface ComboBoxFields { - availableFields: IndexPatternFieldBase[]; - selectedFields: IndexPatternFieldBase[]; + availableFields: DataViewFieldBase[]; + selectedFields: DataViewFieldBase[]; } const getComboBoxFields = ( - indexPattern: IndexPatternBase | undefined, - selectedField: IndexPatternFieldBase | undefined, + indexPattern: DataViewBase | undefined, + selectedField: DataViewFieldBase | undefined, fieldTypeFilter: string[] ): ComboBoxFields => { const existingFields = getExistingFields(indexPattern); @@ -113,29 +113,27 @@ const getComboBoxFields = ( const getComboBoxProps = (fields: ComboBoxFields): GetGenericComboBoxPropsReturn => { const { availableFields, selectedFields } = fields; - return getGenericComboBoxProps({ + return getGenericComboBoxProps({ getLabel: (field) => field.name, options: availableFields, selectedOptions: selectedFields, }); }; -const getExistingFields = (indexPattern: IndexPatternBase | undefined): IndexPatternFieldBase[] => { +const getExistingFields = (indexPattern: DataViewBase | undefined): DataViewFieldBase[] => { return indexPattern != null ? indexPattern.fields : []; }; -const getSelectedFields = ( - selectedField: IndexPatternFieldBase | undefined -): IndexPatternFieldBase[] => { +const getSelectedFields = (selectedField: DataViewFieldBase | undefined): DataViewFieldBase[] => { return selectedField ? [selectedField] : []; }; const getAvailableFields = ( - existingFields: IndexPatternFieldBase[], - selectedFields: IndexPatternFieldBase[], + existingFields: DataViewFieldBase[], + selectedFields: DataViewFieldBase[], fieldTypeFilter: string[] -): IndexPatternFieldBase[] => { - const fieldsByName = new Map(); +): DataViewFieldBase[] => { + const fieldsByName = new Map(); existingFields.forEach((f) => fieldsByName.set(f.name, f)); selectedFields.forEach((f) => fieldsByName.set(f.name, f)); diff --git a/packages/kbn-securitysolution-hook-utils/src/use_async/index.test.ts b/packages/kbn-securitysolution-hook-utils/src/use_async/index.test.ts index 886a3dd27befc..7503f2c5c2be8 100644 --- a/packages/kbn-securitysolution-hook-utils/src/use_async/index.test.ts +++ b/packages/kbn-securitysolution-hook-utils/src/use_async/index.test.ts @@ -18,6 +18,13 @@ interface TestArgs { type TestReturn = Promise; describe('useAsync', () => { + /** + * Timeout for both jest tests and for the waitForNextUpdate. + * jest tests default to 5 seconds and waitForNextUpdate defaults to 1 second. + * 20_0000 = 20,000 milliseconds = 20 seconds + */ + const timeout = 20_000; + let fn: jest.Mock; let args: TestArgs; @@ -31,16 +38,20 @@ describe('useAsync', () => { expect(fn).not.toHaveBeenCalled(); }); - it('invokes the function when start is called', async () => { - const { result, waitForNextUpdate } = renderHook(() => useAsync(fn)); + it( + 'invokes the function when start is called', + async () => { + const { result, waitForNextUpdate } = renderHook(() => useAsync(fn)); - act(() => { - result.current.start(args); - }); - await waitForNextUpdate(); + act(() => { + result.current.start(args); + }); + await waitForNextUpdate({ timeout }); - expect(fn).toHaveBeenCalled(); - }); + expect(fn).toHaveBeenCalled(); + }, + timeout + ); it('invokes the function with start args', async () => { const { result, waitForNextUpdate } = renderHook(() => useAsync(fn)); @@ -49,84 +60,99 @@ describe('useAsync', () => { act(() => { result.current.start(args); }); - await waitForNextUpdate(); + await waitForNextUpdate({ timeout }); expect(fn).toHaveBeenCalledWith(expectedArgs); }); - it('populates result with the resolved value of the fn', async () => { - const { result, waitForNextUpdate } = renderHook(() => useAsync(fn)); - fn.mockResolvedValue({ resolved: 'value' }); - - act(() => { - result.current.start(args); - }); - await waitForNextUpdate(); - - expect(result.current.result).toEqual({ resolved: 'value' }); - expect(result.current.error).toBeUndefined(); - }); - - it('populates error if function rejects', async () => { - fn.mockRejectedValue(new Error('whoops')); - const { result, waitForNextUpdate } = renderHook(() => useAsync(fn)); - - act(() => { - result.current.start(args); - }); - await waitForNextUpdate(); - - expect(result.current.result).toBeUndefined(); - expect(result.current.error).toEqual(new Error('whoops')); - }); - - it('populates the loading state while the function is pending', async () => { - let resolve: () => void; - fn.mockImplementation(() => new Promise((_resolve) => (resolve = _resolve))); - - const { result, waitForNextUpdate } = renderHook(() => useAsync(fn)); - - act(() => { - result.current.start(args); - }); - - expect(result.current.loading).toBe(true); - - act(() => resolve()); - await waitForNextUpdate(); - - expect(result.current.loading).toBe(false); - }); - - it('multiple start calls reset state', async () => { - let resolve: (result: string) => void; - fn.mockImplementation(() => new Promise((_resolve) => (resolve = _resolve))); - - const { result, waitForNextUpdate } = renderHook(() => useAsync(fn)); - - act(() => { - result.current.start(args); - }); - - expect(result.current.loading).toBe(true); - - act(() => resolve('result')); - await waitForNextUpdate(); - - expect(result.current.loading).toBe(false); - expect(result.current.result).toBe('result'); - - act(() => { - result.current.start(args); - }); - - expect(result.current.loading).toBe(true); - expect(result.current.result).toBe(undefined); - - act(() => resolve('result')); - await waitForNextUpdate(); - - expect(result.current.loading).toBe(false); - expect(result.current.result).toBe('result'); - }); + it( + 'populates result with the resolved value of the fn', + async () => { + const { result, waitForNextUpdate } = renderHook(() => useAsync(fn)); + fn.mockResolvedValue({ resolved: 'value' }); + + act(() => { + result.current.start(args); + }); + await waitForNextUpdate({ timeout }); + + expect(result.current.result).toEqual({ resolved: 'value' }); + expect(result.current.error).toBeUndefined(); + }, + timeout + ); + + it( + 'populates error if function rejects', + async () => { + fn.mockRejectedValue(new Error('whoops')); + const { result, waitForNextUpdate } = renderHook(() => useAsync(fn)); + + act(() => { + result.current.start(args); + }); + await waitForNextUpdate({ timeout }); + + expect(result.current.result).toBeUndefined(); + expect(result.current.error).toEqual(new Error('whoops')); + }, + timeout + ); + + it( + 'populates the loading state while the function is pending', + async () => { + let resolve: () => void; + fn.mockImplementation(() => new Promise((_resolve) => (resolve = _resolve))); + + const { result, waitForNextUpdate } = renderHook(() => useAsync(fn)); + + act(() => { + result.current.start(args); + }); + + expect(result.current.loading).toBe(true); + + act(() => resolve()); + await waitForNextUpdate({ timeout }); + + expect(result.current.loading).toBe(false); + }, + timeout + ); + + it( + 'multiple start calls reset state', + async () => { + let resolve: (result: string) => void; + fn.mockImplementation(() => new Promise((_resolve) => (resolve = _resolve))); + + const { result, waitForNextUpdate } = renderHook(() => useAsync(fn)); + + act(() => { + result.current.start(args); + }); + + expect(result.current.loading).toBe(true); + + act(() => resolve('result')); + await waitForNextUpdate({ timeout }); + + expect(result.current.loading).toBe(false); + expect(result.current.result).toBe('result'); + + act(() => { + result.current.start(args); + }); + + expect(result.current.loading).toBe(true); + expect(result.current.result).toBe(undefined); + act(() => resolve('result')); + await waitForNextUpdate({ timeout }); + + expect(result.current.loading).toBe(false); + expect(result.current.result).toBe('result'); + }, + timeout + ); }); diff --git a/packages/kbn-securitysolution-list-constants/jest.config.js b/packages/kbn-securitysolution-list-constants/jest.config.js deleted file mode 100644 index 21dffdfcf5a68..0000000000000 --- a/packages/kbn-securitysolution-list-constants/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: ['/packages/kbn-securitysolution-list-constants'], -}; diff --git a/packages/kbn-securitysolution-rules/jest.config.js b/packages/kbn-securitysolution-rules/jest.config.js deleted file mode 100644 index 99368edd5372c..0000000000000 --- a/packages/kbn-securitysolution-rules/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: ['/packages/kbn-securitysolution-rules'], -}; diff --git a/packages/kbn-securitysolution-t-grid/jest.config.js b/packages/kbn-securitysolution-t-grid/jest.config.js deleted file mode 100644 index 21e7d2d71b61a..0000000000000 --- a/packages/kbn-securitysolution-t-grid/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: ['/packages/kbn-securitysolution-t-grid'], -}; diff --git a/packages/kbn-server-route-repository/src/decode_request_params.test.ts b/packages/kbn-server-route-repository/src/decode_request_params.test.ts index 08ef303ad0b3a..a5c1a2b49eb19 100644 --- a/packages/kbn-server-route-repository/src/decode_request_params.test.ts +++ b/packages/kbn-server-route-repository/src/decode_request_params.test.ts @@ -5,7 +5,7 @@ * in compliance with, at your election, the Elastic License 2.0 or the Server * Side Public License, v 1. */ -import { jsonRt } from '@kbn/io-ts-utils'; +import { jsonRt } from '@kbn/io-ts-utils/json_rt'; import * as t from 'io-ts'; import { decodeRequestParams } from './decode_request_params'; @@ -69,7 +69,7 @@ describe('decodeRequestParams', () => { }; expect(decode).toThrowErrorMatchingInlineSnapshot(` - "Excess keys are not allowed: + "Excess keys are not allowed: path.extraKey" `); }); diff --git a/packages/kbn-server-route-repository/src/decode_request_params.ts b/packages/kbn-server-route-repository/src/decode_request_params.ts index 00492d69b8ac5..4df6fa3333c50 100644 --- a/packages/kbn-server-route-repository/src/decode_request_params.ts +++ b/packages/kbn-server-route-repository/src/decode_request_params.ts @@ -10,7 +10,7 @@ import { omitBy, isPlainObject, isEmpty } from 'lodash'; import { isLeft } from 'fp-ts/lib/Either'; import { PathReporter } from 'io-ts/lib/PathReporter'; import Boom from '@hapi/boom'; -import { strictKeysRt } from '@kbn/io-ts-utils'; +import { strictKeysRt } from '@kbn/io-ts-utils/strict_keys_rt'; import { RouteParamsRT } from './typings'; interface KibanaRequestParams { diff --git a/packages/kbn-test/jest-preset.js b/packages/kbn-test/jest-preset.js index 0199aa6e311b6..db64f070b37d9 100644 --- a/packages/kbn-test/jest-preset.js +++ b/packages/kbn-test/jest-preset.js @@ -46,7 +46,15 @@ module.exports = { modulePathIgnorePatterns: ['__fixtures__/', 'target/'], // Use this configuration option to add custom reporters to Jest - reporters: ['default', '@kbn/test/target_node/jest/junit_reporter'], + reporters: [ + 'default', + [ + '@kbn/test/target_node/jest/junit_reporter', + { + rootDirectory: '.', + }, + ], + ], // The paths to modules that run some code to configure or set up the testing environment before each test setupFiles: [ diff --git a/packages/kbn-test/src/functional_test_runner/functional_test_runner.ts b/packages/kbn-test/src/functional_test_runner/functional_test_runner.ts index b5d379d3426e7..4130cd8d138b8 100644 --- a/packages/kbn-test/src/functional_test_runner/functional_test_runner.ts +++ b/packages/kbn-test/src/functional_test_runner/functional_test_runner.ts @@ -78,18 +78,33 @@ export class FunctionalTestRunner { // replace the function of custom service providers so that they return // promise-like objects which never resolve, essentially disabling them // allowing us to load the test files and populate the mocha suites - const readStubbedProviderSpec = (type: string, providers: any) => + const readStubbedProviderSpec = (type: string, providers: any, skip: string[]) => readProviderSpec(type, providers).map((p) => ({ ...p, - fn: () => ({ - then: () => {}, - }), + fn: skip.includes(p.name) + ? (...args: unknown[]) => { + const result = p.fn(...args); + if ('then' in result) { + throw new Error( + `Provider [${p.name}] returns a promise so it can't loaded during test analysis` + ); + } + + return result; + } + : () => ({ + then: () => {}, + }), })); const providers = new ProviderCollection(this.log, [ ...coreProviders, - ...readStubbedProviderSpec('Service', config.get('services')), - ...readStubbedProviderSpec('PageObject', config.get('pageObjects')), + ...readStubbedProviderSpec( + 'Service', + config.get('services'), + config.get('servicesRequiredForTestAnalysis') + ), + ...readStubbedProviderSpec('PageObject', config.get('pageObjects'), []), ]); const mocha = await setupMocha(this.lifecycle, this.log, config, providers); diff --git a/packages/kbn-test/src/functional_test_runner/integration_tests/__fixtures__/failure_hooks/config.js b/packages/kbn-test/src/functional_test_runner/integration_tests/__fixtures__/failure_hooks/config.js index 0b9cfd88b4cbb..1375e5a3df2fd 100644 --- a/packages/kbn-test/src/functional_test_runner/integration_tests/__fixtures__/failure_hooks/config.js +++ b/packages/kbn-test/src/functional_test_runner/integration_tests/__fixtures__/failure_hooks/config.js @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import { delay } from 'bluebird'; +import { setTimeout as setTimeoutAsync } from 'timers/promises'; export default function () { return { @@ -22,13 +22,13 @@ export default function () { lifecycle.testFailure.add(async (err, test) => { log.info('testFailure %s %s', err.message, test.fullTitle()); - await delay(10); + await setTimeoutAsync(10); log.info('testFailureAfterDelay %s %s', err.message, test.fullTitle()); }); lifecycle.testHookFailure.add(async (err, test) => { log.info('testHookFailure %s %s', err.message, test.fullTitle()); - await delay(10); + await setTimeoutAsync(10); log.info('testHookFailureAfterDelay %s %s', err.message, test.fullTitle()); }); }, diff --git a/packages/kbn-test/src/functional_test_runner/lib/config/schema.ts b/packages/kbn-test/src/functional_test_runner/lib/config/schema.ts index 7fae313c68bd3..a9ceaa643a60f 100644 --- a/packages/kbn-test/src/functional_test_runner/lib/config/schema.ts +++ b/packages/kbn-test/src/functional_test_runner/lib/config/schema.ts @@ -89,6 +89,7 @@ export const schema = Joi.object() }) .default(), + servicesRequiredForTestAnalysis: Joi.array().items(Joi.string()).default([]), services: Joi.object().pattern(ID_PATTERN, Joi.func().required()).default(), pageObjects: Joi.object().pattern(ID_PATTERN, Joi.func().required()).default(), diff --git a/packages/kbn-test/src/jest/run.ts b/packages/kbn-test/src/jest/run.ts index 4a5dd4e9281ba..697402adf3dd1 100644 --- a/packages/kbn-test/src/jest/run.ts +++ b/packages/kbn-test/src/jest/run.ts @@ -44,6 +44,7 @@ declare global { export function runJest(configName = 'jest.config.js') { const argv = buildArgv(process.argv); + const devConfigName = 'jest.config.dev.js'; const log = new ToolingLog({ level: argv.verbose ? 'verbose' : 'info', @@ -52,11 +53,12 @@ export function runJest(configName = 'jest.config.js') { const runStartTime = Date.now(); const reportTime = getTimeReporter(log, 'scripts/jest'); - let cwd: string; + let testFiles: string[]; + const cwd: string = process.env.INIT_CWD || process.cwd(); + if (!argv.config) { - cwd = process.env.INIT_CWD || process.cwd(); testFiles = argv._.splice(2).map((p) => resolve(cwd, p)); const commonTestFiles = commonBasePath(testFiles); const testFilesProvided = testFiles.length > 0; @@ -66,18 +68,25 @@ export function runJest(configName = 'jest.config.js') { log.verbose('commonTestFiles:', commonTestFiles); let configPath; + let devConfigPath; // sets the working directory to the cwd or the common // base directory of the provided test files let wd = testFilesProvided ? commonTestFiles : cwd; + devConfigPath = resolve(wd, devConfigName); configPath = resolve(wd, configName); - while (!existsSync(configPath)) { + while (!existsSync(configPath) && !existsSync(devConfigPath)) { wd = resolve(wd, '..'); + devConfigPath = resolve(wd, devConfigName); configPath = resolve(wd, configName); } + if (existsSync(devConfigPath)) { + configPath = devConfigPath; + } + log.verbose(`no config provided, found ${configPath}`); process.argv.push('--config', configPath); diff --git a/packages/kbn-test/src/jest/run_check_jest_configs_cli.ts b/packages/kbn-test/src/jest/run_check_jest_configs_cli.ts index 5895ef193fbfe..cf37ee82d61e9 100644 --- a/packages/kbn-test/src/jest/run_check_jest_configs_cli.ts +++ b/packages/kbn-test/src/jest/run_check_jest_configs_cli.ts @@ -26,7 +26,16 @@ const template: string = `module.exports = { }; `; -const roots: string[] = ['x-pack/plugins', 'packages', 'src/plugins', 'test', 'src']; +const roots: string[] = [ + 'x-pack/plugins/security_solution/public', + 'x-pack/plugins/security_solution/server', + 'x-pack/plugins/security_solution', + 'x-pack/plugins', + 'packages', + 'src/plugins', + 'test', + 'src', +]; export async function runCheckJestConfigsCli() { run( @@ -76,7 +85,9 @@ export async function runCheckJestConfigsCli() { modulePath, }); - writeFileSync(resolve(root, name, 'jest.config.js'), content); + const configPath = resolve(root, name, 'jest.config.js'); + log.info('created %s', configPath); + writeFileSync(configPath, content); } else { log.warning(`Unable to determind where to place jest.config.js for ${file}`); } diff --git a/packages/kbn-test/src/jest/setup/polyfills.js b/packages/kbn-test/src/jest/setup/polyfills.js index 48b597d280b4a..ebe6178dbdd91 100644 --- a/packages/kbn-test/src/jest/setup/polyfills.js +++ b/packages/kbn-test/src/jest/setup/polyfills.js @@ -6,13 +6,6 @@ * Side Public License, v 1. */ -// bluebird < v3.3.5 does not work with MutationObserver polyfill -// when MutationObserver exists, bluebird avoids using node's builtin async schedulers -const bluebird = require('bluebird'); -bluebird.Promise.setScheduler(function (fn) { - global.setImmediate.call(global, fn); -}); - const MutationObserver = require('mutation-observer'); Object.defineProperty(window, 'MutationObserver', { value: MutationObserver }); diff --git a/packages/kbn-test/src/jest/utils/testbed/index.ts b/packages/kbn-test/src/jest/utils/testbed/index.ts index dfa5f011853c0..0e839c180b6b6 100644 --- a/packages/kbn-test/src/jest/utils/testbed/index.ts +++ b/packages/kbn-test/src/jest/utils/testbed/index.ts @@ -7,4 +7,12 @@ */ export { registerTestBed } from './testbed'; -export type { TestBed, TestBedConfig, SetupFunc, UnwrapPromise } from './types'; +export type { + TestBed, + TestBedConfig, + AsyncTestBedConfig, + SetupFunc, + UnwrapPromise, + SyncSetupFunc, + AsyncSetupFunc, +} from './types'; diff --git a/packages/kbn-test/src/jest/utils/testbed/testbed.ts b/packages/kbn-test/src/jest/utils/testbed/testbed.ts index 472b9f2df939c..240ec25a9c296 100644 --- a/packages/kbn-test/src/jest/utils/testbed/testbed.ts +++ b/packages/kbn-test/src/jest/utils/testbed/testbed.ts @@ -16,7 +16,14 @@ import { mountComponentAsync, getJSXComponentWithProps, } from './mount_component'; -import { TestBedConfig, TestBed, SetupFunc } from './types'; +import { + TestBedConfig, + AsyncTestBedConfig, + TestBed, + SetupFunc, + SyncSetupFunc, + AsyncSetupFunc, +} from './types'; const defaultConfig: TestBedConfig = { defaultProps: {}, @@ -48,10 +55,18 @@ const defaultConfig: TestBedConfig = { }); ``` */ -export const registerTestBed = ( +export function registerTestBed( + Component: ComponentType, + config: AsyncTestBedConfig +): AsyncSetupFunc; +export function registerTestBed( Component: ComponentType, config?: TestBedConfig -): SetupFunc => { +): SyncSetupFunc; +export function registerTestBed( + Component: ComponentType, + config?: AsyncTestBedConfig | TestBedConfig +): SetupFunc { const { defaultProps = defaultConfig.defaultProps, memoryRouter = defaultConfig.memoryRouter!, @@ -188,7 +203,7 @@ export const registerTestBed = ( value, isAsync = false ) => { - const formInput = typeof input === 'string' ? find(input) : (input as ReactWrapper); + const formInput = typeof input === 'string' ? find(input) : input; if (!formInput.length) { throw new Error(`Input "${input}" was not found.`); @@ -207,7 +222,7 @@ export const registerTestBed = ( value, doUpdateComponent = true ) => { - const formSelect = typeof select === 'string' ? find(select) : (select as ReactWrapper); + const formSelect = typeof select === 'string' ? find(select) : select; if (!formSelect.length) { throw new Error(`Select "${select}" was not found.`); @@ -314,7 +329,7 @@ export const registerTestBed = ( router.history.push(url); }; - return { + const testBed: TestBed = { component, exists, find, @@ -336,8 +351,10 @@ export const registerTestBed = ( navigateTo, }, }; + + return testBed; } }; return setup; -}; +} diff --git a/packages/kbn-test/src/jest/utils/testbed/types.ts b/packages/kbn-test/src/jest/utils/testbed/types.ts index bba504951c0bc..121b848e51b51 100644 --- a/packages/kbn-test/src/jest/utils/testbed/types.ts +++ b/packages/kbn-test/src/jest/utils/testbed/types.ts @@ -7,10 +7,13 @@ */ import { Store } from 'redux'; -import { ReactWrapper } from 'enzyme'; +import { ReactWrapper as GenericReactWrapper } from 'enzyme'; import { LocationDescriptor } from 'history'; +export type AsyncSetupFunc = (props?: any) => Promise>; +export type SyncSetupFunc = (props?: any) => TestBed; export type SetupFunc = (props?: any) => TestBed | Promise>; +export type ReactWrapper = GenericReactWrapper; export interface EuiTableMetaData { /** Array of rows of the table. Each row exposes its reactWrapper and its columns */ @@ -51,7 +54,7 @@ export interface TestBed { find('myForm.nameInput'); ``` */ - find: (testSubject: T, reactWrapper?: ReactWrapper) => ReactWrapper; + find: (testSubject: T, reactWrapper?: ReactWrapper) => ReactWrapper; /** * Update the props of the mounted component * @@ -147,15 +150,23 @@ export interface TestBed { }; } -export interface TestBedConfig { +export interface BaseTestBedConfig { /** The default props to pass to the mounted component. */ defaultProps?: Record; /** Configuration object for the react-router `MemoryRouter. */ memoryRouter?: MemoryRouterConfig; /** An optional redux store. You can also provide a function that returns a store. */ store?: (() => Store) | Store | null; +} + +export interface AsyncTestBedConfig extends BaseTestBedConfig { + /* Mount the component asynchronously. When using "hooked" components with _useEffect()_ calls, you need to set this to "true". */ + doMountAsync: true; +} + +export interface TestBedConfig extends BaseTestBedConfig { /* Mount the component asynchronously. When using "hooked" components with _useEffect()_ calls, you need to set this to "true". */ - doMountAsync?: boolean; + doMountAsync?: false; } export interface MemoryRouterConfig { diff --git a/packages/kbn-test/src/mocha/junit_report_generation.test.js b/packages/kbn-test/src/mocha/junit_report_generation.test.js index e4e4499f556fd..c19550349fd85 100644 --- a/packages/kbn-test/src/mocha/junit_report_generation.test.js +++ b/packages/kbn-test/src/mocha/junit_report_generation.test.js @@ -7,9 +7,9 @@ */ import { resolve } from 'path'; -import { readFileSync } from 'fs'; +import { readFile } from 'fs/promises'; +import { promisify } from 'util'; -import { fromNode as fcb } from 'bluebird'; import { parseString } from 'xml2js'; import del from 'del'; import Mocha from 'mocha'; @@ -22,6 +22,8 @@ const DURATION_REGEX = /^\d+\.\d{3}$/; const ISO_DATE_SEC_REGEX = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}$/; const XML_PATH = getUniqueJunitReportPath(PROJECT_DIR, 'test'); +const parseStringAsync = promisify(parseString); + describe('dev/mocha/junit report generation', () => { afterEach(() => { del.sync(resolve(PROJECT_DIR, 'target')); @@ -39,7 +41,7 @@ describe('dev/mocha/junit report generation', () => { mocha.addFile(resolve(PROJECT_DIR, 'test.js')); await new Promise((resolve) => mocha.run(resolve)); - const report = await fcb((cb) => parseString(readFileSync(XML_PATH), cb)); + const report = await parseStringAsync(await readFile(XML_PATH)); // test case results are wrapped in expect(report).toEqual({ diff --git a/packages/kbn-typed-react-router-config/src/create_router.test.tsx b/packages/kbn-typed-react-router-config/src/create_router.test.tsx index 9837d45ddd869..ac337f8bb5b87 100644 --- a/packages/kbn-typed-react-router-config/src/create_router.test.tsx +++ b/packages/kbn-typed-react-router-config/src/create_router.test.tsx @@ -7,7 +7,7 @@ */ import React from 'react'; import * as t from 'io-ts'; -import { toNumberRt } from '@kbn/io-ts-utils'; +import { toNumberRt } from '@kbn/io-ts-utils/to_number_rt'; import { createRouter } from './create_router'; import { createMemoryHistory } from 'history'; import { route } from './route'; diff --git a/packages/kbn-typed-react-router-config/src/create_router.ts b/packages/kbn-typed-react-router-config/src/create_router.ts index 77c2bba14e85a..89ff4fc6b0c6c 100644 --- a/packages/kbn-typed-react-router-config/src/create_router.ts +++ b/packages/kbn-typed-react-router-config/src/create_router.ts @@ -15,16 +15,10 @@ import { } from 'react-router-config'; import qs from 'query-string'; import { findLastIndex, merge, compact } from 'lodash'; -import type { deepExactRt as deepExactRtTyped, mergeRt as mergeRtTyped } from '@kbn/io-ts-utils'; -// @ts-expect-error -import { deepExactRt as deepExactRtNonTyped } from '@kbn/io-ts-utils/target_node/deep_exact_rt'; -// @ts-expect-error -import { mergeRt as mergeRtNonTyped } from '@kbn/io-ts-utils/target_node/merge_rt'; +import { mergeRt } from '@kbn/io-ts-utils/merge_rt'; +import { deepExactRt } from '@kbn/io-ts-utils/deep_exact_rt'; import { FlattenRoutesOf, Route, Router } from './types'; -const deepExactRt: typeof deepExactRtTyped = deepExactRtNonTyped; -const mergeRt: typeof mergeRtTyped = mergeRtNonTyped; - function toReactRouterPath(path: string) { return path.replace(/(?:{([^\/]+)})/g, ':$1'); } diff --git a/packages/kbn-ui-shared-deps-npm/BUILD.bazel b/packages/kbn-ui-shared-deps-npm/BUILD.bazel index bbad873429b2b..416a4d4799b7b 100644 --- a/packages/kbn-ui-shared-deps-npm/BUILD.bazel +++ b/packages/kbn-ui-shared-deps-npm/BUILD.bazel @@ -23,7 +23,6 @@ filegroup( ) NPM_MODULE_EXTRA_FILES = [ - "eui_theme_vars/package.json", "package.json", "README.md" ] diff --git a/packages/kbn-ui-shared-deps-npm/eui_theme_vars/package.json b/packages/kbn-ui-shared-deps-npm/eui_theme_vars/package.json deleted file mode 100644 index a2448adf4d096..0000000000000 --- a/packages/kbn-ui-shared-deps-npm/eui_theme_vars/package.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "main": "../target_node/eui_theme_vars.js", - "types": "../target_types/eui_theme_vars.d.ts" -} \ No newline at end of file diff --git a/packages/kbn-ui-shared-deps-src/src/index.js b/packages/kbn-ui-shared-deps-src/src/index.js index 3e3643d3e2988..630cf75c447fd 100644 --- a/packages/kbn-ui-shared-deps-src/src/index.js +++ b/packages/kbn-ui-shared-deps-src/src/index.js @@ -59,8 +59,7 @@ exports.externals = { '@elastic/eui/lib/services': '__kbnSharedDeps__.ElasticEuiLibServices', '@elastic/eui/lib/services/format': '__kbnSharedDeps__.ElasticEuiLibServicesFormat', '@elastic/eui/dist/eui_charts_theme': '__kbnSharedDeps__.ElasticEuiChartsTheme', - '@elastic/eui/dist/eui_theme_light.json': '__kbnSharedDeps__.Theme.euiLightVars', - '@elastic/eui/dist/eui_theme_dark.json': '__kbnSharedDeps__.Theme.euiDarkVars', + // transient dep of eui 'react-beautiful-dnd': '__kbnSharedDeps__.ReactBeautifulDnD', lodash: '__kbnSharedDeps__.Lodash', diff --git a/packages/kbn-ui-shared-deps-src/src/theme.ts b/packages/kbn-ui-shared-deps-src/src/theme.ts index f058913cdeeab..33b8a594bfa5d 100644 --- a/packages/kbn-ui-shared-deps-src/src/theme.ts +++ b/packages/kbn-ui-shared-deps-src/src/theme.ts @@ -6,7 +6,9 @@ * Side Public License, v 1. */ +/* eslint-disable-next-line @kbn/eslint/module_migration */ import { default as v8Light } from '@elastic/eui/dist/eui_theme_amsterdam_light.json'; +/* eslint-disable-next-line @kbn/eslint/module_migration */ import { default as v8Dark } from '@elastic/eui/dist/eui_theme_amsterdam_dark.json'; const globals: any = typeof window === 'undefined' ? {} : window; diff --git a/packages/kbn-utils/src/path/index.test.ts b/packages/kbn-utils/src/path/index.test.ts index daa2cb8dc9a5d..307d47af9ac50 100644 --- a/packages/kbn-utils/src/path/index.test.ts +++ b/packages/kbn-utils/src/path/index.test.ts @@ -7,21 +7,35 @@ */ import { accessSync, constants } from 'fs'; -import { getConfigPath, getDataPath, getConfigDirectory } from './'; +import { createAbsolutePathSerializer } from '@kbn/dev-utils'; +import { getConfigPath, getDataPath, getLogsPath, getConfigDirectory } from './'; + +expect.addSnapshotSerializer(createAbsolutePathSerializer()); describe('Default path finder', () => { - it('should find a kibana.yml', () => { - const configPath = getConfigPath(); - expect(() => accessSync(configPath, constants.R_OK)).not.toThrow(); + it('should expose a path to the config directory', () => { + expect(getConfigDirectory()).toMatchInlineSnapshot('/config'); }); - it('should find a data directory', () => { - const dataPath = getDataPath(); - expect(() => accessSync(dataPath, constants.R_OK)).not.toThrow(); + it('should expose a path to the kibana.yml', () => { + expect(getConfigPath()).toMatchInlineSnapshot('/config/kibana.yml'); + }); + + it('should expose a path to the data directory', () => { + expect(getDataPath()).toMatchInlineSnapshot('/data'); + }); + + it('should expose a path to the logs directory', () => { + expect(getLogsPath()).toMatchInlineSnapshot('/logs'); }); it('should find a config directory', () => { const configDirectory = getConfigDirectory(); expect(() => accessSync(configDirectory, constants.R_OK)).not.toThrow(); }); + + it('should find a kibana.yml', () => { + const configPath = getConfigPath(); + expect(() => accessSync(configPath, constants.R_OK)).not.toThrow(); + }); }); diff --git a/packages/kbn-utils/src/path/index.ts b/packages/kbn-utils/src/path/index.ts index 15d6a3eddf01e..c839522441c7c 100644 --- a/packages/kbn-utils/src/path/index.ts +++ b/packages/kbn-utils/src/path/index.ts @@ -27,6 +27,8 @@ const CONFIG_DIRECTORIES = [ const DATA_PATHS = [join(REPO_ROOT, 'data'), '/var/lib/kibana'].filter(isString); +const LOGS_PATHS = [join(REPO_ROOT, 'logs'), '/var/log/kibana'].filter(isString); + function findFile(paths: string[]) { const availablePath = paths.find((configPath) => { try { @@ -57,6 +59,12 @@ export const getConfigDirectory = () => findFile(CONFIG_DIRECTORIES); */ export const getDataPath = () => findFile(DATA_PATHS); +/** + * Get the directory containing logs + * @internal + */ +export const getLogsPath = () => findFile(LOGS_PATHS); + export type PathConfigType = TypeOf; export const config = { diff --git a/packages/kbn-utils/src/streams/map_stream.test.ts b/packages/kbn-utils/src/streams/map_stream.test.ts index 2c3df67fdf35c..94c01d04f7dc9 100644 --- a/packages/kbn-utils/src/streams/map_stream.test.ts +++ b/packages/kbn-utils/src/streams/map_stream.test.ts @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import { delay } from 'bluebird'; +import { setTimeout as setTimeoutAsync } from 'timers/promises'; import { createPromiseFromStreams } from './promise_from_streams'; import { createListStream } from './list_stream'; @@ -39,7 +39,7 @@ describe('createMapStream()', () => { const result = await createPromiseFromStreams([ createListStream([1, 2, 3]), createMapStream(async (n: number, i: number) => { - await delay(n); + await setTimeoutAsync(n); return n * i; }), createConcatStream([]), diff --git a/scripts/docs.js b/scripts/docs.js index 6522079c7aca3..f310903b90bac 100644 --- a/scripts/docs.js +++ b/scripts/docs.js @@ -7,4 +7,4 @@ */ require('../src/setup_node_env'); -require('../src/docs/cli'); +require('../src/dev/run_build_docs_cli').runBuildDocsCli(); diff --git a/src/core/public/doc_links/doc_links_service.ts b/src/core/public/doc_links/doc_links_service.ts index 7284a1a3f06f0..ee4e50627074a 100644 --- a/src/core/public/doc_links/doc_links_service.ts +++ b/src/core/public/doc_links/doc_links_service.ts @@ -22,6 +22,7 @@ export class DocLinksService { // Documentation for `main` branches is still published at a `master` URL. const DOC_LINK_VERSION = kibanaBranch === 'main' ? 'master' : kibanaBranch; const ELASTIC_WEBSITE_URL = 'https://www.elastic.co/'; + const STACK_DOCS = `${ELASTIC_WEBSITE_URL}guide/en/elastic-stack/${DOC_LINK_VERSION}/`; const ELASTICSEARCH_DOCS = `${ELASTIC_WEBSITE_URL}guide/en/elasticsearch/reference/${DOC_LINK_VERSION}/`; const KIBANA_DOCS = `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/`; const FLEET_DOCS = `${ELASTIC_WEBSITE_URL}guide/en/fleet/${DOC_LINK_VERSION}/`; @@ -36,6 +37,9 @@ export class DocLinksService { links: { settings: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/settings.html`, elasticStackGetStarted: `${STACK_GETTING_STARTED}get-started-elastic-stack.html`, + upgrade: { + upgradingElasticStack: `${STACK_DOCS}upgrading-elastic-stack.html`, + }, apm: { kibanaSettings: `${KIBANA_DOCS}apm-settings-in-kibana.html`, supportedServiceMaps: `${KIBANA_DOCS}service-maps.html#service-maps-supported`, @@ -114,6 +118,7 @@ export class DocLinksService { range: `${ELASTICSEARCH_DOCS}search-aggregations-bucket-range-aggregation.html`, significant_terms: `${ELASTICSEARCH_DOCS}search-aggregations-bucket-significantterms-aggregation.html`, terms: `${ELASTICSEARCH_DOCS}search-aggregations-bucket-terms-aggregation.html`, + terms_doc_count_error: `${ELASTICSEARCH_DOCS}search-aggregations-bucket-terms-aggregation.html#_per_bucket_document_count_error`, avg: `${ELASTICSEARCH_DOCS}search-aggregations-metrics-avg-aggregation.html`, avg_bucket: `${ELASTICSEARCH_DOCS}search-aggregations-pipeline-avg-bucket-aggregation.html`, max_bucket: `${ELASTICSEARCH_DOCS}search-aggregations-pipeline-max-bucket-aggregation.html`, @@ -154,11 +159,15 @@ export class DocLinksService { introduction: `${KIBANA_DOCS}index-patterns.html`, fieldFormattersNumber: `${KIBANA_DOCS}numeral.html`, fieldFormattersString: `${KIBANA_DOCS}field-formatters-string.html`, - runtimeFields: `${KIBANA_DOCS}managing-index-patterns.html#runtime-fields`, + runtimeFields: `${KIBANA_DOCS}managing-data-views.html#runtime-fields`, }, addData: `${KIBANA_DOCS}connect-to-elasticsearch.html`, kibana: `${KIBANA_DOCS}index.html`, - upgradeAssistant: `${KIBANA_DOCS}upgrade-assistant.html`, + upgradeAssistant: { + overview: `${KIBANA_DOCS}upgrade-assistant.html`, + batchReindex: `${KIBANA_DOCS}batch-start-resume-reindex.html`, + remoteReindex: `${ELASTICSEARCH_DOCS}docs-reindex.html#reindex-from-remote`, + }, rollupJobs: `${KIBANA_DOCS}data-rollups.html`, elasticsearch: { docsBase: `${ELASTICSEARCH_DOCS}`, @@ -222,10 +231,11 @@ export class DocLinksService { remoteClustersProxy: `${ELASTICSEARCH_DOCS}remote-clusters.html#proxy-mode`, remoteClusersProxySettings: `${ELASTICSEARCH_DOCS}remote-clusters-settings.html#remote-cluster-proxy-settings`, scriptParameters: `${ELASTICSEARCH_DOCS}modules-scripting-using.html#prefer-params`, - setupUpgrade: `${ELASTICSEARCH_DOCS}setup-upgrade.html`, shardAllocationSettings: `${ELASTICSEARCH_DOCS}modules-cluster.html#cluster-shard-allocation-settings`, transportSettings: `${ELASTICSEARCH_DOCS}modules-network.html#common-network-settings`, typesRemoval: `${ELASTICSEARCH_DOCS}removal-of-types.html`, + setupUpgrade: `${ELASTICSEARCH_DOCS}setup-upgrade.html`, + apiCompatibilityHeader: `${ELASTICSEARCH_DOCS}api-conventions.html#api-compatibility`, }, siem: { guide: `${SECURITY_SOLUTION_DOCS}index.html`, @@ -289,6 +299,7 @@ export class DocLinksService { outlierDetectionRoc: `${ELASTIC_WEBSITE_URL}guide/en/machine-learning/${DOC_LINK_VERSION}/ml-dfa-finding-outliers.html#ml-dfanalytics-roc`, regressionEvaluation: `${ELASTIC_WEBSITE_URL}guide/en/machine-learning/${DOC_LINK_VERSION}/ml-dfa-regression.html#ml-dfanalytics-regression-evaluation`, classificationAucRoc: `${ELASTIC_WEBSITE_URL}guide/en/machine-learning/${DOC_LINK_VERSION}/ml-dfa-classification.html#ml-dfanalytics-class-aucroc`, + setUpgradeMode: `${ELASTICSEARCH_DOCS}ml-set-upgrade-mode.html`, }, transforms: { guide: `${ELASTICSEARCH_DOCS}transforms.html`, @@ -305,9 +316,9 @@ export class DocLinksService { }, observability: { guide: `${ELASTIC_WEBSITE_URL}guide/en/observability/${DOC_LINK_VERSION}/index.html`, - infrastructureThreshold: `{ELASTIC_WEBSITE_URL}guide/en/observability/${DOC_LINK_VERSION}/infrastructure-threshold-alert.html`, - logsThreshold: `{ELASTIC_WEBSITE_URL}guide/en/observability/${DOC_LINK_VERSION}/logs-threshold-alert.html`, - metricsThreshold: `{ELASTIC_WEBSITE_URL}guide/en/observability/${DOC_LINK_VERSION}/metrics-threshold-alert.html`, + infrastructureThreshold: `${ELASTIC_WEBSITE_URL}guide/en/observability/${DOC_LINK_VERSION}/infrastructure-threshold-alert.html`, + logsThreshold: `${ELASTIC_WEBSITE_URL}guide/en/observability/${DOC_LINK_VERSION}/logs-threshold-alert.html`, + metricsThreshold: `${ELASTIC_WEBSITE_URL}guide/en/observability/${DOC_LINK_VERSION}/metrics-threshold-alert.html`, monitorStatus: `${ELASTIC_WEBSITE_URL}guide/en/observability/${DOC_LINK_VERSION}/monitor-status-alert.html`, monitorUptime: `${ELASTIC_WEBSITE_URL}guide/en/observability/${DOC_LINK_VERSION}/monitor-uptime.html`, tlsCertificate: `${ELASTIC_WEBSITE_URL}guide/en/observability/${DOC_LINK_VERSION}/tls-certificate-alert.html`, @@ -479,6 +490,7 @@ export class DocLinksService { fleetServerAddFleetServer: `${FLEET_DOCS}fleet-server.html#add-fleet-server`, settings: `${FLEET_DOCS}fleet-settings.html#fleet-server-hosts-setting`, settingsFleetServerHostSettings: `${FLEET_DOCS}fleet-settings.html#fleet-server-hosts-setting`, + settingsFleetServerProxySettings: `${KIBANA_DOCS}fleet-settings-kb.html#fleet-data-visualizer-settings`, troubleshooting: `${FLEET_DOCS}fleet-troubleshooting.html`, elasticAgent: `${FLEET_DOCS}elastic-agent-installation.html`, beatsAgentComparison: `${FLEET_DOCS}beats-agent-comparison.html`, @@ -490,6 +502,7 @@ export class DocLinksService { upgradeElasticAgent712lower: `${FLEET_DOCS}upgrade-elastic-agent.html#upgrade-7.12-lower`, learnMoreBlog: `${ELASTIC_WEBSITE_URL}blog/elastic-agent-and-fleet-make-it-easier-to-integrate-your-systems-with-elastic`, apiKeysLearnMore: `${KIBANA_DOCS}api-keys.html`, + onPremRegistry: `${ELASTIC_WEBSITE_URL}guide/en/integrations-developer/${DOC_LINK_VERSION}/air-gapped.html`, }, ecs: { guide: `${ELASTIC_WEBSITE_URL}guide/en/ecs/current/index.html`, @@ -507,6 +520,9 @@ export class DocLinksService { rubyOverview: `${ELASTIC_WEBSITE_URL}guide/en/elasticsearch/client/ruby-api/${DOC_LINK_VERSION}/ruby_client.html`, rustGuide: `${ELASTIC_WEBSITE_URL}guide/en/elasticsearch/client/rust-api/${DOC_LINK_VERSION}/index.html`, }, + endpoints: { + troubleshooting: `${ELASTIC_WEBSITE_URL}guide/en/security/${DOC_LINK_VERSION}/ts-management.html#ts-endpoints`, + }, }, }); } @@ -519,6 +535,9 @@ export interface DocLinksStart { readonly links: { readonly settings: string; readonly elasticStackGetStarted: string; + readonly upgrade: { + readonly upgradingElasticStack: string; + }; readonly apm: { readonly kibanaSettings: string; readonly supportedServiceMaps: string; @@ -595,6 +614,7 @@ export interface DocLinksStart { readonly range: string; readonly significant_terms: string; readonly terms: string; + readonly terms_doc_count_error: string; readonly avg: string; readonly avg_bucket: string; readonly max_bucket: string; @@ -642,7 +662,11 @@ export interface DocLinksStart { }; readonly addData: string; readonly kibana: string; - readonly upgradeAssistant: string; + readonly upgradeAssistant: { + readonly overview: string; + readonly batchReindex: string; + readonly remoteReindex: string; + }; readonly rollupJobs: string; readonly elasticsearch: Record; readonly siem: { @@ -745,6 +769,7 @@ export interface DocLinksStart { fleetServerAddFleetServer: string; settings: string; settingsFleetServerHostSettings: string; + settingsFleetServerProxySettings: string; troubleshooting: string; elasticAgent: string; datastreams: string; @@ -754,6 +779,7 @@ export interface DocLinksStart { upgradeElasticAgent712lower: string; learnMoreBlog: string; apiKeysLearnMore: string; + onPremRegistry: string; }>; readonly ecs: { readonly guide: string; @@ -770,5 +796,8 @@ export interface DocLinksStart { readonly rubyOverview: string; readonly rustGuide: string; }; + readonly endpoints: { + readonly troubleshooting: string; + }; }; } diff --git a/src/core/public/mocks.ts b/src/core/public/mocks.ts index bd7623beba651..39d2dc3d5c497 100644 --- a/src/core/public/mocks.ts +++ b/src/core/public/mocks.ts @@ -169,5 +169,5 @@ export const coreMock = { createStart: createCoreStartMock, createPluginInitializerContext: pluginInitializerContextMock, createStorage: createStorageMock, - createAppMountParamters: createAppMountParametersMock, + createAppMountParameters: createAppMountParametersMock, }; diff --git a/src/core/public/overlays/flyout/flyout_service.tsx b/src/core/public/overlays/flyout/flyout_service.tsx index 6e986cc8ecb48..79047738da4dd 100644 --- a/src/core/public/overlays/flyout/flyout_service.tsx +++ b/src/core/public/overlays/flyout/flyout_service.tsx @@ -8,7 +8,7 @@ /* eslint-disable max-classes-per-file */ -import { EuiFlyout, EuiFlyoutSize } from '@elastic/eui'; +import { EuiFlyout, EuiFlyoutSize, EuiOverlayMaskProps } from '@elastic/eui'; import React from 'react'; import { render, unmountComponentAtNode } from 'react-dom'; import { Subject } from 'rxjs'; @@ -86,6 +86,7 @@ export interface OverlayFlyoutOpenOptions { size?: EuiFlyoutSize; maxWidth?: boolean | number | string; hideCloseButton?: boolean; + maskProps?: EuiOverlayMaskProps; /** * EuiFlyout onClose handler. * If provided the consumer is responsible for calling flyout.close() to close the flyout; diff --git a/src/core/public/public.api.md b/src/core/public/public.api.md index 6c377bd2870ae..1dc7ead282927 100644 --- a/src/core/public/public.api.md +++ b/src/core/public/public.api.md @@ -4,8 +4,11 @@ ```ts +/// + import { Action } from 'history'; import Boom from '@hapi/boom'; +import { ByteSizeValue } from '@kbn/config-schema'; import { ConfigPath } from '@kbn/config'; import { DetailedPeerCertificate } from 'tls'; import { EnvironmentMode } from '@kbn/config'; @@ -15,12 +18,13 @@ import { EuiButtonEmptyProps } from '@elastic/eui'; import { EuiConfirmModalProps } from '@elastic/eui'; import { EuiFlyoutSize } from '@elastic/eui'; import { EuiGlobalToastListToast } from '@elastic/eui'; -import { History } from 'history'; +import { EuiOverlayMaskProps } from '@elastic/eui'; +import { History as History_2 } from 'history'; import { Href } from 'history'; import { IconType } from '@elastic/eui'; import { IncomingHttpHeaders } from 'http'; -import { KibanaClient } from '@elastic/elasticsearch/lib/api/kibana'; -import { Location } from 'history'; +import type { KibanaClient } from '@elastic/elasticsearch/lib/api/kibana'; +import { Location as Location_2 } from 'history'; import { LocationDescriptorObject } from 'history'; import { Logger } from '@kbn/logging'; import { LogMeta } from '@kbn/logging'; @@ -30,21 +34,21 @@ import { Observable } from 'rxjs'; import { PackageInfo } from '@kbn/config'; import { Path } from 'history'; import { PeerCertificate } from 'tls'; -import { PublicMethodsOf } from '@kbn/utility-types'; +import type { PublicMethodsOf } from '@kbn/utility-types'; import { PublicUiSettingsParams as PublicUiSettingsParams_2 } from 'src/core/server/types'; -import React from 'react'; +import { default as React_2 } from 'react'; import { RecursiveReadonly } from '@kbn/utility-types'; -import { Request } from '@hapi/hapi'; +import { Request as Request_2 } from '@hapi/hapi'; import * as Rx from 'rxjs'; import { SchemaTypeError } from '@kbn/config-schema'; -import { TransportRequestOptions } from '@elastic/elasticsearch'; -import { TransportRequestParams } from '@elastic/elasticsearch'; -import { TransportResult } from '@elastic/elasticsearch'; +import type { TransportRequestOptions } from '@elastic/elasticsearch'; +import type { TransportRequestParams } from '@elastic/elasticsearch'; +import type { TransportResult } from '@elastic/elasticsearch'; import { Type } from '@kbn/config-schema'; import { TypeOf } from '@kbn/config-schema'; import { UiCounterMetricType } from '@kbn/analytics'; import { UnregisterCallback } from 'history'; -import { URL } from 'url'; +import { URL as URL_2 } from 'url'; import { UserProvidedValues as UserProvidedValues_2 } from 'src/core/server/types'; // @internal (undocumented) @@ -250,7 +254,7 @@ export type ChromeHelpExtensionLinkBase = Pick; // (undocumented) stop(): void; - } +} // @internal (undocumented) export const DEFAULT_APP_CATEGORIES: Record; @@ -477,6 +481,9 @@ export interface DocLinksStart { readonly links: { readonly settings: string; readonly elasticStackGetStarted: string; + readonly upgrade: { + readonly upgradingElasticStack: string; + }; readonly apm: { readonly kibanaSettings: string; readonly supportedServiceMaps: string; @@ -553,6 +560,7 @@ export interface DocLinksStart { readonly range: string; readonly significant_terms: string; readonly terms: string; + readonly terms_doc_count_error: string; readonly avg: string; readonly avg_bucket: string; readonly max_bucket: string; @@ -600,7 +608,11 @@ export interface DocLinksStart { }; readonly addData: string; readonly kibana: string; - readonly upgradeAssistant: string; + readonly upgradeAssistant: { + readonly overview: string; + readonly batchReindex: string; + readonly remoteReindex: string; + }; readonly rollupJobs: string; readonly elasticsearch: Record; readonly siem: { @@ -703,6 +715,7 @@ export interface DocLinksStart { fleetServerAddFleetServer: string; settings: string; settingsFleetServerHostSettings: string; + settingsFleetServerProxySettings: string; troubleshooting: string; elasticAgent: string; datastreams: string; @@ -712,6 +725,7 @@ export interface DocLinksStart { upgradeElasticAgent712lower: string; learnMoreBlog: string; apiKeysLearnMore: string; + onPremRegistry: string; }>; readonly ecs: { readonly guide: string; @@ -728,6 +742,9 @@ export interface DocLinksStart { readonly rubyOverview: string; readonly rustGuide: string; }; + readonly endpoints: { + readonly troubleshooting: string; + }; }; } @@ -899,7 +916,7 @@ export type HttpStart = HttpSetup; // @public export interface I18nStart { Context: ({ children }: { - children: React.ReactNode; + children: React_2.ReactNode; }) => JSX.Element; } @@ -1048,6 +1065,8 @@ export interface OverlayFlyoutOpenOptions { // (undocumented) hideCloseButton?: boolean; // (undocumented) + maskProps?: EuiOverlayMaskProps; + // (undocumented) maxWidth?: boolean | number | string; onClose?: (flyout: OverlayRef) => void; // (undocumented) @@ -1121,7 +1140,7 @@ export interface OverlayStart { export { PackageInfo } // @public -export interface Plugin { +interface Plugin_2 { // (undocumented) setup(core: CoreSetup, plugins: TPluginsSetup): TSetup; // (undocumented) @@ -1129,9 +1148,10 @@ export interface Plugin = (core: PluginInitializerContext) => Plugin | AsyncPlugin; +export type PluginInitializer = (core: PluginInitializerContext) => Plugin_2 | AsyncPlugin; // @public export interface PluginInitializerContext { @@ -1592,10 +1612,10 @@ export interface SavedObjectsUpdateOptions { } // @public -export class ScopedHistory implements History { - constructor(parentHistory: History, basePath: string); +export class ScopedHistory implements History_2 { + constructor(parentHistory: History_2, basePath: string); get action(): Action; - block: (prompt?: string | boolean | History.TransitionPromptHook | undefined) => UnregisterCallback; + block: (prompt?: string | boolean | History_2.TransitionPromptHook | undefined) => UnregisterCallback; createHref: (location: LocationDescriptorObject, { prependBasePath }?: { prependBasePath?: boolean | undefined; }) => Href; @@ -1604,11 +1624,11 @@ export class ScopedHistory implements History void; goForward: () => void; get length(): number; - listen: (listener: (location: Location, action: Action) => void) => UnregisterCallback; - get location(): Location; + listen: (listener: (location: Location_2, action: Action) => void) => UnregisterCallback; + get location(): Location_2; push: (pathOrLocation: Path | LocationDescriptorObject, state?: HistoryLocationState | undefined) => void; replace: (pathOrLocation: Path | LocationDescriptorObject, state?: HistoryLocationState | undefined) => void; - } +} // @public export class SimpleSavedObject { @@ -1684,7 +1704,7 @@ export class ToastsApi implements IToasts { overlays: OverlayStart; i18n: I18nStart; }): void; - } +} // @public (undocumented) export type ToastsSetup = IToasts; @@ -1739,7 +1759,6 @@ export interface UserProvidedValues { userValue?: T; } - // Warnings were encountered during analysis: // // src/core/public/core_system.ts:168:21 - (ae-forgotten-export) The symbol "InternalApplicationStart" needs to be exported by the entry point index.d.ts diff --git a/src/core/server/logging/__snapshots__/logging_system.test.ts.snap b/src/core/server/logging/__snapshots__/logging_system.test.ts.snap index 49035cdda3915..e369d7b0cba37 100644 --- a/src/core/server/logging/__snapshots__/logging_system.test.ts.snap +++ b/src/core/server/logging/__snapshots__/logging_system.test.ts.snap @@ -117,7 +117,10 @@ Object { "message": "trace message", "meta": undefined, "pid": Any, + "spanId": undefined, "timestamp": 2012-02-01T14:33:22.011Z, + "traceId": undefined, + "transactionId": undefined, } `; @@ -133,6 +136,9 @@ Object { "some": "value", }, "pid": Any, + "spanId": undefined, "timestamp": 2012-02-01T14:33:22.011Z, + "traceId": undefined, + "transactionId": undefined, } `; diff --git a/src/core/server/logging/appenders/rewrite/policies/meta/meta_policy.test.ts b/src/core/server/logging/appenders/rewrite/policies/meta/meta_policy.test.ts index f03c5bd9d1f8f..47799ef5d4335 100644 --- a/src/core/server/logging/appenders/rewrite/policies/meta/meta_policy.test.ts +++ b/src/core/server/logging/appenders/rewrite/policies/meta/meta_policy.test.ts @@ -29,26 +29,27 @@ describe('MetaRewritePolicy', () => { // @ts-expect-error ECS custom meta const log = createLogRecord({ a: 'before' }); const policy = createPolicy('update', [{ path: 'a', value: 'after' }]); + // @ts-expect-error ECS custom meta expect(policy.rewrite(log).meta!.a).toBe('after'); }); it('updates nested properties in LogMeta', () => { - // @ts-expect-error ECS custom meta - const log = createLogRecord({ a: 'before a', b: { c: 'before b.c' }, d: [0, 1] }); + const log = createLogRecord({ + error: { message: 'before b.c' }, + tags: ['0', '1'], + }); const policy = createPolicy('update', [ - { path: 'a', value: 'after a' }, - { path: 'b.c', value: 'after b.c' }, - { path: 'd[1]', value: 2 }, + { path: 'error.message', value: 'after b.c' }, + { path: 'tags[1]', value: '2' }, ]); expect(policy.rewrite(log).meta).toMatchInlineSnapshot(` Object { - "a": "after a", - "b": Object { - "c": "after b.c", + "error": Object { + "message": "after b.c", }, - "d": Array [ - 0, - 2, + "tags": Array [ + "0", + "2", ], } `); @@ -80,14 +81,13 @@ describe('MetaRewritePolicy', () => { it(`does not add properties which don't exist yet`, () => { const policy = createPolicy('update', [ - { path: 'a.b', value: 'foo' }, - { path: 'a.c', value: 'bar' }, + { path: 'error.message', value: 'foo' }, + { path: 'error.id', value: 'bar' }, ]); - // @ts-expect-error ECS custom meta - const log = createLogRecord({ a: { b: 'existing meta' } }); + const log = createLogRecord({ error: { message: 'existing meta' } }); const { meta } = policy.rewrite(log); - expect(meta!.a.b).toBe('foo'); - expect(meta!.a.c).toBeUndefined(); + expect(meta?.error?.message).toBe('foo'); + expect(meta?.error?.id).toBeUndefined(); }); it('does not touch anything outside of LogMeta', () => { @@ -110,22 +110,19 @@ describe('MetaRewritePolicy', () => { describe('mode: remove', () => { it('removes existing properties in LogMeta', () => { - // @ts-expect-error ECS custom meta - const log = createLogRecord({ a: 'goodbye' }); - const policy = createPolicy('remove', [{ path: 'a' }]); - expect(policy.rewrite(log).meta!.a).toBeUndefined(); + const log = createLogRecord({ error: { message: 'before' } }); + const policy = createPolicy('remove', [{ path: 'error' }]); + expect(policy.rewrite(log).meta?.error).toBeUndefined(); }); it('removes nested properties in LogMeta', () => { - // @ts-expect-error ECS custom meta - const log = createLogRecord({ a: 'a', b: { c: 'b.c' }, d: [0, 1] }); - const policy = createPolicy('remove', [{ path: 'b.c' }, { path: 'd[1]' }]); + const log = createLogRecord({ error: { message: 'reason' }, tags: ['0', '1'] }); + const policy = createPolicy('remove', [{ path: 'error.message' }, { path: 'tags[1]' }]); expect(policy.rewrite(log).meta).toMatchInlineSnapshot(` Object { - "a": "a", - "b": Object {}, - "d": Array [ - 0, + "error": Object {}, + "tags": Array [ + "0", undefined, ], } @@ -133,12 +130,11 @@ describe('MetaRewritePolicy', () => { }); it('has no effect if property does not exist', () => { - // @ts-expect-error ECS custom meta - const log = createLogRecord({ a: 'a' }); + const log = createLogRecord({ error: {} }); const policy = createPolicy('remove', [{ path: 'b' }]); expect(policy.rewrite(log).meta).toMatchInlineSnapshot(` Object { - "a": "a", + "error": Object {}, } `); }); diff --git a/src/core/server/logging/layouts/__snapshots__/json_layout.test.ts.snap b/src/core/server/logging/layouts/__snapshots__/json_layout.test.ts.snap index 48bbb19447411..0809dbffce670 100644 --- a/src/core/server/logging/layouts/__snapshots__/json_layout.test.ts.snap +++ b/src/core/server/logging/layouts/__snapshots__/json_layout.test.ts.snap @@ -88,3 +88,26 @@ Object { }, } `; + +exports[`\`format()\` correctly formats record and includes correct ECS version. 7`] = ` +Object { + "@timestamp": "2012-02-01T09:30:22.011-05:00", + "log": Object { + "level": "TRACE", + "logger": "context-7", + }, + "message": "message-6", + "process": Object { + "pid": 5355, + }, + "span": Object { + "id": "spanId-1", + }, + "trace": Object { + "id": "traceId-1", + }, + "transaction": Object { + "id": "transactionId-1", + }, +} +`; diff --git a/src/core/server/logging/layouts/json_layout.test.ts b/src/core/server/logging/layouts/json_layout.test.ts index 56184ebd67aee..d3bf2eab473a4 100644 --- a/src/core/server/logging/layouts/json_layout.test.ts +++ b/src/core/server/logging/layouts/json_layout.test.ts @@ -58,6 +58,16 @@ const records: LogRecord[] = [ timestamp, pid: 5355, }, + { + context: 'context-7', + level: LogLevel.Trace, + message: 'message-6', + timestamp, + pid: 5355, + spanId: 'spanId-1', + traceId: 'traceId-1', + transactionId: 'transactionId-1', + }, ]; test('`createConfigSchema()` creates correct schema.', () => { @@ -88,6 +98,7 @@ test('`format()` correctly formats record with meta-data', () => { timestamp, pid: 5355, meta: { + // @ts-expect-error ECS custom meta version: { from: 'v7', to: 'v8', @@ -130,6 +141,7 @@ test('`format()` correctly formats error record with meta-data', () => { timestamp, pid: 5355, meta: { + // @ts-expect-error ECS custom meta version: { from: 'v7', to: 'v8', @@ -172,6 +184,7 @@ test('format() meta can merge override logs', () => { pid: 3, meta: { log: { + // @ts-expect-error ECS custom meta kbn_custom_field: 'hello', }, }, @@ -203,6 +216,7 @@ test('format() meta can not override message', () => { context: 'bar', pid: 3, meta: { + // @ts-expect-error cannot override message message: 'baz', }, }) @@ -232,7 +246,8 @@ test('format() meta can not override ecs version', () => { context: 'bar', pid: 3, meta: { - message: 'baz', + // @ts-expect-error cannot override ecs version + ecs: 1, }, }) ) @@ -262,6 +277,7 @@ test('format() meta can not override logger or level', () => { pid: 3, meta: { log: { + // @ts-expect-error cannot override log.level level: 'IGNORE', logger: 'me', }, @@ -293,6 +309,7 @@ test('format() meta can not override timestamp', () => { context: 'bar', pid: 3, meta: { + // @ts-expect-error cannot override @timestamp '@timestamp': '2099-02-01T09:30:22.011-05:00', }, }) @@ -310,3 +327,40 @@ test('format() meta can not override timestamp', () => { }, }); }); + +test('format() meta can not override tracing properties', () => { + const layout = new JsonLayout(); + expect( + JSON.parse( + layout.format({ + message: 'foo', + timestamp, + level: LogLevel.Debug, + context: 'bar', + pid: 3, + meta: { + span: { id: 'span_override' }, + trace: { id: 'trace_override' }, + transaction: { id: 'transaction_override' }, + }, + spanId: 'spanId-1', + traceId: 'traceId-1', + transactionId: 'transactionId-1', + }) + ) + ).toStrictEqual({ + ecs: { version: expect.any(String) }, + '@timestamp': '2012-02-01T09:30:22.011-05:00', + message: 'foo', + log: { + level: 'DEBUG', + logger: 'bar', + }, + process: { + pid: 3, + }, + span: { id: 'spanId-1' }, + trace: { id: 'traceId-1' }, + transaction: { id: 'transactionId-1' }, + }); +}); diff --git a/src/core/server/logging/layouts/json_layout.ts b/src/core/server/logging/layouts/json_layout.ts index f0717f49a6b15..5c23e7ac1a911 100644 --- a/src/core/server/logging/layouts/json_layout.ts +++ b/src/core/server/logging/layouts/json_layout.ts @@ -54,6 +54,9 @@ export class JsonLayout implements Layout { process: { pid: record.pid, }, + span: record.spanId ? { id: record.spanId } : undefined, + trace: record.traceId ? { id: record.traceId } : undefined, + transaction: record.transactionId ? { id: record.transactionId } : undefined, }; const output = record.meta ? merge({ ...record.meta }, log) : log; diff --git a/src/core/server/logging/layouts/pattern_layout.test.ts b/src/core/server/logging/layouts/pattern_layout.test.ts index bb66722d4c2d0..22495736227b3 100644 --- a/src/core/server/logging/layouts/pattern_layout.test.ts +++ b/src/core/server/logging/layouts/pattern_layout.test.ts @@ -118,6 +118,7 @@ test('`format()` correctly formats record with meta data.', () => { timestamp, pid: 5355, meta: { + // @ts-expect-error not valid ECS field from: 'v7', to: 'v8', }, @@ -177,6 +178,7 @@ test('`format()` allows specifying pattern with meta.', () => { to: 'v8', }, }; + // @ts-expect-error not valid ECS field expect(layout.format(record)).toBe('context-{"from":"v7","to":"v8"}-message'); }); diff --git a/src/core/server/logging/logger.ts b/src/core/server/logging/logger.ts index e025c28a88f0e..2c9283da54897 100644 --- a/src/core/server/logging/logger.ts +++ b/src/core/server/logging/logger.ts @@ -5,7 +5,7 @@ * in compliance with, at your election, the Elastic License 2.0 or the Server * Side Public License, v 1. */ - +import apmAgent from 'elastic-apm-node'; import { Appender, LogLevel, LogRecord, LoggerFactory, LogMeta, Logger } from '@kbn/logging'; function isError(x: any): x is Error { @@ -73,6 +73,7 @@ export class BaseLogger implements Logger { meta, timestamp: new Date(), pid: process.pid, + ...this.getTraceIds(), }; } @@ -83,6 +84,15 @@ export class BaseLogger implements Logger { meta, timestamp: new Date(), pid: process.pid, + ...this.getTraceIds(), + }; + } + + private getTraceIds() { + return { + spanId: apmAgent.currentTraceIds['span.id'], + traceId: apmAgent.currentTraceIds['trace.id'], + transactionId: apmAgent.currentTraceIds['transaction.id'], }; } } diff --git a/src/core/server/saved_objects/migrations/README.md b/src/core/server/saved_objects/migrations/README.md index 69331c3751c8e..60bf84eef87a6 100644 --- a/src/core/server/saved_objects/migrations/README.md +++ b/src/core/server/saved_objects/migrations/README.md @@ -1,222 +1,504 @@ -# Saved Object Migrations +- [Introduction](#introduction) +- [Algorithm steps](#algorithm-steps) + - [INIT](#init) + - [Next action](#next-action) + - [New control state](#new-control-state) + - [CREATE_NEW_TARGET](#create_new_target) + - [Next action](#next-action-1) + - [New control state](#new-control-state-1) + - [LEGACY_SET_WRITE_BLOCK](#legacy_set_write_block) + - [Next action](#next-action-2) + - [New control state](#new-control-state-2) + - [LEGACY_CREATE_REINDEX_TARGET](#legacy_create_reindex_target) + - [Next action](#next-action-3) + - [New control state](#new-control-state-3) + - [LEGACY_REINDEX](#legacy_reindex) + - [Next action](#next-action-4) + - [New control state](#new-control-state-4) + - [LEGACY_REINDEX_WAIT_FOR_TASK](#legacy_reindex_wait_for_task) + - [Next action](#next-action-5) + - [New control state](#new-control-state-5) + - [LEGACY_DELETE](#legacy_delete) + - [Next action](#next-action-6) + - [New control state](#new-control-state-6) + - [WAIT_FOR_YELLOW_SOURCE](#wait_for_yellow_source) + - [Next action](#next-action-7) + - [New control state](#new-control-state-7) + - [SET_SOURCE_WRITE_BLOCK](#set_source_write_block) + - [Next action](#next-action-8) + - [New control state](#new-control-state-8) + - [CREATE_REINDEX_TEMP](#create_reindex_temp) + - [Next action](#next-action-9) + - [New control state](#new-control-state-9) + - [REINDEX_SOURCE_TO_TEMP_OPEN_PIT](#reindex_source_to_temp_open_pit) + - [Next action](#next-action-10) + - [New control state](#new-control-state-10) + - [REINDEX_SOURCE_TO_TEMP_READ](#reindex_source_to_temp_read) + - [Next action](#next-action-11) + - [New control state](#new-control-state-11) + - [REINDEX_SOURCE_TO_TEMP_TRANSFORM](#REINDEX_SOURCE_TO_TEMP_TRANSFORM) + - [Next action](#next-action-12) + - [New control state](#new-control-state-12) + - [REINDEX_SOURCE_TO_TEMP_INDEX_BULK](#reindex_source_to_temp_index_bulk) + - [Next action](#next-action-13) + - [New control state](#new-control-state-13) + - [REINDEX_SOURCE_TO_TEMP_CLOSE_PIT](#reindex_source_to_temp_close_pit) + - [Next action](#next-action-14) + - [New control state](#new-control-state-14) + - [SET_TEMP_WRITE_BLOCK](#set_temp_write_block) + - [Next action](#next-action-15) + - [New control state](#new-control-state-15) + - [CLONE_TEMP_TO_TARGET](#clone_temp_to_target) + - [Next action](#next-action-16) + - [New control state](#new-control-state-16) + - [OUTDATED_DOCUMENTS_SEARCH](#outdated_documents_search) + - [Next action](#next-action-17) + - [New control state](#new-control-state-17) + - [OUTDATED_DOCUMENTS_TRANSFORM](#outdated_documents_transform) + - [Next action](#next-action-18) + - [New control state](#new-control-state-18) + - [UPDATE_TARGET_MAPPINGS](#update_target_mappings) + - [Next action](#next-action-19) + - [New control state](#new-control-state-19) + - [UPDATE_TARGET_MAPPINGS_WAIT_FOR_TASK](#update_target_mappings_wait_for_task) + - [Next action](#next-action-20) + - [New control state](#new-control-state-20) + - [MARK_VERSION_INDEX_READY_CONFLICT](#mark_version_index_ready_conflict) + - [Next action](#next-action-21) + - [New control state](#new-control-state-21) +- [Manual QA Test Plan](#manual-qa-test-plan) + - [1. Legacy pre-migration](#1-legacy-pre-migration) + - [2. Plugins enabled/disabled](#2-plugins-enableddisabled) + - [Test scenario 1 (enable a plugin after migration):](#test-scenario-1-enable-a-plugin-after-migration) + - [Test scenario 2 (disable a plugin after migration):](#test-scenario-2-disable-a-plugin-after-migration) + - [Test scenario 3 (multiple instances, enable a plugin after migration):](#test-scenario-3-multiple-instances-enable-a-plugin-after-migration) + - [Test scenario 4 (multiple instances, mixed plugin enabled configs):](#test-scenario-4-multiple-instances-mixed-plugin-enabled-configs) + +# Introduction +In the past, the risk of downtime caused by Kibana's saved object upgrade +migrations have discouraged users from adopting the latest features. v2 +migrations aims to solve this problem by minimizing the operational impact on +our users. + +To achieve this it uses a new migration algorithm where every step of the +algorithm is idempotent. No matter at which step a Kibana instance gets +interrupted, it can always restart the migration from the beginning and repeat +all the steps without requiring any user intervention. This doesn't mean +migrations will never fail, but when they fail for intermittent reasons like +an Elasticsearch cluster running out of heap, Kibana will automatically be +able to successfully complete the migration once the cluster has enough heap. + +For more background information on the problem see the [saved object +migrations +RFC](https://github.com/elastic/kibana/blob/main/rfcs/text/0013_saved_object_migrations.md). + +# Algorithm steps +The design goals for the algorithm was to keep downtime below 10 minutes for +100k saved objects while guaranteeing no data loss and keeping steps as simple +and explicit as possible. + +The algorithm is implemented as a state-action machine based on https://www.microsoft.com/en-us/research/uploads/prod/2016/12/Computation-and-State-Machines.pdf + +The state-action machine defines it's behaviour in steps. Each step is a +transition from a control state s_i to the contral state s_i+1 caused by an +action a_i. -Migrations are the mechanism by which saved object indices are kept up to date with the Kibana system. Plugin authors write their plugins to work with a certain set of mappings, and documents of a certain shape. Migrations ensure that the index actually conforms to those expectations. - -## Migrating the index - -When Kibana boots, prior to serving any requests, it performs a check to see if the kibana index needs to be migrated. - -- If there are out of date docs, or mapping changes, or the current index is not aliased, the index is migrated. -- If the Kibana index does not exist, it is created. - -All of this happens prior to Kibana serving any http requests. - -Here is the gist of what happens if an index migration is necessary: - -* If `.kibana` (or whatever the Kibana index is named) is not an alias, it will be converted to one: - * Reindex `.kibana` into `.kibana_1` - * Delete `.kibana` - * Create an alias `.kibana` that points to `.kibana_1` -* Create a `.kibana_2` index -* Copy all documents from `.kibana_1` into `.kibana_2`, running them through any applicable migrations -* Point the `.kibana` alias to `.kibana_2` - -## Migrating Kibana clusters - -If Kibana is being run in a cluster, migrations will be coordinated so that they only run on one Kibana instance at a time. This is done in a fairly rudimentary way. Let's say we have two Kibana instances, kibana1 and kibana2. - -* kibana1 and kibana2 both start simultaneously and detect that the index requires migration -* kibana1 begins the migration and creates index `.kibana_4` -* kibana2 tries to begin the migration, but fails with the error `.kibana_4 already exists` -* kibana2 logs that it failed to create the migration index, and instead begins polling - * Every few seconds, kibana2 instance checks the `.kibana` index to see if it is done migrating - * Once `.kibana` is determined to be up to date, the kibana2 instance continues booting - -In this example, if the `.kibana_4` index existed prior to Kibana booting, the entire migration process will fail, as all Kibana instances will assume another instance is migrating to the `.kibana_4` index. This problem is only fixable by deleting the `.kibana_4` index. - -## Import / export - -If a user attempts to import FanciPlugin 1.0 documents into a Kibana system that is running FanciPlugin 2.0, those documents will be migrated prior to being persisted in the Kibana index. If a user attempts to import documents having a migration version that is _greater_ than the current Kibana version, the documents will fail to import. - -## Validation - -It might happen that a user modifies their FanciPlugin 1.0 export file to have documents with a migrationVersion of 2.0.0. In this scenario, Kibana will store those documents as if they are up to date, even though they are not, and the result will be unknown, but probably undesirable behavior. - -Similarly, Kibana server APIs assume that they are sent up to date documents unless a document specifies a migrationVersion. This means that out-of-date callers of our APIs will send us out-of-date documents, and those documents will be accepted and stored as if they are up-to-date. - -To prevent this from happening, migration authors should _always_ write a [validation](../validation) function that throws an error if a document is not up to date, and this validation function should always be updated any time a new migration is added for the relevant document types. - -## Document ownership - -In the eyes of the migration system, only one plugin can own a saved object type, or a root-level property on a saved object. - -So, let's say we have a document that looks like this: - -```js -{ - type: 'dashboard', - attributes: { title: 'whatever' }, - securityKey: '324234234kjlke2', -} -``` - -In this document, one plugin might own the `dashboard` type, and another plugin might own the `securityKey` type. If two or more plugins define securityKey migrations `{ migrations: { securityKey: { ... } } }`, Kibana will fail to start. - -To write a migration for this document, the dashboard plugin might look something like this: - -```js -uiExports: { - migrations: { - // This is whatever value your document's "type" field is - dashboard: { - // Takes a pre 1.9.0 dashboard doc, and converts it to 1.9.0 - '1.9.0': (doc) => { - doc.attributes.title = doc.attributes.title.toUpperCase(); - return doc; - }, - - // Takes a 1.9.0 dashboard doc, and converts it to a 2.0.0 - '2.0.0': (doc) => { - doc.attributes.title = doc.attributes.title + '!!!'; - return doc; - }, - }, - }, - // ... normal uiExport stuff -} -``` - -After Kibana migrates the index, our example document would have `{ attributes: { title: 'WHATEVER!!' } }`. - -Each migration function only needs to be able to handle documents belonging to the previous version. The initial migration function (in this example, `1.9.0`) needs to be more flexible, as it may be passed documents of any pre `1.9.0` shape. - -## Disabled plugins - -If a plugin is disabled, all of its documents are retained in the Kibana index. They can be imported and exported. When the plugin is re-enabled, Kibana will migrate any out of date documents that were imported or retained while it was disabled. - -## Configuration - -Kibana index migrations expose a few config settings which might be tweaked: - -* `migrations.scrollDuration` - The - [scroll](https://www.elastic.co/guide/en/elasticsearch/reference/current/search-request-scroll.html#scroll-search-context) - value used to read batches of documents from the source index. Defaults to - `15m`. -* `migrations.batchSize` - The number of documents to read / transform / write - at a time during index migrations -* `migrations.pollInterval` - How often, in milliseconds, secondary Kibana - instances will poll to see if the primary Kibana instance has finished - migrating the index. -* `migrations.skip` - Skip running migrations on startup (defaults to false). - This should only be used for running integration tests without a running - elasticsearch cluster. Note: even though migrations won't run on startup, - individual docs will still be migrated when read from ES. - -## Example - -To illustrate how migrations work, let's walk through an example, using a fictional plugin: `FanciPlugin`. - -FanciPlugin 1.0 had a mapping that looked like this: - -```js -{ - fanci: { - properties: { - fanciName: { type: 'keyword' }, - }, - }, -} -``` - -But in 2.0, it was decided that `fanciName` should be renamed to `title`. - -So, FanciPlugin 2.0 has a mapping that looks like this: - -```js -{ - fanci: { - properties: { - title: { type: 'keyword' }, - }, - }, -} -``` - -Note, the `fanciName` property is gone altogether. The problem is that lots of people have used FanciPlugin 1.0, and there are lots of documents out in the wild that have the `fanciName` property. FanciPlugin 2.0 won't know how to handle these documents, as it now expects that property to be called `title`. - -To solve this problem, the FanciPlugin authors write a migration which will take all 1.0 documents and transform them into 2.0 documents. - -FanciPlugin's uiExports is modified to have a migrations section that looks like this: - -```js -uiExports: { - migrations: { - // This is whatever value your document's "type" field is - fanci: { - // This is the version of the plugin for which this migration was written, and - // should follow semver conventions. Here, doc is a pre 2.0.0 document which this - // function will modify to have the shape we expect in 2.0.0 - '2.0.0': (doc) => { - const { fanciName } = doc.attributes; - - delete doc.attributes.fanciName; - doc.attributes.title = fanciName; - - return doc; - }, - }, - }, - // ... normal uiExport stuff -} ``` - -Now, whenever Kibana boots, if FanciPlugin is enabled, Kibana scans its index for any documents that have type 'fanci' and have a `migrationVersion.fanci` property that is anything other than `2.0.0`. If any such documents are found, the index is determined to be out of date (or at least of the wrong version), and Kibana attempts to migrate the index. - -At the end of the migration, Kibana's fanci documents will look something like this: - -```js -{ - id: 'someid', - type: 'fanci', - attributes: { - title: 'Shazm!', - }, - migrationVersion: { fanci: '2.0.0' }, -} +s_i -> a_i -> s_i+1 +s_i+1 -> a_i+1 -> s_i+2 ``` -Note, the migrationVersion property has been added, and it contains information about what migrations were applied to the document. - -## Source code - -The migrations source code is grouped into two folders: - -* `core` - Contains index-agnostic, general migration logic, which could be reused for indices other than `.kibana` -* `kibana` - Contains a relatively light-weight wrapper around core, which provides `.kibana` index-specific logic - -Generally, the code eschews classes in favor of functions and basic data structures. The publicly exported code is all class-based, however, in an attempt to conform to Kibana norms. - -### Core - -There are three core entry points. - -* index_migrator - Logic for migrating an index -* document_migrator - Logic for migrating an individual document, used by index_migrator, but also by the saved object client to migrate docs during document creation -* build_active_mappings - Logic to convert mapping properties into a full index mapping object, including the core properties required by any saved object index - -## Testing - -Run Jest tests: - -Documentation: https://www.elastic.co/guide/en/kibana/current/development-tests.html#_unit_testing +Given a control state s1, `next(s1)` returns the next action to execute. +Actions are asynchronous, once the action resolves, we can use the action +response to determine the next state to transition to as defined by the +function `model(state, response)`. +We can then loosely define a step as: ``` -yarn test:jest src/core/server/saved_objects/migrations --watch +s_i+1 = model(s_i, await next(s_i)()) ``` -Run integration tests: +When there are no more actions returned by `next` the state-action machine +terminates such as in the DONE and FATAL control states. + +What follows is a list of all control states. For each control state the +following is described: + - _next action_: the next action triggered by the current control state + - _new control state_: based on the action response, the possible new control states that the machine will transition to + +Since the algorithm runs once for each saved object index the steps below +always reference a single saved object index `.kibana`. When Kibana starts up, +all the steps are also repeated for the `.kibana_task_manager` index but this +is left out of the description for brevity. + +## INIT +### Next action +`fetchIndices` + +Fetch the saved object indices, mappings and aliases to find the source index +and determine whether we’re migrating from a legacy index or a v1 migrations +index. + +### New control state +1. If `.kibana` and the version specific aliases both exists and are pointing +to the same index. This version's migration has already been completed. Since +the same version could have plugins enabled at any time that would introduce +new transforms or mappings. + → `OUTDATED_DOCUMENTS_SEARCH` + +2. If `.kibana` is pointing to an index that belongs to a later version of +Kibana .e.g. a 7.11.0 instance found the `.kibana` alias pointing to +`.kibana_7.12.0_001` fail the migration + → `FATAL` + +3. If the `.kibana` alias exists we’re migrating from either a v1 or v2 index +and the migration source index is the index the `.kibana` alias points to. + → `WAIT_FOR_YELLOW_SOURCE` + +4. If `.kibana` is a concrete index, we’re migrating from a legacy index + → `LEGACY_SET_WRITE_BLOCK` + +5. If there are no `.kibana` indices, this is a fresh deployment. Initialize a + new saved objects index + → `CREATE_NEW_TARGET` + +## CREATE_NEW_TARGET +### Next action +`createIndex` + +Create the target index. This operation is idempotent, if the index already exist, we wait until its status turns yellow + +### New control state + → `MARK_VERSION_INDEX_READY` + +## LEGACY_SET_WRITE_BLOCK +### Next action +`setWriteBlock` + +Set a write block on the legacy index to prevent any older Kibana instances +from writing to the index while the migration is in progress which could cause +lost acknowledged writes. + +This is the first of a series of `LEGACY_*` control states that will: + - reindex the concrete legacy `.kibana` index into a `.kibana_pre6.5.0_001` index + - delete the concrete `.kibana` _index_ so that we're able to create a `.kibana` _alias_ + +### New control state +1. If the write block was successfully added + → `LEGACY_CREATE_REINDEX_TARGET` +2. 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. + → `LEGACY_CREATE_REINDEX_TARGET` + +## LEGACY_CREATE_REINDEX_TARGET +### Next action +`createIndex` + +Create a new `.kibana_pre6.5.0_001` index into which we can reindex the legacy +index. (Since the task manager index was converted from a data index into a +saved objects index in 7.4 it will be reindexed into `.kibana_pre7.4.0_001`) +### New control state + → `LEGACY_REINDEX` + +## LEGACY_REINDEX +### Next action +`reindex` + +Let Elasticsearch reindex the legacy index into `.kibana_pre6.5.0_001`. (For +the task manager index we specify a `preMigrationScript` to convert the +original task manager documents into valid saved objects) +### New control state + → `LEGACY_REINDEX_WAIT_FOR_TASK` + + +## LEGACY_REINDEX_WAIT_FOR_TASK +### Next action +`waitForReindexTask` + +Wait for up to 60s for the reindex task to complete. +### New control state +1. If the reindex task completed + → `LEGACY_DELETE` +2. If the reindex task failed with a `target_index_had_write_block` or + `index_not_found_exception` another instance already completed this step + → `LEGACY_DELETE` +3. If the reindex task is still in progress + → `LEGACY_REINDEX_WAIT_FOR_TASK` + +## LEGACY_DELETE +### Next action +`updateAliases` + +Use the updateAliases API to atomically remove the legacy index and create a +new `.kibana` alias that points to `.kibana_pre6.5.0_001`. +### New control state +1. If the action succeeds + → `SET_SOURCE_WRITE_BLOCK` +2. If the action fails with `remove_index_not_a_concrete_index` or + `index_not_found_exception` another instance has already completed this step. + → `SET_SOURCE_WRITE_BLOCK` + +## WAIT_FOR_YELLOW_SOURCE +### Next action +`waitForIndexStatusYellow` + +Wait for the Elasticsearch cluster to be in "yellow" state. It means the index's primary shard is allocated and the index is ready for searching/indexing documents, but ES wasn't able to allocate the replicas. +We don't have as much data redundancy as we could have, but it's enough to start the migration. + +### New control state + → `SET_SOURCE_WRITE_BLOCK` + +## SET_SOURCE_WRITE_BLOCK +### Next action +`setWriteBlock` + +Set a write block on the source index to prevent any older Kibana instances from writing to the index while the migration is in progress which could cause lost acknowledged writes. + +### New control state + → `CREATE_REINDEX_TEMP` + +## CREATE_REINDEX_TEMP +### Next action +`createIndex` + +This operation is idempotent, if the index already exist, we wait until its status turns yellow. + +- Because we will be transforming documents before writing them into this index, we can already set the mappings to the target mappings for this version. The source index might contain documents belonging to a disabled plugin. So set `dynamic: false` mappings for any unknown saved object types. +- (Since we never query the temporary index we can potentially disable refresh to speed up indexing performance. Profile to see if gains justify complexity) + +### New control state + → `REINDEX_SOURCE_TO_TEMP_OPEN_PIT` + +## REINDEX_SOURCE_TO_TEMP_OPEN_PIT +### Next action +`openPIT` + +Open a PIT. Since there is a write block on the source index there is basically no overhead to keeping the PIT so we can lean towards a larger `keep_alive` value like 10 minutes. +### New control state + → `REINDEX_SOURCE_TO_TEMP_READ` + +## REINDEX_SOURCE_TO_TEMP_READ +### Next action +`readNextBatchOfSourceDocuments` + +Read the next batch of outdated documents from the source index by using search after with our PIT. + +### New control state +1. If the batch contained > 0 documents + → `REINDEX_SOURCE_TO_TEMP_TRANSFORM` +2. If there are no more documents returned + → `REINDEX_SOURCE_TO_TEMP_CLOSE_PIT` + +## REINDEX_SOURCE_TO_TEMP_TRANSFORM +### Next action +`transformRawDocs` + +Transform the current batch of documents + +In order to support sharing saved objects to multiple spaces in 8.0, the +transforms will also regenerate document `_id`'s. To ensure that this step +remains idempotent, the new `_id` is deterministically generated using UUIDv5 +ensuring that each Kibana instance generates the same new `_id` for the same document. +### New control state + → `REINDEX_SOURCE_TO_TEMP_INDEX_BULK` +## REINDEX_SOURCE_TO_TEMP_INDEX_BULK +### Next action +`bulkIndexTransformedDocuments` + +Use the bulk API create action to write a batch of up-to-date documents. The +create action ensures that there will be only one write per reindexed document +even if multiple Kibana instances are performing this step. Use +`refresh=false` to speed up the create actions, the `UPDATE_TARGET_MAPPINGS` +step will ensure that the index is refreshed before we start serving traffic. + +The following errors are ignored because it means another instance already +completed this step: + - documents already exist in the temp index + - temp index has a write block + - temp index is not found +### New control state +1. If `currentBatch` is the last batch in `transformedDocBatches` + → `REINDEX_SOURCE_TO_TEMP_READ` +2. If there are more batches left in `transformedDocBatches` + → `REINDEX_SOURCE_TO_TEMP_INDEX_BULK` + +## REINDEX_SOURCE_TO_TEMP_CLOSE_PIT +### Next action +`closePIT` + +### New control state + → `SET_TEMP_WRITE_BLOCK` + +## SET_TEMP_WRITE_BLOCK +### Next action +`setWriteBlock` + +Set a write block on the temporary index so that we can clone it. +### New control state + → `CLONE_TEMP_TO_TARGET` + +## CLONE_TEMP_TO_TARGET +### Next action +`cloneIndex` + +Ask elasticsearch to clone the temporary index into the target index. If the target index already exists (because another node already started the clone operation), wait until the clone is complete by waiting for a yellow index status. + +We can’t use the temporary index as our target index because one instance can complete the migration, delete a document, and then a second instance starts the reindex operation and re-creates the deleted document. By cloning the temporary index and only accepting writes/deletes from the cloned target index, we prevent lost acknowledged deletes. + +### New control state + → `OUTDATED_DOCUMENTS_SEARCH` + +## OUTDATED_DOCUMENTS_SEARCH +### Next action +`searchForOutdatedDocuments` + +Search for outdated saved object documents. Will return one batch of +documents. + +If another instance has a disabled plugin it will reindex that plugin's +documents without transforming them. Because this instance doesn't know which +plugins were disabled by the instance that performed the +`REINDEX_SOURCE_TO_TEMP_TRANSFORM` step, we need to search for outdated documents +and transform them to ensure that everything is up to date. + +### New control state +1. Found outdated documents? + → `OUTDATED_DOCUMENTS_TRANSFORM` +2. All documents up to date + → `UPDATE_TARGET_MAPPINGS` + +## OUTDATED_DOCUMENTS_TRANSFORM +### Next action +`transformRawDocs` + `bulkOverwriteTransformedDocuments` + +Once transformed we use an index operation to overwrite the outdated document with the up-to-date version. Optimistic concurrency control ensures that we only overwrite the document once so that any updates/writes by another instance which already completed the migration aren’t overwritten and lost. + +### New control state + → `OUTDATED_DOCUMENTS_SEARCH` + +## UPDATE_TARGET_MAPPINGS +### Next action +`updateAndPickupMappings` + +If another instance has some plugins disabled it will disable the mappings of that plugin's types when creating the temporary index. This action will +update the mappings and then use an update_by_query to ensure that all fields are “picked-up” and ready to be searched over. + +### New control state + → `UPDATE_TARGET_MAPPINGS_WAIT_FOR_TASK` + +## UPDATE_TARGET_MAPPINGS_WAIT_FOR_TASK +### Next action +`updateAliases` + +Atomically apply the `versionIndexReadyActions` using the _alias actions API. By performing the following actions we guarantee that if multiple versions of Kibana started the upgrade in parallel, only one version will succeed. + +1. verify that the current alias is still pointing to the source index +2. Point the version alias and the current alias to the target index. +3. Remove the temporary index + +### New control state +1. If all the actions succeed we’re ready to serve traffic + → `DONE` +2. If action (1) fails with alias_not_found_exception or action (3) fails with index_not_found_exception another instance already completed the migration + → `MARK_VERSION_INDEX_READY_CONFLICT` + +## MARK_VERSION_INDEX_READY_CONFLICT +### Next action +`fetchIndices` + +Fetch the saved object indices + +### New control state +If another instance completed a migration from the same source we need to verify that it is running the same version. + +1. If the current and version aliases are pointing to the same index the instance that completed the migration was on the same version and it’s safe to start serving traffic. + → `DONE` +2. If the other instance was running a different version we fail the migration. Once we restart one of two things can happen: the other instance is an older version and we will restart the migration, or, it’s a newer version and we will refuse to start up. + → `FATAL` + +# Manual QA Test Plan +## 1. Legacy pre-migration +When upgrading from a legacy index additional steps are required before the +regular migration process can start. + +We have the following potential legacy indices: + - v5.x index that wasn't upgraded -> kibana should refuse to start the migration + - v5.x index that was upgraded to v6.x: `.kibana-6` _index_ with `.kibana` _alias_ + - < v6.5 `.kibana` _index_ (Saved Object Migrations were + introduced in v6.5 https://github.com/elastic/kibana/pull/20243) + - TODO: Test versions which introduced the `kibana_index_template` template? + - < v7.4 `.kibana_task_manager` _index_ (Task Manager started + using Saved Objects in v7.4 https://github.com/elastic/kibana/pull/39829) + +Test plan: +1. Ensure that the different versions of Kibana listed above can successfully + upgrade to 7.11. +2. Ensure that multiple Kibana nodes can migrate a legacy index in parallel + (choose a representative legacy version to test with e.g. v6.4). Add a lot + of Saved Objects to Kibana to increase the time it takes for a migration to + complete which will make it easier to introduce failures. + 1. If all instances are started in parallel the upgrade should succeed + 2. If nodes are randomly restarted shortly after they start participating + in the migration the upgrade should either succeed or never complete. + However, if a fatal error occurs it should never result in permanent + failure. + 1. Start one instance, wait 500 ms + 2. Start a second instance + 3. If an instance starts a saved object migration, wait X ms before + killing the process and restarting the migration. + 4. Keep decreasing X until migrations are barely able to complete. + 5. If a migration fails with a fatal error, start a Kibana that doesn't + get restarted. Given enough time, it should always be able to + successfully complete the migration. + +For a successful migration the following behaviour should be observed: + 1. The `.kibana` index should be reindexed into a `.kibana_pre6.5.0` index + 2. The `.kibana` index should be deleted + 3. The `.kibana_index_template` should be deleted + 4. The `.kibana_pre6.5.0` index should have a write block applied + 5. Documents from `.kibana_pre6.5.0` should be migrated into `.kibana_7.11.0_001` + 6. Once migration has completed, the `.kibana_current` and `.kibana_7.11.0` + aliases should point to the `.kibana_7.11.0_001` index. + +## 2. Plugins enabled/disabled +Kibana plugins can be disabled/enabled at any point in time. We need to ensure +that Saved Object documents are migrated for all the possible sequences of +enabling, disabling, before or after a version upgrade. + +### Test scenario 1 (enable a plugin after migration): +1. Start an old version of Kibana (< 7.11) +2. Create a document that we know will be migrated in a later version (i.e. + create a `dashboard`) +3. Disable the plugin to which the document belongs (i.e `dashboard` plugin) +4. Upgrade Kibana to v7.11 making sure the plugin in step (3) is still disabled. +5. Enable the plugin from step (3) +6. Restart Kibana +7. Ensure that the document from step (2) has been migrated + (`migrationVersion` contains 7.11.0) + +### Test scenario 2 (disable a plugin after migration): +1. Start an old version of Kibana (< 7.11) +2. Create a document that we know will be migrated in a later version (i.e. + create a `dashboard`) +3. Upgrade Kibana to v7.11 making sure the plugin in step (3) is enabled. +4. Disable the plugin to which the document belongs (i.e `dashboard` plugin) +6. Restart Kibana +7. Ensure that Kibana logs a warning, but continues to start even though there + are saved object documents which don't belong to an enable plugin + +### Test scenario 3 (multiple instances, enable a plugin after migration): +Follow the steps from 'Test scenario 1', but perform the migration with +multiple instances of Kibana + +### Test scenario 4 (multiple instances, mixed plugin enabled configs): +We don't support this upgrade scenario, but it's worth making sure we don't +have data loss when there's a user error. +1. Start an old version of Kibana (< 7.11) +2. Create a document that we know will be migrated in a later version (i.e. + create a `dashboard`) +3. Disable the plugin to which the document belongs (i.e `dashboard` plugin) +4. Upgrade Kibana to v7.11 using multiple instances of Kibana. The plugin from + step (3) should be enabled on half of the instances and disabled on the + other half. +5. Ensure that the document from step (2) has been migrated + (`migrationVersion` contains 7.11.0) -``` -node scripts/functional_tests_server -node scripts/functional_test_runner --config test/api_integration/config.js --grep migration -``` diff --git a/src/core/server/saved_objects/migrations/kibana/__snapshots__/kibana_migrator.test.ts.snap b/src/core/server/saved_objects/migrations/__snapshots__/kibana_migrator.test.ts.snap similarity index 100% rename from src/core/server/saved_objects/migrations/kibana/__snapshots__/kibana_migrator.test.ts.snap rename to src/core/server/saved_objects/migrations/__snapshots__/kibana_migrator.test.ts.snap diff --git a/src/core/server/saved_objects/migrationsv2/__snapshots__/migrations_state_action_machine.test.ts.snap b/src/core/server/saved_objects/migrations/__snapshots__/migrations_state_action_machine.test.ts.snap similarity index 90% rename from src/core/server/saved_objects/migrationsv2/__snapshots__/migrations_state_action_machine.test.ts.snap rename to src/core/server/saved_objects/migrations/__snapshots__/migrations_state_action_machine.test.ts.snap index c3512d8fd50bd..760a83fa65cf1 100644 --- a/src/core/server/saved_objects/migrationsv2/__snapshots__/migrations_state_action_machine.test.ts.snap +++ b/src/core/server/saved_objects/migrations/__snapshots__/migrations_state_action_machine.test.ts.snap @@ -84,11 +84,26 @@ Object { "type": "file-upload-telemetry", }, }, + Object { + "term": Object { + "type": "fleet-agent-actions", + }, + }, Object { "term": Object { "type": "fleet-agent-events", }, }, + Object { + "term": Object { + "type": "fleet-agents", + }, + }, + Object { + "term": Object { + "type": "fleet-enrollment-api-keys", + }, + }, Object { "term": Object { "type": "ml-telemetry", @@ -225,11 +240,26 @@ Object { "type": "file-upload-telemetry", }, }, + Object { + "term": Object { + "type": "fleet-agent-actions", + }, + }, Object { "term": Object { "type": "fleet-agent-events", }, }, + Object { + "term": Object { + "type": "fleet-agents", + }, + }, + Object { + "term": Object { + "type": "fleet-enrollment-api-keys", + }, + }, Object { "term": Object { "type": "ml-telemetry", @@ -370,11 +400,26 @@ Object { "type": "file-upload-telemetry", }, }, + Object { + "term": Object { + "type": "fleet-agent-actions", + }, + }, Object { "term": Object { "type": "fleet-agent-events", }, }, + Object { + "term": Object { + "type": "fleet-agents", + }, + }, + Object { + "term": Object { + "type": "fleet-enrollment-api-keys", + }, + }, Object { "term": Object { "type": "ml-telemetry", @@ -519,11 +564,26 @@ Object { "type": "file-upload-telemetry", }, }, + Object { + "term": Object { + "type": "fleet-agent-actions", + }, + }, Object { "term": Object { "type": "fleet-agent-events", }, }, + Object { + "term": Object { + "type": "fleet-agents", + }, + }, + Object { + "term": Object { + "type": "fleet-enrollment-api-keys", + }, + }, Object { "term": Object { "type": "ml-telemetry", @@ -705,11 +765,26 @@ Object { "type": "file-upload-telemetry", }, }, + Object { + "term": Object { + "type": "fleet-agent-actions", + }, + }, Object { "term": Object { "type": "fleet-agent-events", }, }, + Object { + "term": Object { + "type": "fleet-agents", + }, + }, + Object { + "term": Object { + "type": "fleet-enrollment-api-keys", + }, + }, Object { "term": Object { "type": "ml-telemetry", @@ -857,11 +932,26 @@ Object { "type": "file-upload-telemetry", }, }, + Object { + "term": Object { + "type": "fleet-agent-actions", + }, + }, Object { "term": Object { "type": "fleet-agent-events", }, }, + Object { + "term": Object { + "type": "fleet-agents", + }, + }, + Object { + "term": Object { + "type": "fleet-enrollment-api-keys", + }, + }, Object { "term": Object { "type": "ml-telemetry", diff --git a/src/core/server/saved_objects/migrationsv2/actions/bulk_overwrite_transformed_documents.test.ts b/src/core/server/saved_objects/migrations/actions/bulk_overwrite_transformed_documents.test.ts similarity index 100% rename from src/core/server/saved_objects/migrationsv2/actions/bulk_overwrite_transformed_documents.test.ts rename to src/core/server/saved_objects/migrations/actions/bulk_overwrite_transformed_documents.test.ts diff --git a/src/core/server/saved_objects/migrationsv2/actions/bulk_overwrite_transformed_documents.ts b/src/core/server/saved_objects/migrations/actions/bulk_overwrite_transformed_documents.ts similarity index 100% rename from src/core/server/saved_objects/migrationsv2/actions/bulk_overwrite_transformed_documents.ts rename to src/core/server/saved_objects/migrations/actions/bulk_overwrite_transformed_documents.ts diff --git a/src/core/server/saved_objects/migrationsv2/actions/calculate_exclude_filters.test.ts b/src/core/server/saved_objects/migrations/actions/calculate_exclude_filters.test.ts similarity index 100% rename from src/core/server/saved_objects/migrationsv2/actions/calculate_exclude_filters.test.ts rename to src/core/server/saved_objects/migrations/actions/calculate_exclude_filters.test.ts diff --git a/src/core/server/saved_objects/migrationsv2/actions/calculate_exclude_filters.ts b/src/core/server/saved_objects/migrations/actions/calculate_exclude_filters.ts similarity index 100% rename from src/core/server/saved_objects/migrationsv2/actions/calculate_exclude_filters.ts rename to src/core/server/saved_objects/migrations/actions/calculate_exclude_filters.ts diff --git a/src/core/server/saved_objects/migrationsv2/actions/catch_retryable_es_client_errors.test.ts b/src/core/server/saved_objects/migrations/actions/catch_retryable_es_client_errors.test.ts similarity index 100% rename from src/core/server/saved_objects/migrationsv2/actions/catch_retryable_es_client_errors.test.ts rename to src/core/server/saved_objects/migrations/actions/catch_retryable_es_client_errors.test.ts diff --git a/src/core/server/saved_objects/migrationsv2/actions/catch_retryable_es_client_errors.ts b/src/core/server/saved_objects/migrations/actions/catch_retryable_es_client_errors.ts similarity index 100% rename from src/core/server/saved_objects/migrationsv2/actions/catch_retryable_es_client_errors.ts rename to src/core/server/saved_objects/migrations/actions/catch_retryable_es_client_errors.ts diff --git a/src/core/server/saved_objects/migrationsv2/actions/check_for_unknown_docs.test.ts b/src/core/server/saved_objects/migrations/actions/check_for_unknown_docs.test.ts similarity index 100% rename from src/core/server/saved_objects/migrationsv2/actions/check_for_unknown_docs.test.ts rename to src/core/server/saved_objects/migrations/actions/check_for_unknown_docs.test.ts diff --git a/src/core/server/saved_objects/migrationsv2/actions/check_for_unknown_docs.ts b/src/core/server/saved_objects/migrations/actions/check_for_unknown_docs.ts similarity index 100% rename from src/core/server/saved_objects/migrationsv2/actions/check_for_unknown_docs.ts rename to src/core/server/saved_objects/migrations/actions/check_for_unknown_docs.ts diff --git a/src/core/server/saved_objects/migrationsv2/actions/clone_index.test.ts b/src/core/server/saved_objects/migrations/actions/clone_index.test.ts similarity index 100% rename from src/core/server/saved_objects/migrationsv2/actions/clone_index.test.ts rename to src/core/server/saved_objects/migrations/actions/clone_index.test.ts diff --git a/src/core/server/saved_objects/migrationsv2/actions/clone_index.ts b/src/core/server/saved_objects/migrations/actions/clone_index.ts similarity index 96% rename from src/core/server/saved_objects/migrationsv2/actions/clone_index.ts rename to src/core/server/saved_objects/migrations/actions/clone_index.ts index 5674535c80328..d7994f5a465d2 100644 --- a/src/core/server/saved_objects/migrationsv2/actions/clone_index.ts +++ b/src/core/server/saved_objects/migrations/actions/clone_index.ts @@ -127,11 +127,11 @@ export const cloneIndex = ({ // If the cluster state was updated and all shards ackd we're done return TaskEither.right(res); } 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, index: target, timeout }), TaskEither.map((value) => { - /** 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 { acknowledged: true, shardsAcknowledged: true }; }) ); diff --git a/src/core/server/saved_objects/migrationsv2/actions/close_pit.test.ts b/src/core/server/saved_objects/migrations/actions/close_pit.test.ts similarity index 100% rename from src/core/server/saved_objects/migrationsv2/actions/close_pit.test.ts rename to src/core/server/saved_objects/migrations/actions/close_pit.test.ts diff --git a/src/core/server/saved_objects/migrationsv2/actions/close_pit.ts b/src/core/server/saved_objects/migrations/actions/close_pit.ts similarity index 100% rename from src/core/server/saved_objects/migrationsv2/actions/close_pit.ts rename to src/core/server/saved_objects/migrations/actions/close_pit.ts diff --git a/src/core/server/saved_objects/migrationsv2/actions/constants.ts b/src/core/server/saved_objects/migrations/actions/constants.ts similarity index 100% rename from src/core/server/saved_objects/migrationsv2/actions/constants.ts rename to src/core/server/saved_objects/migrations/actions/constants.ts diff --git a/src/core/server/saved_objects/migrationsv2/actions/create_index.test.ts b/src/core/server/saved_objects/migrations/actions/create_index.test.ts similarity index 100% rename from src/core/server/saved_objects/migrationsv2/actions/create_index.test.ts rename to src/core/server/saved_objects/migrations/actions/create_index.test.ts diff --git a/src/core/server/saved_objects/migrationsv2/actions/create_index.ts b/src/core/server/saved_objects/migrations/actions/create_index.ts similarity index 100% rename from src/core/server/saved_objects/migrationsv2/actions/create_index.ts rename to src/core/server/saved_objects/migrations/actions/create_index.ts diff --git a/src/core/server/saved_objects/migrationsv2/actions/es_errors.test.ts b/src/core/server/saved_objects/migrations/actions/es_errors.test.ts similarity index 100% rename from src/core/server/saved_objects/migrationsv2/actions/es_errors.test.ts rename to src/core/server/saved_objects/migrations/actions/es_errors.test.ts diff --git a/src/core/server/saved_objects/migrationsv2/actions/es_errors.ts b/src/core/server/saved_objects/migrations/actions/es_errors.ts similarity index 100% rename from src/core/server/saved_objects/migrationsv2/actions/es_errors.ts rename to src/core/server/saved_objects/migrations/actions/es_errors.ts diff --git a/src/core/server/saved_objects/migrationsv2/actions/fetch_indices.test.ts b/src/core/server/saved_objects/migrations/actions/fetch_indices.test.ts similarity index 100% rename from src/core/server/saved_objects/migrationsv2/actions/fetch_indices.test.ts rename to src/core/server/saved_objects/migrations/actions/fetch_indices.test.ts diff --git a/src/core/server/saved_objects/migrationsv2/actions/fetch_indices.ts b/src/core/server/saved_objects/migrations/actions/fetch_indices.ts similarity index 100% rename from src/core/server/saved_objects/migrationsv2/actions/fetch_indices.ts rename to src/core/server/saved_objects/migrations/actions/fetch_indices.ts diff --git a/src/core/server/saved_objects/migrationsv2/actions/index.ts b/src/core/server/saved_objects/migrations/actions/index.ts similarity index 100% rename from src/core/server/saved_objects/migrationsv2/actions/index.ts rename to src/core/server/saved_objects/migrations/actions/index.ts diff --git a/src/core/server/saved_objects/migrationsv2/actions/integration_tests/actions.test.ts b/src/core/server/saved_objects/migrations/actions/integration_tests/actions.test.ts similarity index 99% rename from src/core/server/saved_objects/migrationsv2/actions/integration_tests/actions.test.ts rename to src/core/server/saved_objects/migrations/actions/integration_tests/actions.test.ts index 8cf49440f76b8..1b6a668fe57fd 100644 --- a/src/core/server/saved_objects/migrationsv2/actions/integration_tests/actions.test.ts +++ b/src/core/server/saved_objects/migrations/actions/integration_tests/actions.test.ts @@ -39,7 +39,7 @@ import { import * as Either from 'fp-ts/lib/Either'; import * as Option from 'fp-ts/lib/Option'; import { errors } from '@elastic/elasticsearch'; -import { DocumentsTransformFailed, DocumentsTransformSuccess } from '../../../migrations/core'; +import { DocumentsTransformFailed, DocumentsTransformSuccess } from '../../core'; import { TaskEither } from 'fp-ts/lib/TaskEither'; import Path from 'path'; @@ -292,7 +292,8 @@ describe('migration actions', () => { }); }); - describe('cloneIndex', () => { + // FAILED ES PROMOTION: https://github.com/elastic/kibana/issues/117856 + describe.skip('cloneIndex', () => { afterAll(async () => { try { await client.indices.delete({ index: 'clone_*' }); @@ -409,14 +410,15 @@ describe('migration actions', () => { timeout: '0s', })(); - await expect(cloneIndexPromise).resolves.toMatchObject({ - _tag: 'Left', - left: { - error: expect.any(errors.ResponseError), - message: expect.stringMatching(/\"timed_out\":true/), - type: 'retryable_es_client_error', - }, - }); + await expect(cloneIndexPromise).resolves.toMatchInlineSnapshot(` + Object { + "_tag": "Left", + "left": Object { + "message": "Timeout waiting for the status of the [clone_red_index] index to become 'yellow'", + "type": "retryable_es_client_error", + }, + } + `); }); }); diff --git a/src/core/server/saved_objects/migrationsv2/actions/integration_tests/archives/7.7.2_xpack_100k_obj.zip b/src/core/server/saved_objects/migrations/actions/integration_tests/archives/7.7.2_xpack_100k_obj.zip similarity index 100% rename from src/core/server/saved_objects/migrationsv2/actions/integration_tests/archives/7.7.2_xpack_100k_obj.zip rename to src/core/server/saved_objects/migrations/actions/integration_tests/archives/7.7.2_xpack_100k_obj.zip diff --git a/src/core/server/saved_objects/migrationsv2/actions/integration_tests/es_errors.test.ts b/src/core/server/saved_objects/migrations/actions/integration_tests/es_errors.test.ts similarity index 100% rename from src/core/server/saved_objects/migrationsv2/actions/integration_tests/es_errors.test.ts rename to src/core/server/saved_objects/migrations/actions/integration_tests/es_errors.test.ts diff --git a/src/core/server/saved_objects/migrationsv2/actions/open_pit.test.ts b/src/core/server/saved_objects/migrations/actions/open_pit.test.ts similarity index 100% rename from src/core/server/saved_objects/migrationsv2/actions/open_pit.test.ts rename to src/core/server/saved_objects/migrations/actions/open_pit.test.ts diff --git a/src/core/server/saved_objects/migrationsv2/actions/open_pit.ts b/src/core/server/saved_objects/migrations/actions/open_pit.ts similarity index 100% rename from src/core/server/saved_objects/migrationsv2/actions/open_pit.ts rename to src/core/server/saved_objects/migrations/actions/open_pit.ts diff --git a/src/core/server/saved_objects/migrationsv2/actions/pickup_updated_mappings.test.ts b/src/core/server/saved_objects/migrations/actions/pickup_updated_mappings.test.ts similarity index 100% rename from src/core/server/saved_objects/migrationsv2/actions/pickup_updated_mappings.test.ts rename to src/core/server/saved_objects/migrations/actions/pickup_updated_mappings.test.ts diff --git a/src/core/server/saved_objects/migrationsv2/actions/pickup_updated_mappings.ts b/src/core/server/saved_objects/migrations/actions/pickup_updated_mappings.ts similarity index 100% rename from src/core/server/saved_objects/migrationsv2/actions/pickup_updated_mappings.ts rename to src/core/server/saved_objects/migrations/actions/pickup_updated_mappings.ts diff --git a/src/core/server/saved_objects/migrationsv2/actions/read_with_pit.test.ts b/src/core/server/saved_objects/migrations/actions/read_with_pit.test.ts similarity index 100% rename from src/core/server/saved_objects/migrationsv2/actions/read_with_pit.test.ts rename to src/core/server/saved_objects/migrations/actions/read_with_pit.test.ts diff --git a/src/core/server/saved_objects/migrationsv2/actions/read_with_pit.ts b/src/core/server/saved_objects/migrations/actions/read_with_pit.ts similarity index 100% rename from src/core/server/saved_objects/migrationsv2/actions/read_with_pit.ts rename to src/core/server/saved_objects/migrations/actions/read_with_pit.ts diff --git a/src/core/server/saved_objects/migrationsv2/actions/refresh_index.test.ts b/src/core/server/saved_objects/migrations/actions/refresh_index.test.ts similarity index 100% rename from src/core/server/saved_objects/migrationsv2/actions/refresh_index.test.ts rename to src/core/server/saved_objects/migrations/actions/refresh_index.test.ts diff --git a/src/core/server/saved_objects/migrationsv2/actions/refresh_index.ts b/src/core/server/saved_objects/migrations/actions/refresh_index.ts similarity index 100% rename from src/core/server/saved_objects/migrationsv2/actions/refresh_index.ts rename to src/core/server/saved_objects/migrations/actions/refresh_index.ts diff --git a/src/core/server/saved_objects/migrationsv2/actions/reindex.test.ts b/src/core/server/saved_objects/migrations/actions/reindex.test.ts similarity index 100% rename from src/core/server/saved_objects/migrationsv2/actions/reindex.test.ts rename to src/core/server/saved_objects/migrations/actions/reindex.test.ts diff --git a/src/core/server/saved_objects/migrationsv2/actions/reindex.ts b/src/core/server/saved_objects/migrations/actions/reindex.ts similarity index 100% rename from src/core/server/saved_objects/migrationsv2/actions/reindex.ts rename to src/core/server/saved_objects/migrations/actions/reindex.ts diff --git a/src/core/server/saved_objects/migrationsv2/actions/remove_write_block.test.ts b/src/core/server/saved_objects/migrations/actions/remove_write_block.test.ts similarity index 100% rename from src/core/server/saved_objects/migrationsv2/actions/remove_write_block.test.ts rename to src/core/server/saved_objects/migrations/actions/remove_write_block.test.ts diff --git a/src/core/server/saved_objects/migrationsv2/actions/remove_write_block.ts b/src/core/server/saved_objects/migrations/actions/remove_write_block.ts similarity index 100% rename from src/core/server/saved_objects/migrationsv2/actions/remove_write_block.ts rename to src/core/server/saved_objects/migrations/actions/remove_write_block.ts diff --git a/src/core/server/saved_objects/migrationsv2/actions/search_for_outdated_documents.test.ts b/src/core/server/saved_objects/migrations/actions/search_for_outdated_documents.test.ts similarity index 100% rename from src/core/server/saved_objects/migrationsv2/actions/search_for_outdated_documents.test.ts rename to src/core/server/saved_objects/migrations/actions/search_for_outdated_documents.test.ts diff --git a/src/core/server/saved_objects/migrationsv2/actions/search_for_outdated_documents.ts b/src/core/server/saved_objects/migrations/actions/search_for_outdated_documents.ts similarity index 100% rename from src/core/server/saved_objects/migrationsv2/actions/search_for_outdated_documents.ts rename to src/core/server/saved_objects/migrations/actions/search_for_outdated_documents.ts diff --git a/src/core/server/saved_objects/migrationsv2/actions/set_write_block.test.ts b/src/core/server/saved_objects/migrations/actions/set_write_block.test.ts similarity index 100% rename from src/core/server/saved_objects/migrationsv2/actions/set_write_block.test.ts rename to src/core/server/saved_objects/migrations/actions/set_write_block.test.ts diff --git a/src/core/server/saved_objects/migrationsv2/actions/set_write_block.ts b/src/core/server/saved_objects/migrations/actions/set_write_block.ts similarity index 100% rename from src/core/server/saved_objects/migrationsv2/actions/set_write_block.ts rename to src/core/server/saved_objects/migrations/actions/set_write_block.ts diff --git a/src/core/server/saved_objects/migrationsv2/actions/transform_docs.ts b/src/core/server/saved_objects/migrations/actions/transform_docs.ts similarity index 100% rename from src/core/server/saved_objects/migrationsv2/actions/transform_docs.ts rename to src/core/server/saved_objects/migrations/actions/transform_docs.ts diff --git a/src/core/server/saved_objects/migrationsv2/actions/update_aliases.test.ts b/src/core/server/saved_objects/migrations/actions/update_aliases.test.ts similarity index 100% rename from src/core/server/saved_objects/migrationsv2/actions/update_aliases.test.ts rename to src/core/server/saved_objects/migrations/actions/update_aliases.test.ts diff --git a/src/core/server/saved_objects/migrationsv2/actions/update_aliases.ts b/src/core/server/saved_objects/migrations/actions/update_aliases.ts similarity index 100% rename from src/core/server/saved_objects/migrationsv2/actions/update_aliases.ts rename to src/core/server/saved_objects/migrations/actions/update_aliases.ts diff --git a/src/core/server/saved_objects/migrationsv2/actions/update_and_pickup_mappings.test.ts b/src/core/server/saved_objects/migrations/actions/update_and_pickup_mappings.test.ts similarity index 100% rename from src/core/server/saved_objects/migrationsv2/actions/update_and_pickup_mappings.test.ts rename to src/core/server/saved_objects/migrations/actions/update_and_pickup_mappings.test.ts diff --git a/src/core/server/saved_objects/migrationsv2/actions/update_and_pickup_mappings.ts b/src/core/server/saved_objects/migrations/actions/update_and_pickup_mappings.ts similarity index 100% rename from src/core/server/saved_objects/migrationsv2/actions/update_and_pickup_mappings.ts rename to src/core/server/saved_objects/migrations/actions/update_and_pickup_mappings.ts diff --git a/src/core/server/saved_objects/migrationsv2/actions/verify_reindex.ts b/src/core/server/saved_objects/migrations/actions/verify_reindex.ts similarity index 100% rename from src/core/server/saved_objects/migrationsv2/actions/verify_reindex.ts rename to src/core/server/saved_objects/migrations/actions/verify_reindex.ts diff --git a/src/core/server/saved_objects/migrationsv2/actions/wait_for_index_status_yellow.test.ts b/src/core/server/saved_objects/migrations/actions/wait_for_index_status_yellow.test.ts similarity index 100% rename from src/core/server/saved_objects/migrationsv2/actions/wait_for_index_status_yellow.test.ts rename to src/core/server/saved_objects/migrations/actions/wait_for_index_status_yellow.test.ts diff --git a/src/core/server/saved_objects/migrationsv2/actions/wait_for_index_status_yellow.ts b/src/core/server/saved_objects/migrations/actions/wait_for_index_status_yellow.ts similarity index 82% rename from src/core/server/saved_objects/migrationsv2/actions/wait_for_index_status_yellow.ts rename to src/core/server/saved_objects/migrations/actions/wait_for_index_status_yellow.ts index 2880dfaff0d48..9d4df0ced8c0b 100644 --- a/src/core/server/saved_objects/migrationsv2/actions/wait_for_index_status_yellow.ts +++ b/src/core/server/saved_objects/migrations/actions/wait_for_index_status_yellow.ts @@ -40,8 +40,18 @@ export const waitForIndexStatusYellow = }: WaitForIndexStatusYellowParams): TaskEither.TaskEither => () => { return client.cluster - .health({ index, wait_for_status: 'yellow', timeout }) - .then(() => { + .health({ + index, + wait_for_status: 'yellow', + timeout, + }) + .then((res) => { + if (res.body.timed_out === true) { + return Either.left({ + type: 'retryable_es_client_error' as const, + message: `Timeout waiting for the status of the [${index}] index to become 'yellow'`, + }); + } return Either.right({}); }) .catch(catchRetryableEsClientErrors); diff --git a/src/core/server/saved_objects/migrationsv2/actions/wait_for_pickup_updated_mappings_task.test.ts b/src/core/server/saved_objects/migrations/actions/wait_for_pickup_updated_mappings_task.test.ts similarity index 100% rename from src/core/server/saved_objects/migrationsv2/actions/wait_for_pickup_updated_mappings_task.test.ts rename to src/core/server/saved_objects/migrations/actions/wait_for_pickup_updated_mappings_task.test.ts diff --git a/src/core/server/saved_objects/migrationsv2/actions/wait_for_pickup_updated_mappings_task.ts b/src/core/server/saved_objects/migrations/actions/wait_for_pickup_updated_mappings_task.ts similarity index 100% rename from src/core/server/saved_objects/migrationsv2/actions/wait_for_pickup_updated_mappings_task.ts rename to src/core/server/saved_objects/migrations/actions/wait_for_pickup_updated_mappings_task.ts diff --git a/src/core/server/saved_objects/migrationsv2/actions/wait_for_reindex_task.test.ts b/src/core/server/saved_objects/migrations/actions/wait_for_reindex_task.test.ts similarity index 100% rename from src/core/server/saved_objects/migrationsv2/actions/wait_for_reindex_task.test.ts rename to src/core/server/saved_objects/migrations/actions/wait_for_reindex_task.test.ts diff --git a/src/core/server/saved_objects/migrationsv2/actions/wait_for_reindex_task.ts b/src/core/server/saved_objects/migrations/actions/wait_for_reindex_task.ts similarity index 100% rename from src/core/server/saved_objects/migrationsv2/actions/wait_for_reindex_task.ts rename to src/core/server/saved_objects/migrations/actions/wait_for_reindex_task.ts diff --git a/src/core/server/saved_objects/migrationsv2/actions/wait_for_task.test.ts b/src/core/server/saved_objects/migrations/actions/wait_for_task.test.ts similarity index 100% rename from src/core/server/saved_objects/migrationsv2/actions/wait_for_task.test.ts rename to src/core/server/saved_objects/migrations/actions/wait_for_task.test.ts diff --git a/src/core/server/saved_objects/migrationsv2/actions/wait_for_task.ts b/src/core/server/saved_objects/migrations/actions/wait_for_task.ts similarity index 100% rename from src/core/server/saved_objects/migrationsv2/actions/wait_for_task.ts rename to src/core/server/saved_objects/migrations/actions/wait_for_task.ts diff --git a/src/core/server/saved_objects/migrations/core/__snapshots__/elastic_index.test.ts.snap b/src/core/server/saved_objects/migrations/core/__snapshots__/elastic_index.test.ts.snap deleted file mode 100644 index 6bd567be204d0..0000000000000 --- a/src/core/server/saved_objects/migrations/core/__snapshots__/elastic_index.test.ts.snap +++ /dev/null @@ -1,40 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`ElasticIndex write writes documents in bulk to the index 1`] = ` -Array [ - Object { - "body": Array [ - Object { - "index": Object { - "_id": "niceguy:fredrogers", - "_index": ".myalias", - }, - }, - Object { - "niceguy": Object { - "aka": "Mr Rogers", - }, - "quotes": Array [ - "The greatest gift you ever give is your honest self.", - ], - "type": "niceguy", - }, - Object { - "index": Object { - "_id": "badguy:rickygervais", - "_index": ".myalias", - }, - }, - Object { - "badguy": Object { - "aka": "Dominic Badguy", - }, - "migrationVersion": Object { - "badguy": "2.3.4", - }, - "type": "badguy", - }, - ], - }, -] -`; diff --git a/src/core/server/saved_objects/migrations/core/call_cluster.ts b/src/core/server/saved_objects/migrations/core/call_cluster.ts deleted file mode 100644 index 156689c8d96f9..0000000000000 --- a/src/core/server/saved_objects/migrations/core/call_cluster.ts +++ /dev/null @@ -1,28 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -/** - * This file is nothing more than type signatures for the subset of - * elasticsearch.js that migrations use. There is no actual logic / - * funcationality contained here. - */ - -import * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; - -export type AliasAction = - | { - remove_index: { index: string }; - } - | { remove: { index: string; alias: string } } - | { add: { index: string; alias: string } }; - -export interface RawDoc { - _id: estypes.Id; - _source: any; - _type?: string; -} diff --git a/src/core/server/saved_objects/migrations/core/disable_unknown_type_mapping_fields.test.ts b/src/core/server/saved_objects/migrations/core/disable_unknown_type_mapping_fields.test.ts new file mode 100644 index 0000000000000..1cf77069e1e4d --- /dev/null +++ b/src/core/server/saved_objects/migrations/core/disable_unknown_type_mapping_fields.test.ts @@ -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 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 { disableUnknownTypeMappingFields } from './disable_unknown_type_mapping_fields'; + +describe('disableUnknownTypeMappingFields', () => { + const sourceMappings = { + _meta: { + migrationMappingPropertyHashes: { + unknown_type: 'md5hash', + unknown_core_field: 'md5hash', + known_type: 'oldmd5hash', + }, + }, + properties: { + unknown_type: { + properties: { + unused_field: { type: 'text' }, + }, + }, + unknown_core_field: { type: 'keyword' }, + known_type: { + properties: { + field_1: { type: 'text' }, + old_field: { type: 'boolean' }, + }, + }, + }, + } as const; + const activeMappings = { + _meta: { + migrationMappingPropertyHashes: { + known_type: 'md5hash', + }, + }, + properties: { + known_type: { + properties: { + new_field: { type: 'binary' }, + field_1: { type: 'keyword' }, + }, + }, + }, + } as const; + const targetMappings = disableUnknownTypeMappingFields(activeMappings, sourceMappings); + + it('disables complex field mappings from unknown types in the source mappings', () => { + expect(targetMappings.properties.unknown_type).toEqual({ dynamic: false, properties: {} }); + }); + + it('retains unknown core field mappings from the source mappings', () => { + expect(targetMappings.properties.unknown_core_field).toEqual({ type: 'keyword' }); + }); + + it('overrides source mappings with known types from active mappings', () => { + expect(targetMappings.properties.known_type).toEqual({ + properties: { + new_field: { type: 'binary' }, + field_1: { type: 'keyword' }, // was type text in source mappings + // old_field was present in source but omitted in active mappings + }, + }); + }); + + it('retains the active mappings _meta ignoring any _meta fields in the source mappings', () => { + expect(targetMappings._meta).toEqual({ + migrationMappingPropertyHashes: { + known_type: 'md5hash', + }, + }); + }); + + it('does not fail if the source mapping does not have `properties` defined', () => { + const missingPropertiesMappings = { + ...sourceMappings, + properties: undefined, + }; + const result = disableUnknownTypeMappingFields( + activeMappings, + // @ts-expect-error `properties` should not be undefined + missingPropertiesMappings + ); + + expect(Object.keys(result.properties)).toEqual(['known_type']); + }); +}); diff --git a/src/core/server/saved_objects/migrations/core/disable_unknown_type_mapping_fields.ts b/src/core/server/saved_objects/migrations/core/disable_unknown_type_mapping_fields.ts new file mode 100644 index 0000000000000..d11e3a40df8d8 --- /dev/null +++ b/src/core/server/saved_objects/migrations/core/disable_unknown_type_mapping_fields.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 { SavedObjectsMappingProperties, IndexMapping } from '../../mappings'; + +/** + * Merges the active mappings and the source mappings while disabling the + * fields of any unknown Saved Object types present in the source index's + * mappings. + * + * Since the Saved Objects index has `dynamic: strict` defined at the + * top-level, only Saved Object types for which a mapping exists can be + * inserted into the index. To ensure that we can continue to store Saved + * Object documents belonging to a disabled plugin we define a mapping for all + * the unknown Saved Object types that were present in the source index's + * mappings. To limit the field count as much as possible, these unkwnown + * type's mappings are set to `dynamic: false`. + * + * (Since we're using the source index mappings instead of looking at actual + * document types in the inedx, we potentially add more "unknown types" than + * what would be necessary to support migrating all the data over to the + * target index.) + * + * @param activeMappings The mappings compiled from all the Saved Object types + * known to this Kibana node. + * @param sourceMappings The mappings of index used as the migration source. + * @returns The mappings that should be applied to the target index. + */ +export function disableUnknownTypeMappingFields( + activeMappings: IndexMapping, + sourceMappings: IndexMapping +): IndexMapping { + const targetTypes = Object.keys(activeMappings.properties); + + const disabledTypesProperties = Object.keys(sourceMappings.properties ?? {}) + .filter((sourceType) => { + const isObjectType = 'properties' in sourceMappings.properties[sourceType]; + // Only Object/Nested datatypes can be excluded from the field count by + // using `dynamic: false`. + return !targetTypes.includes(sourceType) && isObjectType; + }) + .reduce((disabledTypesAcc, sourceType) => { + disabledTypesAcc[sourceType] = { dynamic: false, properties: {} }; + return disabledTypesAcc; + }, {} as SavedObjectsMappingProperties); + + return { + ...activeMappings, + properties: { + ...sourceMappings.properties, + ...disabledTypesProperties, + ...activeMappings.properties, + }, + }; +} diff --git a/src/core/server/saved_objects/migrations/core/elastic_index.test.ts b/src/core/server/saved_objects/migrations/core/elastic_index.test.ts deleted file mode 100644 index 2cdeb479f50f9..0000000000000 --- a/src/core/server/saved_objects/migrations/core/elastic_index.test.ts +++ /dev/null @@ -1,702 +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 estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; -import _ from 'lodash'; -import { elasticsearchClientMock } from '../../../elasticsearch/client/mocks'; -import * as Index from './elastic_index'; - -describe('ElasticIndex', () => { - let client: ReturnType; - - beforeEach(() => { - client = elasticsearchClientMock.createElasticsearchClient(); - }); - describe('fetchInfo', () => { - test('it handles 404', async () => { - client.indices.get.mockResolvedValue( - elasticsearchClientMock.createSuccessTransportRequestPromise({}, { statusCode: 404 }) - ); - - const info = await Index.fetchInfo(client, '.kibana-test'); - expect(info).toEqual({ - aliases: {}, - exists: false, - indexName: '.kibana-test', - mappings: { dynamic: 'strict', properties: {} }, - }); - - expect(client.indices.get).toHaveBeenCalledWith({ index: '.kibana-test' }, { ignore: [404] }); - }); - - test('decorates index info with exists and indexName', async () => { - client.indices.get.mockImplementation((params) => { - const index = params!.index as string; - return elasticsearchClientMock.createSuccessTransportRequestPromise({ - [index]: { - aliases: { foo: index }, - mappings: { dynamic: 'strict', properties: { a: 'b' } as any }, - settings: {}, - }, - } as estypes.IndicesGetResponse); - }); - - const info = await Index.fetchInfo(client, '.baz'); - expect(info).toEqual({ - aliases: { foo: '.baz' }, - mappings: { dynamic: 'strict', properties: { a: 'b' } }, - exists: true, - indexName: '.baz', - settings: {}, - }); - }); - }); - - describe('createIndex', () => { - test('calls indices.create', async () => { - await Index.createIndex(client, '.abcd', { foo: 'bar' } as any); - - expect(client.indices.create).toHaveBeenCalledTimes(1); - expect(client.indices.create).toHaveBeenCalledWith({ - body: { - mappings: { foo: 'bar' }, - settings: { - auto_expand_replicas: '0-1', - number_of_shards: 1, - }, - }, - index: '.abcd', - }); - }); - }); - - describe('claimAlias', () => { - test('handles unaliased indices', async () => { - client.indices.getAlias.mockResolvedValue( - elasticsearchClientMock.createSuccessTransportRequestPromise({}, { statusCode: 404 }) - ); - - await Index.claimAlias(client, '.hola-42', '.hola'); - - expect(client.indices.getAlias).toHaveBeenCalledWith( - { - name: '.hola', - }, - { ignore: [404] } - ); - expect(client.indices.updateAliases).toHaveBeenCalledWith({ - body: { - actions: [{ add: { index: '.hola-42', alias: '.hola' } }], - }, - }); - expect(client.indices.refresh).toHaveBeenCalledWith({ - index: '.hola-42', - }); - }); - - test('removes existing alias', async () => { - client.indices.getAlias.mockResolvedValue( - elasticsearchClientMock.createSuccessTransportRequestPromise({ - '.my-fanci-index': { aliases: { '.muchacha': {} } }, - }) - ); - - await Index.claimAlias(client, '.ze-index', '.muchacha'); - - expect(client.indices.getAlias).toHaveBeenCalledTimes(1); - expect(client.indices.updateAliases).toHaveBeenCalledWith({ - body: { - actions: [ - { remove: { index: '.my-fanci-index', alias: '.muchacha' } }, - { add: { index: '.ze-index', alias: '.muchacha' } }, - ], - }, - }); - expect(client.indices.refresh).toHaveBeenCalledWith({ - index: '.ze-index', - }); - }); - - test('allows custom alias actions', async () => { - client.indices.getAlias.mockResolvedValue( - elasticsearchClientMock.createSuccessTransportRequestPromise({ - '.my-fanci-index': { aliases: { '.muchacha': {} } }, - }) - ); - - await Index.claimAlias(client, '.ze-index', '.muchacha', [ - { remove_index: { index: 'awww-snap!' } }, - ]); - - expect(client.indices.getAlias).toHaveBeenCalledTimes(1); - expect(client.indices.updateAliases).toHaveBeenCalledWith({ - body: { - actions: [ - { remove_index: { index: 'awww-snap!' } }, - { remove: { index: '.my-fanci-index', alias: '.muchacha' } }, - { add: { index: '.ze-index', alias: '.muchacha' } }, - ], - }, - }); - expect(client.indices.refresh).toHaveBeenCalledWith({ - index: '.ze-index', - }); - }); - }); - - describe('convertToAlias', () => { - test('it creates the destination index, then reindexes to it', async () => { - client.indices.getAlias.mockResolvedValue( - elasticsearchClientMock.createSuccessTransportRequestPromise({ - '.my-fanci-index': { aliases: { '.muchacha': {} } }, - }) - ); - client.reindex.mockResolvedValue( - elasticsearchClientMock.createSuccessTransportRequestPromise({ - task: 'abc', - } as estypes.ReindexResponse) - ); - client.tasks.get.mockResolvedValue( - elasticsearchClientMock.createSuccessTransportRequestPromise({ - completed: true, - } as estypes.TasksGetResponse) - ); - - const info = { - aliases: {}, - exists: true, - indexName: '.ze-index', - mappings: { - dynamic: 'strict' as const, - properties: { foo: { type: 'keyword' } }, - }, - } as const; - - await Index.convertToAlias( - client, - info, - '.muchacha', - 10, - `ctx._id = ctx._source.type + ':' + ctx._id` - ); - - expect(client.indices.create).toHaveBeenCalledWith({ - body: { - mappings: { - dynamic: 'strict', - properties: { foo: { type: 'keyword' } }, - }, - settings: { auto_expand_replicas: '0-1', number_of_shards: 1 }, - }, - index: '.ze-index', - }); - - expect(client.reindex).toHaveBeenCalledWith({ - body: { - dest: { index: '.ze-index' }, - source: { index: '.muchacha', size: 10 }, - script: { - source: `ctx._id = ctx._source.type + ':' + ctx._id`, - lang: 'painless', - }, - }, - refresh: true, - wait_for_completion: false, - }); - - expect(client.tasks.get).toHaveBeenCalledWith({ - task_id: 'abc', - }); - - expect(client.indices.updateAliases).toHaveBeenCalledWith({ - body: { - actions: [ - { remove_index: { index: '.muchacha' } }, - { remove: { alias: '.muchacha', index: '.my-fanci-index' } }, - { add: { index: '.ze-index', alias: '.muchacha' } }, - ], - }, - }); - - expect(client.indices.refresh).toHaveBeenCalledWith({ - index: '.ze-index', - }); - }); - - test('throws error if re-index task fails', async () => { - client.indices.getAlias.mockResolvedValue( - elasticsearchClientMock.createSuccessTransportRequestPromise({ - '.my-fanci-index': { aliases: { '.muchacha': {} } }, - }) - ); - client.reindex.mockResolvedValue( - elasticsearchClientMock.createSuccessTransportRequestPromise({ - task: 'abc', - } as estypes.ReindexResponse) - ); - client.tasks.get.mockResolvedValue( - // @ts-expect-error @elastic/elasticsearch GetTaskResponse requires a `task` property even on errors - elasticsearchClientMock.createSuccessTransportRequestPromise({ - completed: true, - error: { - type: 'search_phase_execution_exception', - reason: 'all shards failed', - failed_shards: [], - }, - } as estypes.TasksGetResponse) - ); - - const info = { - aliases: {}, - exists: true, - indexName: '.ze-index', - mappings: { - dynamic: 'strict', - properties: { foo: { type: 'keyword' } }, - }, - }; - - // @ts-expect-error - await expect(Index.convertToAlias(client, info, '.muchacha', 10)).rejects.toThrow( - /Re-index failed \[search_phase_execution_exception\] all shards failed/ - ); - - expect(client.indices.create).toHaveBeenCalledWith({ - body: { - mappings: { - dynamic: 'strict', - properties: { foo: { type: 'keyword' } }, - }, - settings: { auto_expand_replicas: '0-1', number_of_shards: 1 }, - }, - index: '.ze-index', - }); - - expect(client.reindex).toHaveBeenCalledWith({ - body: { - dest: { index: '.ze-index' }, - source: { index: '.muchacha', size: 10 }, - }, - refresh: true, - wait_for_completion: false, - }); - - expect(client.tasks.get).toHaveBeenCalledWith({ - task_id: 'abc', - }); - }); - }); - - describe('write', () => { - test('writes documents in bulk to the index', async () => { - client.bulk.mockResolvedValue( - elasticsearchClientMock.createSuccessTransportRequestPromise({ - items: [] as any[], - } as estypes.BulkResponse) - ); - - const index = '.myalias'; - const docs = [ - { - _id: 'niceguy:fredrogers', - _source: { - type: 'niceguy', - niceguy: { - aka: 'Mr Rogers', - }, - quotes: ['The greatest gift you ever give is your honest self.'], - }, - }, - { - _id: 'badguy:rickygervais', - _source: { - type: 'badguy', - badguy: { - aka: 'Dominic Badguy', - }, - migrationVersion: { badguy: '2.3.4' }, - }, - }, - ]; - - await Index.write(client, index, docs); - - expect(client.bulk).toHaveBeenCalled(); - expect(client.bulk.mock.calls[0]).toMatchSnapshot(); - }); - - test('fails if any document fails', async () => { - client.bulk.mockResolvedValue( - elasticsearchClientMock.createSuccessTransportRequestPromise({ - items: [{ index: { error: { type: 'shazm', reason: 'dern' } } }], - } as estypes.BulkResponse) - ); - - const index = '.myalias'; - const docs = [ - { - _id: 'niceguy:fredrogers', - _source: { - type: 'niceguy', - niceguy: { - aka: 'Mr Rogers', - }, - }, - }, - ]; - - await expect(Index.write(client as any, index, docs)).rejects.toThrow(/dern/); - expect(client.bulk).toHaveBeenCalledTimes(1); - }); - }); - - describe('reader', () => { - test('returns docs in batches', async () => { - const index = '.myalias'; - const batch1 = [ - { - _id: 'such:1', - _source: { type: 'such', such: { num: 1 } }, - }, - ]; - const batch2 = [ - { - _id: 'aaa:2', - _source: { type: 'aaa', aaa: { num: 2 } }, - }, - { - _id: 'bbb:3', - _source: { - bbb: { num: 3 }, - migrationVersion: { bbb: '3.2.5' }, - type: 'bbb', - }, - }, - ]; - - client.search = jest.fn().mockReturnValue( - elasticsearchClientMock.createSuccessTransportRequestPromise({ - _scroll_id: 'x', - _shards: { success: 1, total: 1 }, - hits: { hits: _.cloneDeep(batch1) }, - }) - ); - client.scroll = jest - .fn() - .mockReturnValueOnce( - elasticsearchClientMock.createSuccessTransportRequestPromise({ - _scroll_id: 'y', - _shards: { success: 1, total: 1 }, - hits: { hits: _.cloneDeep(batch2) }, - }) - ) - .mockReturnValueOnce( - elasticsearchClientMock.createSuccessTransportRequestPromise({ - _scroll_id: 'z', - _shards: { success: 1, total: 1 }, - hits: { hits: [] }, - }) - ); - - const read = Index.reader(client, index, { batchSize: 100, scrollDuration: '5m' }); - - expect(await read()).toEqual(batch1); - expect(await read()).toEqual(batch2); - expect(await read()).toEqual([]); - - expect(client.search).toHaveBeenCalledWith({ - body: { - size: 100, - query: Index.excludeUnusedTypesQuery, - }, - index, - scroll: '5m', - }); - expect(client.scroll).toHaveBeenCalledWith({ - scroll: '5m', - scroll_id: 'x', - }); - expect(client.scroll).toHaveBeenCalledWith({ - scroll: '5m', - scroll_id: 'y', - }); - expect(client.clearScroll).toHaveBeenCalledWith({ - scroll_id: 'z', - }); - }); - - test('returns all root-level properties', async () => { - const index = '.myalias'; - const batch = [ - { - _id: 'such:1', - _source: { - acls: '3230a', - foos: { is: 'fun' }, - such: { num: 1 }, - type: 'such', - }, - }, - ]; - - client.search = jest.fn().mockReturnValueOnce( - elasticsearchClientMock.createSuccessTransportRequestPromise({ - _scroll_id: 'x', - _shards: { success: 1, total: 1 }, - hits: { hits: _.cloneDeep(batch) }, - }) - ); - client.scroll = jest.fn().mockReturnValueOnce( - elasticsearchClientMock.createSuccessTransportRequestPromise({ - _scroll_id: 'z', - _shards: { success: 1, total: 1 }, - hits: { hits: [] }, - }) - ); - - const read = Index.reader(client, index, { - batchSize: 100, - scrollDuration: '5m', - }); - - expect(await read()).toEqual(batch); - }); - - test('fails if not all shards were successful', async () => { - const index = '.myalias'; - - client.search = jest.fn().mockReturnValueOnce( - elasticsearchClientMock.createSuccessTransportRequestPromise({ - _shards: { successful: 1, total: 2 }, - }) - ); - - const read = Index.reader(client, index, { - batchSize: 100, - scrollDuration: '5m', - }); - - await expect(read()).rejects.toThrow(/shards failed/); - }); - - test('handles shards not being returned', async () => { - const index = '.myalias'; - const batch = [ - { - _id: 'such:1', - _source: { - acls: '3230a', - foos: { is: 'fun' }, - such: { num: 1 }, - type: 'such', - }, - }, - ]; - - client.search = jest.fn().mockReturnValueOnce( - elasticsearchClientMock.createSuccessTransportRequestPromise({ - _scroll_id: 'x', - hits: { hits: _.cloneDeep(batch) }, - }) - ); - client.scroll = jest.fn().mockReturnValueOnce( - elasticsearchClientMock.createSuccessTransportRequestPromise({ - _scroll_id: 'z', - hits: { hits: [] }, - }) - ); - - const read = Index.reader(client, index, { - batchSize: 100, - scrollDuration: '5m', - }); - - expect(await read()).toEqual(batch); - }); - }); - - describe('migrationsUpToDate', () => { - // A helper to reduce boilerplate in the hasMigration tests that follow. - async function testMigrationsUpToDate({ - index = '.myindex', - mappings, - count, - migrations, - kibanaVersion, - }: any) { - client.indices.get = jest.fn().mockReturnValueOnce( - elasticsearchClientMock.createSuccessTransportRequestPromise({ - [index]: { mappings }, - }) - ); - client.count = jest.fn().mockReturnValueOnce( - elasticsearchClientMock.createSuccessTransportRequestPromise({ - count, - _shards: { success: 1, total: 1 }, - }) - ); - - const hasMigrations = await Index.migrationsUpToDate( - client, - index, - migrations, - kibanaVersion - ); - return { hasMigrations }; - } - - test('is false if the index mappings do not contain migrationVersion', async () => { - const { hasMigrations } = await testMigrationsUpToDate({ - index: '.myalias', - mappings: { - properties: { - dashboard: { type: 'text' }, - }, - }, - count: 0, - migrations: { dashy: '2.3.4' }, - kibanaVersion: '7.10.0', - }); - - expect(hasMigrations).toBeFalsy(); - expect(client.indices.get).toHaveBeenCalledWith( - { - index: '.myalias', - }, - { - ignore: [404], - } - ); - }); - - test('is true if there are no migrations defined', async () => { - const { hasMigrations } = await testMigrationsUpToDate({ - index: '.myalias', - mappings: { - properties: { - migrationVersion: { - dynamic: 'true', - type: 'object', - }, - dashboard: { type: 'text' }, - }, - }, - count: 2, - migrations: {}, - kibanaVersion: '7.10.0', - }); - - expect(hasMigrations).toBeTruthy(); - expect(client.indices.get).toHaveBeenCalledTimes(1); - }); - - test('is true if there are no documents out of date', async () => { - const { hasMigrations } = await testMigrationsUpToDate({ - index: '.myalias', - mappings: { - properties: { - migrationVersion: { - dynamic: 'true', - type: 'object', - }, - dashboard: { type: 'text' }, - }, - }, - count: 0, - migrations: { dashy: '23.2.5' }, - }); - - expect(hasMigrations).toBeTruthy(); - expect(client.indices.get).toHaveBeenCalledTimes(1); - expect(client.count).toHaveBeenCalledTimes(1); - }); - - test('is false if there are documents out of date', async () => { - const { hasMigrations } = await testMigrationsUpToDate({ - index: '.myalias', - mappings: { - properties: { - migrationVersion: { - dynamic: 'true', - type: 'object', - }, - dashboard: { type: 'text' }, - }, - }, - count: 3, - migrations: { dashy: '23.2.5' }, - kibanaVersion: '7.10.0', - }); - - expect(hasMigrations).toBeFalsy(); - expect(client.indices.get).toHaveBeenCalledTimes(1); - expect(client.count).toHaveBeenCalledTimes(1); - }); - - test('counts docs that are out of date', async () => { - await testMigrationsUpToDate({ - index: '.myalias', - mappings: { - properties: { - migrationVersion: { - dynamic: 'true', - type: 'object', - }, - dashboard: { type: 'text' }, - }, - }, - count: 0, - migrations: { - dashy: '23.2.5', - bashy: '99.9.3', - flashy: '3.4.5', - }, - kibanaVersion: '7.10.0', - }); - - function shouldClause(type: string, version: string) { - return { - bool: { - must: [ - { exists: { field: type } }, - { - bool: { - must_not: { term: { [`migrationVersion.${type}`]: version } }, - }, - }, - ], - }, - }; - } - - expect(client.count).toHaveBeenCalledWith({ - body: { - query: { - bool: { - should: [ - shouldClause('dashy', '23.2.5'), - shouldClause('bashy', '99.9.3'), - shouldClause('flashy', '3.4.5'), - { - bool: { - must_not: { - term: { - coreMigrationVersion: '7.10.0', - }, - }, - }, - }, - ], - }, - }, - }, - index: '.myalias', - }); - }); - }); -}); diff --git a/src/core/server/saved_objects/migrations/core/elastic_index.ts b/src/core/server/saved_objects/migrations/core/elastic_index.ts deleted file mode 100644 index 64df079897722..0000000000000 --- a/src/core/server/saved_objects/migrations/core/elastic_index.ts +++ /dev/null @@ -1,425 +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. - */ - -/* - * This module contains various functions for querying and manipulating - * elasticsearch indices. - */ - -import _ from 'lodash'; -import * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; -import { MigrationEsClient } from './migration_es_client'; -import { IndexMapping } from '../../mappings'; -import { SavedObjectsMigrationVersion } from '../../types'; -import { AliasAction, RawDoc } from './call_cluster'; -import { SavedObjectsRawDocSource } from '../../serialization'; - -const settings = { number_of_shards: 1, auto_expand_replicas: '0-1' }; - -export interface FullIndexInfo { - aliases: { [name: string]: object }; - exists: boolean; - indexName: string; - mappings: IndexMapping; -} - -/** - * Types that are no longer registered and need to be removed - */ -export const REMOVED_TYPES: string[] = [ - 'apm-services-telemetry', - 'background-session', - 'cases-sub-case', - 'file-upload-telemetry', - // https://github.com/elastic/kibana/issues/91869 - 'fleet-agent-events', - // Was removed in 7.12 - 'ml-telemetry', - 'server', - // https://github.com/elastic/kibana/issues/95617 - 'tsvb-validation-telemetry', - // replaced by osquery-manager-usage-metric - 'osquery-usage-metric', - // Was removed in 7.16 - 'timelion-sheet', -].sort(); - -// When migrating from the outdated index we use a read query which excludes -// saved objects which are no longer used. These saved objects will still be -// kept in the outdated index for backup purposes, but won't be available in -// the upgraded index. -export const excludeUnusedTypesQuery: estypes.QueryDslQueryContainer = { - bool: { - must_not: [ - ...REMOVED_TYPES.map((typeName) => ({ - term: { - type: typeName, - }, - })), - // https://github.com/elastic/kibana/issues/96131 - { - bool: { - must: [ - { - match: { - type: 'search-session', - }, - }, - { - match: { - 'search-session.persisted': false, - }, - }, - ], - }, - }, - ], - }, -}; - -/** - * A slight enhancement to indices.get, that adds indexName, and validates that the - * index mappings are somewhat what we expect. - */ -export async function fetchInfo(client: MigrationEsClient, index: string): Promise { - const { body, statusCode } = await client.indices.get({ index }, { ignore: [404] }); - - if (statusCode === 404) { - return { - aliases: {}, - exists: false, - indexName: index, - mappings: { dynamic: 'strict', properties: {} }, - }; - } - - const [indexName, indexInfo] = Object.entries(body)[0]; - - // @ts-expect-error @elastic/elasticsearch IndexState.alias and IndexState.mappings should be required - return assertIsSupportedIndex({ ...indexInfo, exists: true, indexName }); -} - -/** - * Creates a reader function that serves up batches of documents from the index. We aren't using - * an async generator, as that feature currently breaks Kibana's tooling. - * - * @param client - The elastic search connection - * @param index - The index to be read from - * @param {opts} - * @prop batchSize - The number of documents to read at a time - * @prop scrollDuration - The scroll duration used for scrolling through the index - */ -export function reader( - client: MigrationEsClient, - index: string, - { batchSize = 10, scrollDuration = '15m' }: { batchSize: number; scrollDuration: string } -) { - const scroll = scrollDuration; - let scrollId: string | undefined; - - const nextBatch = () => - scrollId !== undefined - ? client.scroll({ - scroll, - scroll_id: scrollId, - }) - : client.search({ - body: { - size: batchSize, - query: excludeUnusedTypesQuery, - }, - index, - scroll, - }); - - const close = async () => scrollId && (await client.clearScroll({ scroll_id: scrollId })); - - return async function read() { - const result = await nextBatch(); - assertResponseIncludeAllShards(result.body); - - scrollId = result.body._scroll_id; - const docs = result.body.hits.hits; - if (!docs.length) { - await close(); - } - - return docs; - }; -} - -/** - * Writes the specified documents to the index, throws an exception - * if any of the documents fail to save. - */ -export async function write(client: MigrationEsClient, index: string, docs: RawDoc[]) { - const { body } = await client.bulk({ - body: docs.reduce((acc: object[], doc: RawDoc) => { - acc.push({ - index: { - _id: doc._id, - _index: index, - }, - }); - - acc.push(doc._source); - - return acc; - }, []), - }); - - const err = _.find(body.items, 'index.error.reason'); - - if (!err) { - return; - } - - const exception: any = new Error(err.index!.error!.reason); - exception.detail = err; - throw exception; -} - -/** - * Checks to see if the specified index is up to date. It does this by checking - * that the index has the expected mappings and by counting - * the number of documents that have a property which has migrations defined for it, - * but which has not had those migrations applied. We don't want to cache the - * results of this function (e.g. in context or somewhere), as it is important that - * it performs the check *each* time it is called, rather than memoizing itself, - * as this is used to determine if migrations are complete. - * - * @param client - The connection to ElasticSearch - * @param index - * @param migrationVersion - The latest versions of the migrations - */ -export async function migrationsUpToDate( - client: MigrationEsClient, - index: string, - migrationVersion: SavedObjectsMigrationVersion, - kibanaVersion: string, - retryCount: number = 10 -): Promise { - try { - const indexInfo = await fetchInfo(client, index); - - if (!indexInfo.mappings.properties?.migrationVersion) { - return false; - } - - // If no migrations are actually defined, we're up to date! - if (Object.keys(migrationVersion).length <= 0) { - return true; - } - - const { body } = await client.count({ - body: { - query: { - bool: { - should: [ - ...Object.entries(migrationVersion).map(([type, latestVersion]) => ({ - bool: { - must: [ - { exists: { field: type } }, - { - bool: { - must_not: { term: { [`migrationVersion.${type}`]: latestVersion } }, - }, - }, - ], - }, - })), - { - bool: { - must_not: { - term: { - coreMigrationVersion: kibanaVersion, - }, - }, - }, - }, - ], - }, - }, - }, - index, - }); - - assertResponseIncludeAllShards(body); - - return body.count === 0; - } catch (e) { - // retry for Service Unavailable - if (e.status !== 503 || retryCount === 0) { - throw e; - } - - await new Promise((r) => setTimeout(r, 1000)); - - return await migrationsUpToDate(client, index, migrationVersion, kibanaVersion, retryCount - 1); - } -} - -export async function createIndex( - client: MigrationEsClient, - index: string, - mappings?: IndexMapping -) { - await client.indices.create({ - body: { mappings, settings }, - index, - }); -} - -/** - * Converts an index to an alias. The `alias` parameter is the desired alias name which currently - * is a concrete index. This function will reindex `alias` into a new index, delete the `alias` - * index, and then create an alias `alias` that points to the new index. - * - * @param client - The ElasticSearch connection - * @param info - Information about the mappings and name of the new index - * @param alias - The name of the index being converted to an alias - */ -export async function convertToAlias( - client: MigrationEsClient, - info: FullIndexInfo, - alias: string, - batchSize: number, - script?: string -) { - await client.indices.create({ - body: { mappings: info.mappings, settings }, - index: info.indexName, - }); - - await reindex(client, alias, info.indexName, batchSize, script); - - await claimAlias(client, info.indexName, alias, [{ remove_index: { index: alias } }]); -} - -/** - * Points the specified alias to the specified index. This is an exclusive - * alias, meaning that it will only point to one index at a time, so we - * remove any other indices from the alias. - * - * @param {MigrationEsClient} client - * @param {string} index - * @param {string} alias - * @param {AliasAction[]} aliasActions - Optional actions to be added to the updateAliases call - */ -export async function claimAlias( - client: MigrationEsClient, - index: string, - alias: string, - aliasActions: AliasAction[] = [] -) { - const { body, statusCode } = await client.indices.getAlias({ name: alias }, { ignore: [404] }); - const aliasInfo = statusCode === 404 ? {} : body; - const removeActions = Object.keys(aliasInfo).map((key) => ({ remove: { index: key, alias } })); - - await client.indices.updateAliases({ - body: { - actions: aliasActions.concat(removeActions).concat({ add: { index, alias } }), - }, - }); - - await client.indices.refresh({ index }); -} - -/** - * This is a rough check to ensure that the index being migrated satisfies at least - * some rudimentary expectations. Past Kibana indices had multiple root documents, etc - * and the migration system does not (yet?) handle those indices. They need to be upgraded - * via v5 -> v6 upgrade tools first. This file contains index-agnostic logic, and this - * check is itself index-agnostic, though the error hint is a bit Kibana specific. - * - * @param {FullIndexInfo} indexInfo - */ -function assertIsSupportedIndex(indexInfo: FullIndexInfo) { - const mappings = indexInfo.mappings as any; - const isV7Index = !!mappings.properties; - - if (!isV7Index) { - throw new Error( - `Index ${indexInfo.indexName} belongs to a version of Kibana ` + - `that cannot be automatically migrated. Reset it or use the X-Pack upgrade assistant.` - ); - } - - return indexInfo; -} - -/** - * Provides protection against reading/re-indexing against an index with missing - * shards which could result in data loss. This shouldn't be common, as the Saved - * Object indices should only ever have a single shard. This is more to handle - * instances where customers manually expand the shards of an index. - */ -function assertResponseIncludeAllShards({ _shards }: { _shards: estypes.ShardStatistics }) { - if (!_.has(_shards, 'total') || !_.has(_shards, 'successful')) { - return; - } - - const failed = _shards.total - _shards.successful; - - if (failed > 0) { - throw new Error( - `Re-index failed :: ${failed} of ${_shards.total} shards failed. ` + - `Check Elasticsearch cluster health for more information.` - ); - } -} - -/** - * Reindexes from source to dest, polling for the reindex completion. - */ -async function reindex( - client: MigrationEsClient, - source: string, - dest: string, - batchSize: number, - script?: string -) { - // We poll instead of having the request wait for completion, as for large indices, - // the request times out on the Elasticsearch side of things. We have a relatively tight - // polling interval, as the request is fairly efficient, and we don't - // want to block index migrations for too long on this. - const pollInterval = 250; - const { body: reindexBody } = await client.reindex({ - body: { - dest: { index: dest }, - source: { index: source, size: batchSize }, - script: script - ? { - source: script, - lang: 'painless', - } - : undefined, - }, - refresh: true, - wait_for_completion: false, - }); - - const task = reindexBody.task; - - let completed = false; - - while (!completed) { - await new Promise((r) => setTimeout(r, pollInterval)); - - const { body } = await client.tasks.get({ - task_id: String(task), - }); - - const e = body.error; - if (e) { - throw new Error(`Re-index failed [${e.type}] ${e.reason} :: ${JSON.stringify(e)}`); - } - - completed = body.completed; - } -} diff --git a/src/core/server/saved_objects/migrations/core/index.ts b/src/core/server/saved_objects/migrations/core/index.ts index 84733f1bca061..0d17432a3b3d0 100644 --- a/src/core/server/saved_objects/migrations/core/index.ts +++ b/src/core/server/saved_objects/migrations/core/index.ts @@ -7,16 +7,14 @@ */ export { DocumentMigrator } from './document_migrator'; -export { IndexMigrator } from './index_migrator'; export { buildActiveMappings } from './build_active_mappings'; export type { LogFn, SavedObjectsMigrationLogger } from './migration_logger'; -export type { MigrationResult, MigrationStatus } from './migration_coordinator'; -export { createMigrationEsClient } from './migration_es_client'; -export type { MigrationEsClient } from './migration_es_client'; -export { excludeUnusedTypesQuery, REMOVED_TYPES } from './elastic_index'; +export { excludeUnusedTypesQuery, REMOVED_TYPES } from './unused_types'; export { TransformSavedObjectDocumentError } from './transform_saved_object_document_error'; export type { DocumentsTransformFailed, DocumentsTransformSuccess, TransformErrorObjects, } from './migrate_raw_docs'; +export { disableUnknownTypeMappingFields } from './disable_unknown_type_mapping_fields'; +export type { MigrationResult, MigrationStatus } from './types'; diff --git a/src/core/server/saved_objects/migrations/core/index_migrator.test.ts b/src/core/server/saved_objects/migrations/core/index_migrator.test.ts deleted file mode 100644 index beb0c1d3651c6..0000000000000 --- a/src/core/server/saved_objects/migrations/core/index_migrator.test.ts +++ /dev/null @@ -1,478 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import _ from 'lodash'; -import * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; -import { elasticsearchClientMock } from '../../../elasticsearch/client/mocks'; -import { SavedObjectUnsanitizedDoc, SavedObjectsSerializer } from '../../serialization'; -import { SavedObjectTypeRegistry } from '../../saved_objects_type_registry'; -import { IndexMigrator } from './index_migrator'; -import { MigrationOpts } from './migration_context'; -import { loggingSystemMock } from '../../../logging/logging_system.mock'; - -describe('IndexMigrator', () => { - let testOpts: jest.Mocked & { - client: ReturnType; - }; - - beforeEach(() => { - testOpts = { - batchSize: 10, - client: elasticsearchClientMock.createElasticsearchClient(), - index: '.kibana', - kibanaVersion: '7.10.0', - log: loggingSystemMock.create().get(), - setStatus: jest.fn(), - mappingProperties: {}, - pollInterval: 1, - scrollDuration: '1m', - documentMigrator: { - migrationVersion: {}, - migrate: _.identity, - migrateAndConvert: _.identity, - prepareMigrations: jest.fn(), - }, - serializer: new SavedObjectsSerializer(new SavedObjectTypeRegistry()), - }; - }); - - test('creates the index if it does not exist', async () => { - const { client } = testOpts; - - testOpts.mappingProperties = { foo: { type: 'long' } as any }; - - withIndex(client, { index: { statusCode: 404 }, alias: { statusCode: 404 } }); - - await new IndexMigrator(testOpts).migrate(); - - expect(client.indices.create).toHaveBeenCalledWith({ - body: { - mappings: { - dynamic: 'strict', - _meta: { - migrationMappingPropertyHashes: { - foo: '18c78c995965207ed3f6e7fc5c6e55fe', - migrationVersion: '4a1746014a75ade3a714e1db5763276f', - namespace: '2f4316de49999235636386fe51dc06c1', - namespaces: '2f4316de49999235636386fe51dc06c1', - originId: '2f4316de49999235636386fe51dc06c1', - references: '7997cf5a56cc02bdc9c93361bde732b0', - coreMigrationVersion: '2f4316de49999235636386fe51dc06c1', - type: '2f4316de49999235636386fe51dc06c1', - updated_at: '00da57df13e94e9d98437d13ace4bfe0', - }, - }, - properties: { - foo: { type: 'long' }, - migrationVersion: { dynamic: 'true', type: 'object' }, - namespace: { type: 'keyword' }, - namespaces: { type: 'keyword' }, - originId: { type: 'keyword' }, - type: { type: 'keyword' }, - updated_at: { type: 'date' }, - references: { - type: 'nested', - properties: { - name: { type: 'keyword' }, - type: { type: 'keyword' }, - id: { type: 'keyword' }, - }, - }, - coreMigrationVersion: { type: 'keyword' }, - }, - }, - settings: { number_of_shards: 1, auto_expand_replicas: '0-1' }, - }, - index: '.kibana_1', - }); - }); - - test('returns stats about the migration', async () => { - const { client } = testOpts; - - withIndex(client, { index: { statusCode: 404 }, alias: { statusCode: 404 } }); - - const result = await new IndexMigrator(testOpts).migrate(); - - expect(result).toMatchObject({ - destIndex: '.kibana_1', - sourceIndex: '.kibana', - status: 'migrated', - }); - }); - - test('fails if there are multiple root doc types', async () => { - const { client } = testOpts; - - withIndex(client, { - index: { - '.kibana_1': { - aliases: {}, - mappings: { - foo: { properties: {} }, - doc: { - properties: { - author: { type: 'text' }, - }, - }, - }, - }, - }, - }); - - await expect(new IndexMigrator(testOpts).migrate()).rejects.toThrow( - /use the X-Pack upgrade assistant/ - ); - }); - - test('fails if root doc type is not "doc"', async () => { - const { client } = testOpts; - - withIndex(client, { - index: { - '.kibana_1': { - aliases: {}, - mappings: { - poc: { - properties: { - author: { type: 'text' }, - }, - }, - }, - }, - }, - }); - - await expect(new IndexMigrator(testOpts).migrate()).rejects.toThrow( - /use the X-Pack upgrade assistant/ - ); - }); - - test('retains unknown core field mappings from the previous index', async () => { - const { client } = testOpts; - - testOpts.mappingProperties = { foo: { type: 'text' } as any }; - - withIndex(client, { - index: { - '.kibana_1': { - aliases: {}, - mappings: { - properties: { - unknown_core_field: { type: 'text' }, - }, - }, - }, - }, - }); - - await new IndexMigrator(testOpts).migrate(); - - expect(client.indices.create).toHaveBeenCalledWith({ - body: { - mappings: { - dynamic: 'strict', - _meta: { - migrationMappingPropertyHashes: { - foo: '625b32086eb1d1203564cf85062dd22e', - migrationVersion: '4a1746014a75ade3a714e1db5763276f', - namespace: '2f4316de49999235636386fe51dc06c1', - namespaces: '2f4316de49999235636386fe51dc06c1', - originId: '2f4316de49999235636386fe51dc06c1', - references: '7997cf5a56cc02bdc9c93361bde732b0', - coreMigrationVersion: '2f4316de49999235636386fe51dc06c1', - type: '2f4316de49999235636386fe51dc06c1', - updated_at: '00da57df13e94e9d98437d13ace4bfe0', - }, - }, - properties: { - unknown_core_field: { type: 'text' }, - foo: { type: 'text' }, - migrationVersion: { dynamic: 'true', type: 'object' }, - namespace: { type: 'keyword' }, - namespaces: { type: 'keyword' }, - originId: { type: 'keyword' }, - type: { type: 'keyword' }, - updated_at: { type: 'date' }, - references: { - type: 'nested', - properties: { - name: { type: 'keyword' }, - type: { type: 'keyword' }, - id: { type: 'keyword' }, - }, - }, - coreMigrationVersion: { type: 'keyword' }, - }, - }, - settings: { number_of_shards: 1, auto_expand_replicas: '0-1' }, - }, - index: '.kibana_2', - }); - }); - - test('disables complex field mappings from unknown types in the previous index', async () => { - const { client } = testOpts; - - testOpts.mappingProperties = { foo: { type: 'text' } as any }; - - withIndex(client, { - index: { - '.kibana_1': { - aliases: {}, - mappings: { - properties: { - unknown_complex_field: { properties: { description: { type: 'text' } } }, - }, - }, - }, - }, - }); - - await new IndexMigrator(testOpts).migrate(); - - expect(client.indices.create).toHaveBeenCalledWith({ - body: { - mappings: { - dynamic: 'strict', - _meta: { - migrationMappingPropertyHashes: { - foo: '625b32086eb1d1203564cf85062dd22e', - migrationVersion: '4a1746014a75ade3a714e1db5763276f', - namespace: '2f4316de49999235636386fe51dc06c1', - namespaces: '2f4316de49999235636386fe51dc06c1', - originId: '2f4316de49999235636386fe51dc06c1', - references: '7997cf5a56cc02bdc9c93361bde732b0', - coreMigrationVersion: '2f4316de49999235636386fe51dc06c1', - type: '2f4316de49999235636386fe51dc06c1', - updated_at: '00da57df13e94e9d98437d13ace4bfe0', - }, - }, - properties: { - unknown_complex_field: { dynamic: false, properties: {} }, - foo: { type: 'text' }, - migrationVersion: { dynamic: 'true', type: 'object' }, - namespace: { type: 'keyword' }, - namespaces: { type: 'keyword' }, - originId: { type: 'keyword' }, - type: { type: 'keyword' }, - updated_at: { type: 'date' }, - references: { - type: 'nested', - properties: { - name: { type: 'keyword' }, - type: { type: 'keyword' }, - id: { type: 'keyword' }, - }, - }, - coreMigrationVersion: { type: 'keyword' }, - }, - }, - settings: { number_of_shards: 1, auto_expand_replicas: '0-1' }, - }, - index: '.kibana_2', - }); - }); - - test('points the alias at the dest index', async () => { - const { client } = testOpts; - - withIndex(client, { index: { statusCode: 404 }, alias: { statusCode: 404 } }); - - await new IndexMigrator(testOpts).migrate(); - - expect(client.indices.create).toHaveBeenCalledWith(expect.any(Object)); - expect(client.indices.updateAliases).toHaveBeenCalledWith({ - body: { actions: [{ add: { alias: '.kibana', index: '.kibana_1' } }] }, - }); - }); - - test('removes previous indices from the alias', async () => { - const { client } = testOpts; - - testOpts.documentMigrator.migrationVersion = { - dashboard: '2.4.5', - }; - - withIndex(client, { numOutOfDate: 1 }); - - await new IndexMigrator(testOpts).migrate(); - - expect(client.indices.create).toHaveBeenCalledWith(expect.any(Object)); - expect(client.indices.updateAliases).toHaveBeenCalledWith({ - body: { - actions: [ - { remove: { alias: '.kibana', index: '.kibana_1' } }, - { add: { alias: '.kibana', index: '.kibana_2' } }, - ], - }, - }); - }); - - test('transforms all docs from the original index', async () => { - let count = 0; - const { client } = testOpts; - const migrateAndConvertDoc = jest.fn((doc: SavedObjectUnsanitizedDoc) => { - return [{ ...doc, attributes: { name: ++count } }]; - }); - - testOpts.documentMigrator = { - migrationVersion: { foo: '1.2.3' }, - migrate: jest.fn(), - migrateAndConvert: migrateAndConvertDoc, - prepareMigrations: jest.fn(), - }; - - withIndex(client, { - numOutOfDate: 1, - docs: [ - [{ _id: 'foo:1', _source: { type: 'foo', foo: { name: 'Bar' } } }], - [{ _id: 'foo:2', _source: { type: 'foo', foo: { name: 'Baz' } } }], - ], - }); - - await new IndexMigrator(testOpts).migrate(); - - expect(count).toEqual(2); - expect(migrateAndConvertDoc).toHaveBeenNthCalledWith(1, { - id: '1', - type: 'foo', - attributes: { name: 'Bar' }, - migrationVersion: {}, - references: [], - }); - expect(migrateAndConvertDoc).toHaveBeenNthCalledWith(2, { - id: '2', - type: 'foo', - attributes: { name: 'Baz' }, - migrationVersion: {}, - references: [], - }); - - expect(client.bulk).toHaveBeenCalledTimes(2); - expect(client.bulk).toHaveBeenNthCalledWith(1, { - body: [ - { index: { _id: 'foo:1', _index: '.kibana_2' } }, - { foo: { name: 1 }, type: 'foo', migrationVersion: {}, references: [] }, - ], - }); - expect(client.bulk).toHaveBeenNthCalledWith(2, { - body: [ - { index: { _id: 'foo:2', _index: '.kibana_2' } }, - { foo: { name: 2 }, type: 'foo', migrationVersion: {}, references: [] }, - ], - }); - }); - - test('rejects when the migration function throws an error', async () => { - const { client } = testOpts; - const migrateAndConvertDoc = jest.fn((doc: SavedObjectUnsanitizedDoc) => { - throw new Error('error migrating document'); - }); - - testOpts.documentMigrator = { - migrationVersion: { foo: '1.2.3' }, - migrate: jest.fn(), - migrateAndConvert: migrateAndConvertDoc, - prepareMigrations: jest.fn(), - }; - - withIndex(client, { - numOutOfDate: 1, - docs: [ - [{ _id: 'foo:1', _source: { type: 'foo', foo: { name: 'Bar' } } }], - [{ _id: 'foo:2', _source: { type: 'foo', foo: { name: 'Baz' } } }], - ], - }); - - await expect(new IndexMigrator(testOpts).migrate()).rejects.toThrowErrorMatchingInlineSnapshot( - `"error migrating document"` - ); - }); -}); - -function withIndex( - client: ReturnType, - opts: any = {} -) { - const defaultIndex = { - '.kibana_1': { - aliases: { '.kibana': {} }, - mappings: { - dynamic: 'strict', - properties: { - migrationVersion: { dynamic: 'true', type: 'object' }, - }, - }, - }, - }; - const defaultAlias = { - '.kibana_1': {}, - }; - const { numOutOfDate = 0 } = opts; - const { alias = defaultAlias } = opts; - const { index = defaultIndex } = opts; - const { docs = [] } = opts; - const searchResult = (i: number) => ({ - _scroll_id: i, - _shards: { - successful: 1, - total: 1, - }, - hits: { - hits: docs[i] || [], - }, - }); - - let scrollCallCounter = 1; - - client.indices.get.mockReturnValue( - elasticsearchClientMock.createSuccessTransportRequestPromise(index, { - statusCode: index.statusCode, - }) - ); - client.indices.getAlias.mockReturnValue( - elasticsearchClientMock.createSuccessTransportRequestPromise(alias, { - statusCode: index.statusCode, - }) - ); - client.reindex.mockReturnValue( - elasticsearchClientMock.createSuccessTransportRequestPromise({ - task: 'zeid', - _shards: { successful: 1, total: 1 }, - } as estypes.ReindexResponse) - ); - client.tasks.get.mockReturnValue( - elasticsearchClientMock.createSuccessTransportRequestPromise({ - completed: true, - } as estypes.TasksGetResponse) - ); - client.search.mockReturnValue( - elasticsearchClientMock.createSuccessTransportRequestPromise(searchResult(0) as any) - ); - client.bulk.mockReturnValue( - elasticsearchClientMock.createSuccessTransportRequestPromise({ - items: [] as any[], - } as estypes.BulkResponse) - ); - client.count.mockReturnValue( - elasticsearchClientMock.createSuccessTransportRequestPromise({ - count: numOutOfDate, - _shards: { successful: 1, total: 1 }, - } as estypes.CountResponse) - ); - // @ts-expect-error - client.scroll.mockImplementation(() => { - if (scrollCallCounter <= docs.length) { - const result = searchResult(scrollCallCounter); - scrollCallCounter++; - return elasticsearchClientMock.createSuccessTransportRequestPromise(result); - } - return elasticsearchClientMock.createSuccessTransportRequestPromise({}); - }); -} diff --git a/src/core/server/saved_objects/migrations/core/index_migrator.ts b/src/core/server/saved_objects/migrations/core/index_migrator.ts deleted file mode 100644 index 0ec6fe89de1f1..0000000000000 --- a/src/core/server/saved_objects/migrations/core/index_migrator.ts +++ /dev/null @@ -1,194 +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 { diffMappings } from './build_active_mappings'; -import * as Index from './elastic_index'; -import { migrateRawDocs } from './migrate_raw_docs'; -import { Context, migrationContext, MigrationOpts } from './migration_context'; -import { coordinateMigration, MigrationResult } from './migration_coordinator'; - -/* - * Core logic for migrating the mappings and documents in an index. - */ -export class IndexMigrator { - private opts: MigrationOpts; - - /** - * Creates an instance of IndexMigrator. - * - * @param {MigrationOpts} opts - */ - constructor(opts: MigrationOpts) { - this.opts = opts; - } - - /** - * Migrates the index, or, if another Kibana instance appears to be running the migration, - * waits for the migration to complete. - * - * @returns {Promise} - */ - public async migrate(): Promise { - const context = await migrationContext(this.opts); - - return coordinateMigration({ - log: context.log, - - pollInterval: context.pollInterval, - - setStatus: context.setStatus, - - async isMigrated() { - return !(await requiresMigration(context)); - }, - - async runMigration() { - if (await requiresMigration(context)) { - return migrateIndex(context); - } - - return { status: 'skipped' }; - }, - }); - } -} - -/** - * Determines what action the migration system needs to take (none, patch, migrate). - */ -async function requiresMigration(context: Context): Promise { - const { client, alias, documentMigrator, dest, kibanaVersion, log } = context; - - // Have all of our known migrations been run against the index? - const hasMigrations = await Index.migrationsUpToDate( - client, - alias, - documentMigrator.migrationVersion, - kibanaVersion - ); - - if (!hasMigrations) { - return true; - } - - // Is our index aliased? - const refreshedSource = await Index.fetchInfo(client, alias); - - if (!refreshedSource.aliases[alias]) { - return true; - } - - // Do the actual index mappings match our expectations? - const diffResult = diffMappings(refreshedSource.mappings, dest.mappings); - - if (diffResult) { - log.info(`Detected mapping change in "${diffResult.changedProp}"`); - - return true; - } - - return false; -} - -/** - * Performs an index migration if the source index exists, otherwise - * this simply creates the dest index with the proper mappings. - */ -async function migrateIndex(context: Context): Promise { - const startTime = Date.now(); - const { client, alias, source, dest, log } = context; - - await deleteIndexTemplates(context); - - log.info(`Creating index ${dest.indexName}.`); - - await Index.createIndex(client, dest.indexName, dest.mappings); - - await migrateSourceToDest(context); - - log.info(`Pointing alias ${alias} to ${dest.indexName}.`); - - await Index.claimAlias(client, dest.indexName, alias); - - const result: MigrationResult = { - status: 'migrated', - destIndex: dest.indexName, - sourceIndex: source.indexName, - elapsedMs: Date.now() - startTime, - }; - - log.info(`Finished in ${result.elapsedMs}ms.`); - - return result; -} - -/** - * If the obsoleteIndexTemplatePattern option is specified, this will delete any index templates - * that match it. - */ -async function deleteIndexTemplates({ client, log, obsoleteIndexTemplatePattern }: Context) { - if (!obsoleteIndexTemplatePattern) { - return; - } - - const { body: templates } = await client.cat.templates({ - format: 'json', - name: obsoleteIndexTemplatePattern, - }); - - if (!templates.length) { - return; - } - - const templateNames = templates.map((t) => t.name); - - log.info(`Removing index templates: ${templateNames}`); - - return Promise.all(templateNames.map((name) => client.indices.deleteTemplate({ name: name! }))); -} - -/** - * Moves all docs from sourceIndex to destIndex, migrating each as necessary. - * This moves documents from the concrete index, rather than the alias, to prevent - * a situation where the alias moves out from under us as we're migrating docs. - */ -async function migrateSourceToDest(context: Context) { - const { client, alias, dest, source, batchSize } = context; - const { scrollDuration, documentMigrator, log, serializer } = context; - - if (!source.exists) { - return; - } - - if (!source.aliases[alias]) { - log.info(`Reindexing ${alias} to ${source.indexName}`); - - await Index.convertToAlias(client, source, alias, batchSize, context.convertToAliasScript); - } - - const read = Index.reader(client, source.indexName, { batchSize, scrollDuration }); - - log.info(`Migrating ${source.indexName} saved objects to ${dest.indexName}`); - - while (true) { - const docs = await read(); - - if (!docs || !docs.length) { - return; - } - - log.debug(`Migrating saved objects ${docs.map((d) => d._id).join(', ')}`); - - await Index.write( - client, - dest.indexName, - // @ts-expect-error @elastic/elasticsearch _source is optional - await migrateRawDocs(serializer, documentMigrator.migrateAndConvert, docs) - ); - } -} diff --git a/src/core/server/saved_objects/migrations/core/migration_context.test.ts b/src/core/server/saved_objects/migrations/core/migration_context.test.ts deleted file mode 100644 index 0ca858c34e8ba..0000000000000 --- a/src/core/server/saved_objects/migrations/core/migration_context.test.ts +++ /dev/null @@ -1,91 +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 { disableUnknownTypeMappingFields } from './migration_context'; - -describe('disableUnknownTypeMappingFields', () => { - const sourceMappings = { - _meta: { - migrationMappingPropertyHashes: { - unknown_type: 'md5hash', - unknown_core_field: 'md5hash', - known_type: 'oldmd5hash', - }, - }, - properties: { - unknown_type: { - properties: { - unused_field: { type: 'text' }, - }, - }, - unknown_core_field: { type: 'keyword' }, - known_type: { - properties: { - field_1: { type: 'text' }, - old_field: { type: 'boolean' }, - }, - }, - }, - } as const; - const activeMappings = { - _meta: { - migrationMappingPropertyHashes: { - known_type: 'md5hash', - }, - }, - properties: { - known_type: { - properties: { - new_field: { type: 'binary' }, - field_1: { type: 'keyword' }, - }, - }, - }, - } as const; - const targetMappings = disableUnknownTypeMappingFields(activeMappings, sourceMappings); - - it('disables complex field mappings from unknown types in the source mappings', () => { - expect(targetMappings.properties.unknown_type).toEqual({ dynamic: false, properties: {} }); - }); - - it('retains unknown core field mappings from the source mappings', () => { - expect(targetMappings.properties.unknown_core_field).toEqual({ type: 'keyword' }); - }); - - it('overrides source mappings with known types from active mappings', () => { - expect(targetMappings.properties.known_type).toEqual({ - properties: { - new_field: { type: 'binary' }, - field_1: { type: 'keyword' }, // was type text in source mappings - // old_field was present in source but omitted in active mappings - }, - }); - }); - - it('retains the active mappings _meta ignoring any _meta fields in the source mappings', () => { - expect(targetMappings._meta).toEqual({ - migrationMappingPropertyHashes: { - known_type: 'md5hash', - }, - }); - }); - - it('does not fail if the source mapping does not have `properties` defined', () => { - const missingPropertiesMappings = { - ...sourceMappings, - properties: undefined, - }; - const result = disableUnknownTypeMappingFields( - activeMappings, - // @ts-expect-error `properties` should not be undefined - missingPropertiesMappings - ); - - expect(Object.keys(result.properties)).toEqual(['known_type']); - }); -}); diff --git a/src/core/server/saved_objects/migrations/core/migration_context.ts b/src/core/server/saved_objects/migrations/core/migration_context.ts deleted file mode 100644 index 96c47bcf38d0a..0000000000000 --- a/src/core/server/saved_objects/migrations/core/migration_context.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. - */ - -/** - * The MigrationOpts interface defines the minimum set of data required - * in order to properly migrate an index. MigrationContext expands this - * with computed values and values from the index being migrated, and is - * serves as a central blueprint for what migrations will end up doing. - */ - -import { Logger } from '../../../logging'; -import { MigrationEsClient } from './migration_es_client'; -import { SavedObjectsSerializer } from '../../serialization'; -import { - SavedObjectsTypeMappingDefinitions, - SavedObjectsMappingProperties, - IndexMapping, -} from '../../mappings'; -import { buildActiveMappings } from './build_active_mappings'; -import { VersionedTransformer } from './document_migrator'; -import * as Index from './elastic_index'; -import { SavedObjectsMigrationLogger, MigrationLogger } from './migration_logger'; -import { KibanaMigratorStatus } from '../kibana'; - -export interface MigrationOpts { - batchSize: number; - pollInterval: number; - scrollDuration: string; - client: MigrationEsClient; - index: string; - kibanaVersion: string; - log: Logger; - setStatus: (status: KibanaMigratorStatus) => void; - mappingProperties: SavedObjectsTypeMappingDefinitions; - documentMigrator: VersionedTransformer; - serializer: SavedObjectsSerializer; - convertToAliasScript?: string; - - /** - * If specified, templates matching the specified pattern will be removed - * prior to running migrations. For example: 'kibana_index_template*' - */ - obsoleteIndexTemplatePattern?: string; -} - -/** - * @internal - */ -export interface Context { - client: MigrationEsClient; - alias: string; - source: Index.FullIndexInfo; - dest: Index.FullIndexInfo; - documentMigrator: VersionedTransformer; - kibanaVersion: string; - log: SavedObjectsMigrationLogger; - setStatus: (status: KibanaMigratorStatus) => void; - batchSize: number; - pollInterval: number; - scrollDuration: string; - serializer: SavedObjectsSerializer; - obsoleteIndexTemplatePattern?: string; - convertToAliasScript?: string; -} - -/** - * Builds up an uber object which has all of the config options, settings, - * and various info needed to migrate the source index. - */ -export async function migrationContext(opts: MigrationOpts): Promise { - const { log, client, setStatus } = opts; - const alias = opts.index; - const source = createSourceContext(await Index.fetchInfo(client, alias), alias); - const dest = createDestContext(source, alias, opts.mappingProperties); - - return { - client, - alias, - source, - dest, - kibanaVersion: opts.kibanaVersion, - log: new MigrationLogger(log), - setStatus, - batchSize: opts.batchSize, - documentMigrator: opts.documentMigrator, - pollInterval: opts.pollInterval, - scrollDuration: opts.scrollDuration, - serializer: opts.serializer, - obsoleteIndexTemplatePattern: opts.obsoleteIndexTemplatePattern, - convertToAliasScript: opts.convertToAliasScript, - }; -} - -function createSourceContext(source: Index.FullIndexInfo, alias: string) { - if (source.exists && source.indexName === alias) { - return { - ...source, - indexName: nextIndexName(alias, alias), - }; - } - - return source; -} - -function createDestContext( - source: Index.FullIndexInfo, - alias: string, - typeMappingDefinitions: SavedObjectsTypeMappingDefinitions -): Index.FullIndexInfo { - const targetMappings = disableUnknownTypeMappingFields( - buildActiveMappings(typeMappingDefinitions), - source.mappings - ); - - return { - aliases: {}, - exists: false, - indexName: nextIndexName(source.indexName, alias), - mappings: targetMappings, - }; -} - -/** - * Merges the active mappings and the source mappings while disabling the - * fields of any unknown Saved Object types present in the source index's - * mappings. - * - * Since the Saved Objects index has `dynamic: strict` defined at the - * top-level, only Saved Object types for which a mapping exists can be - * inserted into the index. To ensure that we can continue to store Saved - * Object documents belonging to a disabled plugin we define a mapping for all - * the unknown Saved Object types that were present in the source index's - * mappings. To limit the field count as much as possible, these unkwnown - * type's mappings are set to `dynamic: false`. - * - * (Since we're using the source index mappings instead of looking at actual - * document types in the inedx, we potentially add more "unknown types" than - * what would be necessary to support migrating all the data over to the - * target index.) - * - * @param activeMappings The mappings compiled from all the Saved Object types - * known to this Kibana node. - * @param sourceMappings The mappings of index used as the migration source. - * @returns The mappings that should be applied to the target index. - */ -export function disableUnknownTypeMappingFields( - activeMappings: IndexMapping, - sourceMappings: IndexMapping -): IndexMapping { - const targetTypes = Object.keys(activeMappings.properties); - - const disabledTypesProperties = Object.keys(sourceMappings.properties ?? {}) - .filter((sourceType) => { - const isObjectType = 'properties' in sourceMappings.properties[sourceType]; - // Only Object/Nested datatypes can be excluded from the field count by - // using `dynamic: false`. - return !targetTypes.includes(sourceType) && isObjectType; - }) - .reduce((disabledTypesAcc, sourceType) => { - disabledTypesAcc[sourceType] = { dynamic: false, properties: {} }; - return disabledTypesAcc; - }, {} as SavedObjectsMappingProperties); - - return { - ...activeMappings, - properties: { - ...sourceMappings.properties, - ...disabledTypesProperties, - ...activeMappings.properties, - }, - }; -} - -/** - * Gets the next index name in a sequence, based on specified current index's info. - * We're using a numeric counter to create new indices. So, `.kibana_1`, `.kibana_2`, etc - * There are downsides to this, but it seemed like a simple enough approach. - */ -function nextIndexName(indexName: string, alias: string) { - const indexSuffix = (indexName.match(/[\d]+$/) || [])[0]; - const indexNum = parseInt(indexSuffix, 10) || 0; - return `${alias}_${indexNum + 1}`; -} diff --git a/src/core/server/saved_objects/migrations/core/migration_coordinator.test.ts b/src/core/server/saved_objects/migrations/core/migration_coordinator.test.ts deleted file mode 100644 index 63476a15d77cd..0000000000000 --- a/src/core/server/saved_objects/migrations/core/migration_coordinator.test.ts +++ /dev/null @@ -1,75 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 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 { coordinateMigration } from './migration_coordinator'; -import { createSavedObjectsMigrationLoggerMock } from '../mocks'; - -describe('coordinateMigration', () => { - const log = createSavedObjectsMigrationLoggerMock(); - - test('waits for isMigrated, if there is an index conflict', async () => { - const pollInterval = 1; - const runMigration = jest.fn(() => { - // eslint-disable-next-line no-throw-literal - throw { body: { error: { index: '.foo', type: 'resource_already_exists_exception' } } }; - }); - const isMigrated = jest.fn(); - const setStatus = jest.fn(); - - isMigrated.mockResolvedValueOnce(false).mockResolvedValueOnce(true); - - await coordinateMigration({ - log, - runMigration, - pollInterval, - isMigrated, - setStatus, - }); - - expect(runMigration).toHaveBeenCalledTimes(1); - expect(isMigrated).toHaveBeenCalledTimes(2); - const warnings = log.warning.mock.calls.filter((msg: any) => /deleting index \.foo/.test(msg)); - expect(warnings.length).toEqual(1); - }); - - test('does not poll if the runMigration succeeds', async () => { - const pollInterval = 1; - const runMigration = jest.fn(() => Promise.resolve()); - const isMigrated = jest.fn(() => Promise.resolve(true)); - const setStatus = jest.fn(); - - await coordinateMigration({ - log, - runMigration, - pollInterval, - isMigrated, - setStatus, - }); - expect(isMigrated).not.toHaveBeenCalled(); - }); - - test('does not swallow exceptions', async () => { - const pollInterval = 1; - const runMigration = jest.fn(() => { - throw new Error('Doh'); - }); - const isMigrated = jest.fn(() => Promise.resolve(true)); - const setStatus = jest.fn(); - - await expect( - coordinateMigration({ - log, - runMigration, - pollInterval, - isMigrated, - setStatus, - }) - ).rejects.toThrow(/Doh/); - expect(isMigrated).not.toHaveBeenCalled(); - }); -}); diff --git a/src/core/server/saved_objects/migrations/core/migration_coordinator.ts b/src/core/server/saved_objects/migrations/core/migration_coordinator.ts deleted file mode 100644 index 5b99f050b0ece..0000000000000 --- a/src/core/server/saved_objects/migrations/core/migration_coordinator.ts +++ /dev/null @@ -1,124 +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. - */ - -/* - * This provides a mechanism for preventing multiple Kibana instances from - * simultaneously running migrations on the same index. It synchronizes this - * by handling index creation conflicts, and putting this instance into a - * poll loop that periodically checks to see if the index is migrated. - * - * The reason we have to coordinate this, rather than letting each Kibana instance - * perform duplicate work, is that if we allowed each Kibana to simply run migrations in - * parallel, they would each try to reindex and each try to create the destination index. - * If those indices already exist, it may be due to contention between multiple Kibana - * instances (which is safe to ignore), but it may be due to a partially completed migration, - * or someone tampering with the Kibana alias. In these cases, it's not clear that we should - * just migrate data into an existing index. Such an action could result in data loss. Instead, - * we should probably fail, and the Kibana sys-admin should clean things up before relaunching - * Kibana. - */ - -import _ from 'lodash'; -import { KibanaMigratorStatus } from '../kibana'; -import { SavedObjectsMigrationLogger } from './migration_logger'; - -const DEFAULT_POLL_INTERVAL = 15000; - -export type MigrationStatus = - | 'waiting_to_start' - | 'waiting_for_other_nodes' - | 'running' - | 'completed'; - -export type MigrationResult = - | { status: 'skipped' } - | { status: 'patched' } - | { - status: 'migrated'; - destIndex: string; - sourceIndex: string; - elapsedMs: number; - }; - -interface Opts { - runMigration: () => Promise; - isMigrated: () => Promise; - setStatus: (status: KibanaMigratorStatus) => void; - log: SavedObjectsMigrationLogger; - pollInterval?: number; -} - -/** - * Runs the migration specified by opts. If the migration fails due to an index - * creation conflict, this falls into a polling loop, checking every pollInterval - * milliseconds if the index is migrated. - * - * @export - * @param {Opts} opts - * @prop {Migration} runMigration - A function that runs the index migration - * @prop {IsMigrated} isMigrated - A function which checks if the index is already migrated - * @prop {Logger} log - The migration logger - * @prop {number} pollInterval - How often, in ms, to check that the index is migrated - * @returns - */ -export async function coordinateMigration(opts: Opts): Promise { - try { - return await opts.runMigration(); - } catch (error) { - const waitingIndex = handleIndexExists(error, opts.log); - if (waitingIndex) { - opts.setStatus({ status: 'waiting_for_other_nodes', waitingIndex }); - await waitForMigration(opts.isMigrated, opts.pollInterval); - return { status: 'skipped' }; - } - throw error; - } -} - -/** - * If the specified error is an index exists error, this logs a warning, - * and is the cue for us to fall into a polling loop, waiting for some - * other Kibana instance to complete the migration. - */ -function handleIndexExists(error: any, log: SavedObjectsMigrationLogger): string | undefined { - const isIndexExistsError = - _.get(error, 'body.error.type') === 'resource_already_exists_exception'; - if (!isIndexExistsError) { - return undefined; - } - - const index = _.get(error, 'body.error.index'); - - log.warning( - `Another Kibana instance appears to be migrating the index. Waiting for ` + - `that migration to complete. If no other Kibana instance is attempting ` + - `migrations, you can get past this message by deleting index ${index} and ` + - `restarting Kibana.` - ); - - return index; -} - -/** - * Polls isMigrated every pollInterval milliseconds until it returns true. - */ -async function waitForMigration( - isMigrated: () => Promise, - pollInterval = DEFAULT_POLL_INTERVAL -) { - while (true) { - if (await isMigrated()) { - return; - } - await sleep(pollInterval); - } -} - -function sleep(ms: number) { - return new Promise((r) => setTimeout(r, ms)); -} diff --git a/src/core/server/saved_objects/migrations/core/migration_es_client.test.mock.ts b/src/core/server/saved_objects/migrations/core/migration_es_client.test.mock.ts deleted file mode 100644 index 593973ad2e9ba..0000000000000 --- a/src/core/server/saved_objects/migrations/core/migration_es_client.test.mock.ts +++ /dev/null @@ -1,12 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -export const migrationRetryCallClusterMock = jest.fn((fn) => fn()); -jest.doMock('../../../elasticsearch/client/retry_call_cluster', () => ({ - migrationRetryCallCluster: migrationRetryCallClusterMock, -})); diff --git a/src/core/server/saved_objects/migrations/core/migration_es_client.test.ts b/src/core/server/saved_objects/migrations/core/migration_es_client.test.ts deleted file mode 100644 index 75dbdf55e55fc..0000000000000 --- a/src/core/server/saved_objects/migrations/core/migration_es_client.test.ts +++ /dev/null @@ -1,55 +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 { migrationRetryCallClusterMock } from './migration_es_client.test.mock'; - -import { createMigrationEsClient, MigrationEsClient } from './migration_es_client'; -import { elasticsearchClientMock } from '../../../elasticsearch/client/mocks'; -import { loggerMock } from '../../../logging/logger.mock'; -import { SavedObjectsErrorHelpers } from '../../service/lib/errors'; - -describe('MigrationEsClient', () => { - let client: ReturnType; - let migrationEsClient: MigrationEsClient; - - beforeEach(() => { - client = elasticsearchClientMock.createElasticsearchClient(); - migrationEsClient = createMigrationEsClient(client, loggerMock.create()); - migrationRetryCallClusterMock.mockClear(); - }); - - it('delegates call to ES client method', async () => { - expect(migrationEsClient.bulk).toStrictEqual(expect.any(Function)); - await migrationEsClient.bulk({ body: [] }); - expect(client.bulk).toHaveBeenCalledTimes(1); - }); - - it('wraps a method call in migrationRetryCallClusterMock', async () => { - await migrationEsClient.bulk({ body: [] }); - expect(migrationRetryCallClusterMock).toHaveBeenCalledTimes(1); - }); - - it('sets maxRetries: 0 to delegate retry logic to migrationRetryCallCluster', async () => { - expect(migrationEsClient.bulk).toStrictEqual(expect.any(Function)); - await migrationEsClient.bulk({ body: [] }); - expect(client.bulk).toHaveBeenCalledWith( - expect.any(Object), - expect.objectContaining({ maxRetries: 0 }) - ); - }); - - it('do not transform elasticsearch errors into saved objects errors', async () => { - expect.assertions(1); - client.bulk = jest.fn().mockRejectedValue(new Error('reason')); - try { - await migrationEsClient.bulk({ body: [] }); - } catch (e) { - expect(SavedObjectsErrorHelpers.isSavedObjectsClientError(e)).toBe(false); - } - }); -}); diff --git a/src/core/server/saved_objects/migrations/core/migration_es_client.ts b/src/core/server/saved_objects/migrations/core/migration_es_client.ts deleted file mode 100644 index 243b724eb2a67..0000000000000 --- a/src/core/server/saved_objects/migrations/core/migration_es_client.ts +++ /dev/null @@ -1,78 +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 type { Client, TransportRequestOptions } from '@elastic/elasticsearch'; -import { get } from 'lodash'; -import { set } from '@elastic/safer-lodash-set'; - -import { ElasticsearchClient } from '../../../elasticsearch'; -import { migrationRetryCallCluster } from '../../../elasticsearch/client/retry_call_cluster'; -import { Logger } from '../../../logging'; - -const methods = [ - 'bulk', - 'cat.templates', - 'clearScroll', - 'count', - 'indices.create', - 'indices.deleteTemplate', - 'indices.get', - 'indices.getAlias', - 'indices.refresh', - 'indices.updateAliases', - 'reindex', - 'search', - 'scroll', - 'tasks.get', -] as const; - -type MethodName = typeof methods[number]; - -export interface MigrationEsClient { - bulk: ElasticsearchClient['bulk']; - cat: { - templates: ElasticsearchClient['cat']['templates']; - }; - clearScroll: ElasticsearchClient['clearScroll']; - count: ElasticsearchClient['count']; - indices: { - create: ElasticsearchClient['indices']['create']; - delete: ElasticsearchClient['indices']['delete']; - deleteTemplate: ElasticsearchClient['indices']['deleteTemplate']; - get: ElasticsearchClient['indices']['get']; - getAlias: ElasticsearchClient['indices']['getAlias']; - refresh: ElasticsearchClient['indices']['refresh']; - updateAliases: ElasticsearchClient['indices']['updateAliases']; - }; - reindex: ElasticsearchClient['reindex']; - search: ElasticsearchClient['search']; - scroll: ElasticsearchClient['scroll']; - tasks: { - get: ElasticsearchClient['tasks']['get']; - }; -} - -export function createMigrationEsClient( - client: ElasticsearchClient | Client, - log: Logger, - delay?: number -): MigrationEsClient { - return methods.reduce((acc: MigrationEsClient, key: MethodName) => { - set(acc, key, async (params?: unknown, options?: TransportRequestOptions) => { - const fn = get(client, key); - if (!fn) { - throw new Error(`unknown ElasticsearchClient client method [${key}]`); - } - return await migrationRetryCallCluster( - () => fn.call(client, params, { maxRetries: 0, meta: true, ...options }), - log, - delay - ); - }); - return acc; - }, {} as MigrationEsClient); -} diff --git a/src/core/server/saved_objects/migrations/core/types.ts b/src/core/server/saved_objects/migrations/core/types.ts new file mode 100644 index 0000000000000..61985d8f10996 --- /dev/null +++ b/src/core/server/saved_objects/migrations/core/types.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 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 type MigrationStatus = + | 'waiting_to_start' + | 'waiting_for_other_nodes' + | 'running' + | 'completed'; + +export type MigrationResult = + | { status: 'skipped' } + | { status: 'patched' } + | { + status: 'migrated'; + destIndex: string; + sourceIndex: string; + elapsedMs: number; + }; diff --git a/src/core/server/saved_objects/migrations/core/unused_types.ts b/src/core/server/saved_objects/migrations/core/unused_types.ts new file mode 100644 index 0000000000000..ddcadc09502f4 --- /dev/null +++ b/src/core/server/saved_objects/migrations/core/unused_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 * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; + +/** + * Types that are no longer registered and need to be removed + */ +export const REMOVED_TYPES: string[] = [ + 'apm-services-telemetry', + 'background-session', + 'cases-sub-case', + 'file-upload-telemetry', + // https://github.com/elastic/kibana/issues/91869 + 'fleet-agent-events', + // https://github.com/elastic/obs-dc-team/issues/334 + 'fleet-agents', + 'fleet-agent-actions', + 'fleet-enrollment-api-keys', + // Was removed in 7.12 + 'ml-telemetry', + 'server', + // https://github.com/elastic/kibana/issues/95617 + 'tsvb-validation-telemetry', + // replaced by osquery-manager-usage-metric + 'osquery-usage-metric', + // Was removed in 7.16 + 'timelion-sheet', +].sort(); + +// When migrating from the outdated index we use a read query which excludes +// saved objects which are no longer used. These saved objects will still be +// kept in the outdated index for backup purposes, but won't be available in +// the upgraded index. +export const excludeUnusedTypesQuery: estypes.QueryDslQueryContainer = { + bool: { + must_not: [ + ...REMOVED_TYPES.map((typeName) => ({ + term: { + type: typeName, + }, + })), + // https://github.com/elastic/kibana/issues/96131 + { + bool: { + must: [ + { + match: { + type: 'search-session', + }, + }, + { + match: { + 'search-session.persisted': false, + }, + }, + ], + }, + }, + ], + }, +}; diff --git a/src/core/server/saved_objects/migrations/index.ts b/src/core/server/saved_objects/migrations/index.ts index 20b86ee6d3739..91be12425c605 100644 --- a/src/core/server/saved_objects/migrations/index.ts +++ b/src/core/server/saved_objects/migrations/index.ts @@ -7,8 +7,8 @@ */ export type { MigrationResult } from './core'; -export { KibanaMigrator } from './kibana'; -export type { IKibanaMigrator } from './kibana'; +export { KibanaMigrator } from './kibana_migrator'; +export type { IKibanaMigrator, KibanaMigratorStatus } from './kibana_migrator'; export type { SavedObjectMigrationFn, SavedObjectMigrationMap, diff --git a/src/core/server/saved_objects/migrationsv2/initial_state.test.ts b/src/core/server/saved_objects/migrations/initial_state.test.ts similarity index 100% rename from src/core/server/saved_objects/migrationsv2/initial_state.test.ts rename to src/core/server/saved_objects/migrations/initial_state.test.ts diff --git a/src/core/server/saved_objects/migrationsv2/initial_state.ts b/src/core/server/saved_objects/migrations/initial_state.ts similarity index 98% rename from src/core/server/saved_objects/migrationsv2/initial_state.ts rename to src/core/server/saved_objects/migrations/initial_state.ts index a61967be9242c..f074f123c8930 100644 --- a/src/core/server/saved_objects/migrationsv2/initial_state.ts +++ b/src/core/server/saved_objects/migrations/initial_state.ts @@ -11,7 +11,7 @@ import { IndexMapping } from '../mappings'; import { SavedObjectsMigrationVersion } from '../../../types'; import { SavedObjectsMigrationConfigType } from '../saved_objects_config'; import type { ISavedObjectTypeRegistry } from '../saved_objects_type_registry'; -import { InitState } from './types'; +import { InitState } from './state'; import { excludeUnusedTypesQuery } from '../migrations/core'; /** diff --git a/src/core/server/saved_objects/migrationsv2/integration_tests/.gitignore b/src/core/server/saved_objects/migrations/integration_tests/.gitignore similarity index 100% rename from src/core/server/saved_objects/migrationsv2/integration_tests/.gitignore rename to src/core/server/saved_objects/migrations/integration_tests/.gitignore diff --git a/src/core/server/saved_objects/migrationsv2/integration_tests/7.7.2_xpack_100k.test.ts b/src/core/server/saved_objects/migrations/integration_tests/7.7.2_xpack_100k.test.ts similarity index 100% rename from src/core/server/saved_objects/migrationsv2/integration_tests/7.7.2_xpack_100k.test.ts rename to src/core/server/saved_objects/migrations/integration_tests/7.7.2_xpack_100k.test.ts diff --git a/src/core/server/saved_objects/migrationsv2/integration_tests/7_13_0_failed_action_tasks.test.ts b/src/core/server/saved_objects/migrations/integration_tests/7_13_0_failed_action_tasks.test.ts similarity index 100% rename from src/core/server/saved_objects/migrationsv2/integration_tests/7_13_0_failed_action_tasks.test.ts rename to src/core/server/saved_objects/migrations/integration_tests/7_13_0_failed_action_tasks.test.ts diff --git a/src/core/server/saved_objects/migrationsv2/integration_tests/7_13_0_transform_failures.test.ts b/src/core/server/saved_objects/migrations/integration_tests/7_13_0_transform_failures.test.ts similarity index 100% rename from src/core/server/saved_objects/migrationsv2/integration_tests/7_13_0_transform_failures.test.ts rename to src/core/server/saved_objects/migrations/integration_tests/7_13_0_transform_failures.test.ts diff --git a/src/core/server/saved_objects/migrationsv2/integration_tests/7_13_0_unknown_types.test.ts b/src/core/server/saved_objects/migrations/integration_tests/7_13_0_unknown_types.test.ts similarity index 100% rename from src/core/server/saved_objects/migrationsv2/integration_tests/7_13_0_unknown_types.test.ts rename to src/core/server/saved_objects/migrations/integration_tests/7_13_0_unknown_types.test.ts diff --git a/src/core/server/saved_objects/migrationsv2/integration_tests/archives/7.13.0_5k_so_node_01.zip b/src/core/server/saved_objects/migrations/integration_tests/archives/7.13.0_5k_so_node_01.zip similarity index 100% rename from src/core/server/saved_objects/migrationsv2/integration_tests/archives/7.13.0_5k_so_node_01.zip rename to src/core/server/saved_objects/migrations/integration_tests/archives/7.13.0_5k_so_node_01.zip diff --git a/src/core/server/saved_objects/migrationsv2/integration_tests/archives/7.13.0_5k_so_node_02.zip b/src/core/server/saved_objects/migrations/integration_tests/archives/7.13.0_5k_so_node_02.zip similarity index 100% rename from src/core/server/saved_objects/migrationsv2/integration_tests/archives/7.13.0_5k_so_node_02.zip rename to src/core/server/saved_objects/migrations/integration_tests/archives/7.13.0_5k_so_node_02.zip diff --git a/src/core/server/saved_objects/migrationsv2/integration_tests/archives/7.13.0_concurrent_5k_foo.zip b/src/core/server/saved_objects/migrations/integration_tests/archives/7.13.0_concurrent_5k_foo.zip similarity index 100% rename from src/core/server/saved_objects/migrationsv2/integration_tests/archives/7.13.0_concurrent_5k_foo.zip rename to src/core/server/saved_objects/migrations/integration_tests/archives/7.13.0_concurrent_5k_foo.zip diff --git a/src/core/server/saved_objects/migrationsv2/integration_tests/archives/7.13.0_with_corrupted_so.zip b/src/core/server/saved_objects/migrations/integration_tests/archives/7.13.0_with_corrupted_so.zip similarity index 100% rename from src/core/server/saved_objects/migrationsv2/integration_tests/archives/7.13.0_with_corrupted_so.zip rename to src/core/server/saved_objects/migrations/integration_tests/archives/7.13.0_with_corrupted_so.zip diff --git a/src/core/server/saved_objects/migrationsv2/integration_tests/archives/7.13.0_with_unknown_so.zip b/src/core/server/saved_objects/migrations/integration_tests/archives/7.13.0_with_unknown_so.zip similarity index 100% rename from src/core/server/saved_objects/migrationsv2/integration_tests/archives/7.13.0_with_unknown_so.zip rename to src/core/server/saved_objects/migrations/integration_tests/archives/7.13.0_with_unknown_so.zip diff --git a/src/core/server/saved_objects/migrationsv2/integration_tests/archives/7.13.2_so_with_multiple_namespaces.zip b/src/core/server/saved_objects/migrations/integration_tests/archives/7.13.2_so_with_multiple_namespaces.zip similarity index 100% rename from src/core/server/saved_objects/migrationsv2/integration_tests/archives/7.13.2_so_with_multiple_namespaces.zip rename to src/core/server/saved_objects/migrations/integration_tests/archives/7.13.2_so_with_multiple_namespaces.zip diff --git a/src/core/server/saved_objects/migrationsv2/integration_tests/archives/7.13_1.5k_failed_action_tasks.zip b/src/core/server/saved_objects/migrations/integration_tests/archives/7.13_1.5k_failed_action_tasks.zip similarity index 100% rename from src/core/server/saved_objects/migrationsv2/integration_tests/archives/7.13_1.5k_failed_action_tasks.zip rename to src/core/server/saved_objects/migrations/integration_tests/archives/7.13_1.5k_failed_action_tasks.zip diff --git a/src/core/server/saved_objects/migrationsv2/integration_tests/archives/7.14.0_xpack_sample_saved_objects.zip b/src/core/server/saved_objects/migrations/integration_tests/archives/7.14.0_xpack_sample_saved_objects.zip similarity index 100% rename from src/core/server/saved_objects/migrationsv2/integration_tests/archives/7.14.0_xpack_sample_saved_objects.zip rename to src/core/server/saved_objects/migrations/integration_tests/archives/7.14.0_xpack_sample_saved_objects.zip diff --git a/src/core/server/saved_objects/migrationsv2/integration_tests/archives/7.3.0_xpack_sample_saved_objects.zip b/src/core/server/saved_objects/migrations/integration_tests/archives/7.3.0_xpack_sample_saved_objects.zip similarity index 100% rename from src/core/server/saved_objects/migrationsv2/integration_tests/archives/7.3.0_xpack_sample_saved_objects.zip rename to src/core/server/saved_objects/migrations/integration_tests/archives/7.3.0_xpack_sample_saved_objects.zip diff --git a/src/core/server/saved_objects/migrationsv2/integration_tests/archives/7.7.2_xpack_100k_obj.zip b/src/core/server/saved_objects/migrations/integration_tests/archives/7.7.2_xpack_100k_obj.zip similarity index 100% rename from src/core/server/saved_objects/migrationsv2/integration_tests/archives/7.7.2_xpack_100k_obj.zip rename to src/core/server/saved_objects/migrations/integration_tests/archives/7.7.2_xpack_100k_obj.zip diff --git a/src/core/server/saved_objects/migrationsv2/integration_tests/archives/7_13_corrupt_and_transform_failures_docs.zip b/src/core/server/saved_objects/migrations/integration_tests/archives/7_13_corrupt_and_transform_failures_docs.zip similarity index 100% rename from src/core/server/saved_objects/migrationsv2/integration_tests/archives/7_13_corrupt_and_transform_failures_docs.zip rename to src/core/server/saved_objects/migrations/integration_tests/archives/7_13_corrupt_and_transform_failures_docs.zip diff --git a/src/core/server/saved_objects/migrationsv2/integration_tests/archives/8.0.0_document_migration_failure.zip b/src/core/server/saved_objects/migrations/integration_tests/archives/8.0.0_document_migration_failure.zip similarity index 100% rename from src/core/server/saved_objects/migrationsv2/integration_tests/archives/8.0.0_document_migration_failure.zip rename to src/core/server/saved_objects/migrations/integration_tests/archives/8.0.0_document_migration_failure.zip diff --git a/src/core/server/saved_objects/migrationsv2/integration_tests/archives/8.0.0_migrated_with_corrupt_outdated_docs.zip b/src/core/server/saved_objects/migrations/integration_tests/archives/8.0.0_migrated_with_corrupt_outdated_docs.zip similarity index 100% rename from src/core/server/saved_objects/migrationsv2/integration_tests/archives/8.0.0_migrated_with_corrupt_outdated_docs.zip rename to src/core/server/saved_objects/migrations/integration_tests/archives/8.0.0_migrated_with_corrupt_outdated_docs.zip diff --git a/src/core/server/saved_objects/migrationsv2/integration_tests/archives/8.0.0_migrated_with_outdated_docs.zip b/src/core/server/saved_objects/migrations/integration_tests/archives/8.0.0_migrated_with_outdated_docs.zip similarity index 100% rename from src/core/server/saved_objects/migrationsv2/integration_tests/archives/8.0.0_migrated_with_outdated_docs.zip rename to src/core/server/saved_objects/migrations/integration_tests/archives/8.0.0_migrated_with_outdated_docs.zip diff --git a/src/core/server/saved_objects/migrationsv2/integration_tests/archives/8.0.0_v1_migrations_sample_data_saved_objects.zip b/src/core/server/saved_objects/migrations/integration_tests/archives/8.0.0_v1_migrations_sample_data_saved_objects.zip similarity index 100% rename from src/core/server/saved_objects/migrationsv2/integration_tests/archives/8.0.0_v1_migrations_sample_data_saved_objects.zip rename to src/core/server/saved_objects/migrations/integration_tests/archives/8.0.0_v1_migrations_sample_data_saved_objects.zip diff --git a/src/core/server/saved_objects/migrationsv2/integration_tests/batch_size_bytes.test.ts b/src/core/server/saved_objects/migrations/integration_tests/batch_size_bytes.test.ts similarity index 100% rename from src/core/server/saved_objects/migrationsv2/integration_tests/batch_size_bytes.test.ts rename to src/core/server/saved_objects/migrations/integration_tests/batch_size_bytes.test.ts diff --git a/src/core/server/saved_objects/migrationsv2/integration_tests/batch_size_bytes_exceeds_es_content_length.test.ts b/src/core/server/saved_objects/migrations/integration_tests/batch_size_bytes_exceeds_es_content_length.test.ts similarity index 100% rename from src/core/server/saved_objects/migrationsv2/integration_tests/batch_size_bytes_exceeds_es_content_length.test.ts rename to src/core/server/saved_objects/migrations/integration_tests/batch_size_bytes_exceeds_es_content_length.test.ts diff --git a/src/core/server/saved_objects/migrationsv2/integration_tests/cleanup.test.ts b/src/core/server/saved_objects/migrations/integration_tests/cleanup.test.ts similarity index 100% rename from src/core/server/saved_objects/migrationsv2/integration_tests/cleanup.test.ts rename to src/core/server/saved_objects/migrations/integration_tests/cleanup.test.ts diff --git a/src/core/server/saved_objects/migrationsv2/integration_tests/collects_corrupt_docs.test.ts b/src/core/server/saved_objects/migrations/integration_tests/collects_corrupt_docs.test.ts similarity index 100% rename from src/core/server/saved_objects/migrationsv2/integration_tests/collects_corrupt_docs.test.ts rename to src/core/server/saved_objects/migrations/integration_tests/collects_corrupt_docs.test.ts diff --git a/src/core/server/saved_objects/migrationsv2/integration_tests/corrupt_outdated_docs.test.ts b/src/core/server/saved_objects/migrations/integration_tests/corrupt_outdated_docs.test.ts similarity index 100% rename from src/core/server/saved_objects/migrationsv2/integration_tests/corrupt_outdated_docs.test.ts rename to src/core/server/saved_objects/migrations/integration_tests/corrupt_outdated_docs.test.ts diff --git a/src/core/server/saved_objects/migrationsv2/integration_tests/migration_from_older_v1.test.ts b/src/core/server/saved_objects/migrations/integration_tests/migration_from_older_v1.test.ts similarity index 100% rename from src/core/server/saved_objects/migrationsv2/integration_tests/migration_from_older_v1.test.ts rename to src/core/server/saved_objects/migrations/integration_tests/migration_from_older_v1.test.ts diff --git a/src/core/server/saved_objects/migrationsv2/integration_tests/migration_from_same_v1.test.ts b/src/core/server/saved_objects/migrations/integration_tests/migration_from_same_v1.test.ts similarity index 100% rename from src/core/server/saved_objects/migrationsv2/integration_tests/migration_from_same_v1.test.ts rename to src/core/server/saved_objects/migrations/integration_tests/migration_from_same_v1.test.ts diff --git a/src/core/server/saved_objects/migrationsv2/integration_tests/multiple_es_nodes.test.ts b/src/core/server/saved_objects/migrations/integration_tests/multiple_es_nodes.test.ts similarity index 100% rename from src/core/server/saved_objects/migrationsv2/integration_tests/multiple_es_nodes.test.ts rename to src/core/server/saved_objects/migrations/integration_tests/multiple_es_nodes.test.ts diff --git a/src/core/server/saved_objects/migrationsv2/integration_tests/multiple_kibana_nodes.test.ts b/src/core/server/saved_objects/migrations/integration_tests/multiple_kibana_nodes.test.ts similarity index 100% rename from src/core/server/saved_objects/migrationsv2/integration_tests/multiple_kibana_nodes.test.ts rename to src/core/server/saved_objects/migrations/integration_tests/multiple_kibana_nodes.test.ts diff --git a/src/core/server/saved_objects/migrationsv2/integration_tests/outdated_docs.test.ts b/src/core/server/saved_objects/migrations/integration_tests/outdated_docs.test.ts similarity index 100% rename from src/core/server/saved_objects/migrationsv2/integration_tests/outdated_docs.test.ts rename to src/core/server/saved_objects/migrations/integration_tests/outdated_docs.test.ts diff --git a/src/core/server/saved_objects/migrationsv2/integration_tests/rewriting_id.test.ts b/src/core/server/saved_objects/migrations/integration_tests/rewriting_id.test.ts similarity index 100% rename from src/core/server/saved_objects/migrationsv2/integration_tests/rewriting_id.test.ts rename to src/core/server/saved_objects/migrations/integration_tests/rewriting_id.test.ts diff --git a/src/core/server/saved_objects/migrationsv2/integration_tests/type_registrations.test.ts b/src/core/server/saved_objects/migrations/integration_tests/type_registrations.test.ts similarity index 100% rename from src/core/server/saved_objects/migrationsv2/integration_tests/type_registrations.test.ts rename to src/core/server/saved_objects/migrations/integration_tests/type_registrations.test.ts diff --git a/src/core/server/saved_objects/migrations/kibana/__mocks__/kibana_migrator.ts b/src/core/server/saved_objects/migrations/kibana/__mocks__/kibana_migrator.ts deleted file mode 100644 index 35dc08d50072d..0000000000000 --- a/src/core/server/saved_objects/migrations/kibana/__mocks__/kibana_migrator.ts +++ /dev/null @@ -1,15 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import { mockKibanaMigrator } from '../kibana_migrator.mock'; - -export const mockKibanaMigratorInstance = mockKibanaMigrator.create(); - -const mockConstructor = jest.fn().mockImplementation(() => mockKibanaMigratorInstance); - -export const KibanaMigrator = mockConstructor; diff --git a/src/core/server/saved_objects/migrations/kibana/index.ts b/src/core/server/saved_objects/migrations/kibana/index.ts deleted file mode 100644 index 52755ee0aed71..0000000000000 --- a/src/core/server/saved_objects/migrations/kibana/index.ts +++ /dev/null @@ -1,10 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -export { KibanaMigrator } from './kibana_migrator'; -export type { IKibanaMigrator, KibanaMigratorStatus } from './kibana_migrator'; diff --git a/src/core/server/saved_objects/migrations/kibana/kibana_migrator.ts b/src/core/server/saved_objects/migrations/kibana/kibana_migrator.ts deleted file mode 100644 index 198983538c93d..0000000000000 --- a/src/core/server/saved_objects/migrations/kibana/kibana_migrator.ts +++ /dev/null @@ -1,227 +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. - */ - -/* - * This file contains the logic for managing the Kibana index version - * (the shape of the mappings and documents in the index). - */ - -import { BehaviorSubject } from 'rxjs'; -import Semver from 'semver'; -import { ElasticsearchClient } from '../../../elasticsearch'; -import { Logger } from '../../../logging'; -import { IndexMapping, SavedObjectsTypeMappingDefinitions } from '../../mappings'; -import { - SavedObjectUnsanitizedDoc, - SavedObjectsSerializer, - SavedObjectsRawDoc, -} from '../../serialization'; -import { buildActiveMappings, MigrationResult, MigrationStatus } from '../core'; -import { DocumentMigrator, VersionedTransformer } from '../core/document_migrator'; -import { createIndexMap } from '../core/build_index_map'; -import { SavedObjectsMigrationConfigType } from '../../saved_objects_config'; -import { ISavedObjectTypeRegistry } from '../../saved_objects_type_registry'; -import { SavedObjectsType } from '../../types'; -import { runResilientMigrator } from '../../migrationsv2'; -import { migrateRawDocsSafely } from '../core/migrate_raw_docs'; - -export interface KibanaMigratorOptions { - client: ElasticsearchClient; - typeRegistry: ISavedObjectTypeRegistry; - soMigrationsConfig: SavedObjectsMigrationConfigType; - kibanaIndex: string; - kibanaVersion: string; - logger: Logger; - migrationsRetryDelay?: number; -} - -export type IKibanaMigrator = Pick; - -export interface KibanaMigratorStatus { - status: MigrationStatus; - result?: MigrationResult[]; - waitingIndex?: string; -} - -/** - * Manages the shape of mappings and documents in the Kibana index. - */ -export class KibanaMigrator { - private readonly client: ElasticsearchClient; - private readonly documentMigrator: VersionedTransformer; - private readonly kibanaIndex: string; - private readonly log: Logger; - private readonly mappingProperties: SavedObjectsTypeMappingDefinitions; - private readonly typeRegistry: ISavedObjectTypeRegistry; - private readonly serializer: SavedObjectsSerializer; - private migrationResult?: Promise; - private readonly status$ = new BehaviorSubject({ - status: 'waiting_to_start', - }); - private readonly activeMappings: IndexMapping; - // 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. - */ - constructor({ - client, - typeRegistry, - kibanaIndex, - soMigrationsConfig, - kibanaVersion, - logger, - migrationsRetryDelay, - }: KibanaMigratorOptions) { - this.client = client; - this.kibanaIndex = kibanaIndex; - this.soMigrationsConfig = soMigrationsConfig; - this.typeRegistry = typeRegistry; - this.serializer = new SavedObjectsSerializer(this.typeRegistry); - this.mappingProperties = mergeTypes(this.typeRegistry.getAllTypes()); - this.log = logger; - this.kibanaVersion = kibanaVersion; - this.documentMigrator = new DocumentMigrator({ - kibanaVersion: this.kibanaVersion, - typeRegistry, - log: this.log, - }); - // Building the active mappings (and associated md5sums) is an expensive - // operation so we cache the result - this.activeMappings = buildActiveMappings(this.mappingProperties); - } - - /** - * Migrates the mappings and documents in the Kibana index. By default, this will run only - * once and subsequent calls will return the result of the original call. - * - * @param rerun - If true, method will run a new migration when called again instead of - * returning the result of the initial migration. This should only be used when factors external - * to Kibana itself alter the kibana index causing the saved objects mappings or data to change - * after the Kibana server performed the initial migration. - * - * @remarks When the `rerun` parameter is set to true, no checks are performed to ensure that no migration - * is currently running. Chained or concurrent calls to `runMigrations({ rerun: true })` can lead to - * multiple migrations running at the same time. When calling with this parameter, it's expected that the calling - * code should ensure that the initial call resolves before calling the function again. - * - * @returns - A promise which resolves once all migrations have been applied. - * The promise resolves with an array of migration statuses, one for each - * elasticsearch index which was migrated. - */ - public runMigrations({ rerun = false }: { rerun?: boolean } = {}): Promise< - Array<{ status: string }> - > { - if (this.migrationResult === undefined || rerun) { - // Reruns are only used by CI / EsArchiver. Publishing status updates on reruns results in slowing down CI - // unnecessarily, so we skip it in this case. - if (!rerun) { - this.status$.next({ status: 'running' }); - } - this.migrationResult = this.runMigrationsInternal().then((result) => { - // Similar to above, don't publish status updates when rerunning in CI. - if (!rerun) { - this.status$.next({ status: 'completed', result }); - } - return result; - }); - } - - return this.migrationResult; - } - - public prepareMigrations() { - this.documentMigrator.prepareMigrations(); - } - - public getStatus$() { - return this.status$.asObservable(); - } - - private runMigrationsInternal() { - const indexMap = createIndexMap({ - kibanaIndexName: this.kibanaIndex, - indexMap: this.mappingProperties, - registry: this.typeRegistry, - }); - - this.log.debug('Applying registered migrations for the following saved object types:'); - Object.entries(this.documentMigrator.migrationVersion) - .sort(([t1, v1], [t2, v2]) => { - return Semver.compare(v1, v2); - }) - .forEach(([type, migrationVersion]) => { - this.log.debug(`migrationVersion: ${migrationVersion} saved object type: ${type}`); - }); - - const migrators = Object.keys(indexMap).map((index) => { - return { - migrate: (): Promise => { - return runResilientMigrator({ - client: this.client, - kibanaVersion: this.kibanaVersion, - targetMappings: buildActiveMappings(indexMap[index].typeMappings), - logger: this.log, - preMigrationScript: indexMap[index].script, - transformRawDocs: (rawDocs: SavedObjectsRawDoc[]) => - migrateRawDocsSafely({ - serializer: this.serializer, - knownTypes: new Set(this.typeRegistry.getAllTypes().map((t) => t.name)), - migrateDoc: this.documentMigrator.migrateAndConvert, - rawDocs, - }), - migrationVersionPerType: this.documentMigrator.migrationVersion, - indexPrefix: index, - migrationsConfig: this.soMigrationsConfig, - typeRegistry: this.typeRegistry, - }); - }, - }; - }); - - return Promise.all(migrators.map((migrator) => migrator.migrate())); - } - - /** - * Gets all the index mappings defined by Kibana's enabled plugins. - * - */ - public getActiveMappings(): IndexMapping { - return this.activeMappings; - } - - /** - * Migrates an individual doc to the latest version, as defined by the plugin migrations. - * - * @param doc - The saved object to migrate - * @returns `doc` with all registered migrations applied. - */ - public migrateDocument(doc: SavedObjectUnsanitizedDoc): SavedObjectUnsanitizedDoc { - return this.documentMigrator.migrate(doc); - } -} - -/** - * Merges savedObjectMappings properties into a single object, verifying that - * no mappings are redefined. - */ -export function mergeTypes(types: SavedObjectsType[]): SavedObjectsTypeMappingDefinitions { - return types.reduce((acc, { name: type, mappings }) => { - const duplicate = acc.hasOwnProperty(type); - if (duplicate) { - throw new Error(`Type ${type} is already defined.`); - } - return { - ...acc, - [type]: mappings, - }; - }, {}); -} diff --git a/src/core/server/saved_objects/migrations/kibana/kibana_migrator.mock.ts b/src/core/server/saved_objects/migrations/kibana_migrator.mock.ts similarity index 83% rename from src/core/server/saved_objects/migrations/kibana/kibana_migrator.mock.ts rename to src/core/server/saved_objects/migrations/kibana_migrator.mock.ts index 660300ea867ff..24486a9336122 100644 --- a/src/core/server/saved_objects/migrations/kibana/kibana_migrator.mock.ts +++ b/src/core/server/saved_objects/migrations/kibana_migrator.mock.ts @@ -7,11 +7,11 @@ */ import { IKibanaMigrator, KibanaMigratorStatus } from './kibana_migrator'; -import { buildActiveMappings } from '../core'; +import { buildActiveMappings } from './core'; + const { mergeTypes } = jest.requireActual('./kibana_migrator'); -import { SavedObjectsType } from '../../types'; +import { SavedObjectsType } from '../types'; import { BehaviorSubject } from 'rxjs'; -import { ByteSizeValue } from '@kbn/config-schema'; const defaultSavedObjectTypes: SavedObjectsType[] = [ { @@ -36,14 +36,6 @@ const createMigrator = ( ) => { const mockMigrator: jest.Mocked = { kibanaVersion: '8.0.0-testing', - soMigrationsConfig: { - batchSize: 100, - maxBatchSizeBytes: ByteSizeValue.parse('30kb'), - scrollDuration: '15m', - pollInterval: 1500, - skip: false, - retryAttempts: 10, - }, runMigrations: jest.fn(), getActiveMappings: jest.fn(), migrateDocument: jest.fn(), diff --git a/src/core/server/saved_objects/migrations/kibana/kibana_migrator.test.ts b/src/core/server/saved_objects/migrations/kibana_migrator.test.ts similarity index 96% rename from src/core/server/saved_objects/migrations/kibana/kibana_migrator.test.ts rename to src/core/server/saved_objects/migrations/kibana_migrator.test.ts index fe3d6c469726d..eb7b72f144031 100644 --- a/src/core/server/saved_objects/migrations/kibana/kibana_migrator.test.ts +++ b/src/core/server/saved_objects/migrations/kibana_migrator.test.ts @@ -9,19 +9,19 @@ import { take } from 'rxjs/operators'; import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; -import { elasticsearchClientMock } from '../../../elasticsearch/client/mocks'; +import { elasticsearchClientMock } from '../../elasticsearch/client/mocks'; import { KibanaMigratorOptions, KibanaMigrator } from './kibana_migrator'; -import { loggingSystemMock } from '../../../logging/logging_system.mock'; -import { SavedObjectTypeRegistry } from '../../saved_objects_type_registry'; -import { SavedObjectsType } from '../../types'; -import { DocumentMigrator } from '../core/document_migrator'; +import { loggingSystemMock } from '../../logging/logging_system.mock'; +import { SavedObjectTypeRegistry } from '../saved_objects_type_registry'; +import { SavedObjectsType } from '../types'; +import { DocumentMigrator } from './core/document_migrator'; import { ByteSizeValue } from '@kbn/config-schema'; -jest.mock('../core/document_migrator', () => { +jest.mock('./core/document_migrator', () => { return { // Create a mock for spying on the constructor DocumentMigrator: jest.fn().mockImplementation((...args) => { - const { DocumentMigrator: RealDocMigrator } = jest.requireActual('../core/document_migrator'); + const { DocumentMigrator: RealDocMigrator } = jest.requireActual('./core/document_migrator'); return new RealDocMigrator(args[0]); }), }; diff --git a/src/core/server/saved_objects/migrations/kibana_migrator.ts b/src/core/server/saved_objects/migrations/kibana_migrator.ts new file mode 100644 index 0000000000000..fa1172c0684a7 --- /dev/null +++ b/src/core/server/saved_objects/migrations/kibana_migrator.ts @@ -0,0 +1,223 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 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. + */ + +/* + * This file contains the logic for managing the Kibana index version + * (the shape of the mappings and documents in the index). + */ + +import { BehaviorSubject } from 'rxjs'; +import Semver from 'semver'; +import { ElasticsearchClient } from '../../elasticsearch'; +import { Logger } from '../../logging'; +import { IndexMapping, SavedObjectsTypeMappingDefinitions } from '../mappings'; +import { + SavedObjectUnsanitizedDoc, + SavedObjectsSerializer, + SavedObjectsRawDoc, +} from '../serialization'; +import { buildActiveMappings, MigrationResult, MigrationStatus } from './core'; +import { DocumentMigrator, VersionedTransformer } from './core/document_migrator'; +import { createIndexMap } from './core/build_index_map'; +import { SavedObjectsMigrationConfigType } from '../saved_objects_config'; +import { ISavedObjectTypeRegistry } from '../saved_objects_type_registry'; +import { SavedObjectsType } from '../types'; +import { runResilientMigrator } from './run_resilient_migrator'; +import { migrateRawDocsSafely } from './core/migrate_raw_docs'; + +export interface KibanaMigratorOptions { + client: ElasticsearchClient; + typeRegistry: ISavedObjectTypeRegistry; + soMigrationsConfig: SavedObjectsMigrationConfigType; + kibanaIndex: string; + kibanaVersion: string; + logger: Logger; +} + +export type IKibanaMigrator = Pick; + +export interface KibanaMigratorStatus { + status: MigrationStatus; + result?: MigrationResult[]; + waitingIndex?: string; +} + +/** + * Manages the shape of mappings and documents in the Kibana index. + */ +export class KibanaMigrator { + private readonly client: ElasticsearchClient; + private readonly documentMigrator: VersionedTransformer; + private readonly kibanaIndex: string; + private readonly log: Logger; + private readonly mappingProperties: SavedObjectsTypeMappingDefinitions; + private readonly typeRegistry: ISavedObjectTypeRegistry; + private readonly serializer: SavedObjectsSerializer; + private migrationResult?: Promise; + private readonly status$ = new BehaviorSubject({ + status: 'waiting_to_start', + }); + private readonly activeMappings: IndexMapping; + private readonly soMigrationsConfig: SavedObjectsMigrationConfigType; + public readonly kibanaVersion: string; + + /** + * Creates an instance of KibanaMigrator. + */ + constructor({ + client, + typeRegistry, + kibanaIndex, + soMigrationsConfig, + kibanaVersion, + logger, + }: KibanaMigratorOptions) { + this.client = client; + this.kibanaIndex = kibanaIndex; + this.soMigrationsConfig = soMigrationsConfig; + this.typeRegistry = typeRegistry; + this.serializer = new SavedObjectsSerializer(this.typeRegistry); + this.mappingProperties = mergeTypes(this.typeRegistry.getAllTypes()); + this.log = logger; + this.kibanaVersion = kibanaVersion; + this.documentMigrator = new DocumentMigrator({ + kibanaVersion: this.kibanaVersion, + typeRegistry, + log: this.log, + }); + // Building the active mappings (and associated md5sums) is an expensive + // operation so we cache the result + this.activeMappings = buildActiveMappings(this.mappingProperties); + } + + /** + * Migrates the mappings and documents in the Kibana index. By default, this will run only + * once and subsequent calls will return the result of the original call. + * + * @param rerun - If true, method will run a new migration when called again instead of + * returning the result of the initial migration. This should only be used when factors external + * to Kibana itself alter the kibana index causing the saved objects mappings or data to change + * after the Kibana server performed the initial migration. + * + * @remarks When the `rerun` parameter is set to true, no checks are performed to ensure that no migration + * is currently running. Chained or concurrent calls to `runMigrations({ rerun: true })` can lead to + * multiple migrations running at the same time. When calling with this parameter, it's expected that the calling + * code should ensure that the initial call resolves before calling the function again. + * + * @returns - A promise which resolves once all migrations have been applied. + * The promise resolves with an array of migration statuses, one for each + * elasticsearch index which was migrated. + */ + public runMigrations({ rerun = false }: { rerun?: boolean } = {}): Promise< + Array<{ status: string }> + > { + if (this.migrationResult === undefined || rerun) { + // Reruns are only used by CI / EsArchiver. Publishing status updates on reruns results in slowing down CI + // unnecessarily, so we skip it in this case. + if (!rerun) { + this.status$.next({ status: 'running' }); + } + this.migrationResult = this.runMigrationsInternal().then((result) => { + // Similar to above, don't publish status updates when rerunning in CI. + if (!rerun) { + this.status$.next({ status: 'completed', result }); + } + return result; + }); + } + + return this.migrationResult; + } + + public prepareMigrations() { + this.documentMigrator.prepareMigrations(); + } + + public getStatus$() { + return this.status$.asObservable(); + } + + private runMigrationsInternal() { + const indexMap = createIndexMap({ + kibanaIndexName: this.kibanaIndex, + indexMap: this.mappingProperties, + registry: this.typeRegistry, + }); + + this.log.debug('Applying registered migrations for the following saved object types:'); + Object.entries(this.documentMigrator.migrationVersion) + .sort(([t1, v1], [t2, v2]) => { + return Semver.compare(v1, v2); + }) + .forEach(([type, migrationVersion]) => { + this.log.debug(`migrationVersion: ${migrationVersion} saved object type: ${type}`); + }); + + const migrators = Object.keys(indexMap).map((index) => { + return { + migrate: (): Promise => { + return runResilientMigrator({ + client: this.client, + kibanaVersion: this.kibanaVersion, + targetMappings: buildActiveMappings(indexMap[index].typeMappings), + logger: this.log, + preMigrationScript: indexMap[index].script, + transformRawDocs: (rawDocs: SavedObjectsRawDoc[]) => + migrateRawDocsSafely({ + serializer: this.serializer, + knownTypes: new Set(this.typeRegistry.getAllTypes().map((t) => t.name)), + migrateDoc: this.documentMigrator.migrateAndConvert, + rawDocs, + }), + migrationVersionPerType: this.documentMigrator.migrationVersion, + indexPrefix: index, + migrationsConfig: this.soMigrationsConfig, + typeRegistry: this.typeRegistry, + }); + }, + }; + }); + + return Promise.all(migrators.map((migrator) => migrator.migrate())); + } + + /** + * Gets all the index mappings defined by Kibana's enabled plugins. + * + */ + public getActiveMappings(): IndexMapping { + return this.activeMappings; + } + + /** + * Migrates an individual doc to the latest version, as defined by the plugin migrations. + * + * @param doc - The saved object to migrate + * @returns `doc` with all registered migrations applied. + */ + public migrateDocument(doc: SavedObjectUnsanitizedDoc): SavedObjectUnsanitizedDoc { + return this.documentMigrator.migrate(doc); + } +} + +/** + * Merges savedObjectMappings properties into a single object, verifying that + * no mappings are redefined. + */ +export function mergeTypes(types: SavedObjectsType[]): SavedObjectsTypeMappingDefinitions { + return types.reduce((acc, { name: type, mappings }) => { + const duplicate = acc.hasOwnProperty(type); + if (duplicate) { + throw new Error(`Type ${type} is already defined.`); + } + return { + ...acc, + [type]: mappings, + }; + }, {}); +} diff --git a/src/core/server/saved_objects/migrationsv2/migrations_state_action_machine.test.ts b/src/core/server/saved_objects/migrations/migrations_state_action_machine.test.ts similarity index 99% rename from src/core/server/saved_objects/migrationsv2/migrations_state_action_machine.test.ts rename to src/core/server/saved_objects/migrations/migrations_state_action_machine.test.ts index c53bd7bbc53dd..3bc07c0fea0c1 100644 --- a/src/core/server/saved_objects/migrationsv2/migrations_state_action_machine.test.ts +++ b/src/core/server/saved_objects/migrations/migrations_state_action_machine.test.ts @@ -15,7 +15,7 @@ import * as Option from 'fp-ts/lib/Option'; import { errors } from '@elastic/elasticsearch'; import { elasticsearchClientMock } from '../../elasticsearch/client/mocks'; import { LoggerAdapter } from '../../logging/logger_adapter'; -import { AllControlStates, State } from './types'; +import { AllControlStates, State } from './state'; import { createInitialState } from './initial_state'; import { ByteSizeValue } from '@kbn/config-schema'; diff --git a/src/core/server/saved_objects/migrationsv2/migrations_state_action_machine.ts b/src/core/server/saved_objects/migrations/migrations_state_action_machine.ts similarity index 99% rename from src/core/server/saved_objects/migrationsv2/migrations_state_action_machine.ts rename to src/core/server/saved_objects/migrations/migrations_state_action_machine.ts index 3a5e592a8b9bf..87b78102371d3 100644 --- a/src/core/server/saved_objects/migrationsv2/migrations_state_action_machine.ts +++ b/src/core/server/saved_objects/migrations/migrations_state_action_machine.ts @@ -13,7 +13,7 @@ import type { ElasticsearchClient } from '../../elasticsearch'; import { getErrorMessage, getRequestDebugMeta } from '../../elasticsearch'; import { Model, Next, stateActionMachine } from './state_action_machine'; import { cleanup } from './migrations_state_machine_cleanup'; -import { ReindexSourceToTempTransform, ReindexSourceToTempIndexBulk, State } from './types'; +import { ReindexSourceToTempTransform, ReindexSourceToTempIndexBulk, State } from './state'; import { SavedObjectsRawDoc } from '../serialization'; interface StateTransitionLogMeta extends LogMeta { diff --git a/src/core/server/saved_objects/migrationsv2/migrations_state_machine_cleanup.mocks.ts b/src/core/server/saved_objects/migrations/migrations_state_machine_cleanup.mocks.ts similarity index 100% rename from src/core/server/saved_objects/migrationsv2/migrations_state_machine_cleanup.mocks.ts rename to src/core/server/saved_objects/migrations/migrations_state_machine_cleanup.mocks.ts diff --git a/src/core/server/saved_objects/migrationsv2/migrations_state_machine_cleanup.ts b/src/core/server/saved_objects/migrations/migrations_state_machine_cleanup.ts similarity index 94% rename from src/core/server/saved_objects/migrationsv2/migrations_state_machine_cleanup.ts rename to src/core/server/saved_objects/migrations/migrations_state_machine_cleanup.ts index 9c0ef0d1a2cb6..ff8ff57d41ce4 100644 --- a/src/core/server/saved_objects/migrationsv2/migrations_state_machine_cleanup.ts +++ b/src/core/server/saved_objects/migrations/migrations_state_machine_cleanup.ts @@ -8,7 +8,7 @@ import type { ElasticsearchClient } from '../../elasticsearch'; import * as Actions from './actions'; -import type { State } from './types'; +import type { State } from './state'; export async function cleanup(client: ElasticsearchClient, state?: State) { if (!state) return; diff --git a/src/core/server/saved_objects/migrationsv2/model/create_batches.test.ts b/src/core/server/saved_objects/migrations/model/create_batches.test.ts similarity index 100% rename from src/core/server/saved_objects/migrationsv2/model/create_batches.test.ts rename to src/core/server/saved_objects/migrations/model/create_batches.test.ts diff --git a/src/core/server/saved_objects/migrationsv2/model/create_batches.ts b/src/core/server/saved_objects/migrations/model/create_batches.ts similarity index 100% rename from src/core/server/saved_objects/migrationsv2/model/create_batches.ts rename to src/core/server/saved_objects/migrations/model/create_batches.ts diff --git a/src/core/server/saved_objects/migrationsv2/model/extract_errors.test.ts b/src/core/server/saved_objects/migrations/model/extract_errors.test.ts similarity index 100% rename from src/core/server/saved_objects/migrationsv2/model/extract_errors.test.ts rename to src/core/server/saved_objects/migrations/model/extract_errors.test.ts diff --git a/src/core/server/saved_objects/migrationsv2/model/extract_errors.ts b/src/core/server/saved_objects/migrations/model/extract_errors.ts similarity index 97% rename from src/core/server/saved_objects/migrationsv2/model/extract_errors.ts rename to src/core/server/saved_objects/migrations/model/extract_errors.ts index 082e6344afffc..3dabb09043376 100644 --- a/src/core/server/saved_objects/migrationsv2/model/extract_errors.ts +++ b/src/core/server/saved_objects/migrations/model/extract_errors.ts @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import { TransformErrorObjects } from '../../migrations/core'; +import { TransformErrorObjects } from '../core'; import { CheckForUnknownDocsFoundDoc } from '../actions'; /** diff --git a/src/core/server/saved_objects/migrations/model/helpers.ts b/src/core/server/saved_objects/migrations/model/helpers.ts new file mode 100644 index 0000000000000..c3a4c85679680 --- /dev/null +++ b/src/core/server/saved_objects/migrations/model/helpers.ts @@ -0,0 +1,90 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { gt, valid } from 'semver'; +import { State } from '../state'; +import { IndexMapping } from '../../mappings'; +import { FetchIndexResponse } from '../actions'; + +/** + * A helper function/type for ensuring that all control state's are handled. + */ +export function throwBadControlState(p: never): never; +export function throwBadControlState(controlState: any) { + throw new Error('Unexpected control state: ' + controlState); +} + +/** + * A helper function/type for ensuring that all response types are handled. + */ +export function throwBadResponse(state: State, p: never): never; +export function throwBadResponse(state: State, res: any): never { + throw new Error( + `${state.controlState} received unexpected action response: ` + JSON.stringify(res) + ); +} + +/** + * Merge the _meta.migrationMappingPropertyHashes mappings of an index with + * the given target mappings. + * + * @remarks When another instance already completed a migration, the existing + * target index might contain documents and mappings created by a plugin that + * is disabled in the current Kibana instance performing this migration. + * Mapping updates are commutative (deeply merged) by Elasticsearch, except + * for the `_meta` key. By merging the `_meta.migrationMappingPropertyHashes` + * mappings from the existing target index index into the targetMappings we + * ensure that any `migrationPropertyHashes` for disabled plugins aren't lost. + * + * Right now we don't use these `migrationPropertyHashes` but it could be used + * in the future to detect if mappings were changed. If mappings weren't + * changed we don't need to reindex but can clone the index to save disk space. + * + * @param targetMappings + * @param indexMappings + */ +export function mergeMigrationMappingPropertyHashes( + targetMappings: IndexMapping, + indexMappings: IndexMapping +) { + return { + ...targetMappings, + _meta: { + migrationMappingPropertyHashes: { + ...indexMappings._meta?.migrationMappingPropertyHashes, + ...targetMappings._meta?.migrationMappingPropertyHashes, + }, + }, + }; +} + +export function indexBelongsToLaterVersion(indexName: string, kibanaVersion: string): boolean { + const version = valid(indexVersion(indexName)); + return version != null ? gt(version, kibanaVersion) : false; +} + +/** + * Extracts the version number from a >= 7.11 index + * @param indexName A >= v7.11 index name + */ +export function indexVersion(indexName?: string): string | undefined { + return (indexName?.match(/.+_(\d+\.\d+\.\d+)_\d+/) || [])[1]; +} + +/** + * Creates a record of alias -> index name pairs + */ +export function getAliases(indices: FetchIndexResponse) { + return Object.keys(indices).reduce((acc, index) => { + Object.keys(indices[index].aliases || {}).forEach((alias) => { + // TODO throw if multiple .kibana aliases point to the same index? + acc[alias] = index; + }); + return acc; + }, {} as Record); +} diff --git a/src/core/server/saved_objects/migrationsv2/model/index.ts b/src/core/server/saved_objects/migrations/model/index.ts similarity index 100% rename from src/core/server/saved_objects/migrationsv2/model/index.ts rename to src/core/server/saved_objects/migrations/model/index.ts diff --git a/src/core/server/saved_objects/migrationsv2/model/model.test.ts b/src/core/server/saved_objects/migrations/model/model.test.ts similarity index 99% rename from src/core/server/saved_objects/migrationsv2/model/model.test.ts rename to src/core/server/saved_objects/migrations/model/model.test.ts index e4ab5a0f11039..7cd5f63640d1d 100644 --- a/src/core/server/saved_objects/migrationsv2/model/model.test.ts +++ b/src/core/server/saved_objects/migrations/model/model.test.ts @@ -40,7 +40,7 @@ import type { ReindexSourceToTempIndexBulk, CheckUnknownDocumentsState, CalculateExcludeFiltersState, -} from '../types'; +} from '../state'; import { SavedObjectsRawDoc } from '../../serialization'; import { TransformErrorObjects, TransformSavedObjectDocumentError } from '../../migrations/core'; import { AliasAction, RetryableEsClientError } from '../actions'; diff --git a/src/core/server/saved_objects/migrationsv2/model/model.ts b/src/core/server/saved_objects/migrations/model/model.ts similarity index 99% rename from src/core/server/saved_objects/migrationsv2/model/model.ts rename to src/core/server/saved_objects/migrations/model/model.ts index ff27045dd91ce..522a43a737cb7 100644 --- a/src/core/server/saved_objects/migrationsv2/model/model.ts +++ b/src/core/server/saved_objects/migrations/model/model.ts @@ -8,12 +8,13 @@ import * as Either from 'fp-ts/lib/Either'; import * as Option from 'fp-ts/lib/Option'; - import * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; + import { AliasAction, isLeftTypeof } from '../actions'; -import { AllActionStates, MigrationLog, State } from '../types'; +import { MigrationLog } from '../types'; +import { AllActionStates, State } from '../state'; import type { ResponseType } from '../next'; -import { disableUnknownTypeMappingFields } from '../../migrations/core/migration_context'; +import { disableUnknownTypeMappingFields } from '../core'; import { createInitialProgress, incrementProcessedProgress, diff --git a/src/core/server/saved_objects/migrationsv2/model/progress.test.ts b/src/core/server/saved_objects/migrations/model/progress.test.ts similarity index 100% rename from src/core/server/saved_objects/migrationsv2/model/progress.test.ts rename to src/core/server/saved_objects/migrations/model/progress.test.ts diff --git a/src/core/server/saved_objects/migrationsv2/model/progress.ts b/src/core/server/saved_objects/migrations/model/progress.ts similarity index 100% rename from src/core/server/saved_objects/migrationsv2/model/progress.ts rename to src/core/server/saved_objects/migrations/model/progress.ts diff --git a/src/core/server/saved_objects/migrationsv2/model/retry_state.test.ts b/src/core/server/saved_objects/migrations/model/retry_state.test.ts similarity index 99% rename from src/core/server/saved_objects/migrationsv2/model/retry_state.test.ts rename to src/core/server/saved_objects/migrations/model/retry_state.test.ts index d49e570e0cdef..5a195f8597182 100644 --- a/src/core/server/saved_objects/migrationsv2/model/retry_state.test.ts +++ b/src/core/server/saved_objects/migrations/model/retry_state.test.ts @@ -7,7 +7,7 @@ */ import { resetRetryState, delayRetryState } from './retry_state'; -import { State } from '../types'; +import { State } from '../state'; const createState = (parts: Record) => { return parts as State; diff --git a/src/core/server/saved_objects/migrationsv2/model/retry_state.ts b/src/core/server/saved_objects/migrations/model/retry_state.ts similarity index 97% rename from src/core/server/saved_objects/migrationsv2/model/retry_state.ts rename to src/core/server/saved_objects/migrations/model/retry_state.ts index 5d69d32a7160c..02057a6af2061 100644 --- a/src/core/server/saved_objects/migrationsv2/model/retry_state.ts +++ b/src/core/server/saved_objects/migrations/model/retry_state.ts @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import { State } from '../types'; +import { State } from '../state'; export const delayRetryState = ( state: S, diff --git a/src/core/server/saved_objects/migrationsv2/model/types.ts b/src/core/server/saved_objects/migrations/model/types.ts similarity index 100% rename from src/core/server/saved_objects/migrationsv2/model/types.ts rename to src/core/server/saved_objects/migrations/model/types.ts diff --git a/src/core/server/saved_objects/migrationsv2/next.test.ts b/src/core/server/saved_objects/migrations/next.test.ts similarity index 96% rename from src/core/server/saved_objects/migrationsv2/next.test.ts rename to src/core/server/saved_objects/migrations/next.test.ts index a34480fc311cd..98a8690844872 100644 --- a/src/core/server/saved_objects/migrationsv2/next.test.ts +++ b/src/core/server/saved_objects/migrations/next.test.ts @@ -8,7 +8,7 @@ import { ElasticsearchClient } from '../../elasticsearch'; import { next } from './next'; -import { State } from './types'; +import { State } from './state'; describe('migrations v2 next', () => { it.todo('when state.retryDelay > 0 delays execution of the next action'); diff --git a/src/core/server/saved_objects/migrationsv2/next.ts b/src/core/server/saved_objects/migrations/next.ts similarity index 99% rename from src/core/server/saved_objects/migrationsv2/next.ts rename to src/core/server/saved_objects/migrations/next.ts index 433c0998f7567..1368ca308110d 100644 --- a/src/core/server/saved_objects/migrationsv2/next.ts +++ b/src/core/server/saved_objects/migrations/next.ts @@ -31,7 +31,6 @@ import type { CloneTempToSource, SetTempWriteBlock, WaitForYellowSourceState, - TransformRawDocs, TransformedDocumentsBulkIndex, ReindexSourceToTempIndexBulk, OutdatedDocumentsSearchOpenPit, @@ -41,7 +40,8 @@ import type { OutdatedDocumentsRefresh, CheckUnknownDocumentsState, CalculateExcludeFiltersState, -} from './types'; +} from './state'; +import { TransformRawDocs } from './types'; import * as Actions from './actions'; import { ElasticsearchClient } from '../../elasticsearch'; diff --git a/src/core/server/saved_objects/migrationsv2/index.ts b/src/core/server/saved_objects/migrations/run_resilient_migrator.ts similarity index 100% rename from src/core/server/saved_objects/migrationsv2/index.ts rename to src/core/server/saved_objects/migrations/run_resilient_migrator.ts diff --git a/src/core/server/saved_objects/migrations/state.ts b/src/core/server/saved_objects/migrations/state.ts new file mode 100644 index 0000000000000..7073167bfbd1b --- /dev/null +++ b/src/core/server/saved_objects/migrations/state.ts @@ -0,0 +1,448 @@ +/* + * Copyright 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 Option from 'fp-ts/lib/Option'; +import * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; +import { ControlState } from './state_action_machine'; +import { AliasAction } from './actions'; +import { IndexMapping } from '../mappings'; +import { SavedObjectsRawDoc } from '..'; +import { TransformErrorObjects } from '../migrations/core'; +import { SavedObjectTypeExcludeFromUpgradeFilterHook } from '../types'; +import { MigrationLog, Progress } from './types'; + +export interface BaseState extends ControlState { + /** The first part of the index name such as `.kibana` or `.kibana_task_manager` */ + readonly indexPrefix: string; + /** + * The name of the concrete legacy index (if it exists) e.g. `.kibana` for < + * 6.5 or `.kibana_task_manager` for < 7.4 + */ + readonly legacyIndex: string; + /** Kibana version number */ + readonly kibanaVersion: string; + /** The mappings to apply to the target index */ + readonly targetIndexMappings: IndexMapping; + /** + * Special mappings set when creating the temp index into which we reindex. + * These mappings have `dynamic: false` to allow for any kind of outdated + * document to be written to the index, but still define mappings for the + * `migrationVersion` and `type` fields so that we can search for and + * transform outdated documents. + */ + readonly tempIndexMappings: IndexMapping; + /** Script to apply to a legacy index before it can be used as a migration source */ + readonly preMigrationScript: Option.Option; + readonly outdatedDocumentsQuery: estypes.QueryDslQueryContainer; + 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; + + /** + * The number of documents to process in each batch. This determines the + * maximum number of documents that will be read and written in a single + * request. + * + * The higher the value, the faster the migration process will be performed + * since it reduces the number of round trips between Kibana and + * Elasticsearch servers. For the migration speed, we have to pay the price + * of increased memory consumption and HTTP payload size. + * + * Since we cannot control the size in bytes of a batch when reading, + * Elasticsearch might fail with a circuit_breaking_exception when it + * retrieves a set of saved objects of significant size. In this case, you + * should set a smaller batchSize value and restart the migration process + * again. + * + * When writing batches, we limit the number of documents in a batch + * (batchSize) as well as the size of the batch in bytes (maxBatchSizeBytes). + */ + readonly batchSize: number; + /** + * When writing batches, limits the batch size in bytes to ensure that we + * don't construct HTTP requests which would exceed Elasticsearch's + * http.max_content_length which defaults to 100mb. + */ + readonly maxBatchSizeBytes: number; + readonly logs: MigrationLog[]; + /** + * The current alias e.g. `.kibana` which always points to the latest + * version index + */ + readonly currentAlias: string; + /** + * The version alias e.g. `.kibana_7.11.0` which points to the index used + * by this version of Kibana e.g. `.kibana_7.11.0_001` + */ + readonly versionAlias: string; + /** + * The index used by this version of Kibana e.g. `.kibana_7.11.0_001` + */ + readonly versionIndex: string; + /** + * An alias on the target index used as part of an "reindex block" that + * prevents lost deletes e.g. `.kibana_7.11.0_reindex`. + */ + readonly tempIndex: string; + /** + * When reindexing we use a source query to exclude saved objects types which + * are no longer used. These saved objects will still be kept in the outdated + * index for backup purposes, but won't be available in the upgraded index. + */ + readonly unusedTypesQuery: estypes.QueryDslQueryContainer; + /** + * The list of known SO types that are registered. + */ + readonly knownTypes: string[]; + /** + * All exclude filter hooks registered for types on this index. Keyed by type name. + */ + readonly excludeFromUpgradeFilterHooks: Record< + string, + SavedObjectTypeExcludeFromUpgradeFilterHook + >; +} + +export interface InitState extends BaseState { + readonly controlState: 'INIT'; +} + +export interface PostInitState extends BaseState { + /** + * The source index is the index from which the migration reads. If the + * Option is a none, we didn't do any migration from a source index, either: + * - this is a blank ES cluster and we will perform the CREATE_NEW_TARGET + * step + * - another Kibana instance already did the source migration and finished + * the MARK_VERSION_INDEX_READY step + */ + readonly sourceIndex: Option.Option; + /** The target index is the index to which the migration writes */ + readonly targetIndex: string; + readonly versionIndexReadyActions: Option.Option; + readonly outdatedDocumentsQuery: estypes.QueryDslQueryContainer; +} + +export interface DoneState extends PostInitState { + /** Migration completed successfully */ + readonly controlState: 'DONE'; +} + +export interface FatalState extends BaseState { + /** Migration terminated with a failure */ + readonly controlState: 'FATAL'; + /** The reason the migration was terminated */ + readonly reason: string; +} + +export interface WaitForYellowSourceState extends BaseState { + /** Wait for the source index to be yellow before requesting it. */ + readonly controlState: 'WAIT_FOR_YELLOW_SOURCE'; + readonly sourceIndex: Option.Some; + readonly sourceIndexMappings: IndexMapping; +} + +export interface CheckUnknownDocumentsState extends BaseState { + /** Check if any unknown document is present in the source index */ + readonly controlState: 'CHECK_UNKNOWN_DOCUMENTS'; + readonly sourceIndex: Option.Some; + readonly sourceIndexMappings: IndexMapping; +} + +export interface SetSourceWriteBlockState extends PostInitState { + /** Set a write block on the source index to prevent any further writes */ + readonly controlState: 'SET_SOURCE_WRITE_BLOCK'; + readonly sourceIndex: Option.Some; +} + +export interface CalculateExcludeFiltersState extends PostInitState { + readonly controlState: 'CALCULATE_EXCLUDE_FILTERS'; + readonly sourceIndex: Option.Some; +} + +export interface CreateNewTargetState extends PostInitState { + /** Blank ES cluster, create a new version-specific target index */ + readonly controlState: 'CREATE_NEW_TARGET'; + readonly sourceIndex: Option.None; + readonly versionIndexReadyActions: Option.Some; +} + +export interface CreateReindexTempState extends PostInitState { + /** + * Create a target index with mappings from the source index and registered + * plugins + */ + readonly controlState: 'CREATE_REINDEX_TEMP'; + readonly sourceIndex: Option.Some; +} + +export interface ReindexSourceToTempOpenPit extends PostInitState { + /** Open PIT to the source index */ + readonly controlState: 'REINDEX_SOURCE_TO_TEMP_OPEN_PIT'; + readonly sourceIndex: Option.Some; +} + +export interface ReindexSourceToTempRead extends PostInitState { + readonly controlState: 'REINDEX_SOURCE_TO_TEMP_READ'; + readonly sourceIndexPitId: string; + readonly lastHitSortValue: number[] | undefined; + readonly corruptDocumentIds: string[]; + readonly transformErrors: TransformErrorObjects[]; + readonly progress: Progress; +} + +export interface ReindexSourceToTempClosePit extends PostInitState { + readonly controlState: 'REINDEX_SOURCE_TO_TEMP_CLOSE_PIT'; + readonly sourceIndexPitId: string; +} + +export interface ReindexSourceToTempTransform extends PostInitState { + readonly controlState: 'REINDEX_SOURCE_TO_TEMP_TRANSFORM'; + readonly outdatedDocuments: SavedObjectsRawDoc[]; + readonly sourceIndexPitId: string; + readonly lastHitSortValue: number[] | undefined; + readonly corruptDocumentIds: string[]; + readonly transformErrors: TransformErrorObjects[]; + readonly progress: Progress; +} + +export interface ReindexSourceToTempIndexBulk extends PostInitState { + readonly controlState: 'REINDEX_SOURCE_TO_TEMP_INDEX_BULK'; + readonly transformedDocBatches: [SavedObjectsRawDoc[]]; + readonly currentBatch: number; + readonly sourceIndexPitId: string; + readonly lastHitSortValue: number[] | undefined; + readonly progress: Progress; +} + +export type SetTempWriteBlock = PostInitState & { + /** + * + */ + readonly controlState: 'SET_TEMP_WRITE_BLOCK'; + readonly sourceIndex: Option.Some; +}; + +export interface CloneTempToSource extends PostInitState { + /** + * Clone the temporary reindex index into + */ + readonly controlState: 'CLONE_TEMP_TO_TARGET'; + readonly sourceIndex: Option.Some; +} + +export interface RefreshTarget extends PostInitState { + /** Refresh temp index before searching for outdated docs */ + readonly controlState: 'REFRESH_TARGET'; + readonly targetIndex: string; +} + +export interface UpdateTargetMappingsState extends PostInitState { + /** Update the mappings of the target index */ + readonly controlState: 'UPDATE_TARGET_MAPPINGS'; +} + +export interface UpdateTargetMappingsWaitForTaskState extends PostInitState { + /** Update the mappings of the target index */ + readonly controlState: 'UPDATE_TARGET_MAPPINGS_WAIT_FOR_TASK'; + readonly updateTargetMappingsTaskId: string; +} + +export interface OutdatedDocumentsSearchOpenPit extends PostInitState { + /** Open PiT for target index to search for outdated documents */ + readonly controlState: 'OUTDATED_DOCUMENTS_SEARCH_OPEN_PIT'; +} + +export interface OutdatedDocumentsSearchRead extends PostInitState { + /** Search for outdated documents in the target index */ + readonly controlState: 'OUTDATED_DOCUMENTS_SEARCH_READ'; + readonly pitId: string; + readonly lastHitSortValue: number[] | undefined; + readonly hasTransformedDocs: boolean; + readonly corruptDocumentIds: string[]; + readonly transformErrors: TransformErrorObjects[]; + readonly progress: Progress; +} + +export interface OutdatedDocumentsSearchClosePit extends PostInitState { + /** Close PiT for target index when found all outdated documents */ + readonly controlState: 'OUTDATED_DOCUMENTS_SEARCH_CLOSE_PIT'; + readonly pitId: string; + readonly hasTransformedDocs: boolean; +} + +export interface OutdatedDocumentsRefresh extends PostInitState { + /** Reindex transformed documents */ + readonly controlState: 'OUTDATED_DOCUMENTS_REFRESH'; + readonly targetIndex: string; +} + +export interface OutdatedDocumentsTransform extends PostInitState { + /** Transform a batch of outdated documents to their latest version*/ + readonly controlState: 'OUTDATED_DOCUMENTS_TRANSFORM'; + readonly pitId: string; + readonly outdatedDocuments: SavedObjectsRawDoc[]; + readonly lastHitSortValue: number[] | undefined; + readonly hasTransformedDocs: boolean; + readonly corruptDocumentIds: string[]; + readonly transformErrors: TransformErrorObjects[]; + readonly progress: Progress; +} + +export interface TransformedDocumentsBulkIndex extends PostInitState { + /** + * Write the up-to-date transformed documents to the target index + */ + readonly controlState: 'TRANSFORMED_DOCUMENTS_BULK_INDEX'; + readonly transformedDocBatches: SavedObjectsRawDoc[][]; + readonly currentBatch: number; + readonly lastHitSortValue: number[] | undefined; + readonly hasTransformedDocs: boolean; + readonly pitId: string; + readonly progress: Progress; +} + +export interface MarkVersionIndexReady extends PostInitState { + /** + * Marks the version-specific index as ready. Once this step is complete, + * future Kibana instances will not have to prepare a target index by e.g. + * cloning a source index or creating a new index. + * + * To account for newly installed or enabled plugins, Kibana will still + * perform the `UPDATE_TARGET_MAPPINGS*` and `OUTDATED_DOCUMENTS_*` steps + * every time it is restarted. + */ + readonly controlState: 'MARK_VERSION_INDEX_READY'; + readonly versionIndexReadyActions: Option.Some; +} + +export interface MarkVersionIndexReadyConflict extends PostInitState { + /** + * If the MARK_VERSION_INDEX_READY step fails another instance was + * performing the migration in parallel and won the race to marking the + * migration as complete. This step ensures that the instance that won the + * race is running the same version of Kibana, if it does, the migration is + * complete and we can go to DONE. + * + * If it was a different version of Kibana that completed the migration fail + * the migration by going to FATAL. If this instance restarts it will either + * notice that a newer version already completed the migration and refuse to + * start up, or if it was an older version that completed the migration + * start a new migration to the latest version. + */ + readonly controlState: 'MARK_VERSION_INDEX_READY_CONFLICT'; +} + +/** + * If we're migrating from a legacy index we need to perform some additional + * steps to prepare this index so that it can be used as a migration 'source'. + */ +export interface LegacyBaseState extends PostInitState { + readonly sourceIndex: Option.Some; + readonly legacyPreMigrationDoneActions: AliasAction[]; + /** + * The mappings read from the legacy index, used to create a new reindex + * target index. + */ + readonly legacyReindexTargetMappings: IndexMapping; +} + +export interface LegacySetWriteBlockState extends LegacyBaseState { + /** Set a write block on the legacy index to prevent any further writes */ + readonly controlState: 'LEGACY_SET_WRITE_BLOCK'; +} + +export interface LegacyCreateReindexTargetState extends LegacyBaseState { + /** + * Create a new index into which we can reindex the legacy index. This + * index will have the same mappings as the legacy index. Once the legacy + * pre-migration is complete, this index will be used a migration 'source'. + */ + readonly controlState: 'LEGACY_CREATE_REINDEX_TARGET'; +} + +export interface LegacyReindexState extends LegacyBaseState { + /** + * Reindex the legacy index into the new index created in the + * LEGACY_CREATE_REINDEX_TARGET step (and apply the preMigration script). + */ + readonly controlState: 'LEGACY_REINDEX'; +} + +export interface LegacyReindexWaitForTaskState extends LegacyBaseState { + /** Wait for the reindex operation to complete */ + readonly controlState: 'LEGACY_REINDEX_WAIT_FOR_TASK'; + readonly legacyReindexTaskId: string; +} + +export interface LegacyDeleteState extends LegacyBaseState { + /** + * After reindexed has completed, delete the legacy index so that it won't + * conflict with the `currentAlias` that we want to create in a later step + * e.g. `.kibana`. + */ + readonly controlState: 'LEGACY_DELETE'; +} + +export type State = Readonly< + | FatalState + | InitState + | DoneState + | WaitForYellowSourceState + | CheckUnknownDocumentsState + | SetSourceWriteBlockState + | CalculateExcludeFiltersState + | CreateNewTargetState + | CreateReindexTempState + | ReindexSourceToTempOpenPit + | ReindexSourceToTempRead + | ReindexSourceToTempClosePit + | ReindexSourceToTempTransform + | ReindexSourceToTempIndexBulk + | SetTempWriteBlock + | CloneTempToSource + | UpdateTargetMappingsState + | UpdateTargetMappingsWaitForTaskState + | OutdatedDocumentsSearchOpenPit + | OutdatedDocumentsSearchRead + | OutdatedDocumentsSearchClosePit + | OutdatedDocumentsTransform + | RefreshTarget + | OutdatedDocumentsRefresh + | MarkVersionIndexReady + | MarkVersionIndexReadyConflict + | TransformedDocumentsBulkIndex + | LegacyCreateReindexTargetState + | LegacySetWriteBlockState + | LegacyReindexState + | LegacyReindexWaitForTaskState + | LegacyDeleteState +>; + +export type AllControlStates = State['controlState']; +/** + * All control states that trigger an action (excludes the terminal states + * 'FATAL' and 'DONE'). + */ +export type AllActionStates = Exclude; diff --git a/src/core/server/saved_objects/migrationsv2/state_action_machine.test.ts b/src/core/server/saved_objects/migrations/state_action_machine.test.ts similarity index 100% rename from src/core/server/saved_objects/migrationsv2/state_action_machine.test.ts rename to src/core/server/saved_objects/migrations/state_action_machine.test.ts diff --git a/src/core/server/saved_objects/migrationsv2/state_action_machine.ts b/src/core/server/saved_objects/migrations/state_action_machine.ts similarity index 100% rename from src/core/server/saved_objects/migrationsv2/state_action_machine.ts rename to src/core/server/saved_objects/migrations/state_action_machine.ts diff --git a/src/core/server/saved_objects/migrationsv2/test_helpers/retry.test.ts b/src/core/server/saved_objects/migrations/test_helpers/retry.test.ts similarity index 100% rename from src/core/server/saved_objects/migrationsv2/test_helpers/retry.test.ts rename to src/core/server/saved_objects/migrations/test_helpers/retry.test.ts diff --git a/src/core/server/saved_objects/migrationsv2/test_helpers/retry_async.ts b/src/core/server/saved_objects/migrations/test_helpers/retry_async.ts similarity index 100% rename from src/core/server/saved_objects/migrationsv2/test_helpers/retry_async.ts rename to src/core/server/saved_objects/migrations/test_helpers/retry_async.ts diff --git a/src/core/server/saved_objects/migrations/types.ts b/src/core/server/saved_objects/migrations/types.ts index fe5a79dac12c3..a52ba56bc8ff6 100644 --- a/src/core/server/saved_objects/migrations/types.ts +++ b/src/core/server/saved_objects/migrations/types.ts @@ -6,8 +6,11 @@ * Side Public License, v 1. */ -import { SavedObjectUnsanitizedDoc } from '../serialization'; -import { SavedObjectsMigrationLogger } from './core/migration_logger'; +import * as TaskEither from 'fp-ts/TaskEither'; +import type { SavedObjectUnsanitizedDoc } from '../serialization'; +import type { SavedObjectsMigrationLogger } from './core'; +import { SavedObjectsRawDoc } from '../serialization'; +import { DocumentsTransformFailed, DocumentsTransformSuccess } from './core'; /** * A migration function for a {@link SavedObjectsType | saved object type} @@ -91,3 +94,23 @@ export interface SavedObjectMigrationContext { export interface SavedObjectMigrationMap { [version: string]: SavedObjectMigrationFn; } + +/** @internal */ +export type TransformRawDocs = ( + rawDocs: SavedObjectsRawDoc[] +) => TaskEither.TaskEither; + +/** @internal */ +export type MigrationLogLevel = 'error' | 'info' | 'warning'; + +/** @internal */ +export interface MigrationLog { + level: MigrationLogLevel; + message: string; +} + +/** @internal */ +export interface Progress { + processed: number | undefined; + total: number | undefined; +} diff --git a/src/core/server/saved_objects/migrationsv2/README.md b/src/core/server/saved_objects/migrationsv2/README.md deleted file mode 100644 index 60bf84eef87a6..0000000000000 --- a/src/core/server/saved_objects/migrationsv2/README.md +++ /dev/null @@ -1,504 +0,0 @@ -- [Introduction](#introduction) -- [Algorithm steps](#algorithm-steps) - - [INIT](#init) - - [Next action](#next-action) - - [New control state](#new-control-state) - - [CREATE_NEW_TARGET](#create_new_target) - - [Next action](#next-action-1) - - [New control state](#new-control-state-1) - - [LEGACY_SET_WRITE_BLOCK](#legacy_set_write_block) - - [Next action](#next-action-2) - - [New control state](#new-control-state-2) - - [LEGACY_CREATE_REINDEX_TARGET](#legacy_create_reindex_target) - - [Next action](#next-action-3) - - [New control state](#new-control-state-3) - - [LEGACY_REINDEX](#legacy_reindex) - - [Next action](#next-action-4) - - [New control state](#new-control-state-4) - - [LEGACY_REINDEX_WAIT_FOR_TASK](#legacy_reindex_wait_for_task) - - [Next action](#next-action-5) - - [New control state](#new-control-state-5) - - [LEGACY_DELETE](#legacy_delete) - - [Next action](#next-action-6) - - [New control state](#new-control-state-6) - - [WAIT_FOR_YELLOW_SOURCE](#wait_for_yellow_source) - - [Next action](#next-action-7) - - [New control state](#new-control-state-7) - - [SET_SOURCE_WRITE_BLOCK](#set_source_write_block) - - [Next action](#next-action-8) - - [New control state](#new-control-state-8) - - [CREATE_REINDEX_TEMP](#create_reindex_temp) - - [Next action](#next-action-9) - - [New control state](#new-control-state-9) - - [REINDEX_SOURCE_TO_TEMP_OPEN_PIT](#reindex_source_to_temp_open_pit) - - [Next action](#next-action-10) - - [New control state](#new-control-state-10) - - [REINDEX_SOURCE_TO_TEMP_READ](#reindex_source_to_temp_read) - - [Next action](#next-action-11) - - [New control state](#new-control-state-11) - - [REINDEX_SOURCE_TO_TEMP_TRANSFORM](#REINDEX_SOURCE_TO_TEMP_TRANSFORM) - - [Next action](#next-action-12) - - [New control state](#new-control-state-12) - - [REINDEX_SOURCE_TO_TEMP_INDEX_BULK](#reindex_source_to_temp_index_bulk) - - [Next action](#next-action-13) - - [New control state](#new-control-state-13) - - [REINDEX_SOURCE_TO_TEMP_CLOSE_PIT](#reindex_source_to_temp_close_pit) - - [Next action](#next-action-14) - - [New control state](#new-control-state-14) - - [SET_TEMP_WRITE_BLOCK](#set_temp_write_block) - - [Next action](#next-action-15) - - [New control state](#new-control-state-15) - - [CLONE_TEMP_TO_TARGET](#clone_temp_to_target) - - [Next action](#next-action-16) - - [New control state](#new-control-state-16) - - [OUTDATED_DOCUMENTS_SEARCH](#outdated_documents_search) - - [Next action](#next-action-17) - - [New control state](#new-control-state-17) - - [OUTDATED_DOCUMENTS_TRANSFORM](#outdated_documents_transform) - - [Next action](#next-action-18) - - [New control state](#new-control-state-18) - - [UPDATE_TARGET_MAPPINGS](#update_target_mappings) - - [Next action](#next-action-19) - - [New control state](#new-control-state-19) - - [UPDATE_TARGET_MAPPINGS_WAIT_FOR_TASK](#update_target_mappings_wait_for_task) - - [Next action](#next-action-20) - - [New control state](#new-control-state-20) - - [MARK_VERSION_INDEX_READY_CONFLICT](#mark_version_index_ready_conflict) - - [Next action](#next-action-21) - - [New control state](#new-control-state-21) -- [Manual QA Test Plan](#manual-qa-test-plan) - - [1. Legacy pre-migration](#1-legacy-pre-migration) - - [2. Plugins enabled/disabled](#2-plugins-enableddisabled) - - [Test scenario 1 (enable a plugin after migration):](#test-scenario-1-enable-a-plugin-after-migration) - - [Test scenario 2 (disable a plugin after migration):](#test-scenario-2-disable-a-plugin-after-migration) - - [Test scenario 3 (multiple instances, enable a plugin after migration):](#test-scenario-3-multiple-instances-enable-a-plugin-after-migration) - - [Test scenario 4 (multiple instances, mixed plugin enabled configs):](#test-scenario-4-multiple-instances-mixed-plugin-enabled-configs) - -# Introduction -In the past, the risk of downtime caused by Kibana's saved object upgrade -migrations have discouraged users from adopting the latest features. v2 -migrations aims to solve this problem by minimizing the operational impact on -our users. - -To achieve this it uses a new migration algorithm where every step of the -algorithm is idempotent. No matter at which step a Kibana instance gets -interrupted, it can always restart the migration from the beginning and repeat -all the steps without requiring any user intervention. This doesn't mean -migrations will never fail, but when they fail for intermittent reasons like -an Elasticsearch cluster running out of heap, Kibana will automatically be -able to successfully complete the migration once the cluster has enough heap. - -For more background information on the problem see the [saved object -migrations -RFC](https://github.com/elastic/kibana/blob/main/rfcs/text/0013_saved_object_migrations.md). - -# Algorithm steps -The design goals for the algorithm was to keep downtime below 10 minutes for -100k saved objects while guaranteeing no data loss and keeping steps as simple -and explicit as possible. - -The algorithm is implemented as a state-action machine based on https://www.microsoft.com/en-us/research/uploads/prod/2016/12/Computation-and-State-Machines.pdf - -The state-action machine defines it's behaviour in steps. Each step is a -transition from a control state s_i to the contral state s_i+1 caused by an -action a_i. - -``` -s_i -> a_i -> s_i+1 -s_i+1 -> a_i+1 -> s_i+2 -``` - -Given a control state s1, `next(s1)` returns the next action to execute. -Actions are asynchronous, once the action resolves, we can use the action -response to determine the next state to transition to as defined by the -function `model(state, response)`. - -We can then loosely define a step as: -``` -s_i+1 = model(s_i, await next(s_i)()) -``` - -When there are no more actions returned by `next` the state-action machine -terminates such as in the DONE and FATAL control states. - -What follows is a list of all control states. For each control state the -following is described: - - _next action_: the next action triggered by the current control state - - _new control state_: based on the action response, the possible new control states that the machine will transition to - -Since the algorithm runs once for each saved object index the steps below -always reference a single saved object index `.kibana`. When Kibana starts up, -all the steps are also repeated for the `.kibana_task_manager` index but this -is left out of the description for brevity. - -## INIT -### Next action -`fetchIndices` - -Fetch the saved object indices, mappings and aliases to find the source index -and determine whether we’re migrating from a legacy index or a v1 migrations -index. - -### New control state -1. If `.kibana` and the version specific aliases both exists and are pointing -to the same index. This version's migration has already been completed. Since -the same version could have plugins enabled at any time that would introduce -new transforms or mappings. - → `OUTDATED_DOCUMENTS_SEARCH` - -2. If `.kibana` is pointing to an index that belongs to a later version of -Kibana .e.g. a 7.11.0 instance found the `.kibana` alias pointing to -`.kibana_7.12.0_001` fail the migration - → `FATAL` - -3. If the `.kibana` alias exists we’re migrating from either a v1 or v2 index -and the migration source index is the index the `.kibana` alias points to. - → `WAIT_FOR_YELLOW_SOURCE` - -4. If `.kibana` is a concrete index, we’re migrating from a legacy index - → `LEGACY_SET_WRITE_BLOCK` - -5. If there are no `.kibana` indices, this is a fresh deployment. Initialize a - new saved objects index - → `CREATE_NEW_TARGET` - -## CREATE_NEW_TARGET -### Next action -`createIndex` - -Create the target index. This operation is idempotent, if the index already exist, we wait until its status turns yellow - -### New control state - → `MARK_VERSION_INDEX_READY` - -## LEGACY_SET_WRITE_BLOCK -### Next action -`setWriteBlock` - -Set a write block on the legacy index to prevent any older Kibana instances -from writing to the index while the migration is in progress which could cause -lost acknowledged writes. - -This is the first of a series of `LEGACY_*` control states that will: - - reindex the concrete legacy `.kibana` index into a `.kibana_pre6.5.0_001` index - - delete the concrete `.kibana` _index_ so that we're able to create a `.kibana` _alias_ - -### New control state -1. If the write block was successfully added - → `LEGACY_CREATE_REINDEX_TARGET` -2. 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. - → `LEGACY_CREATE_REINDEX_TARGET` - -## LEGACY_CREATE_REINDEX_TARGET -### Next action -`createIndex` - -Create a new `.kibana_pre6.5.0_001` index into which we can reindex the legacy -index. (Since the task manager index was converted from a data index into a -saved objects index in 7.4 it will be reindexed into `.kibana_pre7.4.0_001`) -### New control state - → `LEGACY_REINDEX` - -## LEGACY_REINDEX -### Next action -`reindex` - -Let Elasticsearch reindex the legacy index into `.kibana_pre6.5.0_001`. (For -the task manager index we specify a `preMigrationScript` to convert the -original task manager documents into valid saved objects) -### New control state - → `LEGACY_REINDEX_WAIT_FOR_TASK` - - -## LEGACY_REINDEX_WAIT_FOR_TASK -### Next action -`waitForReindexTask` - -Wait for up to 60s for the reindex task to complete. -### New control state -1. If the reindex task completed - → `LEGACY_DELETE` -2. If the reindex task failed with a `target_index_had_write_block` or - `index_not_found_exception` another instance already completed this step - → `LEGACY_DELETE` -3. If the reindex task is still in progress - → `LEGACY_REINDEX_WAIT_FOR_TASK` - -## LEGACY_DELETE -### Next action -`updateAliases` - -Use the updateAliases API to atomically remove the legacy index and create a -new `.kibana` alias that points to `.kibana_pre6.5.0_001`. -### New control state -1. If the action succeeds - → `SET_SOURCE_WRITE_BLOCK` -2. If the action fails with `remove_index_not_a_concrete_index` or - `index_not_found_exception` another instance has already completed this step. - → `SET_SOURCE_WRITE_BLOCK` - -## WAIT_FOR_YELLOW_SOURCE -### Next action -`waitForIndexStatusYellow` - -Wait for the Elasticsearch cluster to be in "yellow" state. It means the index's primary shard is allocated and the index is ready for searching/indexing documents, but ES wasn't able to allocate the replicas. -We don't have as much data redundancy as we could have, but it's enough to start the migration. - -### New control state - → `SET_SOURCE_WRITE_BLOCK` - -## SET_SOURCE_WRITE_BLOCK -### Next action -`setWriteBlock` - -Set a write block on the source index to prevent any older Kibana instances from writing to the index while the migration is in progress which could cause lost acknowledged writes. - -### New control state - → `CREATE_REINDEX_TEMP` - -## CREATE_REINDEX_TEMP -### Next action -`createIndex` - -This operation is idempotent, if the index already exist, we wait until its status turns yellow. - -- Because we will be transforming documents before writing them into this index, we can already set the mappings to the target mappings for this version. The source index might contain documents belonging to a disabled plugin. So set `dynamic: false` mappings for any unknown saved object types. -- (Since we never query the temporary index we can potentially disable refresh to speed up indexing performance. Profile to see if gains justify complexity) - -### New control state - → `REINDEX_SOURCE_TO_TEMP_OPEN_PIT` - -## REINDEX_SOURCE_TO_TEMP_OPEN_PIT -### Next action -`openPIT` - -Open a PIT. Since there is a write block on the source index there is basically no overhead to keeping the PIT so we can lean towards a larger `keep_alive` value like 10 minutes. -### New control state - → `REINDEX_SOURCE_TO_TEMP_READ` - -## REINDEX_SOURCE_TO_TEMP_READ -### Next action -`readNextBatchOfSourceDocuments` - -Read the next batch of outdated documents from the source index by using search after with our PIT. - -### New control state -1. If the batch contained > 0 documents - → `REINDEX_SOURCE_TO_TEMP_TRANSFORM` -2. If there are no more documents returned - → `REINDEX_SOURCE_TO_TEMP_CLOSE_PIT` - -## REINDEX_SOURCE_TO_TEMP_TRANSFORM -### Next action -`transformRawDocs` - -Transform the current batch of documents - -In order to support sharing saved objects to multiple spaces in 8.0, the -transforms will also regenerate document `_id`'s. To ensure that this step -remains idempotent, the new `_id` is deterministically generated using UUIDv5 -ensuring that each Kibana instance generates the same new `_id` for the same document. -### New control state - → `REINDEX_SOURCE_TO_TEMP_INDEX_BULK` -## REINDEX_SOURCE_TO_TEMP_INDEX_BULK -### Next action -`bulkIndexTransformedDocuments` - -Use the bulk API create action to write a batch of up-to-date documents. The -create action ensures that there will be only one write per reindexed document -even if multiple Kibana instances are performing this step. Use -`refresh=false` to speed up the create actions, the `UPDATE_TARGET_MAPPINGS` -step will ensure that the index is refreshed before we start serving traffic. - -The following errors are ignored because it means another instance already -completed this step: - - documents already exist in the temp index - - temp index has a write block - - temp index is not found -### New control state -1. If `currentBatch` is the last batch in `transformedDocBatches` - → `REINDEX_SOURCE_TO_TEMP_READ` -2. If there are more batches left in `transformedDocBatches` - → `REINDEX_SOURCE_TO_TEMP_INDEX_BULK` - -## REINDEX_SOURCE_TO_TEMP_CLOSE_PIT -### Next action -`closePIT` - -### New control state - → `SET_TEMP_WRITE_BLOCK` - -## SET_TEMP_WRITE_BLOCK -### Next action -`setWriteBlock` - -Set a write block on the temporary index so that we can clone it. -### New control state - → `CLONE_TEMP_TO_TARGET` - -## CLONE_TEMP_TO_TARGET -### Next action -`cloneIndex` - -Ask elasticsearch to clone the temporary index into the target index. If the target index already exists (because another node already started the clone operation), wait until the clone is complete by waiting for a yellow index status. - -We can’t use the temporary index as our target index because one instance can complete the migration, delete a document, and then a second instance starts the reindex operation and re-creates the deleted document. By cloning the temporary index and only accepting writes/deletes from the cloned target index, we prevent lost acknowledged deletes. - -### New control state - → `OUTDATED_DOCUMENTS_SEARCH` - -## OUTDATED_DOCUMENTS_SEARCH -### Next action -`searchForOutdatedDocuments` - -Search for outdated saved object documents. Will return one batch of -documents. - -If another instance has a disabled plugin it will reindex that plugin's -documents without transforming them. Because this instance doesn't know which -plugins were disabled by the instance that performed the -`REINDEX_SOURCE_TO_TEMP_TRANSFORM` step, we need to search for outdated documents -and transform them to ensure that everything is up to date. - -### New control state -1. Found outdated documents? - → `OUTDATED_DOCUMENTS_TRANSFORM` -2. All documents up to date - → `UPDATE_TARGET_MAPPINGS` - -## OUTDATED_DOCUMENTS_TRANSFORM -### Next action -`transformRawDocs` + `bulkOverwriteTransformedDocuments` - -Once transformed we use an index operation to overwrite the outdated document with the up-to-date version. Optimistic concurrency control ensures that we only overwrite the document once so that any updates/writes by another instance which already completed the migration aren’t overwritten and lost. - -### New control state - → `OUTDATED_DOCUMENTS_SEARCH` - -## UPDATE_TARGET_MAPPINGS -### Next action -`updateAndPickupMappings` - -If another instance has some plugins disabled it will disable the mappings of that plugin's types when creating the temporary index. This action will -update the mappings and then use an update_by_query to ensure that all fields are “picked-up” and ready to be searched over. - -### New control state - → `UPDATE_TARGET_MAPPINGS_WAIT_FOR_TASK` - -## UPDATE_TARGET_MAPPINGS_WAIT_FOR_TASK -### Next action -`updateAliases` - -Atomically apply the `versionIndexReadyActions` using the _alias actions API. By performing the following actions we guarantee that if multiple versions of Kibana started the upgrade in parallel, only one version will succeed. - -1. verify that the current alias is still pointing to the source index -2. Point the version alias and the current alias to the target index. -3. Remove the temporary index - -### New control state -1. If all the actions succeed we’re ready to serve traffic - → `DONE` -2. If action (1) fails with alias_not_found_exception or action (3) fails with index_not_found_exception another instance already completed the migration - → `MARK_VERSION_INDEX_READY_CONFLICT` - -## MARK_VERSION_INDEX_READY_CONFLICT -### Next action -`fetchIndices` - -Fetch the saved object indices - -### New control state -If another instance completed a migration from the same source we need to verify that it is running the same version. - -1. If the current and version aliases are pointing to the same index the instance that completed the migration was on the same version and it’s safe to start serving traffic. - → `DONE` -2. If the other instance was running a different version we fail the migration. Once we restart one of two things can happen: the other instance is an older version and we will restart the migration, or, it’s a newer version and we will refuse to start up. - → `FATAL` - -# Manual QA Test Plan -## 1. Legacy pre-migration -When upgrading from a legacy index additional steps are required before the -regular migration process can start. - -We have the following potential legacy indices: - - v5.x index that wasn't upgraded -> kibana should refuse to start the migration - - v5.x index that was upgraded to v6.x: `.kibana-6` _index_ with `.kibana` _alias_ - - < v6.5 `.kibana` _index_ (Saved Object Migrations were - introduced in v6.5 https://github.com/elastic/kibana/pull/20243) - - TODO: Test versions which introduced the `kibana_index_template` template? - - < v7.4 `.kibana_task_manager` _index_ (Task Manager started - using Saved Objects in v7.4 https://github.com/elastic/kibana/pull/39829) - -Test plan: -1. Ensure that the different versions of Kibana listed above can successfully - upgrade to 7.11. -2. Ensure that multiple Kibana nodes can migrate a legacy index in parallel - (choose a representative legacy version to test with e.g. v6.4). Add a lot - of Saved Objects to Kibana to increase the time it takes for a migration to - complete which will make it easier to introduce failures. - 1. If all instances are started in parallel the upgrade should succeed - 2. If nodes are randomly restarted shortly after they start participating - in the migration the upgrade should either succeed or never complete. - However, if a fatal error occurs it should never result in permanent - failure. - 1. Start one instance, wait 500 ms - 2. Start a second instance - 3. If an instance starts a saved object migration, wait X ms before - killing the process and restarting the migration. - 4. Keep decreasing X until migrations are barely able to complete. - 5. If a migration fails with a fatal error, start a Kibana that doesn't - get restarted. Given enough time, it should always be able to - successfully complete the migration. - -For a successful migration the following behaviour should be observed: - 1. The `.kibana` index should be reindexed into a `.kibana_pre6.5.0` index - 2. The `.kibana` index should be deleted - 3. The `.kibana_index_template` should be deleted - 4. The `.kibana_pre6.5.0` index should have a write block applied - 5. Documents from `.kibana_pre6.5.0` should be migrated into `.kibana_7.11.0_001` - 6. Once migration has completed, the `.kibana_current` and `.kibana_7.11.0` - aliases should point to the `.kibana_7.11.0_001` index. - -## 2. Plugins enabled/disabled -Kibana plugins can be disabled/enabled at any point in time. We need to ensure -that Saved Object documents are migrated for all the possible sequences of -enabling, disabling, before or after a version upgrade. - -### Test scenario 1 (enable a plugin after migration): -1. Start an old version of Kibana (< 7.11) -2. Create a document that we know will be migrated in a later version (i.e. - create a `dashboard`) -3. Disable the plugin to which the document belongs (i.e `dashboard` plugin) -4. Upgrade Kibana to v7.11 making sure the plugin in step (3) is still disabled. -5. Enable the plugin from step (3) -6. Restart Kibana -7. Ensure that the document from step (2) has been migrated - (`migrationVersion` contains 7.11.0) - -### Test scenario 2 (disable a plugin after migration): -1. Start an old version of Kibana (< 7.11) -2. Create a document that we know will be migrated in a later version (i.e. - create a `dashboard`) -3. Upgrade Kibana to v7.11 making sure the plugin in step (3) is enabled. -4. Disable the plugin to which the document belongs (i.e `dashboard` plugin) -6. Restart Kibana -7. Ensure that Kibana logs a warning, but continues to start even though there - are saved object documents which don't belong to an enable plugin - -### Test scenario 3 (multiple instances, enable a plugin after migration): -Follow the steps from 'Test scenario 1', but perform the migration with -multiple instances of Kibana - -### Test scenario 4 (multiple instances, mixed plugin enabled configs): -We don't support this upgrade scenario, but it's worth making sure we don't -have data loss when there's a user error. -1. Start an old version of Kibana (< 7.11) -2. Create a document that we know will be migrated in a later version (i.e. - create a `dashboard`) -3. Disable the plugin to which the document belongs (i.e `dashboard` plugin) -4. Upgrade Kibana to v7.11 using multiple instances of Kibana. The plugin from - step (3) should be enabled on half of the instances and disabled on the - other half. -5. Ensure that the document from step (2) has been migrated - (`migrationVersion` contains 7.11.0) - diff --git a/src/core/server/saved_objects/migrationsv2/model/helpers.ts b/src/core/server/saved_objects/migrationsv2/model/helpers.ts deleted file mode 100644 index 4e920608594b1..0000000000000 --- a/src/core/server/saved_objects/migrationsv2/model/helpers.ts +++ /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 { gt, valid } from 'semver'; -import { State } from '../types'; -import { IndexMapping } from '../../mappings'; -import { FetchIndexResponse } from '../actions'; - -/** - * A helper function/type for ensuring that all control state's are handled. - */ -export function throwBadControlState(p: never): never; -export function throwBadControlState(controlState: any) { - throw new Error('Unexpected control state: ' + controlState); -} - -/** - * A helper function/type for ensuring that all response types are handled. - */ -export function throwBadResponse(state: State, p: never): never; -export function throwBadResponse(state: State, res: any): never { - throw new Error( - `${state.controlState} received unexpected action response: ` + JSON.stringify(res) - ); -} - -/** - * Merge the _meta.migrationMappingPropertyHashes mappings of an index with - * the given target mappings. - * - * @remarks When another instance already completed a migration, the existing - * target index might contain documents and mappings created by a plugin that - * is disabled in the current Kibana instance performing this migration. - * Mapping updates are commutative (deeply merged) by Elasticsearch, except - * for the `_meta` key. By merging the `_meta.migrationMappingPropertyHashes` - * mappings from the existing target index index into the targetMappings we - * ensure that any `migrationPropertyHashes` for disabled plugins aren't lost. - * - * Right now we don't use these `migrationPropertyHashes` but it could be used - * in the future to detect if mappings were changed. If mappings weren't - * changed we don't need to reindex but can clone the index to save disk space. - * - * @param targetMappings - * @param indexMappings - */ -export function mergeMigrationMappingPropertyHashes( - targetMappings: IndexMapping, - indexMappings: IndexMapping -) { - return { - ...targetMappings, - _meta: { - migrationMappingPropertyHashes: { - ...indexMappings._meta?.migrationMappingPropertyHashes, - ...targetMappings._meta?.migrationMappingPropertyHashes, - }, - }, - }; -} - -export function indexBelongsToLaterVersion(indexName: string, kibanaVersion: string): boolean { - const version = valid(indexVersion(indexName)); - return version != null ? gt(version, kibanaVersion) : false; -} - -/** - * Extracts the version number from a >= 7.11 index - * @param indexName A >= v7.11 index name - */ -export function indexVersion(indexName?: string): string | undefined { - return (indexName?.match(/.+_(\d+\.\d+\.\d+)_\d+/) || [])[1]; -} - -/** - * Creates a record of alias -> index name pairs - */ -export function getAliases(indices: FetchIndexResponse) { - return Object.keys(indices).reduce((acc, index) => { - Object.keys(indices[index].aliases || {}).forEach((alias) => { - // TODO throw if multiple .kibana aliases point to the same index? - acc[alias] = index; - }); - return acc; - }, {} as Record); -} diff --git a/src/core/server/saved_objects/migrationsv2/types.ts b/src/core/server/saved_objects/migrationsv2/types.ts deleted file mode 100644 index e68e04e5267cc..0000000000000 --- a/src/core/server/saved_objects/migrationsv2/types.ts +++ /dev/null @@ -1,468 +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 TaskEither from 'fp-ts/lib/TaskEither'; -import * as Option from 'fp-ts/lib/Option'; -import * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; -import { ControlState } from './state_action_machine'; -import { AliasAction } from './actions'; -import { IndexMapping } from '../mappings'; -import { SavedObjectsRawDoc } from '..'; -import { TransformErrorObjects } from '../migrations/core'; -import { - DocumentsTransformFailed, - DocumentsTransformSuccess, -} from '../migrations/core/migrate_raw_docs'; -import { SavedObjectTypeExcludeFromUpgradeFilterHook } from '../types'; - -export type MigrationLogLevel = 'error' | 'info' | 'warning'; - -export interface MigrationLog { - level: MigrationLogLevel; - message: string; -} - -export interface Progress { - processed: number | undefined; - total: number | undefined; -} - -export interface BaseState extends ControlState { - /** The first part of the index name such as `.kibana` or `.kibana_task_manager` */ - readonly indexPrefix: string; - /** - * The name of the concrete legacy index (if it exists) e.g. `.kibana` for < - * 6.5 or `.kibana_task_manager` for < 7.4 - */ - readonly legacyIndex: string; - /** Kibana version number */ - readonly kibanaVersion: string; - /** The mappings to apply to the target index */ - readonly targetIndexMappings: IndexMapping; - /** - * Special mappings set when creating the temp index into which we reindex. - * These mappings have `dynamic: false` to allow for any kind of outdated - * document to be written to the index, but still define mappings for the - * `migrationVersion` and `type` fields so that we can search for and - * transform outdated documents. - */ - readonly tempIndexMappings: IndexMapping; - /** Script to apply to a legacy index before it can be used as a migration source */ - readonly preMigrationScript: Option.Option; - readonly outdatedDocumentsQuery: estypes.QueryDslQueryContainer; - 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; - - /** - * The number of documents to process in each batch. This determines the - * maximum number of documents that will be read and written in a single - * request. - * - * The higher the value, the faster the migration process will be performed - * since it reduces the number of round trips between Kibana and - * Elasticsearch servers. For the migration speed, we have to pay the price - * of increased memory consumption and HTTP payload size. - * - * Since we cannot control the size in bytes of a batch when reading, - * Elasticsearch might fail with a circuit_breaking_exception when it - * retrieves a set of saved objects of significant size. In this case, you - * should set a smaller batchSize value and restart the migration process - * again. - * - * When writing batches, we limit the number of documents in a batch - * (batchSize) as well as the size of the batch in bytes (maxBatchSizeBytes). - */ - readonly batchSize: number; - /** - * When writing batches, limits the batch size in bytes to ensure that we - * don't construct HTTP requests which would exceed Elasticsearch's - * http.max_content_length which defaults to 100mb. - */ - readonly maxBatchSizeBytes: number; - readonly logs: MigrationLog[]; - /** - * The current alias e.g. `.kibana` which always points to the latest - * version index - */ - readonly currentAlias: string; - /** - * The version alias e.g. `.kibana_7.11.0` which points to the index used - * by this version of Kibana e.g. `.kibana_7.11.0_001` - */ - readonly versionAlias: string; - /** - * The index used by this version of Kibana e.g. `.kibana_7.11.0_001` - */ - readonly versionIndex: string; - /** - * An alias on the target index used as part of an "reindex block" that - * prevents lost deletes e.g. `.kibana_7.11.0_reindex`. - */ - readonly tempIndex: string; - /** - * When reindexing we use a source query to exclude saved objects types which - * are no longer used. These saved objects will still be kept in the outdated - * index for backup purposes, but won't be available in the upgraded index. - */ - readonly unusedTypesQuery: estypes.QueryDslQueryContainer; - /** - * The list of known SO types that are registered. - */ - readonly knownTypes: string[]; - /** - * All exclude filter hooks registered for types on this index. Keyed by type name. - */ - readonly excludeFromUpgradeFilterHooks: Record< - string, - SavedObjectTypeExcludeFromUpgradeFilterHook - >; -} - -export interface InitState extends BaseState { - readonly controlState: 'INIT'; -} - -export interface PostInitState extends BaseState { - /** - * The source index is the index from which the migration reads. If the - * Option is a none, we didn't do any migration from a source index, either: - * - this is a blank ES cluster and we will perform the CREATE_NEW_TARGET - * step - * - another Kibana instance already did the source migration and finished - * the MARK_VERSION_INDEX_READY step - */ - readonly sourceIndex: Option.Option; - /** The target index is the index to which the migration writes */ - readonly targetIndex: string; - readonly versionIndexReadyActions: Option.Option; - readonly outdatedDocumentsQuery: estypes.QueryDslQueryContainer; -} - -export interface DoneState extends PostInitState { - /** Migration completed successfully */ - readonly controlState: 'DONE'; -} - -export interface FatalState extends BaseState { - /** Migration terminated with a failure */ - readonly controlState: 'FATAL'; - /** The reason the migration was terminated */ - readonly reason: string; -} - -export interface WaitForYellowSourceState extends BaseState { - /** Wait for the source index to be yellow before requesting it. */ - readonly controlState: 'WAIT_FOR_YELLOW_SOURCE'; - readonly sourceIndex: Option.Some; - readonly sourceIndexMappings: IndexMapping; -} - -export interface CheckUnknownDocumentsState extends BaseState { - /** Check if any unknown document is present in the source index */ - readonly controlState: 'CHECK_UNKNOWN_DOCUMENTS'; - readonly sourceIndex: Option.Some; - readonly sourceIndexMappings: IndexMapping; -} - -export interface SetSourceWriteBlockState extends PostInitState { - /** Set a write block on the source index to prevent any further writes */ - readonly controlState: 'SET_SOURCE_WRITE_BLOCK'; - readonly sourceIndex: Option.Some; -} - -export interface CalculateExcludeFiltersState extends PostInitState { - readonly controlState: 'CALCULATE_EXCLUDE_FILTERS'; - readonly sourceIndex: Option.Some; -} - -export interface CreateNewTargetState extends PostInitState { - /** Blank ES cluster, create a new version-specific target index */ - readonly controlState: 'CREATE_NEW_TARGET'; - readonly sourceIndex: Option.None; - readonly versionIndexReadyActions: Option.Some; -} - -export interface CreateReindexTempState extends PostInitState { - /** - * Create a target index with mappings from the source index and registered - * plugins - */ - readonly controlState: 'CREATE_REINDEX_TEMP'; - readonly sourceIndex: Option.Some; -} - -export interface ReindexSourceToTempOpenPit extends PostInitState { - /** Open PIT to the source index */ - readonly controlState: 'REINDEX_SOURCE_TO_TEMP_OPEN_PIT'; - readonly sourceIndex: Option.Some; -} - -export interface ReindexSourceToTempRead extends PostInitState { - readonly controlState: 'REINDEX_SOURCE_TO_TEMP_READ'; - readonly sourceIndexPitId: string; - readonly lastHitSortValue: number[] | undefined; - readonly corruptDocumentIds: string[]; - readonly transformErrors: TransformErrorObjects[]; - readonly progress: Progress; -} - -export interface ReindexSourceToTempClosePit extends PostInitState { - readonly controlState: 'REINDEX_SOURCE_TO_TEMP_CLOSE_PIT'; - readonly sourceIndexPitId: string; -} - -export interface ReindexSourceToTempTransform extends PostInitState { - readonly controlState: 'REINDEX_SOURCE_TO_TEMP_TRANSFORM'; - readonly outdatedDocuments: SavedObjectsRawDoc[]; - readonly sourceIndexPitId: string; - readonly lastHitSortValue: number[] | undefined; - readonly corruptDocumentIds: string[]; - readonly transformErrors: TransformErrorObjects[]; - readonly progress: Progress; -} - -export interface ReindexSourceToTempIndexBulk extends PostInitState { - readonly controlState: 'REINDEX_SOURCE_TO_TEMP_INDEX_BULK'; - readonly transformedDocBatches: [SavedObjectsRawDoc[]]; - readonly currentBatch: number; - readonly sourceIndexPitId: string; - readonly lastHitSortValue: number[] | undefined; - readonly progress: Progress; -} - -export type SetTempWriteBlock = PostInitState & { - /** - * - */ - readonly controlState: 'SET_TEMP_WRITE_BLOCK'; - readonly sourceIndex: Option.Some; -}; - -export interface CloneTempToSource extends PostInitState { - /** - * Clone the temporary reindex index into - */ - readonly controlState: 'CLONE_TEMP_TO_TARGET'; - readonly sourceIndex: Option.Some; -} - -export interface RefreshTarget extends PostInitState { - /** Refresh temp index before searching for outdated docs */ - readonly controlState: 'REFRESH_TARGET'; - readonly targetIndex: string; -} - -export interface UpdateTargetMappingsState extends PostInitState { - /** Update the mappings of the target index */ - readonly controlState: 'UPDATE_TARGET_MAPPINGS'; -} - -export interface UpdateTargetMappingsWaitForTaskState extends PostInitState { - /** Update the mappings of the target index */ - readonly controlState: 'UPDATE_TARGET_MAPPINGS_WAIT_FOR_TASK'; - readonly updateTargetMappingsTaskId: string; -} - -export interface OutdatedDocumentsSearchOpenPit extends PostInitState { - /** Open PiT for target index to search for outdated documents */ - readonly controlState: 'OUTDATED_DOCUMENTS_SEARCH_OPEN_PIT'; -} - -export interface OutdatedDocumentsSearchRead extends PostInitState { - /** Search for outdated documents in the target index */ - readonly controlState: 'OUTDATED_DOCUMENTS_SEARCH_READ'; - readonly pitId: string; - readonly lastHitSortValue: number[] | undefined; - readonly hasTransformedDocs: boolean; - readonly corruptDocumentIds: string[]; - readonly transformErrors: TransformErrorObjects[]; - readonly progress: Progress; -} - -export interface OutdatedDocumentsSearchClosePit extends PostInitState { - /** Close PiT for target index when found all outdated documents */ - readonly controlState: 'OUTDATED_DOCUMENTS_SEARCH_CLOSE_PIT'; - readonly pitId: string; - readonly hasTransformedDocs: boolean; -} - -export interface OutdatedDocumentsRefresh extends PostInitState { - /** Reindex transformed documents */ - readonly controlState: 'OUTDATED_DOCUMENTS_REFRESH'; - readonly targetIndex: string; -} - -export interface OutdatedDocumentsTransform extends PostInitState { - /** Transform a batch of outdated documents to their latest version*/ - readonly controlState: 'OUTDATED_DOCUMENTS_TRANSFORM'; - readonly pitId: string; - readonly outdatedDocuments: SavedObjectsRawDoc[]; - readonly lastHitSortValue: number[] | undefined; - readonly hasTransformedDocs: boolean; - readonly corruptDocumentIds: string[]; - readonly transformErrors: TransformErrorObjects[]; - readonly progress: Progress; -} - -export interface TransformedDocumentsBulkIndex extends PostInitState { - /** - * Write the up-to-date transformed documents to the target index - */ - readonly controlState: 'TRANSFORMED_DOCUMENTS_BULK_INDEX'; - readonly transformedDocBatches: SavedObjectsRawDoc[][]; - readonly currentBatch: number; - readonly lastHitSortValue: number[] | undefined; - readonly hasTransformedDocs: boolean; - readonly pitId: string; - readonly progress: Progress; -} - -export interface MarkVersionIndexReady extends PostInitState { - /** - * Marks the version-specific index as ready. Once this step is complete, - * future Kibana instances will not have to prepare a target index by e.g. - * cloning a source index or creating a new index. - * - * To account for newly installed or enabled plugins, Kibana will still - * perform the `UPDATE_TARGET_MAPPINGS*` and `OUTDATED_DOCUMENTS_*` steps - * every time it is restarted. - */ - readonly controlState: 'MARK_VERSION_INDEX_READY'; - readonly versionIndexReadyActions: Option.Some; -} - -export interface MarkVersionIndexReadyConflict extends PostInitState { - /** - * If the MARK_VERSION_INDEX_READY step fails another instance was - * performing the migration in parallel and won the race to marking the - * migration as complete. This step ensures that the instance that won the - * race is running the same version of Kibana, if it does, the migration is - * complete and we can go to DONE. - * - * If it was a different version of Kibana that completed the migration fail - * the migration by going to FATAL. If this instance restarts it will either - * notice that a newer version already completed the migration and refuse to - * start up, or if it was an older version that completed the migration - * start a new migration to the latest version. - */ - readonly controlState: 'MARK_VERSION_INDEX_READY_CONFLICT'; -} - -/** - * If we're migrating from a legacy index we need to perform some additional - * steps to prepare this index so that it can be used as a migration 'source'. - */ -export interface LegacyBaseState extends PostInitState { - readonly sourceIndex: Option.Some; - readonly legacyPreMigrationDoneActions: AliasAction[]; - /** - * The mappings read from the legacy index, used to create a new reindex - * target index. - */ - readonly legacyReindexTargetMappings: IndexMapping; -} - -export interface LegacySetWriteBlockState extends LegacyBaseState { - /** Set a write block on the legacy index to prevent any further writes */ - readonly controlState: 'LEGACY_SET_WRITE_BLOCK'; -} - -export interface LegacyCreateReindexTargetState extends LegacyBaseState { - /** - * Create a new index into which we can reindex the legacy index. This - * index will have the same mappings as the legacy index. Once the legacy - * pre-migration is complete, this index will be used a migration 'source'. - */ - readonly controlState: 'LEGACY_CREATE_REINDEX_TARGET'; -} - -export interface LegacyReindexState extends LegacyBaseState { - /** - * Reindex the legacy index into the new index created in the - * LEGACY_CREATE_REINDEX_TARGET step (and apply the preMigration script). - */ - readonly controlState: 'LEGACY_REINDEX'; -} - -export interface LegacyReindexWaitForTaskState extends LegacyBaseState { - /** Wait for the reindex operation to complete */ - readonly controlState: 'LEGACY_REINDEX_WAIT_FOR_TASK'; - readonly legacyReindexTaskId: string; -} - -export interface LegacyDeleteState extends LegacyBaseState { - /** - * After reindexed has completed, delete the legacy index so that it won't - * conflict with the `currentAlias` that we want to create in a later step - * e.g. `.kibana`. - */ - readonly controlState: 'LEGACY_DELETE'; -} - -export type State = Readonly< - | FatalState - | InitState - | DoneState - | WaitForYellowSourceState - | CheckUnknownDocumentsState - | SetSourceWriteBlockState - | CalculateExcludeFiltersState - | CreateNewTargetState - | CreateReindexTempState - | ReindexSourceToTempOpenPit - | ReindexSourceToTempRead - | ReindexSourceToTempClosePit - | ReindexSourceToTempTransform - | ReindexSourceToTempIndexBulk - | SetTempWriteBlock - | CloneTempToSource - | UpdateTargetMappingsState - | UpdateTargetMappingsWaitForTaskState - | OutdatedDocumentsSearchOpenPit - | OutdatedDocumentsSearchRead - | OutdatedDocumentsSearchClosePit - | OutdatedDocumentsTransform - | RefreshTarget - | OutdatedDocumentsRefresh - | MarkVersionIndexReady - | MarkVersionIndexReadyConflict - | TransformedDocumentsBulkIndex - | LegacyCreateReindexTargetState - | LegacySetWriteBlockState - | LegacyReindexState - | LegacyReindexWaitForTaskState - | LegacyDeleteState ->; - -export type AllControlStates = State['controlState']; -/** - * All control states that trigger an action (excludes the terminal states - * 'FATAL' and 'DONE'). - */ -export type AllActionStates = Exclude; - -export type TransformRawDocs = ( - rawDocs: SavedObjectsRawDoc[] -) => TaskEither.TaskEither; diff --git a/src/core/server/saved_objects/routes/integration_tests/migrate.test.mocks.ts b/src/core/server/saved_objects/routes/integration_tests/migrate.test.mocks.ts index b12188347f8a7..b8b3a22c5d0fa 100644 --- a/src/core/server/saved_objects/routes/integration_tests/migrate.test.mocks.ts +++ b/src/core/server/saved_objects/routes/integration_tests/migrate.test.mocks.ts @@ -6,10 +6,10 @@ * Side Public License, v 1. */ -import { mockKibanaMigrator } from '../../migrations/kibana/kibana_migrator.mock'; +import { mockKibanaMigrator } from '../../migrations/kibana_migrator.mock'; export const migratorInstanceMock = mockKibanaMigrator.create(); export const KibanaMigratorMock = jest.fn().mockImplementation(() => migratorInstanceMock); -jest.doMock('../../migrations/kibana/kibana_migrator', () => ({ +jest.doMock('../../migrations/kibana_migrator', () => ({ KibanaMigrator: KibanaMigratorMock, })); diff --git a/src/core/server/saved_objects/saved_objects_service.test.mocks.ts b/src/core/server/saved_objects/saved_objects_service.test.mocks.ts index 1faebcc5fcc97..65273827122ec 100644 --- a/src/core/server/saved_objects/saved_objects_service.test.mocks.ts +++ b/src/core/server/saved_objects/saved_objects_service.test.mocks.ts @@ -6,13 +6,13 @@ * Side Public License, v 1. */ -import { mockKibanaMigrator } from './migrations/kibana/kibana_migrator.mock'; +import { mockKibanaMigrator } from './migrations/kibana_migrator.mock'; import { savedObjectsClientProviderMock } from './service/lib/scoped_client_provider.mock'; import { typeRegistryMock } from './saved_objects_type_registry.mock'; export const migratorInstanceMock = mockKibanaMigrator.create(); export const KibanaMigratorMock = jest.fn().mockImplementation(() => migratorInstanceMock); -jest.doMock('./migrations/kibana/kibana_migrator', () => ({ +jest.doMock('./migrations/kibana_migrator', () => ({ KibanaMigrator: KibanaMigratorMock, })); diff --git a/src/core/server/saved_objects/saved_objects_service.ts b/src/core/server/saved_objects/saved_objects_service.ts index baa1636dde13f..a55f370c7ca22 100644 --- a/src/core/server/saved_objects/saved_objects_service.ts +++ b/src/core/server/saved_objects/saved_objects_service.ts @@ -370,10 +370,10 @@ export class SavedObjectsService }; } - public async start( - { elasticsearch, pluginsInitialized = true }: SavedObjectsStartDeps, - migrationsRetryDelay?: number - ): Promise { + public async start({ + elasticsearch, + pluginsInitialized = true, + }: SavedObjectsStartDeps): Promise { if (!this.setupDeps || !this.config) { throw new Error('#setup() needs to be run first'); } @@ -384,8 +384,7 @@ export class SavedObjectsService const migrator = this.createMigrator( this.config.migration, - elasticsearch.client.asInternalUser, - migrationsRetryDelay + elasticsearch.client.asInternalUser ); this.migrator$.next(migrator); @@ -500,8 +499,7 @@ export class SavedObjectsService private createMigrator( soMigrationsConfig: SavedObjectsMigrationConfigType, - client: ElasticsearchClient, - migrationsRetryDelay?: number + client: ElasticsearchClient ): IKibanaMigrator { return new KibanaMigrator({ typeRegistry: this.typeRegistry, @@ -510,7 +508,6 @@ export class SavedObjectsService soMigrationsConfig, kibanaIndex, client, - migrationsRetryDelay, }); } diff --git a/src/core/server/saved_objects/service/lib/__snapshots__/scoped_client_provider.test.js.snap b/src/core/server/saved_objects/service/lib/__snapshots__/scoped_client_provider.test.js.snap deleted file mode 100644 index de53d6ca69bd3..0000000000000 --- a/src/core/server/saved_objects/service/lib/__snapshots__/scoped_client_provider.test.js.snap +++ /dev/null @@ -1,3 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`throws error when more than one scoped saved objects client factory is set 1`] = `"custom client factory is already set, unable to replace the current one"`; diff --git a/src/core/server/saved_objects/service/lib/repository.test.js b/src/core/server/saved_objects/service/lib/repository.test.js deleted file mode 100644 index f61a79ca9de66..0000000000000 --- a/src/core/server/saved_objects/service/lib/repository.test.js +++ /dev/null @@ -1,4674 +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 { - pointInTimeFinderMock, - mockCollectMultiNamespaceReferences, - mockGetBulkOperationError, - mockInternalBulkResolve, - mockUpdateObjectsSpaces, - mockGetCurrentTime, - mockPreflightCheckForCreate, - mockDeleteLegacyUrlAliases, -} from './repository.test.mock'; - -import { SavedObjectsRepository } from './repository'; -import * as getSearchDslNS from './search_dsl/search_dsl'; -import { SavedObjectsErrorHelpers } from './errors'; -import { PointInTimeFinder } from './point_in_time_finder'; -import { ALL_NAMESPACES_STRING } from './utils'; -import { loggerMock } from '../../../logging/logger.mock'; -import { SavedObjectsSerializer } from '../../serialization'; -import { encodeHitVersion } from '../../version'; -import { SavedObjectTypeRegistry } from '../../saved_objects_type_registry'; -import { DocumentMigrator } from '../../migrations/core/document_migrator'; -import { mockKibanaMigrator } from '../../migrations/kibana/kibana_migrator.mock'; -import { LEGACY_URL_ALIAS_TYPE } from '../../object_types'; -import { elasticsearchClientMock } from '../../../elasticsearch/client/mocks'; -import * as esKuery from '@kbn/es-query'; -import { errors as EsErrors } from '@elastic/elasticsearch'; - -const { nodeTypes } = esKuery; - -jest.mock('./search_dsl/search_dsl', () => ({ getSearchDsl: jest.fn() })); - -// BEWARE: The SavedObjectClient depends on the implementation details of the SavedObjectsRepository -// so any breaking changes to this repository are considered breaking changes to the SavedObjectsClient. - -const createBadRequestError = (...args) => - SavedObjectsErrorHelpers.createBadRequestError(...args).output.payload; -const createConflictError = (...args) => - SavedObjectsErrorHelpers.createConflictError(...args).output.payload; -const createGenericNotFoundError = (...args) => - SavedObjectsErrorHelpers.createGenericNotFoundError(...args).output.payload; -const createUnsupportedTypeError = (...args) => - SavedObjectsErrorHelpers.createUnsupportedTypeError(...args).output.payload; - -describe('SavedObjectsRepository', () => { - let client; - let savedObjectsRepository; - let migrator; - let logger; - - let serializer; - const mockTimestamp = '2017-08-14T15:49:14.886Z'; - const mockTimestampFields = { updated_at: mockTimestamp }; - const mockVersionProps = { _seq_no: 1, _primary_term: 1 }; - const mockVersion = encodeHitVersion(mockVersionProps); - - const KIBANA_VERSION = '2.0.0'; - const CUSTOM_INDEX_TYPE = 'customIndex'; - /** This type has namespaceType: 'agnostic'. */ - const NAMESPACE_AGNOSTIC_TYPE = 'globalType'; - /** - * This type has namespaceType: 'multiple'. - * - * That means that the object is serialized with a globally unique ID across namespaces. It also means that the object is shareable across - * namespaces. - **/ - const MULTI_NAMESPACE_TYPE = 'multiNamespaceType'; - /** - * This type has namespaceType: 'multiple-isolated'. - * - * That means that the object is serialized with a globally unique ID across namespaces. It also means that the object is NOT shareable - * across namespaces. This distinction only matters when using the `collectMultiNamespaceReferences` or `updateObjectsSpaces` APIs, or - * when using the `initialNamespaces` argument with the `create` and `bulkCreate` APIs. Those allow you to define or change what - * namespaces an object exists in. - * - * In a nutshell, this type is more restrictive than `MULTI_NAMESPACE_TYPE`, so we use `MULTI_NAMESPACE_ISOLATED_TYPE` for any test cases - * where `MULTI_NAMESPACE_TYPE` would also satisfy the test case. - **/ - const MULTI_NAMESPACE_ISOLATED_TYPE = 'multiNamespaceIsolatedType'; - /** This type has namespaceType: 'multiple', and it uses a custom index. */ - const MULTI_NAMESPACE_CUSTOM_INDEX_TYPE = 'multiNamespaceTypeCustomIndex'; - const HIDDEN_TYPE = 'hiddenType'; - - const mappings = { - properties: { - config: { - properties: { - type: 'keyword', - }, - }, - 'index-pattern': { - properties: { - someField: { - type: 'keyword', - }, - }, - }, - dashboard: { - properties: { - otherField: { - type: 'keyword', - }, - }, - }, - [CUSTOM_INDEX_TYPE]: { - properties: { - type: 'keyword', - }, - }, - [NAMESPACE_AGNOSTIC_TYPE]: { - properties: { - yetAnotherField: { - type: 'keyword', - }, - }, - }, - [MULTI_NAMESPACE_TYPE]: { - properties: { - evenYetAnotherField: { - type: 'keyword', - }, - }, - }, - [MULTI_NAMESPACE_ISOLATED_TYPE]: { - properties: { - evenYetAnotherField: { - type: 'keyword', - }, - }, - }, - [MULTI_NAMESPACE_CUSTOM_INDEX_TYPE]: { - properties: { - evenYetAnotherField: { - type: 'keyword', - }, - }, - }, - [HIDDEN_TYPE]: { - properties: { - someField: { - type: 'keyword', - }, - }, - }, - }, - }; - - const createType = (type) => ({ - name: type, - mappings: { properties: mappings.properties[type].properties }, - migrations: { '1.1.1': (doc) => doc }, - }); - - const registry = new SavedObjectTypeRegistry(); - registry.registerType(createType('config')); - registry.registerType(createType('index-pattern')); - registry.registerType(createType('dashboard')); - registry.registerType({ - ...createType(CUSTOM_INDEX_TYPE), - indexPattern: 'custom', - }); - registry.registerType({ - ...createType(NAMESPACE_AGNOSTIC_TYPE), - namespaceType: 'agnostic', - }); - registry.registerType({ - ...createType(MULTI_NAMESPACE_TYPE), - namespaceType: 'multiple', - }); - registry.registerType({ - ...createType(MULTI_NAMESPACE_ISOLATED_TYPE), - namespaceType: 'multiple-isolated', - }); - registry.registerType({ - ...createType(MULTI_NAMESPACE_CUSTOM_INDEX_TYPE), - namespaceType: 'multiple', - indexPattern: 'custom', - }); - registry.registerType({ - ...createType(HIDDEN_TYPE), - hidden: true, - namespaceType: 'agnostic', - }); - - const documentMigrator = new DocumentMigrator({ - typeRegistry: registry, - kibanaVersion: KIBANA_VERSION, - log: {}, - }); - - const getMockGetResponse = ( - { type, id, references, namespace: objectNamespace, originId }, - namespace - ) => { - let namespaces; - if (objectNamespace) { - namespaces = [objectNamespace]; - } else if (namespace) { - namespaces = Array.isArray(namespace) ? namespace : [namespace]; - } else { - namespaces = ['default']; - } - const namespaceId = namespaces[0] === 'default' ? undefined : namespaces[0]; - - return { - // NOTE: Elasticsearch returns more fields (_index, _type) but the SavedObjectsRepository method ignores these - found: true, - _id: `${ - registry.isSingleNamespace(type) && namespaceId ? `${namespaceId}:` : '' - }${type}:${id}`, - ...mockVersionProps, - _source: { - ...(registry.isSingleNamespace(type) && { namespace: namespaceId }), - ...(registry.isMultiNamespace(type) && { namespaces }), - ...(originId && { originId }), - type, - [type]: { title: 'Testing' }, - references, - specialProperty: 'specialValue', - ...mockTimestampFields, - }, - }; - }; - - const getMockMgetResponse = (objects, namespace) => ({ - docs: objects.map((obj) => - obj.found === false ? obj : getMockGetResponse(obj, obj.initialNamespaces ?? namespace) - ), - }); - - expect.extend({ - toBeDocumentWithoutError(received, type, id) { - if (received.type === type && received.id === id && !received.error) { - return { message: () => `expected type and id not to match without error`, pass: true }; - } else { - return { message: () => `expected type and id to match without error`, pass: false }; - } - }, - }); - const expectSuccess = ({ type, id }) => expect.toBeDocumentWithoutError(type, id); - const expectError = ({ type, id }) => ({ type, id, error: expect.any(Object) }); - const expectErrorResult = ({ type, id }, error, overrides = {}) => ({ - type, - id, - error: { ...error, ...overrides }, - }); - const expectErrorNotFound = (obj, overrides) => - expectErrorResult(obj, createGenericNotFoundError(obj.type, obj.id), overrides); - const expectErrorConflict = (obj, overrides) => - expectErrorResult(obj, createConflictError(obj.type, obj.id), overrides); - const expectErrorInvalidType = (obj, overrides) => - expectErrorResult(obj, createUnsupportedTypeError(obj.type, obj.id), overrides); - - const expectMigrationArgs = (args, contains = true, n = 1) => { - const obj = contains ? expect.objectContaining(args) : expect.not.objectContaining(args); - expect(migrator.migrateDocument).toHaveBeenNthCalledWith(n, obj); - }; - - beforeEach(() => { - pointInTimeFinderMock.mockClear(); - client = elasticsearchClientMock.createElasticsearchClient(); - migrator = mockKibanaMigrator.create(); - documentMigrator.prepareMigrations(); - migrator.migrateDocument = jest.fn().mockImplementation(documentMigrator.migrate); - migrator.runMigrations = async () => ({ status: 'skipped' }); - logger = loggerMock.create(); - - // create a mock serializer "shim" so we can track function calls, but use the real serializer's implementation - serializer = { - isRawSavedObject: jest.fn(), - rawToSavedObject: jest.fn(), - savedObjectToRaw: jest.fn(), - generateRawId: jest.fn(), - generateRawLegacyUrlAliasId: jest.fn(), - trimIdPrefix: jest.fn(), - }; - const _serializer = new SavedObjectsSerializer(registry); - Object.keys(serializer).forEach((key) => { - serializer[key].mockImplementation((...args) => _serializer[key](...args)); - }); - - const allTypes = registry.getAllTypes().map((type) => type.name); - const allowedTypes = [...new Set(allTypes.filter((type) => !registry.isHidden(type)))]; - - savedObjectsRepository = new SavedObjectsRepository({ - index: '.kibana-test', - mappings, - client, - migrator, - typeRegistry: registry, - serializer, - allowedTypes, - logger, - }); - - mockGetCurrentTime.mockReturnValue(mockTimestamp); - getSearchDslNS.getSearchDsl.mockClear(); - }); - - const mockMigrationVersion = { foo: '2.3.4' }; - const mockMigrateDocument = (doc) => ({ - ...doc, - attributes: { - ...doc.attributes, - ...(doc.attributes?.title && { title: `${doc.attributes.title}!!` }), - }, - migrationVersion: mockMigrationVersion, - references: [{ name: 'search_0', type: 'search', id: '123' }], - }); - - describe('#bulkCreate', () => { - beforeEach(() => { - mockPreflightCheckForCreate.mockReset(); - mockPreflightCheckForCreate.mockImplementation(({ objects }) => { - return objects.map(({ type, id }) => ({ type, id })); // respond with no errors by default - }); - }); - - const obj1 = { - type: 'config', - id: '6.0.0-alpha1', - attributes: { title: 'Test One' }, - references: [{ name: 'ref_0', type: 'test', id: '1' }], - originId: 'some-origin-id', // only one of the object args has an originId, this is intentional to test both a positive and negative case - }; - const obj2 = { - type: 'index-pattern', - id: 'logstash-*', - attributes: { title: 'Test Two' }, - references: [{ name: 'ref_0', type: 'test', id: '2' }], - }; - const namespace = 'foo-namespace'; - - const getMockBulkCreateResponse = (objects, namespace) => { - return { - items: objects.map(({ type, id, originId, attributes, references, migrationVersion }) => ({ - create: { - _id: `${namespace ? `${namespace}:` : ''}${type}:${id}`, - _source: { - [type]: attributes, - type, - namespace, - ...(originId && { originId }), - references, - ...mockTimestampFields, - migrationVersion: migrationVersion || { [type]: '1.1.1' }, - }, - ...mockVersionProps, - }, - })), - }; - }; - - const bulkCreateSuccess = async (objects, options) => { - const response = getMockBulkCreateResponse(objects, options?.namespace); - client.bulk.mockResolvedValue( - elasticsearchClientMock.createSuccessTransportRequestPromise(response) - ); - const result = await savedObjectsRepository.bulkCreate(objects, options); - return result; - }; - - // bulk create calls have two objects for each source -- the action, and the source - const expectClientCallArgsAction = ( - objects, - { method, _index = expect.any(String), getId = () => expect.any(String) } - ) => { - const body = []; - for (const { type, id, if_primary_term: ifPrimaryTerm, if_seq_no: ifSeqNo } of objects) { - body.push({ - [method]: { - _index, - _id: getId(type, id), - ...(ifPrimaryTerm && ifSeqNo - ? { if_primary_term: expect.any(Number), if_seq_no: expect.any(Number) } - : {}), - }, - }); - body.push(expect.any(Object)); - } - expect(client.bulk).toHaveBeenCalledWith( - expect.objectContaining({ body }), - expect.anything() - ); - }; - - const expectObjArgs = ({ type, attributes, references }, overrides) => [ - expect.any(Object), - expect.objectContaining({ - [type]: attributes, - references, - type, - ...overrides, - ...mockTimestampFields, - }), - ]; - - const expectSuccessResult = (obj) => ({ - ...obj, - migrationVersion: { [obj.type]: '1.1.1' }, - coreMigrationVersion: KIBANA_VERSION, - version: mockVersion, - namespaces: obj.namespaces ?? [obj.namespace ?? 'default'], - ...mockTimestampFields, - }); - - describe('client calls', () => { - it(`should use the ES bulk action by default`, async () => { - await bulkCreateSuccess([obj1, obj2]); - expect(client.bulk).toHaveBeenCalledTimes(1); - }); - - it(`should use the preflightCheckForCreate action before bulk action for any types that are multi-namespace, when id is defined`, async () => { - const objects = [obj1, { ...obj2, type: MULTI_NAMESPACE_ISOLATED_TYPE }]; - await bulkCreateSuccess(objects); - expect(client.bulk).toHaveBeenCalledTimes(1); - expect(mockPreflightCheckForCreate).toHaveBeenCalledTimes(1); - expect(mockPreflightCheckForCreate).toHaveBeenCalledWith( - expect.objectContaining({ - objects: [ - { - type: MULTI_NAMESPACE_ISOLATED_TYPE, - id: obj2.id, - overwrite: false, - namespaces: ['default'], - }, - ], - }) - ); - }); - - it(`should use the ES create method if ID is undefined and overwrite=true`, async () => { - const objects = [obj1, obj2].map((obj) => ({ ...obj, id: undefined })); - await bulkCreateSuccess(objects, { overwrite: true }); - expectClientCallArgsAction(objects, { method: 'create' }); - }); - - it(`should use the ES create method if ID is undefined and overwrite=false`, async () => { - const objects = [obj1, obj2].map((obj) => ({ ...obj, id: undefined })); - await bulkCreateSuccess(objects); - expectClientCallArgsAction(objects, { method: 'create' }); - }); - - it(`should use the ES index method if ID is defined and overwrite=true`, async () => { - await bulkCreateSuccess([obj1, obj2], { overwrite: true }); - expectClientCallArgsAction([obj1, obj2], { method: 'index' }); - }); - - it(`should use the ES index method with version if ID and version are defined and overwrite=true`, async () => { - await bulkCreateSuccess( - [ - { - ...obj1, - version: mockVersion, - }, - obj2, - ], - { overwrite: true } - ); - - const obj1WithSeq = { - ...obj1, - if_seq_no: mockVersionProps._seq_no, - if_primary_term: mockVersionProps._primary_term, - }; - - expectClientCallArgsAction([obj1WithSeq, obj2], { method: 'index' }); - }); - - it(`should use the ES create method if ID is defined and overwrite=false`, async () => { - await bulkCreateSuccess([obj1, obj2]); - expectClientCallArgsAction([obj1, obj2], { method: 'create' }); - }); - - it(`formats the ES request`, async () => { - await bulkCreateSuccess([obj1, obj2]); - const body = [...expectObjArgs(obj1), ...expectObjArgs(obj2)]; - expect(client.bulk).toHaveBeenCalledWith( - expect.objectContaining({ body }), - expect.anything() - ); - }); - - it(`adds namespace to request body for any types that are single-namespace`, async () => { - await bulkCreateSuccess([obj1, obj2], { namespace }); - const expected = expect.objectContaining({ namespace }); - const body = [expect.any(Object), expected, expect.any(Object), expected]; - expect(client.bulk).toHaveBeenCalledWith( - expect.objectContaining({ body }), - expect.anything() - ); - }); - - it(`normalizes options.namespace from 'default' to undefined`, async () => { - await bulkCreateSuccess([obj1, obj2], { namespace: 'default' }); - const expected = expect.not.objectContaining({ namespace: 'default' }); - const body = [expect.any(Object), expected, expect.any(Object), expected]; - expect(client.bulk).toHaveBeenCalledWith( - expect.objectContaining({ body }), - expect.anything() - ); - }); - - it(`doesn't add namespace to request body for any types that are not single-namespace`, async () => { - const objects = [ - { ...obj1, type: NAMESPACE_AGNOSTIC_TYPE }, - { ...obj2, type: MULTI_NAMESPACE_ISOLATED_TYPE }, - ]; - await bulkCreateSuccess(objects, { namespace }); - const expected = expect.not.objectContaining({ namespace: expect.anything() }); - const body = [expect.any(Object), expected, expect.any(Object), expected]; - expect(client.bulk).toHaveBeenCalledWith( - expect.objectContaining({ body }), - expect.anything() - ); - }); - - it(`adds namespaces to request body for any types that are multi-namespace`, async () => { - const test = async (namespace) => { - const objects = [obj1, obj2].map((x) => ({ ...x, type: MULTI_NAMESPACE_ISOLATED_TYPE })); - const [o1, o2] = objects; - mockPreflightCheckForCreate.mockResolvedValueOnce([ - { type: o1.type, id: o1.id }, // first object does not have an existing document to overwrite - { - type: o2.type, - id: o2.id, - existingDocument: { _source: { namespaces: ['*'] } }, // second object does have an existing document to overwrite - }, - ]); - await bulkCreateSuccess(objects, { namespace, overwrite: true }); - const expected1 = expect.objectContaining({ namespaces: [namespace ?? 'default'] }); - const expected2 = expect.objectContaining({ namespaces: ['*'] }); - const body = [expect.any(Object), expected1, expect.any(Object), expected2]; - expect(client.bulk).toHaveBeenCalledWith( - expect.objectContaining({ body }), - expect.anything() - ); - client.bulk.mockClear(); - mockPreflightCheckForCreate.mockReset(); - }; - await test(undefined); - await test(namespace); - }); - - it(`adds initialNamespaces instead of namespace`, async () => { - const test = async (namespace) => { - const ns2 = 'bar-namespace'; - const ns3 = 'baz-namespace'; - const objects = [ - { ...obj1, type: 'dashboard', initialNamespaces: [ns2] }, - { ...obj1, type: MULTI_NAMESPACE_ISOLATED_TYPE, initialNamespaces: [ns2] }, - { ...obj1, type: MULTI_NAMESPACE_TYPE, initialNamespaces: [ns2, ns3] }, - ]; - const [o1, o2, o3] = objects; - mockPreflightCheckForCreate.mockResolvedValueOnce([ - // first object does not get passed in to preflightCheckForCreate at all - { type: o2.type, id: o2.id }, // second object does not have an existing document to overwrite - { - type: o3.type, - id: o3.id, - existingDocument: { - _source: { namespaces: [namespace ?? 'default', 'something-else'] }, // third object does have an existing document to overwrite - }, - }, - ]); - await bulkCreateSuccess(objects, { namespace, overwrite: true }); - const body = [ - { index: expect.objectContaining({ _id: `${ns2}:dashboard:${o1.id}` }) }, - expect.objectContaining({ namespace: ns2 }), - { - index: expect.objectContaining({ - _id: `${MULTI_NAMESPACE_ISOLATED_TYPE}:${o2.id}`, - }), - }, - expect.objectContaining({ namespaces: [ns2] }), - { index: expect.objectContaining({ _id: `${MULTI_NAMESPACE_TYPE}:${o3.id}` }) }, - expect.objectContaining({ namespaces: [ns2, ns3] }), - ]; - expect(mockPreflightCheckForCreate).toHaveBeenCalledWith( - expect.objectContaining({ - objects: [ - // assert that the initialNamespaces fields were passed into preflightCheckForCreate instead of the current namespace - { type: o2.type, id: o2.id, overwrite: true, namespaces: o2.initialNamespaces }, - { type: o3.type, id: o3.id, overwrite: true, namespaces: o3.initialNamespaces }, - ], - }) - ); - expect(client.bulk).toHaveBeenCalledWith( - expect.objectContaining({ body }), - expect.anything() - ); - client.bulk.mockClear(); - mockPreflightCheckForCreate.mockReset(); - }; - await test(undefined); - await test(namespace); - }); - - it(`normalizes initialNamespaces from 'default' to undefined`, async () => { - const test = async (namespace) => { - const objects = [{ ...obj1, type: 'dashboard', initialNamespaces: ['default'] }]; - await bulkCreateSuccess(objects, { namespace, overwrite: true }); - const body = [ - { index: expect.objectContaining({ _id: `dashboard:${obj1.id}` }) }, - expect.not.objectContaining({ namespace: 'default' }), - ]; - expect(client.bulk).toHaveBeenCalledWith( - expect.objectContaining({ body }), - expect.anything() - ); - client.bulk.mockClear(); - }; - await test(undefined); - await test(namespace); - }); - - it(`doesn't add namespaces to request body for any types that are not multi-namespace`, async () => { - const test = async (namespace) => { - const objects = [obj1, { ...obj2, type: NAMESPACE_AGNOSTIC_TYPE }]; - await bulkCreateSuccess(objects, { namespace, overwrite: true }); - const expected = expect.not.objectContaining({ namespaces: expect.anything() }); - const body = [expect.any(Object), expected, expect.any(Object), expected]; - expect(client.bulk).toHaveBeenCalledWith( - expect.objectContaining({ body }), - expect.anything() - ); - client.bulk.mockClear(); - }; - await test(undefined); - await test(namespace); - }); - - it(`defaults to a refresh setting of wait_for`, async () => { - await bulkCreateSuccess([obj1, obj2]); - expect(client.bulk).toHaveBeenCalledWith( - expect.objectContaining({ refresh: 'wait_for' }), - expect.anything() - ); - }); - - it(`should use default index`, async () => { - await bulkCreateSuccess([obj1, obj2]); - expectClientCallArgsAction([obj1, obj2], { - method: 'create', - _index: '.kibana-test_8.0.0-testing', - }); - }); - - it(`should use custom index`, async () => { - await bulkCreateSuccess([obj1, obj2].map((x) => ({ ...x, type: CUSTOM_INDEX_TYPE }))); - expectClientCallArgsAction([obj1, obj2], { - method: 'create', - _index: 'custom_8.0.0-testing', - }); - }); - - it(`prepends namespace to the id when providing namespace for single-namespace type`, async () => { - const getId = (type, id) => `${namespace}:${type}:${id}`; // test that the raw document ID equals this (e.g., has a namespace prefix) - await bulkCreateSuccess([obj1, obj2], { namespace }); - expectClientCallArgsAction([obj1, obj2], { method: 'create', getId }); - }); - - it(`doesn't prepend namespace to the id when providing no namespace for single-namespace type`, async () => { - const getId = (type, id) => `${type}:${id}`; // test that the raw document ID equals this (e.g., does not have a namespace prefix) - await bulkCreateSuccess([obj1, obj2]); - expectClientCallArgsAction([obj1, obj2], { method: 'create', getId }); - }); - - it(`doesn't prepend namespace to the id when not using single-namespace type`, async () => { - const getId = (type, id) => `${type}:${id}`; // test that the raw document ID equals this (e.g., does not have a namespace prefix) - const objects = [ - { ...obj1, type: NAMESPACE_AGNOSTIC_TYPE }, - { ...obj2, type: MULTI_NAMESPACE_ISOLATED_TYPE }, - ]; - await bulkCreateSuccess(objects, { namespace }); - expectClientCallArgsAction(objects, { method: 'create', getId }); - }); - }); - - describe('errors', () => { - afterEach(() => { - mockGetBulkOperationError.mockReset(); - }); - - const obj3 = { - type: 'dashboard', - id: 'three', - attributes: { title: 'Test Three' }, - references: [{ name: 'ref_0', type: 'test', id: '2' }], - }; - - const bulkCreateError = async (obj, isBulkError, expectedErrorResult) => { - let response; - if (isBulkError) { - // mock the bulk error for only the second object - mockGetBulkOperationError.mockReturnValueOnce(undefined); - mockGetBulkOperationError.mockReturnValueOnce(expectedErrorResult.error); - response = getMockBulkCreateResponse([obj1, obj, obj2]); - } else { - response = getMockBulkCreateResponse([obj1, obj2]); - } - client.bulk.mockResolvedValueOnce( - elasticsearchClientMock.createSuccessTransportRequestPromise(response) - ); - - const objects = [obj1, obj, obj2]; - const result = await savedObjectsRepository.bulkCreate(objects); - expect(client.bulk).toHaveBeenCalled(); - const objCall = isBulkError ? expectObjArgs(obj) : []; - const body = [...expectObjArgs(obj1), ...objCall, ...expectObjArgs(obj2)]; - expect(client.bulk).toHaveBeenCalledWith( - expect.objectContaining({ body }), - expect.anything() - ); - expect(result).toEqual({ - saved_objects: [expectSuccess(obj1), expectedErrorResult, expectSuccess(obj2)], - }); - }; - - it(`throws when options.namespace is '*'`, async () => { - await expect( - savedObjectsRepository.bulkCreate([obj3], { namespace: ALL_NAMESPACES_STRING }) - ).rejects.toThrowError(createBadRequestError('"options.namespace" cannot be "*"')); - }); - - it(`returns error when initialNamespaces is used with a space-agnostic object`, async () => { - const obj = { ...obj3, type: NAMESPACE_AGNOSTIC_TYPE, initialNamespaces: [] }; - await bulkCreateError( - obj, - undefined, - expectErrorResult( - obj, - createBadRequestError('"initialNamespaces" cannot be used on space-agnostic types') - ) - ); - }); - - it(`returns error when initialNamespaces is empty`, async () => { - const obj = { ...obj3, type: MULTI_NAMESPACE_TYPE, initialNamespaces: [] }; - await bulkCreateError( - obj, - undefined, - expectErrorResult( - obj, - createBadRequestError('"initialNamespaces" must be a non-empty array of strings') - ) - ); - }); - - it(`returns error when initialNamespaces is used with a space-isolated object and does not specify a single space`, async () => { - const doTest = async (objType, initialNamespaces) => { - const obj = { ...obj3, type: objType, initialNamespaces }; - await bulkCreateError( - obj, - undefined, - expectErrorResult( - obj, - createBadRequestError( - '"initialNamespaces" can only specify a single space when used with space-isolated types' - ) - ) - ); - }; - await doTest('dashboard', ['spacex', 'spacey']); - await doTest('dashboard', ['*']); - await doTest(MULTI_NAMESPACE_ISOLATED_TYPE, ['spacex', 'spacey']); - await doTest(MULTI_NAMESPACE_ISOLATED_TYPE, ['*']); - }); - - it(`returns error when type is invalid`, async () => { - const obj = { ...obj3, type: 'unknownType' }; - await bulkCreateError(obj, undefined, expectErrorInvalidType(obj)); - }); - - it(`returns error when type is hidden`, async () => { - const obj = { ...obj3, type: HIDDEN_TYPE }; - await bulkCreateError(obj, undefined, expectErrorInvalidType(obj)); - }); - - it(`returns error when there is a conflict from preflightCheckForCreate`, async () => { - const objects = [ - // only the second, third, and fourth objects are passed to preflightCheckForCreate and result in errors - obj1, - { ...obj1, type: MULTI_NAMESPACE_TYPE }, - { ...obj2, type: MULTI_NAMESPACE_TYPE }, - { ...obj3, type: MULTI_NAMESPACE_TYPE }, - obj2, - ]; - const [o1, o2, o3, o4, o5] = objects; - mockPreflightCheckForCreate.mockResolvedValueOnce([ - // first and last objects do not get passed in to preflightCheckForCreate at all - { type: o2.type, id: o2.id, error: { type: 'conflict' } }, - { - type: o3.type, - id: o3.id, - error: { type: 'unresolvableConflict', metadata: { isNotOverwritable: true } }, - }, - { - type: o4.type, - id: o4.id, - error: { type: 'aliasConflict', metadata: { spacesWithConflictingAliases: ['foo'] } }, - }, - ]); - const bulkResponse = getMockBulkCreateResponse([o1, o5]); - client.bulk.mockResolvedValueOnce( - elasticsearchClientMock.createSuccessTransportRequestPromise(bulkResponse) - ); - - const options = { overwrite: true }; - const result = await savedObjectsRepository.bulkCreate(objects, options); - expect(mockPreflightCheckForCreate).toHaveBeenCalled(); - expect(mockPreflightCheckForCreate).toHaveBeenCalledWith( - expect.objectContaining({ - objects: [ - { type: o2.type, id: o2.id, overwrite: true, namespaces: ['default'] }, - { type: o3.type, id: o3.id, overwrite: true, namespaces: ['default'] }, - { type: o4.type, id: o4.id, overwrite: true, namespaces: ['default'] }, - ], - }) - ); - expect(client.bulk).toHaveBeenCalled(); - expect(client.bulk).toHaveBeenCalledWith( - expect.objectContaining({ body: [...expectObjArgs(o1), ...expectObjArgs(o5)] }), - expect.anything() - ); - expect(result).toEqual({ - saved_objects: [ - expectSuccess(o1), - expectErrorConflict(o2), - expectErrorConflict(o3, { metadata: { isNotOverwritable: true } }), - expectErrorConflict(o4, { metadata: { spacesWithConflictingAliases: ['foo'] } }), - expectSuccess(o5), - ], - }); - }); - - it(`returns bulk error`, async () => { - const expectedErrorResult = { type: obj3.type, id: obj3.id, error: 'Oh no, a bulk error!' }; - await bulkCreateError(obj3, true, expectedErrorResult); - }); - }); - - describe('migration', () => { - it(`migrates the docs and serializes the migrated docs`, async () => { - migrator.migrateDocument.mockImplementation(mockMigrateDocument); - await bulkCreateSuccess([obj1, obj2]); - const docs = [obj1, obj2].map((x) => ({ ...x, ...mockTimestampFields })); - expectMigrationArgs(docs[0], true, 1); - expectMigrationArgs(docs[1], true, 2); - - const migratedDocs = docs.map((x) => migrator.migrateDocument(x)); - expect(serializer.savedObjectToRaw).toHaveBeenNthCalledWith(1, migratedDocs[0]); - expect(serializer.savedObjectToRaw).toHaveBeenNthCalledWith(2, migratedDocs[1]); - }); - - it(`adds namespace to body when providing namespace for single-namespace type`, async () => { - await bulkCreateSuccess([obj1, obj2], { namespace }); - expectMigrationArgs({ namespace }, true, 1); - expectMigrationArgs({ namespace }, true, 2); - }); - - it(`doesn't add namespace to body when providing no namespace for single-namespace type`, async () => { - await bulkCreateSuccess([obj1, obj2]); - expectMigrationArgs({ namespace: expect.anything() }, false, 1); - expectMigrationArgs({ namespace: expect.anything() }, false, 2); - }); - - it(`doesn't add namespace to body when not using single-namespace type`, async () => { - const objects = [ - { ...obj1, type: NAMESPACE_AGNOSTIC_TYPE }, - { ...obj2, type: MULTI_NAMESPACE_ISOLATED_TYPE }, - ]; - await bulkCreateSuccess(objects, { namespace }); - expectMigrationArgs({ namespace: expect.anything() }, false, 1); - expectMigrationArgs({ namespace: expect.anything() }, false, 2); - }); - - it(`adds namespaces to body when providing namespace for multi-namespace type`, async () => { - const objects = [obj1, obj2].map((obj) => ({ - ...obj, - type: MULTI_NAMESPACE_ISOLATED_TYPE, - })); - await bulkCreateSuccess(objects, { namespace }); - expectMigrationArgs({ namespaces: [namespace] }, true, 1); - expectMigrationArgs({ namespaces: [namespace] }, true, 2); - }); - - it(`adds default namespaces to body when providing no namespace for multi-namespace type`, async () => { - const objects = [obj1, obj2].map((obj) => ({ - ...obj, - type: MULTI_NAMESPACE_ISOLATED_TYPE, - })); - await bulkCreateSuccess(objects); - expectMigrationArgs({ namespaces: ['default'] }, true, 1); - expectMigrationArgs({ namespaces: ['default'] }, true, 2); - }); - - it(`doesn't add namespaces to body when not using multi-namespace type`, async () => { - const objects = [obj1, { ...obj2, type: NAMESPACE_AGNOSTIC_TYPE }]; - await bulkCreateSuccess(objects); - expectMigrationArgs({ namespaces: expect.anything() }, false, 1); - expectMigrationArgs({ namespaces: expect.anything() }, false, 2); - }); - }); - - describe('returns', () => { - it(`formats the ES response`, async () => { - const result = await bulkCreateSuccess([obj1, obj2]); - expect(result).toEqual({ - saved_objects: [obj1, obj2].map((x) => expectSuccessResult(x)), - }); - }); - - it.todo(`should return objects in the same order regardless of type`); - - it(`handles a mix of successful creates and errors`, async () => { - const obj = { - type: 'unknownType', - id: 'three', - }; - const objects = [obj1, obj, obj2]; - const response = getMockBulkCreateResponse([obj1, obj2]); - client.bulk.mockResolvedValueOnce( - elasticsearchClientMock.createSuccessTransportRequestPromise(response) - ); - const result = await savedObjectsRepository.bulkCreate(objects); - expect(client.bulk).toHaveBeenCalledTimes(1); - expect(result).toEqual({ - saved_objects: [expectSuccessResult(obj1), expectError(obj), expectSuccessResult(obj2)], - }); - }); - - it(`a deserialized saved object`, async () => { - // Test for fix to https://github.com/elastic/kibana/issues/65088 where - // we returned raw ID's when an object without an id was created. - const namespace = 'myspace'; - const response = getMockBulkCreateResponse([obj1, obj2], namespace); - client.bulk.mockResolvedValueOnce( - elasticsearchClientMock.createSuccessTransportRequestPromise(response) - ); - - // Bulk create one object with id unspecified, and one with id specified - const result = await savedObjectsRepository.bulkCreate([{ ...obj1, id: undefined }, obj2], { - namespace, - }); - - // Assert that both raw docs from the ES response are deserialized - expect(serializer.rawToSavedObject).toHaveBeenNthCalledWith(1, { - ...response.items[0].create, - _source: { - ...response.items[0].create._source, - coreMigrationVersion: '2.0.0', // the document migrator adds this to all objects before creation - namespaces: response.items[0].create._source.namespaces, - }, - _id: expect.stringMatching(/^myspace:config:[0-9a-f]{8}-([0-9a-f]{4}-){3}[0-9a-f]{12}$/), - }); - expect(serializer.rawToSavedObject).toHaveBeenNthCalledWith(2, { - ...response.items[1].create, - _source: { - ...response.items[1].create._source, - coreMigrationVersion: '2.0.0', // the document migrator adds this to all objects before creation - namespaces: response.items[1].create._source.namespaces, - }, - }); - - // Assert that ID's are deserialized to remove the type and namespace - expect(result.saved_objects[0].id).toEqual( - expect.stringMatching(/^[0-9a-f]{8}-([0-9a-f]{4}-){3}[0-9a-f]{12}$/) - ); - expect(result.saved_objects[1].id).toEqual(obj2.id); - }); - }); - }); - - describe('#bulkGet', () => { - const obj1 = { - type: 'config', - id: '6.0.0-alpha1', - attributes: { title: 'Testing' }, - references: [ - { - name: 'ref_0', - type: 'test', - id: '1', - }, - ], - originId: 'some-origin-id', // only one of the results has an originId, this is intentional to test both a positive and negative case - }; - const obj2 = { - type: 'index-pattern', - id: 'logstash-*', - attributes: { title: 'Testing' }, - references: [ - { - name: 'ref_0', - type: 'test', - id: '2', - }, - ], - }; - const namespace = 'foo-namespace'; - - const bulkGet = async (objects, options) => - savedObjectsRepository.bulkGet( - objects.map(({ type, id, namespaces }) => ({ type, id, namespaces })), // bulkGet only uses type, id, and optionally namespaces - options - ); - const bulkGetSuccess = async (objects, options) => { - const response = getMockMgetResponse(objects, options?.namespace); - client.mget.mockResolvedValueOnce( - elasticsearchClientMock.createSuccessTransportRequestPromise(response) - ); - const result = await bulkGet(objects, options); - expect(client.mget).toHaveBeenCalledTimes(1); - return result; - }; - - const _expectClientCallArgs = ( - objects, - { _index = expect.any(String), getId = () => expect.any(String) } - ) => { - expect(client.mget).toHaveBeenCalledWith( - expect.objectContaining({ - body: { - docs: objects.map(({ type, id }) => - expect.objectContaining({ - _index, - _id: getId(type, id), - }) - ), - }, - }), - expect.anything() - ); - }; - - describe('client calls', () => { - it(`prepends namespace to the id when providing namespace for single-namespace type`, async () => { - const getId = (type, id) => `${namespace}:${type}:${id}`; // test that the raw document ID equals this (e.g., has a namespace prefix) - await bulkGetSuccess([obj1, obj2], { namespace }); - _expectClientCallArgs([obj1, obj2], { getId }); - }); - - it(`prepends namespace to the id when providing namespaces for single-namespace type`, async () => { - const getId = (type, id) => `${namespace}:${type}:${id}`; // test that the raw document ID equals this (e.g., has a namespace prefix) - const objects = [obj1, obj2].map((obj) => ({ ...obj, namespaces: [namespace] })); - await bulkGetSuccess(objects, { namespace: 'some-other-ns' }); - _expectClientCallArgs([obj1, obj2], { getId }); - }); - - it(`doesn't prepend namespace to the id when providing no namespace for single-namespace type`, async () => { - const getId = (type, id) => `${type}:${id}`; // test that the raw document ID equals this (e.g., does not have a namespace prefix) - await bulkGetSuccess([obj1, obj2]); - _expectClientCallArgs([obj1, obj2], { getId }); - }); - - it(`normalizes options.namespace from 'default' to undefined`, async () => { - const getId = (type, id) => `${type}:${id}`; // test that the raw document ID equals this (e.g., does not have a namespace prefix) - await bulkGetSuccess([obj1, obj2], { namespace: 'default' }); - _expectClientCallArgs([obj1, obj2], { getId }); - }); - - it(`doesn't prepend namespace to the id when not using single-namespace type`, async () => { - const getId = (type, id) => `${type}:${id}`; // test that the raw document ID equals this (e.g., does not have a namespace prefix) - let objects = [obj1, obj2].map((obj) => ({ ...obj, type: NAMESPACE_AGNOSTIC_TYPE })); - await bulkGetSuccess(objects, { namespace }); - _expectClientCallArgs(objects, { getId }); - - client.mget.mockClear(); - objects = [obj1, { ...obj2, namespaces: ['some-other-ns'] }].map((obj) => ({ - ...obj, - type: MULTI_NAMESPACE_ISOLATED_TYPE, - })); - await bulkGetSuccess(objects, { namespace }); - _expectClientCallArgs(objects, { getId }); - }); - }); - - describe('errors', () => { - const bulkGetError = async (obj, isBulkError, expectedErrorResult) => { - let response; - if (isBulkError) { - // mock the bulk error for only the second object - mockGetBulkOperationError.mockReturnValueOnce(undefined); - mockGetBulkOperationError.mockReturnValueOnce(expectedErrorResult.error); - response = getMockMgetResponse([obj1, obj, obj2]); - } else { - response = getMockMgetResponse([obj1, obj2]); - } - client.mget.mockResolvedValueOnce( - elasticsearchClientMock.createSuccessTransportRequestPromise(response) - ); - - const objects = [obj1, obj, obj2]; - const result = await bulkGet(objects); - expect(client.mget).toHaveBeenCalled(); - expect(result).toEqual({ - saved_objects: [expectSuccess(obj1), expectedErrorResult, expectSuccess(obj2)], - }); - }; - - it(`throws when options.namespace is '*'`, async () => { - const obj = { type: 'dashboard', id: 'three' }; - await expect( - savedObjectsRepository.bulkGet([obj], { namespace: ALL_NAMESPACES_STRING }) - ).rejects.toThrowError(createBadRequestError('"options.namespace" cannot be "*"')); - }); - - it(`returns error when namespaces is used with a space-agnostic object`, async () => { - const obj = { type: NAMESPACE_AGNOSTIC_TYPE, id: 'three', namespaces: [] }; - await bulkGetError( - obj, - undefined, - expectErrorResult( - obj, - createBadRequestError('"namespaces" cannot be used on space-agnostic types') - ) - ); - }); - - it(`returns error when namespaces is used with a space-isolated object and does not specify a single space`, async () => { - const doTest = async (objType, namespaces) => { - const obj = { type: objType, id: 'three', namespaces }; - await bulkGetError( - obj, - undefined, - expectErrorResult( - obj, - createBadRequestError( - '"namespaces" can only specify a single space when used with space-isolated types' - ) - ) - ); - }; - await doTest('dashboard', ['spacex', 'spacey']); - await doTest('dashboard', ['*']); - await doTest(MULTI_NAMESPACE_ISOLATED_TYPE, ['spacex', 'spacey']); - await doTest(MULTI_NAMESPACE_ISOLATED_TYPE, ['*']); - }); - - it(`returns error when type is invalid`, async () => { - const obj = { type: 'unknownType', id: 'three' }; - await bulkGetError(obj, undefined, expectErrorInvalidType(obj)); - }); - - it(`returns error when type is hidden`, async () => { - const obj = { type: HIDDEN_TYPE, id: 'three' }; - await bulkGetError(obj, undefined, expectErrorInvalidType(obj)); - }); - - it(`returns error when document is not found`, async () => { - const obj = { type: 'dashboard', id: 'three', found: false }; - await bulkGetError(obj, true, expectErrorNotFound(obj)); - }); - - it(`handles missing ids gracefully`, async () => { - const obj = { type: 'dashboard', id: undefined, found: false }; - await bulkGetError(obj, true, expectErrorNotFound(obj)); - }); - - it(`returns error when type is multi-namespace and the document exists, but not in this namespace`, async () => { - const obj = { - type: MULTI_NAMESPACE_ISOLATED_TYPE, - id: 'three', - namespace: 'bar-namespace', - }; - await bulkGetError(obj, true, expectErrorNotFound(obj)); - }); - }); - - describe('returns', () => { - const expectSuccessResult = ({ type, id }, doc) => ({ - type, - id, - namespaces: doc._source.namespaces ?? ['default'], - ...(doc._source.originId && { originId: doc._source.originId }), - ...(doc._source.updated_at && { updated_at: doc._source.updated_at }), - version: encodeHitVersion(doc), - attributes: doc._source[type], - references: doc._source.references || [], - migrationVersion: doc._source.migrationVersion, - }); - - it(`returns early for empty objects argument`, async () => { - const result = await bulkGet([]); - expect(result).toEqual({ saved_objects: [] }); - expect(client.mget).not.toHaveBeenCalled(); - }); - - it(`formats the ES response`, async () => { - const response = getMockMgetResponse([obj1, obj2]); - client.mget.mockResolvedValueOnce( - elasticsearchClientMock.createSuccessTransportRequestPromise(response) - ); - const result = await bulkGet([obj1, obj2]); - expect(client.mget).toHaveBeenCalledTimes(1); - expect(result).toEqual({ - saved_objects: [ - expectSuccessResult(obj1, response.docs[0]), - expectSuccessResult(obj2, response.docs[1]), - ], - }); - }); - - it(`handles a mix of successful gets and errors`, async () => { - const response = getMockMgetResponse([obj1, obj2]); - client.mget.mockResolvedValueOnce( - elasticsearchClientMock.createSuccessTransportRequestPromise(response) - ); - const obj = { type: 'unknownType', id: 'three' }; - const result = await bulkGet([obj1, obj, obj2]); - expect(client.mget).toHaveBeenCalledTimes(1); - expect(result).toEqual({ - saved_objects: [ - expectSuccessResult(obj1, response.docs[0]), - expectError(obj), - expectSuccessResult(obj2, response.docs[1]), - ], - }); - }); - - it(`includes namespaces property for single-namespace and multi-namespace documents`, async () => { - const obj = { type: MULTI_NAMESPACE_ISOLATED_TYPE, id: 'three' }; - const result = await bulkGetSuccess([obj1, obj]); - expect(result).toEqual({ - saved_objects: [ - expect.objectContaining({ namespaces: ['default'] }), - expect.objectContaining({ namespaces: expect.any(Array) }), - ], - }); - }); - }); - }); - - describe('#bulkResolve', () => { - afterEach(() => { - mockInternalBulkResolve.mockReset(); - }); - - it('passes arguments to the internalBulkResolve module and returns the expected results', async () => { - mockInternalBulkResolve.mockResolvedValue({ - resolved_objects: [ - { saved_object: 'mock-object', outcome: 'exactMatch' }, - { - type: 'obj-type', - id: 'obj-id-2', - error: SavedObjectsErrorHelpers.createGenericNotFoundError('obj-type', 'obj-id-2'), - }, - ], - }); - - const objects = [ - { type: 'obj-type', id: 'obj-id-1' }, - { type: 'obj-type', id: 'obj-id-2' }, - ]; - await expect(savedObjectsRepository.bulkResolve(objects)).resolves.toEqual({ - resolved_objects: [ - { - saved_object: 'mock-object', - outcome: 'exactMatch', - }, - { - saved_object: { - type: 'obj-type', - id: 'obj-id-2', - error: { - error: 'Not Found', - message: 'Saved object [obj-type/obj-id-2] not found', - statusCode: 404, - }, - }, - outcome: 'exactMatch', - }, - ], - }); - expect(mockInternalBulkResolve).toHaveBeenCalledTimes(1); - expect(mockInternalBulkResolve).toHaveBeenCalledWith(expect.objectContaining({ objects })); - }); - - it('throws when internalBulkResolve throws', async () => { - const error = new Error('Oh no!'); - mockInternalBulkResolve.mockRejectedValue(error); - - await expect(savedObjectsRepository.resolve()).rejects.toEqual(error); - }); - }); - - describe('#bulkUpdate', () => { - const obj1 = { - type: 'config', - id: '6.0.0-alpha1', - attributes: { title: 'Test One' }, - }; - const obj2 = { - type: 'index-pattern', - id: 'logstash-*', - attributes: { title: 'Test Two' }, - }; - const references = [{ name: 'ref_0', type: 'test', id: '1' }]; - const originId = 'some-origin-id'; - const namespace = 'foo-namespace'; - - const getMockBulkUpdateResponse = (objects, options, includeOriginId) => ({ - items: objects.map(({ type, id }) => ({ - update: { - _id: `${ - registry.isSingleNamespace(type) && options?.namespace ? `${options?.namespace}:` : '' - }${type}:${id}`, - ...mockVersionProps, - get: { - _source: { - // "includeOriginId" is not an option for the operation; however, if the existing saved object contains an originId attribute, the - // operation will return it in the result. This flag is just used for test purposes to modify the mock cluster call response. - ...(includeOriginId && { originId }), - }, - }, - result: 'updated', - }, - })), - }); - - const bulkUpdateSuccess = async (objects, options, includeOriginId) => { - const multiNamespaceObjects = objects.filter(({ type }) => registry.isMultiNamespace(type)); - if (multiNamespaceObjects?.length) { - const response = getMockMgetResponse(multiNamespaceObjects, options?.namespace); - client.mget.mockResolvedValueOnce( - elasticsearchClientMock.createSuccessTransportRequestPromise(response) - ); - } - const response = getMockBulkUpdateResponse(objects, options?.namespace, includeOriginId); - client.bulk.mockResolvedValueOnce( - elasticsearchClientMock.createSuccessTransportRequestPromise(response) - ); - const result = await savedObjectsRepository.bulkUpdate(objects, options); - expect(client.mget).toHaveBeenCalledTimes(multiNamespaceObjects?.length ? 1 : 0); - return result; - }; - - // bulk create calls have two objects for each source -- the action, and the source - const expectClientCallArgsAction = ( - objects, - { method, _index = expect.any(String), getId = () => expect.any(String), overrides } - ) => { - const body = []; - for (const { type, id } of objects) { - body.push({ - [method]: { - _index, - _id: getId(type, id), - ...overrides, - }, - }); - body.push(expect.any(Object)); - } - expect(client.bulk).toHaveBeenCalledWith( - expect.objectContaining({ body }), - expect.anything() - ); - }; - - const expectObjArgs = ({ type, attributes }) => [ - expect.any(Object), - { - doc: expect.objectContaining({ - [type]: attributes, - ...mockTimestampFields, - }), - }, - ]; - - describe('client calls', () => { - it(`should use the ES bulk action by default`, async () => { - await bulkUpdateSuccess([obj1, obj2]); - expect(client.bulk).toHaveBeenCalled(); - }); - - it(`should use the ES mget action before bulk action for any types that are multi-namespace`, async () => { - const objects = [obj1, { ...obj2, type: MULTI_NAMESPACE_ISOLATED_TYPE }]; - await bulkUpdateSuccess(objects); - expect(client.bulk).toHaveBeenCalled(); - expect(client.mget).toHaveBeenCalled(); - - const docs = [ - expect.objectContaining({ _id: `${MULTI_NAMESPACE_ISOLATED_TYPE}:${obj2.id}` }), - ]; - expect(client.mget).toHaveBeenCalledWith( - expect.objectContaining({ body: { docs } }), - expect.anything() - ); - }); - - it(`formats the ES request`, async () => { - await bulkUpdateSuccess([obj1, obj2]); - const body = [...expectObjArgs(obj1), ...expectObjArgs(obj2)]; - expect(client.bulk).toHaveBeenCalledWith( - expect.objectContaining({ body }), - expect.anything() - ); - }); - - it(`formats the ES request for any types that are multi-namespace`, async () => { - const _obj2 = { ...obj2, type: MULTI_NAMESPACE_ISOLATED_TYPE }; - await bulkUpdateSuccess([obj1, _obj2]); - const body = [...expectObjArgs(obj1), ...expectObjArgs(_obj2)]; - expect(client.bulk).toHaveBeenCalledWith( - expect.objectContaining({ body }), - expect.anything() - ); - }); - - it(`doesnt call Elasticsearch if there are no valid objects to update`, async () => { - const objects = [obj1, obj2].map((x) => ({ ...x, type: 'unknownType' })); - await savedObjectsRepository.bulkUpdate(objects); - expect(client.bulk).toHaveBeenCalledTimes(0); - }); - - it(`defaults to no references`, async () => { - await bulkUpdateSuccess([obj1, obj2]); - const expected = { doc: expect.not.objectContaining({ references: expect.anything() }) }; - const body = [expect.any(Object), expected, expect.any(Object), expected]; - expect(client.bulk).toHaveBeenCalledWith( - expect.objectContaining({ body }), - expect.anything() - ); - }); - - it(`accepts custom references array`, async () => { - const test = async (references) => { - const objects = [obj1, obj2].map((obj) => ({ ...obj, references })); - await bulkUpdateSuccess(objects); - const expected = { doc: expect.objectContaining({ references }) }; - const body = [expect.any(Object), expected, expect.any(Object), expected]; - expect(client.bulk).toHaveBeenCalledWith( - expect.objectContaining({ body }), - expect.anything() - ); - client.bulk.mockClear(); - }; - await test(references); - await test(['string']); - await test([]); - }); - - it(`doesn't accept custom references if not an array`, async () => { - const test = async (references) => { - const objects = [obj1, obj2].map((obj) => ({ ...obj, references })); - await bulkUpdateSuccess(objects); - const expected = { doc: expect.not.objectContaining({ references: expect.anything() }) }; - const body = [expect.any(Object), expected, expect.any(Object), expected]; - expect(client.bulk).toHaveBeenCalledWith( - expect.objectContaining({ body }), - expect.anything() - ); - client.bulk.mockClear(); - }; - await test('string'); - await test(123); - await test(true); - await test(null); - }); - - it(`defaults to a refresh setting of wait_for`, async () => { - await bulkUpdateSuccess([obj1, obj2]); - expect(client.bulk).toHaveBeenCalledWith( - expect.objectContaining({ refresh: 'wait_for' }), - expect.anything() - ); - }); - - it(`defaults to the version of the existing document for multi-namespace types`, async () => { - // only multi-namespace documents are obtained using a pre-flight mget request - const objects = [ - { ...obj1, type: MULTI_NAMESPACE_ISOLATED_TYPE }, - { ...obj2, type: MULTI_NAMESPACE_ISOLATED_TYPE }, - ]; - await bulkUpdateSuccess(objects); - const overrides = { - if_seq_no: mockVersionProps._seq_no, - if_primary_term: mockVersionProps._primary_term, - }; - expectClientCallArgsAction(objects, { method: 'update', overrides }); - }); - - it(`defaults to no version for types that are not multi-namespace`, async () => { - const objects = [obj1, { ...obj2, type: NAMESPACE_AGNOSTIC_TYPE }]; - await bulkUpdateSuccess(objects); - expectClientCallArgsAction(objects, { method: 'update' }); - }); - - it(`accepts version`, async () => { - const version = encodeHitVersion({ _seq_no: 100, _primary_term: 200 }); - // test with both non-multi-namespace and multi-namespace types - const objects = [ - { ...obj1, version }, - { ...obj2, type: MULTI_NAMESPACE_ISOLATED_TYPE, version }, - ]; - await bulkUpdateSuccess(objects); - const overrides = { if_seq_no: 100, if_primary_term: 200 }; - expectClientCallArgsAction(objects, { method: 'update', overrides }, 2); - }); - - it(`prepends namespace to the id when providing namespace for single-namespace type`, async () => { - const getId = (type, id) => `${namespace}:${type}:${id}`; // test that the raw document ID equals this (e.g., has a namespace prefix) - await bulkUpdateSuccess([obj1, obj2], { namespace }); - expectClientCallArgsAction([obj1, obj2], { method: 'update', getId }); - - jest.clearAllMocks(); - // test again with object namespace string that supersedes the operation's namespace ID - await bulkUpdateSuccess([ - { ...obj1, namespace }, - { ...obj2, namespace }, - ]); - expectClientCallArgsAction([obj1, obj2], { method: 'update', getId }); - }); - - it(`doesn't prepend namespace to the id when providing no namespace for single-namespace type`, async () => { - const getId = (type, id) => `${type}:${id}`; // test that the raw document ID equals this (e.g., does not have a namespace prefix) - await bulkUpdateSuccess([obj1, obj2]); - expectClientCallArgsAction([obj1, obj2], { method: 'update', getId }); - - jest.clearAllMocks(); - // test again with object namespace string that supersedes the operation's namespace ID - await bulkUpdateSuccess( - [ - { ...obj1, namespace: 'default' }, - { ...obj2, namespace: 'default' }, - ], - { namespace } - ); - expectClientCallArgsAction([obj1, obj2], { method: 'update', getId }); - }); - - it(`normalizes options.namespace from 'default' to undefined`, async () => { - const getId = (type, id) => `${type}:${id}`; - await bulkUpdateSuccess([obj1, obj2], { namespace: 'default' }); - expectClientCallArgsAction([obj1, obj2], { method: 'update', getId }); - }); - - it(`doesn't prepend namespace to the id when not using single-namespace type`, async () => { - const getId = (type, id) => `${type}:${id}`; // test that the raw document ID equals this (e.g., does not have a namespace prefix) - const overrides = { - // bulkUpdate uses a preflight `get` request for multi-namespace saved objects, and specifies that version on `update` - // we aren't testing for this here, but we need to include Jest assertions so this test doesn't fail - if_primary_term: expect.any(Number), - if_seq_no: expect.any(Number), - }; - const _obj1 = { ...obj1, type: NAMESPACE_AGNOSTIC_TYPE }; - const _obj2 = { ...obj2, type: MULTI_NAMESPACE_ISOLATED_TYPE }; - - await bulkUpdateSuccess([_obj1], { namespace }); - expectClientCallArgsAction([_obj1], { method: 'update', getId }); - client.bulk.mockClear(); - await bulkUpdateSuccess([_obj2], { namespace }); - expectClientCallArgsAction([_obj2], { method: 'update', getId, overrides }, 2); - - jest.clearAllMocks(); - // test again with object namespace string that supersedes the operation's namespace ID - await bulkUpdateSuccess([{ ..._obj1, namespace }]); - expectClientCallArgsAction([_obj1], { method: 'update', getId }); - client.bulk.mockClear(); - await bulkUpdateSuccess([{ ..._obj2, namespace }]); - expectClientCallArgsAction([_obj2], { method: 'update', getId, overrides }, 2); - }); - }); - - describe('errors', () => { - afterEach(() => { - mockGetBulkOperationError.mockReset(); - }); - - const obj = { - type: 'dashboard', - id: 'three', - }; - - const bulkUpdateError = async (obj, isBulkError, expectedErrorResult) => { - const objects = [obj1, obj, obj2]; - const mockResponse = getMockBulkUpdateResponse(objects); - if (isBulkError) { - // mock the bulk error for only the second object - mockGetBulkOperationError.mockReturnValueOnce(undefined); - mockGetBulkOperationError.mockReturnValueOnce(expectedErrorResult.error); - } - client.bulk.mockResolvedValueOnce( - elasticsearchClientMock.createSuccessTransportRequestPromise(mockResponse) - ); - - const result = await savedObjectsRepository.bulkUpdate(objects); - expect(client.bulk).toHaveBeenCalled(); - const objCall = isBulkError ? expectObjArgs(obj) : []; - const body = [...expectObjArgs(obj1), ...objCall, ...expectObjArgs(obj2)]; - expect(client.bulk).toHaveBeenCalledWith( - expect.objectContaining({ body }), - expect.anything() - ); - expect(result).toEqual({ - saved_objects: [expectSuccess(obj1), expectedErrorResult, expectSuccess(obj2)], - }); - }; - - const bulkUpdateMultiError = async ([obj1, _obj, obj2], options, mgetResponse) => { - client.mget.mockResolvedValueOnce( - elasticsearchClientMock.createSuccessTransportRequestPromise(mgetResponse, { - statusCode: mgetResponse.statusCode, - }) - ); - - const bulkResponse = getMockBulkUpdateResponse([obj1, obj2], namespace); - client.bulk.mockResolvedValueOnce( - elasticsearchClientMock.createSuccessTransportRequestPromise(bulkResponse) - ); - - const result = await savedObjectsRepository.bulkUpdate([obj1, _obj, obj2], options); - expect(client.bulk).toHaveBeenCalled(); - expect(client.mget).toHaveBeenCalled(); - const body = [...expectObjArgs(obj1), ...expectObjArgs(obj2)]; - expect(client.bulk).toHaveBeenCalledWith( - expect.objectContaining({ body }), - expect.anything() - ); - - expect(result).toEqual({ - saved_objects: [expectSuccess(obj1), expectErrorNotFound(_obj), expectSuccess(obj2)], - }); - }; - - it(`throws when options.namespace is '*'`, async () => { - await expect( - savedObjectsRepository.bulkUpdate([obj], { namespace: ALL_NAMESPACES_STRING }) - ).rejects.toThrowError(createBadRequestError('"options.namespace" cannot be "*"')); - }); - - it(`returns error when type is invalid`, async () => { - const _obj = { ...obj, type: 'unknownType' }; - await bulkUpdateError(_obj, false, expectErrorNotFound(_obj)); - }); - - it(`returns error when type is hidden`, async () => { - const _obj = { ...obj, type: HIDDEN_TYPE }; - await bulkUpdateError(_obj, false, expectErrorNotFound(_obj)); - }); - - it(`returns error when object namespace is '*'`, async () => { - const _obj = { ...obj, namespace: '*' }; - await bulkUpdateError( - _obj, - false, - expectErrorResult(obj, createBadRequestError('"namespace" cannot be "*"')) - ); - }); - - it(`returns error when ES is unable to find the document (mget)`, async () => { - const _obj = { ...obj, type: MULTI_NAMESPACE_ISOLATED_TYPE, found: false }; - const mgetResponse = getMockMgetResponse([_obj]); - await bulkUpdateMultiError([obj1, _obj, obj2], undefined, mgetResponse); - }); - - it(`returns error when ES is unable to find the index (mget)`, async () => { - const _obj = { ...obj, type: MULTI_NAMESPACE_ISOLATED_TYPE }; - const mgetResponse = { statusCode: 404 }; - await bulkUpdateMultiError([obj1, _obj, obj2], { namespace }, mgetResponse); - }); - - it(`returns error when there is a conflict with an existing multi-namespace saved object (mget)`, async () => { - const _obj = { ...obj, type: MULTI_NAMESPACE_ISOLATED_TYPE }; - const mgetResponse = getMockMgetResponse([_obj], 'bar-namespace'); - await bulkUpdateMultiError([obj1, _obj, obj2], { namespace }, mgetResponse); - }); - - it(`returns bulk error`, async () => { - const expectedErrorResult = { type: obj.type, id: obj.id, error: 'Oh no, a bulk error!' }; - await bulkUpdateError(obj, true, expectedErrorResult); - }); - }); - - describe('returns', () => { - const expectSuccessResult = ({ type, id, attributes, references, namespaces, originId }) => ({ - type, - id, - originId, - attributes, - references, - version: mockVersion, - namespaces: namespaces ?? ['default'], - ...mockTimestampFields, - }); - - it(`formats the ES response`, async () => { - const response = await bulkUpdateSuccess([obj1, obj2]); - expect(response).toEqual({ - saved_objects: [obj1, obj2].map(expectSuccessResult), - }); - }); - - it(`includes references`, async () => { - const objects = [obj1, obj2].map((obj) => ({ ...obj, references })); - const response = await bulkUpdateSuccess(objects); - expect(response).toEqual({ - saved_objects: objects.map(expectSuccessResult), - }); - }); - - it(`handles a mix of successful updates and errors`, async () => { - const obj = { - type: 'unknownType', - id: 'three', - }; - const objects = [obj1, obj, obj2]; - const mockResponse = getMockBulkUpdateResponse(objects); - client.bulk.mockResolvedValueOnce( - elasticsearchClientMock.createSuccessTransportRequestPromise(mockResponse) - ); - - const result = await savedObjectsRepository.bulkUpdate(objects); - expect(client.bulk).toHaveBeenCalledTimes(1); - expect(result).toEqual({ - saved_objects: [expectSuccessResult(obj1), expectError(obj), expectSuccessResult(obj2)], - }); - }); - - it(`includes namespaces property for single-namespace and multi-namespace documents`, async () => { - const obj = { type: MULTI_NAMESPACE_ISOLATED_TYPE, id: 'three' }; - const result = await bulkUpdateSuccess([obj1, obj]); - expect(result).toEqual({ - saved_objects: [ - expect.objectContaining({ namespaces: expect.any(Array) }), - expect.objectContaining({ namespaces: expect.any(Array) }), - ], - }); - }); - - it(`includes originId property if present in cluster call response`, async () => { - const obj = { type: MULTI_NAMESPACE_ISOLATED_TYPE, id: 'three' }; - const result = await bulkUpdateSuccess([obj1, obj], {}, true); - expect(result).toEqual({ - saved_objects: [ - expect.objectContaining({ originId }), - expect.objectContaining({ originId }), - ], - }); - }); - }); - }); - - describe('#checkConflicts', () => { - const obj1 = { type: 'dashboard', id: 'one' }; - const obj2 = { type: 'dashboard', id: 'two' }; - const obj3 = { type: MULTI_NAMESPACE_ISOLATED_TYPE, id: 'three' }; - const obj4 = { type: MULTI_NAMESPACE_ISOLATED_TYPE, id: 'four' }; - const obj5 = { type: MULTI_NAMESPACE_ISOLATED_TYPE, id: 'five' }; - const obj6 = { type: NAMESPACE_AGNOSTIC_TYPE, id: 'six' }; - const obj7 = { type: NAMESPACE_AGNOSTIC_TYPE, id: 'seven' }; - const namespace = 'foo-namespace'; - - const checkConflicts = async (objects, options) => - savedObjectsRepository.checkConflicts( - objects.map(({ type, id }) => ({ type, id })), // checkConflicts only uses type and id - options - ); - const checkConflictsSuccess = async (objects, options) => { - const response = getMockMgetResponse(objects, options?.namespace); - client.mget.mockResolvedValue( - elasticsearchClientMock.createSuccessTransportRequestPromise(response) - ); - const result = await checkConflicts(objects, options); - expect(client.mget).toHaveBeenCalledTimes(1); - return result; - }; - - const _expectClientCallArgs = ( - objects, - { _index = expect.any(String), getId = () => expect.any(String) } - ) => { - expect(client.mget).toHaveBeenCalledWith( - expect.objectContaining({ - body: { - docs: objects.map(({ type, id }) => - expect.objectContaining({ - _index, - _id: getId(type, id), - }) - ), - }, - }), - expect.anything() - ); - }; - - describe('client calls', () => { - it(`doesn't make a cluster call if the objects array is empty`, async () => { - await checkConflicts([]); - expect(client.mget).not.toHaveBeenCalled(); - }); - - it(`prepends namespace to the id when providing namespace for single-namespace type`, async () => { - const getId = (type, id) => `${namespace}:${type}:${id}`; // test that the raw document ID equals this (e.g., has a namespace prefix) - await checkConflictsSuccess([obj1, obj2], { namespace }); - _expectClientCallArgs([obj1, obj2], { getId }); - }); - - it(`doesn't prepend namespace to the id when providing no namespace for single-namespace type`, async () => { - const getId = (type, id) => `${type}:${id}`; // test that the raw document ID equals this (e.g., does not have a namespace prefix) - await checkConflictsSuccess([obj1, obj2]); - _expectClientCallArgs([obj1, obj2], { getId }); - }); - - it(`normalizes options.namespace from 'default' to undefined`, async () => { - const getId = (type, id) => `${type}:${id}`; // test that the raw document ID equals this (e.g., does not have a namespace prefix) - await checkConflictsSuccess([obj1, obj2], { namespace: 'default' }); - _expectClientCallArgs([obj1, obj2], { getId }); - }); - - it(`doesn't prepend namespace to the id when not using single-namespace type`, async () => { - const getId = (type, id) => `${type}:${id}`; // test that the raw document ID equals this (e.g., does not have a namespace prefix) - // obj3 is multi-namespace, and obj6 is namespace-agnostic - await checkConflictsSuccess([obj3, obj6], { namespace }); - _expectClientCallArgs([obj3, obj6], { getId }); - }); - }); - - describe('errors', () => { - it(`throws when options.namespace is '*'`, async () => { - await expect( - savedObjectsRepository.checkConflicts([obj1], { namespace: ALL_NAMESPACES_STRING }) - ).rejects.toThrowError(createBadRequestError('"options.namespace" cannot be "*"')); - }); - }); - - describe('returns', () => { - it(`expected results`, async () => { - const unknownTypeObj = { type: 'unknownType', id: 'three' }; - const hiddenTypeObj = { type: HIDDEN_TYPE, id: 'three' }; - const objects = [unknownTypeObj, hiddenTypeObj, obj1, obj2, obj3, obj4, obj5, obj6, obj7]; - const response = { - status: 200, - docs: [ - getMockGetResponse(obj1), - { found: false }, - getMockGetResponse(obj3), - getMockGetResponse({ ...obj4, namespace: 'bar-namespace' }), - { found: false }, - getMockGetResponse(obj6), - { found: false }, - ], - }; - client.mget.mockResolvedValue( - elasticsearchClientMock.createSuccessTransportRequestPromise(response) - ); - - const result = await checkConflicts(objects); - expect(client.mget).toHaveBeenCalledTimes(1); - expect(result).toEqual({ - errors: [ - { ...unknownTypeObj, error: createUnsupportedTypeError(unknownTypeObj.type) }, - { ...hiddenTypeObj, error: createUnsupportedTypeError(hiddenTypeObj.type) }, - { ...obj1, error: createConflictError(obj1.type, obj1.id) }, - // obj2 was not found so it does not result in a conflict error - { ...obj3, error: createConflictError(obj3.type, obj3.id) }, - { - ...obj4, - error: { - ...createConflictError(obj4.type, obj4.id), - metadata: { isNotOverwritable: true }, - }, - }, - // obj5 was not found so it does not result in a conflict error - { ...obj6, error: createConflictError(obj6.type, obj6.id) }, - // obj7 was not found so it does not result in a conflict error - ], - }); - }); - }); - }); - - describe('#create', () => { - beforeEach(() => { - mockPreflightCheckForCreate.mockReset(); - mockPreflightCheckForCreate.mockImplementation(({ objects }) => { - return objects.map(({ type, id }) => ({ type, id })); // respond with no errors by default - }); - client.create.mockImplementation((params) => - elasticsearchClientMock.createSuccessTransportRequestPromise({ - _id: params.id, - ...mockVersionProps, - }) - ); - }); - - const type = 'index-pattern'; - const attributes = { title: 'Logstash' }; - const id = 'logstash-*'; - const namespace = 'foo-namespace'; - const originId = 'some-origin-id'; - const references = [ - { - name: 'ref_0', - type: 'test', - id: '123', - }, - ]; - - const createSuccess = async (type, attributes, options) => { - const result = await savedObjectsRepository.create(type, attributes, options); - return result; - }; - - describe('client calls', () => { - it(`should use the ES index action if ID is not defined and overwrite=true`, async () => { - await createSuccess(type, attributes, { overwrite: true }); - expect(mockPreflightCheckForCreate).not.toHaveBeenCalled(); - expect(client.index).toHaveBeenCalled(); - }); - - it(`should use the ES create action if ID is not defined and overwrite=false`, async () => { - await createSuccess(type, attributes); - expect(mockPreflightCheckForCreate).not.toHaveBeenCalled(); - expect(client.create).toHaveBeenCalled(); - }); - - it(`should use the ES index with version if ID and version are defined and overwrite=true`, async () => { - await createSuccess(type, attributes, { id, overwrite: true, version: mockVersion }); - expect(mockPreflightCheckForCreate).not.toHaveBeenCalled(); - expect(client.index).toHaveBeenCalled(); - expect(client.index.mock.calls[0][0]).toMatchObject({ - if_seq_no: mockVersionProps._seq_no, - if_primary_term: mockVersionProps._primary_term, - }); - }); - - it(`should use the ES create action if ID is defined and overwrite=false`, async () => { - await createSuccess(type, attributes, { id }); - expect(mockPreflightCheckForCreate).not.toHaveBeenCalled(); - expect(client.create).toHaveBeenCalled(); - }); - - it(`should use the preflightCheckForCreate action then create action if type is multi-namespace, ID is defined, and overwrite=false`, async () => { - await createSuccess(MULTI_NAMESPACE_TYPE, attributes, { id }); - expect(mockPreflightCheckForCreate).toHaveBeenCalled(); - expect(mockPreflightCheckForCreate).toHaveBeenCalledWith( - expect.objectContaining({ - objects: [ - { type: MULTI_NAMESPACE_TYPE, id, overwrite: false, namespaces: ['default'] }, - ], - }) - ); - expect(client.create).toHaveBeenCalled(); - }); - - it(`should use the preflightCheckForCreate action then index action if type is multi-namespace, ID is defined, and overwrite=true`, async () => { - await createSuccess(MULTI_NAMESPACE_ISOLATED_TYPE, attributes, { id, overwrite: true }); - expect(mockPreflightCheckForCreate).toHaveBeenCalled(); - expect(mockPreflightCheckForCreate).toHaveBeenCalledWith( - expect.objectContaining({ - objects: [ - { type: MULTI_NAMESPACE_ISOLATED_TYPE, id, overwrite: true, namespaces: ['default'] }, - ], - }) - ); - expect(client.index).toHaveBeenCalled(); - }); - - it(`defaults to empty references array`, async () => { - await createSuccess(type, attributes, { id }); - expect(client.create.mock.calls[0][0].body.references).toEqual([]); - }); - - it(`accepts custom references array`, async () => { - const test = async (references) => { - await createSuccess(type, attributes, { id, references }); - expect(client.create.mock.calls[0][0].body.references).toEqual(references); - client.create.mockClear(); - }; - await test(references); - await test(['string']); - await test([]); - }); - - it(`doesn't accept custom references if not an array`, async () => { - const test = async (references) => { - await createSuccess(type, attributes, { id, references }); - expect(client.create.mock.calls[0][0].body.references).not.toBeDefined(); - client.create.mockClear(); - }; - await test('string'); - await test(123); - await test(true); - await test(null); - }); - - it(`defaults to no originId`, async () => { - await createSuccess(type, attributes, { id }); - expect(client.create).toHaveBeenCalledWith( - expect.objectContaining({ - body: expect.not.objectContaining({ originId: expect.anything() }), - }), - expect.anything() - ); - }); - - it(`accepts custom originId`, async () => { - await createSuccess(type, attributes, { id, originId }); - expect(client.create).toHaveBeenCalledWith( - expect.objectContaining({ - body: expect.objectContaining({ originId }), - }), - expect.anything() - ); - }); - - it(`defaults to a refresh setting of wait_for`, async () => { - await createSuccess(type, attributes); - expect(client.create).toHaveBeenCalledWith( - expect.objectContaining({ refresh: 'wait_for' }), - expect.anything() - ); - }); - - it(`should use default index`, async () => { - await createSuccess(type, attributes, { id }); - expect(client.create).toHaveBeenCalledWith( - expect.objectContaining({ index: '.kibana-test_8.0.0-testing' }), - expect.anything() - ); - }); - - it(`should use custom index`, async () => { - await createSuccess(CUSTOM_INDEX_TYPE, attributes, { id }); - expect(client.create).toHaveBeenCalledWith( - expect.objectContaining({ index: 'custom_8.0.0-testing' }), - expect.anything() - ); - }); - - it(`self-generates an id if none is provided`, async () => { - await createSuccess(type, attributes); - expect(client.create).toHaveBeenCalledWith( - expect.objectContaining({ - id: expect.objectContaining(/index-pattern:[0-9a-f]{8}-([0-9a-f]{4}-){3}[0-9a-f]{12}/), - }), - expect.anything() - ); - }); - - it(`prepends namespace to the id and adds namespace to the body when providing namespace for single-namespace type`, async () => { - await createSuccess(type, attributes, { id, namespace }); - expect(client.create).toHaveBeenCalledWith( - expect.objectContaining({ - id: `${namespace}:${type}:${id}`, - body: expect.objectContaining({ namespace }), - }), - expect.anything() - ); - }); - - it(`doesn't prepend namespace to the id or add namespace to the body when providing no namespace for single-namespace type`, async () => { - await createSuccess(type, attributes, { id }); - expect(client.create).toHaveBeenCalledWith( - expect.objectContaining({ - id: `${type}:${id}`, - body: expect.not.objectContaining({ namespace: expect.anything() }), - }), - expect.anything() - ); - }); - - it(`normalizes options.namespace from 'default' to undefined`, async () => { - await createSuccess(type, attributes, { id, namespace: 'default' }); - expect(client.create).toHaveBeenCalledWith( - expect.objectContaining({ - id: `${type}:${id}`, - body: expect.not.objectContaining({ namespace: expect.anything() }), - }), - expect.anything() - ); - }); - - it(`doesn't prepend namespace to the id and adds namespaces to body when using multi-namespace type`, async () => { - // first object does not have an existing document to overwrite - await createSuccess(MULTI_NAMESPACE_TYPE, attributes, { id, namespace }); - mockPreflightCheckForCreate.mockResolvedValueOnce([ - { - type: MULTI_NAMESPACE_TYPE, - id, - existingDocument: { _source: { namespaces: ['*'] } }, // second object does have an existing document to overwrite - }, - ]); - await createSuccess(MULTI_NAMESPACE_ISOLATED_TYPE, attributes, { - id, - namespace, - overwrite: true, - }); - - expect(mockPreflightCheckForCreate).toHaveBeenCalledTimes(2); - expect(mockPreflightCheckForCreate).toHaveBeenNthCalledWith( - 1, - expect.objectContaining({ - objects: [ - { type: MULTI_NAMESPACE_TYPE, id, overwrite: false, namespaces: [namespace] }, - ], - }) - ); - expect(mockPreflightCheckForCreate).toHaveBeenNthCalledWith( - 2, - expect.objectContaining({ - objects: [ - { type: MULTI_NAMESPACE_ISOLATED_TYPE, id, overwrite: true, namespaces: [namespace] }, - ], - }) - ); - - expect(client.create).toHaveBeenCalledTimes(1); - expect(client.create).toHaveBeenCalledWith( - expect.objectContaining({ - id: `${MULTI_NAMESPACE_TYPE}:${id}`, - body: expect.objectContaining({ namespaces: [namespace] }), - }), - expect.anything() - ); - expect(client.index).toHaveBeenCalledTimes(1); - expect(client.index).toHaveBeenCalledWith( - expect.objectContaining({ - id: `${MULTI_NAMESPACE_ISOLATED_TYPE}:${id}`, - body: expect.objectContaining({ namespaces: ['*'] }), - }), - expect.anything() - ); - }); - - it(`adds initialNamespaces instead of namespace`, async () => { - const ns2 = 'bar-namespace'; - const ns3 = 'baz-namespace'; - // first object does not get passed in to preflightCheckForCreate at all - await savedObjectsRepository.create('dashboard', attributes, { - id, - namespace, - initialNamespaces: [ns2], - }); - // second object does not have an existing document to overwrite - await savedObjectsRepository.create(MULTI_NAMESPACE_TYPE, attributes, { - id, - namespace, - initialNamespaces: [ns2, ns3], - }); - mockPreflightCheckForCreate.mockResolvedValueOnce([ - { - type: MULTI_NAMESPACE_ISOLATED_TYPE, - id, - existingDocument: { _source: { namespaces: ['something-else'] } }, // third object does have an existing document to overwrite - }, - ]); - await savedObjectsRepository.create(MULTI_NAMESPACE_ISOLATED_TYPE, attributes, { - id, - namespace, - initialNamespaces: [ns2], - overwrite: true, - }); - - expect(mockPreflightCheckForCreate).toHaveBeenCalledTimes(2); - expect(mockPreflightCheckForCreate).toHaveBeenNthCalledWith( - 1, - expect.objectContaining({ - objects: [{ type: MULTI_NAMESPACE_TYPE, id, overwrite: false, namespaces: [ns2, ns3] }], - }) - ); - expect(mockPreflightCheckForCreate).toHaveBeenNthCalledWith( - 2, - expect.objectContaining({ - objects: [ - { type: MULTI_NAMESPACE_ISOLATED_TYPE, id, overwrite: true, namespaces: [ns2] }, - ], - }) - ); - - expect(client.create).toHaveBeenCalledTimes(2); - expect(client.create).toHaveBeenNthCalledWith( - 1, - expect.objectContaining({ - id: `${ns2}:dashboard:${id}`, - body: expect.objectContaining({ namespace: ns2 }), - }), - expect.anything() - ); - expect(client.create).toHaveBeenNthCalledWith( - 2, - expect.objectContaining({ - id: `${MULTI_NAMESPACE_TYPE}:${id}`, - body: expect.objectContaining({ namespaces: [ns2, ns3] }), - }), - expect.anything() - ); - expect(client.index).toHaveBeenCalledTimes(1); - expect(client.index).toHaveBeenCalledWith( - expect.objectContaining({ - id: `${MULTI_NAMESPACE_ISOLATED_TYPE}:${id}`, - body: expect.objectContaining({ namespaces: [ns2] }), - }), - expect.anything() - ); - }); - - it(`normalizes initialNamespaces from 'default' to undefined`, async () => { - await savedObjectsRepository.create('dashboard', attributes, { - id, - namespace, - initialNamespaces: ['default'], - }); - - expect(client.create).toHaveBeenCalledTimes(1); - expect(client.create).toHaveBeenNthCalledWith( - 1, - expect.objectContaining({ - id: `dashboard:${id}`, - body: expect.not.objectContaining({ namespace: 'default' }), - }), - expect.anything() - ); - }); - - it(`doesn't prepend namespace to the id or add namespace or namespaces fields when using namespace-agnostic type`, async () => { - await createSuccess(NAMESPACE_AGNOSTIC_TYPE, attributes, { id, namespace }); - expect(client.create).toHaveBeenCalledWith( - expect.objectContaining({ - id: `${NAMESPACE_AGNOSTIC_TYPE}:${id}`, - body: expect.not.objectContaining({ - namespace: expect.anything(), - namespaces: expect.anything(), - }), - }), - expect.anything() - ); - }); - }); - - describe('errors', () => { - it(`throws when options.initialNamespaces is used with a space-agnostic object`, async () => { - await expect( - savedObjectsRepository.create(NAMESPACE_AGNOSTIC_TYPE, attributes, { - initialNamespaces: [namespace], - }) - ).rejects.toThrowError( - createBadRequestError('"initialNamespaces" cannot be used on space-agnostic types') - ); - }); - - it(`throws when options.initialNamespaces is empty`, async () => { - await expect( - savedObjectsRepository.create(MULTI_NAMESPACE_TYPE, attributes, { initialNamespaces: [] }) - ).rejects.toThrowError( - createBadRequestError('"initialNamespaces" must be a non-empty array of strings') - ); - }); - - it(`throws when options.initialNamespaces is used with a space-isolated object and does not specify a single space`, async () => { - const doTest = async (objType, initialNamespaces) => { - await expect( - savedObjectsRepository.create(objType, attributes, { initialNamespaces }) - ).rejects.toThrowError( - createBadRequestError( - '"initialNamespaces" can only specify a single space when used with space-isolated types' - ) - ); - }; - await doTest('dashboard', ['spacex', 'spacey']); - await doTest('dashboard', ['*']); - await doTest(MULTI_NAMESPACE_ISOLATED_TYPE, ['spacex', 'spacey']); - await doTest(MULTI_NAMESPACE_ISOLATED_TYPE, ['*']); - }); - - it(`throws when options.namespace is '*'`, async () => { - await expect( - savedObjectsRepository.create(type, attributes, { namespace: ALL_NAMESPACES_STRING }) - ).rejects.toThrowError(createBadRequestError('"options.namespace" cannot be "*"')); - }); - - it(`throws when type is invalid`, async () => { - await expect(savedObjectsRepository.create('unknownType', attributes)).rejects.toThrowError( - createUnsupportedTypeError('unknownType') - ); - expect(client.create).not.toHaveBeenCalled(); - }); - - it(`throws when type is hidden`, async () => { - await expect(savedObjectsRepository.create(HIDDEN_TYPE, attributes)).rejects.toThrowError( - createUnsupportedTypeError(HIDDEN_TYPE) - ); - expect(client.create).not.toHaveBeenCalled(); - }); - - it(`throws when there is a conflict from preflightCheckForCreate`, async () => { - mockPreflightCheckForCreate.mockResolvedValueOnce([ - { type: MULTI_NAMESPACE_ISOLATED_TYPE, id, error: { type: 'unresolvableConflict' } }, // error type and metadata dont matter - ]); - await expect( - savedObjectsRepository.create(MULTI_NAMESPACE_ISOLATED_TYPE, attributes, { - id, - overwrite: true, - namespace, - }) - ).rejects.toThrowError(createConflictError(MULTI_NAMESPACE_ISOLATED_TYPE, id)); - expect(mockPreflightCheckForCreate).toHaveBeenCalled(); - }); - - it.todo(`throws when automatic index creation fails`); - - it.todo(`throws when an unexpected failure occurs`); - }); - - describe('migration', () => { - beforeEach(() => { - migrator.migrateDocument.mockImplementation(mockMigrateDocument); - }); - - it(`migrates a document and serializes the migrated doc`, async () => { - const migrationVersion = mockMigrationVersion; - await createSuccess(type, attributes, { id, references, migrationVersion }); - const doc = { type, id, attributes, references, migrationVersion, ...mockTimestampFields }; - expectMigrationArgs(doc); - - const migratedDoc = migrator.migrateDocument(doc); - expect(serializer.savedObjectToRaw).toHaveBeenLastCalledWith(migratedDoc); - }); - - it(`adds namespace to body when providing namespace for single-namespace type`, async () => { - await createSuccess(type, attributes, { id, namespace }); - expectMigrationArgs({ namespace }); - }); - - it(`doesn't add namespace to body when providing no namespace for single-namespace type`, async () => { - await createSuccess(type, attributes, { id }); - expectMigrationArgs({ namespace: expect.anything() }, false); - }); - - it(`doesn't add namespace to body when not using single-namespace type`, async () => { - await createSuccess(NAMESPACE_AGNOSTIC_TYPE, attributes, { id, namespace }); - expectMigrationArgs({ namespace: expect.anything() }, false, 1); - - client.create.mockClear(); - await createSuccess(MULTI_NAMESPACE_ISOLATED_TYPE, attributes, { id }); - expectMigrationArgs({ namespace: expect.anything() }, false, 2); - }); - - it(`adds namespaces to body when providing namespace for multi-namespace type`, async () => { - await createSuccess(MULTI_NAMESPACE_ISOLATED_TYPE, attributes, { id, namespace }); - expectMigrationArgs({ namespaces: [namespace] }); - }); - - it(`adds default namespaces to body when providing no namespace for multi-namespace type`, async () => { - await createSuccess(MULTI_NAMESPACE_ISOLATED_TYPE, attributes, { id }); - expectMigrationArgs({ namespaces: ['default'] }); - }); - - it(`doesn't add namespaces to body when not using multi-namespace type`, async () => { - await createSuccess(type, attributes, { id }); - expectMigrationArgs({ namespaces: expect.anything() }, false, 1); - - client.create.mockClear(); - await createSuccess(NAMESPACE_AGNOSTIC_TYPE, attributes, { id }); - expectMigrationArgs({ namespaces: expect.anything() }, false, 2); - }); - }); - - describe('returns', () => { - it(`formats the ES response`, async () => { - const result = await createSuccess(type, attributes, { - id, - namespace, - references, - originId, - }); - expect(result).toEqual({ - type, - id, - originId, - ...mockTimestampFields, - version: mockVersion, - attributes, - references, - namespaces: [namespace ?? 'default'], - migrationVersion: { [type]: '1.1.1' }, - coreMigrationVersion: KIBANA_VERSION, - }); - }); - }); - }); - - describe('#delete', () => { - const type = 'index-pattern'; - const id = 'logstash-*'; - const namespace = 'foo-namespace'; - - const deleteSuccess = async (type, id, options, internalOptions = {}) => { - const { mockGetResponseValue } = internalOptions; - if (registry.isMultiNamespace(type)) { - const mockGetResponse = - mockGetResponseValue ?? getMockGetResponse({ type, id }, options?.namespace); - client.get.mockResolvedValueOnce( - elasticsearchClientMock.createSuccessTransportRequestPromise(mockGetResponse) - ); - } - client.delete.mockResolvedValueOnce( - elasticsearchClientMock.createSuccessTransportRequestPromise({ result: 'deleted' }) - ); - const result = await savedObjectsRepository.delete(type, id, options); - expect(client.get).toHaveBeenCalledTimes(registry.isMultiNamespace(type) ? 1 : 0); - return result; - }; - - beforeEach(() => { - mockDeleteLegacyUrlAliases.mockClear(); - mockDeleteLegacyUrlAliases.mockResolvedValue(); - }); - - describe('client calls', () => { - it(`should use the ES delete action when not using a multi-namespace type`, async () => { - await deleteSuccess(type, id); - expect(client.get).not.toHaveBeenCalled(); - expect(client.delete).toHaveBeenCalledTimes(1); - }); - - it(`should use ES get action then delete action when using a multi-namespace type`, async () => { - await deleteSuccess(MULTI_NAMESPACE_ISOLATED_TYPE, id); - expect(client.get).toHaveBeenCalledTimes(1); - expect(client.delete).toHaveBeenCalledTimes(1); - }); - - it(`includes the version of the existing document when using a multi-namespace type`, async () => { - await deleteSuccess(MULTI_NAMESPACE_ISOLATED_TYPE, id); - const versionProperties = { - if_seq_no: mockVersionProps._seq_no, - if_primary_term: mockVersionProps._primary_term, - }; - expect(client.delete).toHaveBeenCalledWith( - expect.objectContaining(versionProperties), - expect.anything() - ); - }); - - it(`defaults to a refresh setting of wait_for`, async () => { - await deleteSuccess(type, id); - expect(client.delete).toHaveBeenCalledWith( - expect.objectContaining({ refresh: 'wait_for' }), - expect.anything() - ); - }); - - it(`prepends namespace to the id when providing namespace for single-namespace type`, async () => { - await deleteSuccess(type, id, { namespace }); - expect(client.delete).toHaveBeenCalledWith( - expect.objectContaining({ id: `${namespace}:${type}:${id}` }), - expect.anything() - ); - }); - - it(`doesn't prepend namespace to the id when providing no namespace for single-namespace type`, async () => { - await deleteSuccess(type, id); - expect(client.delete).toHaveBeenCalledWith( - expect.objectContaining({ id: `${type}:${id}` }), - expect.anything() - ); - }); - - it(`normalizes options.namespace from 'default' to undefined`, async () => { - await deleteSuccess(type, id, { namespace: 'default' }); - expect(client.delete).toHaveBeenCalledWith( - expect.objectContaining({ id: `${type}:${id}` }), - expect.anything() - ); - }); - - it(`doesn't prepend namespace to the id when not using single-namespace type`, async () => { - await deleteSuccess(NAMESPACE_AGNOSTIC_TYPE, id, { namespace }); - expect(client.delete).toHaveBeenCalledWith( - expect.objectContaining({ id: `${NAMESPACE_AGNOSTIC_TYPE}:${id}` }), - expect.anything() - ); - - client.delete.mockClear(); - await deleteSuccess(MULTI_NAMESPACE_ISOLATED_TYPE, id, { namespace }); - expect(client.delete).toHaveBeenCalledWith( - expect.objectContaining({ id: `${MULTI_NAMESPACE_ISOLATED_TYPE}:${id}` }), - expect.anything() - ); - }); - }); - - describe('legacy URL aliases', () => { - it(`doesn't delete legacy URL aliases for single-namespace object types`, async () => { - await deleteSuccess(type, id, { namespace }); - expect(mockDeleteLegacyUrlAliases).not.toHaveBeenCalled(); - }); - - // We intentionally do not include a test case for a multi-namespace object with a "not found" preflight result, because that throws - // an error (without deleting aliases) and we already have a test case for that - - it(`deletes legacy URL aliases for multi-namespace object types (all spaces)`, async () => { - const internalOptions = { - mockGetResponseValue: getMockGetResponse( - { type: MULTI_NAMESPACE_TYPE, id }, - ALL_NAMESPACES_STRING - ), - }; - await deleteSuccess(MULTI_NAMESPACE_TYPE, id, { namespace, force: true }, internalOptions); - expect(mockDeleteLegacyUrlAliases).toHaveBeenCalledWith( - expect.objectContaining({ - type: MULTI_NAMESPACE_TYPE, - id, - namespaces: [], - deleteBehavior: 'exclusive', - }) - ); - }); - - it(`deletes legacy URL aliases for multi-namespace object types (specific spaces)`, async () => { - await deleteSuccess(MULTI_NAMESPACE_TYPE, id, { namespace }); // this function mocks a preflight response with the given namespace by default - expect(mockDeleteLegacyUrlAliases).toHaveBeenCalledWith( - expect.objectContaining({ - type: MULTI_NAMESPACE_TYPE, - id, - namespaces: [namespace], - deleteBehavior: 'inclusive', - }) - ); - }); - - it(`logs a message when deleteLegacyUrlAliases returns an error`, async () => { - client.get.mockResolvedValueOnce( - elasticsearchClientMock.createSuccessTransportRequestPromise( - getMockGetResponse({ type: MULTI_NAMESPACE_ISOLATED_TYPE, id, namespace }) - ) - ); - client.delete.mockResolvedValueOnce( - elasticsearchClientMock.createSuccessTransportRequestPromise({ result: 'deleted' }) - ); - mockDeleteLegacyUrlAliases.mockRejectedValueOnce(new Error('Oh no!')); - await savedObjectsRepository.delete(MULTI_NAMESPACE_ISOLATED_TYPE, id, { namespace }); - expect(client.get).toHaveBeenCalledTimes(1); - expect(logger.error).toHaveBeenCalledTimes(1); - expect(logger.error).toHaveBeenCalledWith( - 'Unable to delete aliases when deleting an object: Oh no!' - ); - }); - }); - - describe('errors', () => { - const expectNotFoundError = async (type, id, options) => { - await expect(savedObjectsRepository.delete(type, id, options)).rejects.toThrowError( - createGenericNotFoundError(type, id) - ); - }; - - it(`throws when options.namespace is '*'`, async () => { - await expect( - savedObjectsRepository.delete(type, id, { namespace: ALL_NAMESPACES_STRING }) - ).rejects.toThrowError(createBadRequestError('"options.namespace" cannot be "*"')); - }); - - it(`throws when type is invalid`, async () => { - await expectNotFoundError('unknownType', id); - expect(client.delete).not.toHaveBeenCalled(); - }); - - it(`throws when type is hidden`, async () => { - await expectNotFoundError(HIDDEN_TYPE, id); - expect(client.delete).not.toHaveBeenCalled(); - }); - - it(`throws when ES is unable to find the document during get`, async () => { - client.get.mockResolvedValueOnce( - elasticsearchClientMock.createSuccessTransportRequestPromise({ found: false }, undefined) - ); - await expectNotFoundError(MULTI_NAMESPACE_ISOLATED_TYPE, id); - expect(client.get).toHaveBeenCalledTimes(1); - }); - - it(`throws when ES is unable to find the index during get`, async () => { - client.get.mockResolvedValueOnce( - elasticsearchClientMock.createSuccessTransportRequestPromise({}, { statusCode: 404 }) - ); - await expectNotFoundError(MULTI_NAMESPACE_ISOLATED_TYPE, id); - expect(client.get).toHaveBeenCalledTimes(1); - }); - - it(`throws when the type is multi-namespace and the document exists, but not in this namespace`, async () => { - const response = getMockGetResponse({ type: MULTI_NAMESPACE_ISOLATED_TYPE, id }, namespace); - client.get.mockResolvedValueOnce( - elasticsearchClientMock.createSuccessTransportRequestPromise(response) - ); - await expectNotFoundError(MULTI_NAMESPACE_ISOLATED_TYPE, id, { - namespace: 'bar-namespace', - }); - expect(client.get).toHaveBeenCalledTimes(1); - }); - - it(`throws when the type is multi-namespace and the document has multiple namespaces and the force option is not enabled`, async () => { - const response = getMockGetResponse({ type: MULTI_NAMESPACE_ISOLATED_TYPE, id, namespace }); - response._source.namespaces = [namespace, 'bar-namespace']; - client.get.mockResolvedValueOnce( - elasticsearchClientMock.createSuccessTransportRequestPromise(response) - ); - await expect( - savedObjectsRepository.delete(MULTI_NAMESPACE_ISOLATED_TYPE, id, { namespace }) - ).rejects.toThrowError( - 'Unable to delete saved object that exists in multiple namespaces, use the `force` option to delete it anyway' - ); - expect(client.get).toHaveBeenCalledTimes(1); - }); - - it(`throws when the type is multi-namespace and the document has all namespaces and the force option is not enabled`, async () => { - const response = getMockGetResponse({ type: MULTI_NAMESPACE_ISOLATED_TYPE, id, namespace }); - response._source.namespaces = ['*']; - client.get.mockResolvedValueOnce( - elasticsearchClientMock.createSuccessTransportRequestPromise(response) - ); - await expect( - savedObjectsRepository.delete(MULTI_NAMESPACE_ISOLATED_TYPE, id, { namespace }) - ).rejects.toThrowError( - 'Unable to delete saved object that exists in multiple namespaces, use the `force` option to delete it anyway' - ); - expect(client.get).toHaveBeenCalledTimes(1); - }); - - it(`throws when ES is unable to find the document during delete`, async () => { - client.delete.mockResolvedValueOnce( - elasticsearchClientMock.createSuccessTransportRequestPromise({ result: 'not_found' }) - ); - await expectNotFoundError(type, id); - expect(client.delete).toHaveBeenCalledTimes(1); - }); - - it(`throws when ES is unable to find the index during delete`, async () => { - client.delete.mockResolvedValueOnce( - elasticsearchClientMock.createSuccessTransportRequestPromise({ - error: { type: 'index_not_found_exception' }, - }) - ); - await expectNotFoundError(type, id); - expect(client.delete).toHaveBeenCalledTimes(1); - }); - - it(`throws when ES returns an unexpected response`, async () => { - client.delete.mockResolvedValueOnce( - elasticsearchClientMock.createSuccessTransportRequestPromise({ - result: 'something unexpected', - }) - ); - await expect(savedObjectsRepository.delete(type, id)).rejects.toThrowError( - 'Unexpected Elasticsearch DELETE response' - ); - expect(client.delete).toHaveBeenCalledTimes(1); - }); - }); - - describe('returns', () => { - it(`returns an empty object on success`, async () => { - const result = await deleteSuccess(type, id); - expect(result).toEqual({}); - }); - }); - }); - - describe('#deleteByNamespace', () => { - const namespace = 'foo-namespace'; - const mockUpdateResults = { - took: 15, - timed_out: false, - total: 3, - updated: 2, - deleted: 1, - batches: 1, - version_conflicts: 0, - noops: 0, - retries: { bulk: 0, search: 0 }, - throttled_millis: 0, - requests_per_second: -1.0, - throttled_until_millis: 0, - failures: [], - }; - - const deleteByNamespaceSuccess = async (namespace, options) => { - client.updateByQuery.mockResolvedValueOnce( - elasticsearchClientMock.createSuccessTransportRequestPromise(mockUpdateResults) - ); - const result = await savedObjectsRepository.deleteByNamespace(namespace, options); - expect(getSearchDslNS.getSearchDsl).toHaveBeenCalledTimes(1); - expect(client.updateByQuery).toHaveBeenCalledTimes(1); - return result; - }; - - describe('client calls', () => { - it(`should use the ES updateByQuery action`, async () => { - await deleteByNamespaceSuccess(namespace); - expect(client.updateByQuery).toHaveBeenCalledTimes(1); - }); - - it(`should use all indices for types that are not namespace-agnostic`, async () => { - await deleteByNamespaceSuccess(namespace); - expect(client.updateByQuery).toHaveBeenCalledWith( - expect.objectContaining({ - index: ['.kibana-test_8.0.0-testing', 'custom_8.0.0-testing'], - }), - expect.anything() - ); - }); - }); - - describe('errors', () => { - it(`throws when namespace is not a string or is '*'`, async () => { - const test = async (namespace) => { - await expect(savedObjectsRepository.deleteByNamespace(namespace)).rejects.toThrowError( - `namespace is required, and must be a string` - ); - expect(client.updateByQuery).not.toHaveBeenCalled(); - }; - await test(undefined); - await test(['namespace']); - await test(123); - await test(true); - await test(ALL_NAMESPACES_STRING); - }); - }); - - describe('returns', () => { - it(`returns the query results on success`, async () => { - const result = await deleteByNamespaceSuccess(namespace); - expect(result).toEqual(mockUpdateResults); - }); - }); - - describe('search dsl', () => { - it(`constructs a query using all multi-namespace types, and another using all single-namespace types`, async () => { - await deleteByNamespaceSuccess(namespace); - const allTypes = registry.getAllTypes().map((type) => type.name); - expect(getSearchDslNS.getSearchDsl).toHaveBeenCalledWith(mappings, registry, { - namespaces: [namespace], - type: [ - ...allTypes.filter((type) => !registry.isNamespaceAgnostic(type)), - LEGACY_URL_ALIAS_TYPE, - ], - kueryNode: expect.anything(), - }); - }); - }); - }); - - describe('#removeReferencesTo', () => { - const type = 'type'; - const id = 'id'; - const defaultOptions = {}; - - const updatedCount = 42; - - const removeReferencesToSuccess = async (options = defaultOptions) => { - client.updateByQuery.mockResolvedValueOnce( - elasticsearchClientMock.createSuccessTransportRequestPromise({ - updated: updatedCount, - }) - ); - return await savedObjectsRepository.removeReferencesTo(type, id, options); - }; - - describe('client calls', () => { - it('should use the ES updateByQuery action', async () => { - await removeReferencesToSuccess(); - expect(client.updateByQuery).toHaveBeenCalledTimes(1); - }); - - it('uses the correct default `refresh` value', async () => { - await removeReferencesToSuccess(); - expect(client.updateByQuery).toHaveBeenCalledWith( - expect.objectContaining({ - refresh: true, - }), - expect.any(Object) - ); - }); - - it('merges output of getSearchDsl into es request body', async () => { - const query = { query: 1, aggregations: 2 }; - getSearchDslNS.getSearchDsl.mockReturnValue(query); - await removeReferencesToSuccess({ type }); - - expect(client.updateByQuery).toHaveBeenCalledWith( - expect.objectContaining({ - body: expect.objectContaining({ ...query }), - }), - expect.anything() - ); - }); - - it('should set index to all known SO indices on the request', async () => { - await removeReferencesToSuccess(); - expect(client.updateByQuery).toHaveBeenCalledWith( - expect.objectContaining({ - index: ['.kibana-test_8.0.0-testing', 'custom_8.0.0-testing'], - }), - expect.anything() - ); - }); - - it('should use the `refresh` option in the request', async () => { - const refresh = Symbol(); - - await removeReferencesToSuccess({ refresh }); - expect(client.updateByQuery).toHaveBeenCalledWith( - expect.objectContaining({ - refresh, - }), - expect.anything() - ); - }); - - it('should pass the correct parameters to the update script', async () => { - await removeReferencesToSuccess(); - expect(client.updateByQuery).toHaveBeenCalledWith( - expect.objectContaining({ - body: expect.objectContaining({ - script: expect.objectContaining({ - params: { - type, - id, - }, - }), - }), - }), - expect.anything() - ); - }); - }); - - describe('search dsl', () => { - it(`passes mappings and registry to getSearchDsl`, async () => { - await removeReferencesToSuccess(); - expect(getSearchDslNS.getSearchDsl).toHaveBeenCalledWith( - mappings, - registry, - expect.anything() - ); - }); - - it('passes namespace to getSearchDsl', async () => { - await removeReferencesToSuccess({ namespace: 'some-ns' }); - expect(getSearchDslNS.getSearchDsl).toHaveBeenCalledWith( - mappings, - registry, - expect.objectContaining({ - namespaces: ['some-ns'], - }) - ); - }); - - it('passes hasReference to getSearchDsl', async () => { - await removeReferencesToSuccess(); - expect(getSearchDslNS.getSearchDsl).toHaveBeenCalledWith( - mappings, - registry, - expect.objectContaining({ - hasReference: { - type, - id, - }, - }) - ); - }); - - it('passes all known types to getSearchDsl', async () => { - await removeReferencesToSuccess(); - expect(getSearchDslNS.getSearchDsl).toHaveBeenCalledWith( - mappings, - registry, - expect.objectContaining({ - type: registry.getAllTypes().map((type) => type.name), - }) - ); - }); - }); - - describe('returns', () => { - it('returns the updated count from the ES response', async () => { - const response = await removeReferencesToSuccess(); - expect(response.updated).toBe(updatedCount); - }); - }); - - describe('errors', () => { - it(`throws when ES returns failures`, async () => { - client.updateByQuery.mockResolvedValueOnce( - elasticsearchClientMock.createSuccessTransportRequestPromise({ - updated: 7, - failures: ['failure', 'another-failure'], - }) - ); - - await expect( - savedObjectsRepository.removeReferencesTo(type, id, defaultOptions) - ).rejects.toThrowError(createConflictError(type, id)); - }); - }); - }); - - describe('#find', () => { - const generateSearchResults = (namespace) => { - return { - hits: { - total: 4, - hits: [ - { - _index: '.kibana', - _id: `${namespace ? `${namespace}:` : ''}index-pattern:logstash-*`, - _score: 1, - ...mockVersionProps, - _source: { - namespace, - originId: 'some-origin-id', // only one of the results has an originId, this is intentional to test both a positive and negative case - type: 'index-pattern', - ...mockTimestampFields, - 'index-pattern': { - title: 'logstash-*', - timeFieldName: '@timestamp', - notExpandable: true, - }, - }, - }, - { - _index: '.kibana', - _id: `${namespace ? `${namespace}:` : ''}config:6.0.0-alpha1`, - _score: 2, - ...mockVersionProps, - _source: { - namespace, - type: 'config', - ...mockTimestampFields, - config: { - buildNum: 8467, - defaultIndex: 'logstash-*', - }, - }, - }, - { - _index: '.kibana', - _id: `${namespace ? `${namespace}:` : ''}index-pattern:stocks-*`, - _score: 3, - ...mockVersionProps, - _source: { - namespace, - type: 'index-pattern', - ...mockTimestampFields, - 'index-pattern': { - title: 'stocks-*', - timeFieldName: '@timestamp', - notExpandable: true, - }, - }, - }, - { - _index: '.kibana', - _id: `${NAMESPACE_AGNOSTIC_TYPE}:something`, - _score: 4, - ...mockVersionProps, - _source: { - type: NAMESPACE_AGNOSTIC_TYPE, - ...mockTimestampFields, - [NAMESPACE_AGNOSTIC_TYPE]: { - name: 'bar', - }, - }, - }, - ], - }, - }; - }; - - const type = 'index-pattern'; - const namespace = 'foo-namespace'; - - const findSuccess = async (options, namespace) => { - client.search.mockResolvedValueOnce( - elasticsearchClientMock.createSuccessTransportRequestPromise( - generateSearchResults(namespace) - ) - ); - const result = await savedObjectsRepository.find(options); - expect(getSearchDslNS.getSearchDsl).toHaveBeenCalledTimes(1); - expect(client.search).toHaveBeenCalledTimes(1); - return result; - }; - - describe('client calls', () => { - it(`should use the ES search action`, async () => { - await findSuccess({ type }); - expect(client.search).toHaveBeenCalledTimes(1); - }); - - it(`merges output of getSearchDsl into es request body`, async () => { - const query = { query: 1, aggregations: 2 }; - getSearchDslNS.getSearchDsl.mockReturnValue(query); - await findSuccess({ type }); - - expect(client.search).toHaveBeenCalledWith( - expect.objectContaining({ - body: expect.objectContaining({ ...query }), - }), - expect.anything() - ); - }); - - it(`accepts per_page/page`, async () => { - await findSuccess({ type, perPage: 10, page: 6 }); - expect(client.search).toHaveBeenCalledWith( - expect.objectContaining({ - size: 10, - from: 50, - }), - expect.anything() - ); - }); - - it(`accepts preference`, async () => { - await findSuccess({ type, preference: 'pref' }); - expect(client.search).toHaveBeenCalledWith( - expect.objectContaining({ - preference: 'pref', - }), - expect.anything() - ); - }); - - it(`can filter by fields`, async () => { - await findSuccess({ type, fields: ['title'] }); - expect(client.search).toHaveBeenCalledWith( - expect.objectContaining({ - body: expect.objectContaining({ - _source: [ - `${type}.title`, - 'namespace', - 'namespaces', - 'type', - 'references', - 'migrationVersion', - 'coreMigrationVersion', - 'updated_at', - 'originId', - 'title', - ], - }), - }), - expect.anything() - ); - }); - - it(`should set rest_total_hits_as_int to true on a request`, async () => { - await findSuccess({ type }); - expect(client.search).toHaveBeenCalledWith( - expect.objectContaining({ - rest_total_hits_as_int: true, - }), - expect.anything() - ); - }); - - it(`should not make a client call when attempting to find only invalid or hidden types`, async () => { - const test = async (types) => { - await savedObjectsRepository.find({ type: types }); - expect(client.search).not.toHaveBeenCalled(); - }; - - await test('unknownType'); - await test(HIDDEN_TYPE); - await test(['unknownType', HIDDEN_TYPE]); - }); - }); - - describe('errors', () => { - it(`throws when type is not defined`, async () => { - await expect(savedObjectsRepository.find({})).rejects.toThrowError( - 'options.type must be a string or an array of strings' - ); - expect(client.search).not.toHaveBeenCalled(); - }); - - it(`throws when namespaces is an empty array`, async () => { - await expect( - savedObjectsRepository.find({ type: 'foo', namespaces: [] }) - ).rejects.toThrowError('options.namespaces cannot be an empty array'); - expect(client.search).not.toHaveBeenCalled(); - }); - - it(`throws when type is not falsy and typeToNamespacesMap is defined`, async () => { - await expect( - savedObjectsRepository.find({ type: 'foo', typeToNamespacesMap: new Map() }) - ).rejects.toThrowError( - 'options.type must be an empty string when options.typeToNamespacesMap is used' - ); - expect(client.search).not.toHaveBeenCalled(); - }); - - it(`throws when type is not an empty array and typeToNamespacesMap is defined`, async () => { - const test = async (args) => { - await expect(savedObjectsRepository.find(args)).rejects.toThrowError( - 'options.namespaces must be an empty array when options.typeToNamespacesMap is used' - ); - expect(client.search).not.toHaveBeenCalled(); - }; - await test({ type: '', typeToNamespacesMap: new Map() }); - await test({ type: '', namespaces: ['some-ns'], typeToNamespacesMap: new Map() }); - }); - - it(`throws when searchFields is defined but not an array`, async () => { - await expect( - savedObjectsRepository.find({ type, searchFields: 'string' }) - ).rejects.toThrowError('options.searchFields must be an array'); - expect(client.search).not.toHaveBeenCalled(); - }); - - it(`throws when fields is defined but not an array`, async () => { - await expect(savedObjectsRepository.find({ type, fields: 'string' })).rejects.toThrowError( - 'options.fields must be an array' - ); - expect(client.search).not.toHaveBeenCalled(); - }); - - it(`throws when a preference is provided with pit`, async () => { - await expect( - savedObjectsRepository.find({ type: 'foo', pit: { id: 'abc123' }, preference: 'hi' }) - ).rejects.toThrowError('options.preference must be excluded when options.pit is used'); - expect(client.search).not.toHaveBeenCalled(); - }); - - it(`throws when KQL filter syntax is invalid`, async () => { - const findOpts = { - namespaces: [namespace], - search: 'foo*', - searchFields: ['foo'], - type: ['dashboard'], - sortField: 'name', - sortOrder: 'desc', - defaultSearchOperator: 'AND', - hasReference: { - type: 'foo', - id: '1', - }, - indexPattern: undefined, - filter: 'dashboard.attributes.otherField:<', - }; - - await expect(savedObjectsRepository.find(findOpts)).rejects.toMatchInlineSnapshot(` - [Error: KQLSyntaxError: Expected "(", "{", value, whitespace but "<" found. - dashboard.attributes.otherField:< - --------------------------------^: Bad Request] - `); - expect(getSearchDslNS.getSearchDsl).not.toHaveBeenCalled(); - expect(client.search).not.toHaveBeenCalled(); - }); - }); - - describe('returns', () => { - it(`formats the ES response when there is no namespace`, async () => { - const noNamespaceSearchResults = generateSearchResults(); - client.search.mockResolvedValueOnce( - elasticsearchClientMock.createSuccessTransportRequestPromise(noNamespaceSearchResults) - ); - const count = noNamespaceSearchResults.hits.hits.length; - - const response = await savedObjectsRepository.find({ type }); - - expect(response.total).toBe(count); - expect(response.saved_objects).toHaveLength(count); - - noNamespaceSearchResults.hits.hits.forEach((doc, i) => { - expect(response.saved_objects[i]).toEqual({ - id: doc._id.replace(/(index-pattern|config|globalType)\:/, ''), - type: doc._source.type, - originId: doc._source.originId, - ...mockTimestampFields, - version: mockVersion, - score: doc._score, - attributes: doc._source[doc._source.type], - references: [], - namespaces: doc._source.type === NAMESPACE_AGNOSTIC_TYPE ? undefined : ['default'], - }); - }); - }); - - it(`formats the ES response when there is a namespace`, async () => { - const namespacedSearchResults = generateSearchResults(namespace); - client.search.mockResolvedValueOnce( - elasticsearchClientMock.createSuccessTransportRequestPromise(namespacedSearchResults) - ); - const count = namespacedSearchResults.hits.hits.length; - - const response = await savedObjectsRepository.find({ type, namespaces: [namespace] }); - - expect(response.total).toBe(count); - expect(response.saved_objects).toHaveLength(count); - - namespacedSearchResults.hits.hits.forEach((doc, i) => { - expect(response.saved_objects[i]).toEqual({ - id: doc._id.replace(/(foo-namespace\:)?(index-pattern|config|globalType)\:/, ''), - type: doc._source.type, - originId: doc._source.originId, - ...mockTimestampFields, - version: mockVersion, - score: doc._score, - attributes: doc._source[doc._source.type], - references: [], - namespaces: doc._source.type === NAMESPACE_AGNOSTIC_TYPE ? undefined : [namespace], - }); - }); - }); - - it(`should return empty results when attempting to find only invalid or hidden types`, async () => { - const test = async (types) => { - const result = await savedObjectsRepository.find({ type: types }); - expect(result).toEqual(expect.objectContaining({ saved_objects: [] })); - expect(client.search).not.toHaveBeenCalled(); - }; - - await test('unknownType'); - await test(HIDDEN_TYPE); - await test(['unknownType', HIDDEN_TYPE]); - }); - - it(`should return empty results when attempting to find only invalid or hidden types using typeToNamespacesMap`, async () => { - const test = async (types) => { - const result = await savedObjectsRepository.find({ - typeToNamespacesMap: new Map(types.map((x) => [x, undefined])), - type: '', - namespaces: [], - }); - expect(result).toEqual(expect.objectContaining({ saved_objects: [] })); - expect(client.search).not.toHaveBeenCalled(); - }; - - await test(['unknownType']); - await test([HIDDEN_TYPE]); - await test(['unknownType', HIDDEN_TYPE]); - }); - }); - - describe('search dsl', () => { - const commonOptions = { - type: [type], // cannot be used when `typeToNamespacesMap` is present - namespaces: [namespace], // cannot be used when `typeToNamespacesMap` is present - search: 'foo*', - searchFields: ['foo'], - sortField: 'name', - sortOrder: 'desc', - defaultSearchOperator: 'AND', - hasReference: { - type: 'foo', - id: '1', - }, - kueryNode: undefined, - }; - - it(`passes mappings, registry, and search options to getSearchDsl`, async () => { - await findSuccess(commonOptions, namespace); - expect(getSearchDslNS.getSearchDsl).toHaveBeenCalledWith(mappings, registry, commonOptions); - }); - - it(`accepts typeToNamespacesMap`, async () => { - const relevantOpts = { - ...commonOptions, - type: '', - namespaces: [], - typeToNamespacesMap: new Map([[type, [namespace]]]), // can only be used when `type` is falsy and `namespaces` is an empty array - }; - - await findSuccess(relevantOpts, namespace); - expect(getSearchDslNS.getSearchDsl).toHaveBeenCalledWith(mappings, registry, { - ...relevantOpts, - type: [type], - }); - }); - - it(`accepts hasReferenceOperator`, async () => { - const relevantOpts = { - ...commonOptions, - hasReferenceOperator: 'AND', - }; - - await findSuccess(relevantOpts, namespace); - expect(getSearchDslNS.getSearchDsl).toHaveBeenCalledWith(mappings, registry, { - ...relevantOpts, - hasReferenceOperator: 'AND', - }); - }); - - it(`accepts searchAfter`, async () => { - const relevantOpts = { - ...commonOptions, - searchAfter: [1, 'a'], - }; - - await findSuccess(relevantOpts, namespace); - expect(getSearchDslNS.getSearchDsl).toHaveBeenCalledWith(mappings, registry, { - ...relevantOpts, - searchAfter: [1, 'a'], - }); - }); - - it(`accepts pit`, async () => { - const relevantOpts = { - ...commonOptions, - pit: { id: 'abc123', keepAlive: '2m' }, - }; - - await findSuccess(relevantOpts, namespace); - expect(getSearchDslNS.getSearchDsl).toHaveBeenCalledWith(mappings, registry, { - ...relevantOpts, - pit: { id: 'abc123', keepAlive: '2m' }, - }); - }); - - it(`accepts KQL expression filter and passes KueryNode to getSearchDsl`, async () => { - const findOpts = { - namespaces: [namespace], - search: 'foo*', - searchFields: ['foo'], - type: ['dashboard'], - sortField: 'name', - sortOrder: 'desc', - defaultSearchOperator: 'AND', - hasReference: { - type: 'foo', - id: '1', - }, - indexPattern: undefined, - filter: 'dashboard.attributes.otherField: *', - }; - - await findSuccess(findOpts, namespace); - const { kueryNode } = getSearchDslNS.getSearchDsl.mock.calls[0][2]; - expect(kueryNode).toMatchInlineSnapshot(` - Object { - "arguments": Array [ - Object { - "type": "literal", - "value": "dashboard.otherField", - }, - Object { - "type": "wildcard", - "value": "@kuery-wildcard@", - }, - Object { - "type": "literal", - "value": false, - }, - ], - "function": "is", - "type": "function", - } - `); - }); - - it(`accepts KQL KueryNode filter and passes KueryNode to getSearchDsl`, async () => { - const findOpts = { - namespaces: [namespace], - search: 'foo*', - searchFields: ['foo'], - type: ['dashboard'], - sortField: 'name', - sortOrder: 'desc', - defaultSearchOperator: 'AND', - hasReference: { - type: 'foo', - id: '1', - }, - indexPattern: undefined, - filter: nodeTypes.function.buildNode('is', `dashboard.attributes.otherField`, '*'), - }; - - await findSuccess(findOpts, namespace); - const { kueryNode } = getSearchDslNS.getSearchDsl.mock.calls[0][2]; - expect(kueryNode).toMatchInlineSnapshot(` - Object { - "arguments": Array [ - Object { - "type": "literal", - "value": "dashboard.otherField", - }, - Object { - "type": "wildcard", - "value": "@kuery-wildcard@", - }, - Object { - "type": "literal", - "value": false, - }, - ], - "function": "is", - "type": "function", - } - `); - }); - - it(`supports multiple types`, async () => { - const types = ['config', 'index-pattern']; - await findSuccess({ type: types }); - - expect(getSearchDslNS.getSearchDsl).toHaveBeenCalledWith( - mappings, - registry, - expect.objectContaining({ - type: types, - }) - ); - }); - - it(`filters out invalid types`, async () => { - const types = ['config', 'unknownType', 'index-pattern']; - await findSuccess({ type: types }); - - expect(getSearchDslNS.getSearchDsl).toHaveBeenCalledWith( - mappings, - registry, - expect.objectContaining({ - type: ['config', 'index-pattern'], - }) - ); - }); - - it(`filters out hidden types`, async () => { - const types = ['config', HIDDEN_TYPE, 'index-pattern']; - await findSuccess({ type: types }); - - expect(getSearchDslNS.getSearchDsl).toHaveBeenCalledWith( - mappings, - registry, - expect.objectContaining({ - type: ['config', 'index-pattern'], - }) - ); - }); - }); - }); - - describe('#get', () => { - const type = 'index-pattern'; - const id = 'logstash-*'; - const namespace = 'foo-namespace'; - const originId = 'some-origin-id'; - - const getSuccess = async (type, id, options, includeOriginId) => { - const response = getMockGetResponse( - { - type, - id, - // "includeOriginId" is not an option for the operation; however, if the existing saved object contains an originId attribute, the - // operation will return it in the result. This flag is just used for test purposes to modify the mock cluster call response. - ...(includeOriginId && { originId }), - }, - options?.namespace - ); - client.get.mockResolvedValueOnce( - elasticsearchClientMock.createSuccessTransportRequestPromise(response) - ); - const result = await savedObjectsRepository.get(type, id, options); - expect(client.get).toHaveBeenCalledTimes(1); - return result; - }; - - describe('client calls', () => { - it(`should use the ES get action`, async () => { - await getSuccess(type, id); - expect(client.get).toHaveBeenCalledTimes(1); - }); - - it(`prepends namespace to the id when providing namespace for single-namespace type`, async () => { - await getSuccess(type, id, { namespace }); - expect(client.get).toHaveBeenCalledWith( - expect.objectContaining({ - id: `${namespace}:${type}:${id}`, - }), - expect.anything() - ); - }); - - it(`doesn't prepend namespace to the id when providing no namespace for single-namespace type`, async () => { - await getSuccess(type, id); - expect(client.get).toHaveBeenCalledWith( - expect.objectContaining({ - id: `${type}:${id}`, - }), - expect.anything() - ); - }); - - it(`normalizes options.namespace from 'default' to undefined`, async () => { - await getSuccess(type, id, { namespace: 'default' }); - expect(client.get).toHaveBeenCalledWith( - expect.objectContaining({ - id: `${type}:${id}`, - }), - expect.anything() - ); - }); - - it(`doesn't prepend namespace to the id when not using single-namespace type`, async () => { - await getSuccess(NAMESPACE_AGNOSTIC_TYPE, id, { namespace }); - expect(client.get).toHaveBeenCalledWith( - expect.objectContaining({ - id: `${NAMESPACE_AGNOSTIC_TYPE}:${id}`, - }), - expect.anything() - ); - - client.get.mockClear(); - await getSuccess(MULTI_NAMESPACE_ISOLATED_TYPE, id, { namespace }); - expect(client.get).toHaveBeenCalledWith( - expect.objectContaining({ - id: `${MULTI_NAMESPACE_ISOLATED_TYPE}:${id}`, - }), - expect.anything() - ); - }); - }); - - describe('errors', () => { - const expectNotFoundError = async (type, id, options) => { - await expect(savedObjectsRepository.get(type, id, options)).rejects.toThrowError( - createGenericNotFoundError(type, id) - ); - }; - - it(`throws when options.namespace is '*'`, async () => { - await expect( - savedObjectsRepository.get(type, id, { namespace: ALL_NAMESPACES_STRING }) - ).rejects.toThrowError(createBadRequestError('"options.namespace" cannot be "*"')); - }); - - it(`throws when type is invalid`, async () => { - await expectNotFoundError('unknownType', id); - expect(client.get).not.toHaveBeenCalled(); - }); - - it(`throws when type is hidden`, async () => { - await expectNotFoundError(HIDDEN_TYPE, id); - expect(client.get).not.toHaveBeenCalled(); - }); - - it(`throws when ES is unable to find the document during get`, async () => { - client.get.mockResolvedValueOnce( - elasticsearchClientMock.createSuccessTransportRequestPromise({ found: false }, undefined) - ); - await expectNotFoundError(type, id); - expect(client.get).toHaveBeenCalledTimes(1); - }); - - it(`throws when ES is unable to find the index during get`, async () => { - client.get.mockResolvedValueOnce( - elasticsearchClientMock.createSuccessTransportRequestPromise({}, { statusCode: 404 }) - ); - await expectNotFoundError(type, id); - expect(client.get).toHaveBeenCalledTimes(1); - }); - - it(`throws when type is multi-namespace and the document exists, but not in this namespace`, async () => { - const response = getMockGetResponse({ type: MULTI_NAMESPACE_ISOLATED_TYPE, id }, namespace); - client.get.mockResolvedValueOnce( - elasticsearchClientMock.createSuccessTransportRequestPromise(response) - ); - await expectNotFoundError(MULTI_NAMESPACE_ISOLATED_TYPE, id, { - namespace: 'bar-namespace', - }); - expect(client.get).toHaveBeenCalledTimes(1); - }); - }); - - describe('returns', () => { - it(`formats the ES response`, async () => { - const result = await getSuccess(type, id); - expect(result).toEqual({ - id, - type, - updated_at: mockTimestamp, - version: mockVersion, - attributes: { - title: 'Testing', - }, - references: [], - namespaces: ['default'], - }); - }); - - it(`includes namespaces if type is multi-namespace`, async () => { - const result = await getSuccess(MULTI_NAMESPACE_ISOLATED_TYPE, id); - expect(result).toMatchObject({ - namespaces: expect.any(Array), - }); - }); - - it(`include namespaces if type is not multi-namespace`, async () => { - const result = await getSuccess(type, id); - expect(result).toMatchObject({ - namespaces: ['default'], - }); - }); - - it(`includes originId property if present in cluster call response`, async () => { - const result = await getSuccess(type, id, {}, true); - expect(result).toMatchObject({ originId }); - }); - }); - }); - - describe('#resolve', () => { - afterEach(() => { - mockInternalBulkResolve.mockReset(); - }); - - it('passes arguments to the internalBulkResolve module and returns the result', async () => { - const expectedResult = { saved_object: 'mock-object', outcome: 'exactMatch' }; - mockInternalBulkResolve.mockResolvedValue({ resolved_objects: [expectedResult] }); - - await expect(savedObjectsRepository.resolve('obj-type', 'obj-id')).resolves.toEqual( - expectedResult - ); - expect(mockInternalBulkResolve).toHaveBeenCalledTimes(1); - expect(mockInternalBulkResolve).toHaveBeenCalledWith( - expect.objectContaining({ objects: [{ type: 'obj-type', id: 'obj-id' }] }) - ); - }); - - it('throws when internalBulkResolve result is an error', async () => { - const error = new Error('Oh no!'); - const expectedResult = { type: 'obj-type', id: 'obj-id', error }; - mockInternalBulkResolve.mockResolvedValue({ resolved_objects: [expectedResult] }); - - await expect(savedObjectsRepository.resolve()).rejects.toEqual(error); - }); - - it('throws when internalBulkResolve throws', async () => { - const error = new Error('Oh no!'); - mockInternalBulkResolve.mockRejectedValue(error); - - await expect(savedObjectsRepository.resolve()).rejects.toEqual(error); - }); - }); - - describe('#incrementCounter', () => { - const type = 'config'; - const id = 'one'; - const counterFields = ['buildNum', 'apiCallsCount']; - const namespace = 'foo-namespace'; - const originId = 'some-origin-id'; - - const incrementCounterSuccess = async (type, id, fields, options, internalOptions = {}) => { - const { mockGetResponseValue } = internalOptions; - const isMultiNamespace = registry.isMultiNamespace(type); - if (isMultiNamespace) { - const response = - mockGetResponseValue ?? getMockGetResponse({ type, id }, options?.namespace); - client.get.mockResolvedValueOnce( - elasticsearchClientMock.createSuccessTransportRequestPromise(response) - ); - } - client.update.mockImplementation((params) => - elasticsearchClientMock.createSuccessTransportRequestPromise({ - _id: params.id, - ...mockVersionProps, - _index: '.kibana', - get: { - found: true, - _source: { - type, - ...mockTimestampFields, - [type]: { - ...fields.reduce((acc, field) => { - acc[field] = 8468; - return acc; - }, {}), - defaultIndex: 'logstash-*', - }, - }, - }, - }) - ); - - const result = await savedObjectsRepository.incrementCounter(type, id, fields, options); - expect(client.get).toHaveBeenCalledTimes(isMultiNamespace ? 1 : 0); - return result; - }; - - beforeEach(() => { - mockPreflightCheckForCreate.mockReset(); - mockPreflightCheckForCreate.mockImplementation(({ objects }) => { - return objects.map(({ type, id }) => ({ type, id })); // respond with no errors by default - }); - }); - - describe('client calls', () => { - it(`should use the ES update action if type is not multi-namespace`, async () => { - await incrementCounterSuccess(type, id, counterFields, { namespace }); - expect(client.get).not.toHaveBeenCalled(); - expect(mockPreflightCheckForCreate).not.toHaveBeenCalled(); - expect(client.update).toHaveBeenCalledTimes(1); - }); - - it(`should use the ES get action then update action if type is multi-namespace, ID is defined, and overwrite=true`, async () => { - await incrementCounterSuccess(MULTI_NAMESPACE_ISOLATED_TYPE, id, counterFields, { - namespace, - }); - expect(client.get).toHaveBeenCalledTimes(1); - expect(mockPreflightCheckForCreate).not.toHaveBeenCalled(); - expect(client.update).toHaveBeenCalledTimes(1); - }); - - it(`should check for alias conflicts if a new multi-namespace object would be created`, async () => { - await incrementCounterSuccess( - MULTI_NAMESPACE_ISOLATED_TYPE, - id, - counterFields, - { namespace }, - { mockGetResponseValue: { found: false } } - ); - expect(client.get).toHaveBeenCalledTimes(1); - expect(mockPreflightCheckForCreate).toHaveBeenCalledTimes(1); - expect(client.update).toHaveBeenCalledTimes(1); - }); - - it(`defaults to a refresh setting of wait_for`, async () => { - await incrementCounterSuccess(type, id, counterFields, { namespace }); - expect(client.update).toHaveBeenCalledWith( - expect.objectContaining({ - refresh: 'wait_for', - }), - expect.anything() - ); - }); - - it(`uses the 'upsertAttributes' option when specified`, async () => { - const upsertAttributes = { - foo: 'bar', - hello: 'dolly', - }; - await incrementCounterSuccess(type, id, counterFields, { namespace, upsertAttributes }); - expect(client.update).toHaveBeenCalledWith( - expect.objectContaining({ - body: expect.objectContaining({ - upsert: expect.objectContaining({ - [type]: { - foo: 'bar', - hello: 'dolly', - ...counterFields.reduce((aggs, field) => { - return { - ...aggs, - [field]: 1, - }; - }, {}), - }, - }), - }), - }), - expect.anything() - ); - }); - - it(`prepends namespace to the id when providing namespace for single-namespace type`, async () => { - await incrementCounterSuccess(type, id, counterFields, { namespace }); - expect(client.update).toHaveBeenCalledWith( - expect.objectContaining({ - id: `${namespace}:${type}:${id}`, - }), - expect.anything() - ); - }); - - it(`doesn't prepend namespace to the id when providing no namespace for single-namespace type`, async () => { - await incrementCounterSuccess(type, id, counterFields); - expect(client.update).toHaveBeenCalledWith( - expect.objectContaining({ - id: `${type}:${id}`, - }), - expect.anything() - ); - }); - - it(`normalizes options.namespace from 'default' to undefined`, async () => { - await incrementCounterSuccess(type, id, counterFields, { namespace: 'default' }); - expect(client.update).toHaveBeenCalledWith( - expect.objectContaining({ - id: `${type}:${id}`, - }), - expect.anything() - ); - }); - - it(`doesn't prepend namespace to the id when not using single-namespace type`, async () => { - await incrementCounterSuccess(NAMESPACE_AGNOSTIC_TYPE, id, counterFields, { namespace }); - expect(client.update).toHaveBeenCalledWith( - expect.objectContaining({ - id: `${NAMESPACE_AGNOSTIC_TYPE}:${id}`, - }), - expect.anything() - ); - - client.update.mockClear(); - await incrementCounterSuccess(MULTI_NAMESPACE_ISOLATED_TYPE, id, counterFields, { - namespace, - }); - expect(client.update).toHaveBeenCalledWith( - expect.objectContaining({ - id: `${MULTI_NAMESPACE_ISOLATED_TYPE}:${id}`, - }), - expect.anything() - ); - }); - }); - - describe('errors', () => { - const expectUnsupportedTypeError = async (type, id, field) => { - await expect(savedObjectsRepository.incrementCounter(type, id, field)).rejects.toThrowError( - createUnsupportedTypeError(type) - ); - }; - - it(`throws when options.namespace is '*'`, async () => { - await expect( - savedObjectsRepository.incrementCounter(type, id, counterFields, { - namespace: ALL_NAMESPACES_STRING, - }) - ).rejects.toThrowError(createBadRequestError('"options.namespace" cannot be "*"')); - }); - - it(`throws when type is not a string`, async () => { - const test = async (type) => { - await expect( - savedObjectsRepository.incrementCounter(type, id, counterFields) - ).rejects.toThrowError(`"type" argument must be a string`); - expect(client.update).not.toHaveBeenCalled(); - }; - - await test(null); - await test(42); - await test(false); - await test({}); - }); - - it(`throws when counterField is not CounterField type`, async () => { - const test = async (field) => { - await expect( - savedObjectsRepository.incrementCounter(type, id, field) - ).rejects.toThrowError( - `"counterFields" argument must be of type Array` - ); - expect(client.update).not.toHaveBeenCalled(); - }; - - await test([null]); - await test([42]); - await test([false]); - await test([{}]); - await test([{}, false, 42, null, 'string']); - await test([{ fieldName: 'string' }, false, null, 'string']); - }); - - it(`throws when type is invalid`, async () => { - await expectUnsupportedTypeError('unknownType', id, counterFields); - expect(client.update).not.toHaveBeenCalled(); - }); - - it(`throws when type is hidden`, async () => { - await expectUnsupportedTypeError(HIDDEN_TYPE, id, counterFields); - expect(client.update).not.toHaveBeenCalled(); - }); - - it(`throws when there is a conflict with an existing multi-namespace saved object (get)`, async () => { - const response = getMockGetResponse( - { type: MULTI_NAMESPACE_ISOLATED_TYPE, id }, - 'bar-namespace' - ); - client.get.mockResolvedValueOnce( - elasticsearchClientMock.createSuccessTransportRequestPromise(response) - ); - await expect( - savedObjectsRepository.incrementCounter( - MULTI_NAMESPACE_ISOLATED_TYPE, - id, - counterFields, - { - namespace, - } - ) - ).rejects.toThrowError(createConflictError(MULTI_NAMESPACE_ISOLATED_TYPE, id)); - expect(client.get).toHaveBeenCalledTimes(1); - expect(mockPreflightCheckForCreate).not.toHaveBeenCalled(); - expect(client.update).not.toHaveBeenCalled(); - }); - - it(`throws when there is an alias conflict from preflightCheckForCreate`, async () => { - client.get.mockResolvedValueOnce( - elasticsearchClientMock.createSuccessTransportRequestPromise({ found: false }) - ); - mockPreflightCheckForCreate.mockResolvedValue([{ error: { type: 'aliasConflict' } }]); - await expect( - savedObjectsRepository.incrementCounter( - MULTI_NAMESPACE_ISOLATED_TYPE, - id, - counterFields, - { namespace } - ) - ).rejects.toThrowError(createConflictError(MULTI_NAMESPACE_ISOLATED_TYPE, id)); - expect(client.get).toHaveBeenCalledTimes(1); - expect(mockPreflightCheckForCreate).toHaveBeenCalledTimes(1); - expect(client.update).not.toHaveBeenCalled(); - }); - - it(`does not throw when there is a different error from preflightCheckForCreate`, async () => { - client.get.mockResolvedValueOnce( - elasticsearchClientMock.createSuccessTransportRequestPromise({ found: false }) - ); - mockPreflightCheckForCreate.mockResolvedValue([{ error: { type: 'something-else' } }]); - await incrementCounterSuccess( - MULTI_NAMESPACE_ISOLATED_TYPE, - id, - counterFields, - { namespace }, - { mockGetResponseValue: { found: false } } - ); - expect(client.get).toHaveBeenCalledTimes(1); - expect(mockPreflightCheckForCreate).toHaveBeenCalledTimes(1); - expect(client.update).toHaveBeenCalledTimes(1); - }); - }); - - describe('migration', () => { - beforeEach(() => { - migrator.migrateDocument.mockImplementation(mockMigrateDocument); - }); - - it(`migrates a document and serializes the migrated doc`, async () => { - const migrationVersion = mockMigrationVersion; - await incrementCounterSuccess(type, id, counterFields, { migrationVersion }); - const attributes = { buildNum: 1, apiCallsCount: 1 }; // this is added by the incrementCounter function - const doc = { type, id, attributes, migrationVersion, ...mockTimestampFields }; - expectMigrationArgs(doc); - - const migratedDoc = migrator.migrateDocument(doc); - expect(serializer.savedObjectToRaw).toHaveBeenLastCalledWith(migratedDoc); - }); - }); - - describe('returns', () => { - it(`formats the ES response`, async () => { - client.update.mockImplementation((params) => - elasticsearchClientMock.createSuccessTransportRequestPromise({ - _id: params.id, - ...mockVersionProps, - _index: '.kibana', - get: { - found: true, - _source: { - type: 'config', - ...mockTimestampFields, - config: { - buildNum: 8468, - apiCallsCount: 100, - defaultIndex: 'logstash-*', - }, - originId, - }, - }, - }) - ); - - const response = await savedObjectsRepository.incrementCounter( - 'config', - '6.0.0-alpha1', - ['buildNum', 'apiCallsCount'], - { - namespace: 'foo-namespace', - } - ); - - expect(response).toEqual({ - type: 'config', - id: '6.0.0-alpha1', - ...mockTimestampFields, - version: mockVersion, - references: [], - attributes: { - buildNum: 8468, - apiCallsCount: 100, - defaultIndex: 'logstash-*', - }, - originId, - }); - }); - - it('increments counter by incrementBy config', async () => { - await incrementCounterSuccess(type, id, [{ fieldName: counterFields[0], incrementBy: 3 }]); - - expect(client.update).toBeCalledTimes(1); - expect(client.update).toBeCalledWith( - expect.objectContaining({ - body: expect.objectContaining({ - script: expect.objectContaining({ - params: expect.objectContaining({ - counterFieldNames: [counterFields[0]], - counts: [3], - }), - }), - }), - }), - expect.anything() - ); - }); - - it('does not increment counter when incrementBy is 0', async () => { - await incrementCounterSuccess(type, id, [{ fieldName: counterFields[0], incrementBy: 0 }]); - - expect(client.update).toBeCalledTimes(1); - expect(client.update).toBeCalledWith( - expect.objectContaining({ - body: expect.objectContaining({ - script: expect.objectContaining({ - params: expect.objectContaining({ - counterFieldNames: [counterFields[0]], - counts: [0], - }), - }), - }), - }), - expect.anything() - ); - }); - }); - }); - - describe('#update', () => { - const id = 'logstash-*'; - const type = 'index-pattern'; - const attributes = { title: 'Testing' }; - const namespace = 'foo-namespace'; - const references = [ - { - name: 'ref_0', - type: 'test', - id: '1', - }, - ]; - const originId = 'some-origin-id'; - - const mockUpdateResponse = (type, id, options, includeOriginId) => { - client.update.mockResolvedValueOnce( - elasticsearchClientMock.createSuccessTransportRequestPromise( - { - _id: `${type}:${id}`, - ...mockVersionProps, - result: 'updated', - // don't need the rest of the source for test purposes, just the namespace and namespaces attributes - get: { - _source: { - namespaces: [options?.namespace ?? 'default'], - namespace: options?.namespace, - - // "includeOriginId" is not an option for the operation; however, if the existing saved object contains an originId attribute, the - // operation will return it in the result. This flag is just used for test purposes to modify the mock cluster call response. - ...(includeOriginId && { originId }), - }, - }, - }, - { statusCode: 200 } - ) - ); - }; - - const updateSuccess = async (type, id, attributes, options, internalOptions = {}) => { - const { mockGetResponseValue, includeOriginId } = internalOptions; - if (registry.isMultiNamespace(type)) { - const mockGetResponse = - mockGetResponseValue ?? getMockGetResponse({ type, id }, options?.namespace); - client.get.mockResolvedValueOnce( - elasticsearchClientMock.createSuccessTransportRequestPromise( - { ...mockGetResponse }, - { statusCode: 200 } - ) - ); - } - mockUpdateResponse(type, id, options, includeOriginId); - const result = await savedObjectsRepository.update(type, id, attributes, options); - expect(client.get).toHaveBeenCalledTimes(registry.isMultiNamespace(type) ? 1 : 0); - return result; - }; - - beforeEach(() => { - mockPreflightCheckForCreate.mockReset(); - mockPreflightCheckForCreate.mockImplementation(({ objects }) => { - return objects.map(({ type, id }) => ({ type, id })); // respond with no errors by default - }); - }); - - describe('client calls', () => { - it(`should use the ES update action when type is not multi-namespace`, async () => { - await updateSuccess(type, id, attributes); - expect(client.get).not.toHaveBeenCalled(); - expect(mockPreflightCheckForCreate).not.toHaveBeenCalled(); - expect(client.update).toHaveBeenCalledTimes(1); - }); - - it(`should use the ES get action then update action when type is multi-namespace`, async () => { - await updateSuccess(MULTI_NAMESPACE_ISOLATED_TYPE, id, attributes); - expect(client.get).toHaveBeenCalledTimes(1); - expect(mockPreflightCheckForCreate).not.toHaveBeenCalled(); - expect(client.update).toHaveBeenCalledTimes(1); - }); - - it(`should check for alias conflicts if a new multi-namespace object would be created`, async () => { - await updateSuccess( - MULTI_NAMESPACE_ISOLATED_TYPE, - id, - attributes, - { upsert: true }, - { mockGetResponseValue: { found: false } } - ); - expect(client.get).toHaveBeenCalledTimes(1); - expect(mockPreflightCheckForCreate).toHaveBeenCalledTimes(1); - expect(client.update).toHaveBeenCalledTimes(1); - }); - - it(`defaults to no references array`, async () => { - await updateSuccess(type, id, attributes); - expect(client.update).toHaveBeenCalledWith( - expect.objectContaining({ - body: { doc: expect.not.objectContaining({ references: expect.anything() }) }, - }), - expect.anything() - ); - }); - - it(`accepts custom references array`, async () => { - const test = async (references) => { - await updateSuccess(type, id, attributes, { references }); - expect(client.update).toHaveBeenCalledWith( - expect.objectContaining({ - body: { doc: expect.objectContaining({ references }) }, - }), - expect.anything() - ); - client.update.mockClear(); - }; - await test(references); - await test(['string']); - await test([]); - }); - - it(`uses the 'upsertAttributes' option when specified for a single-namespace type`, async () => { - await updateSuccess(type, id, attributes, { - upsert: { - title: 'foo', - description: 'bar', - }, - }); - expect(client.update).toHaveBeenCalledWith( - expect.objectContaining({ - id: 'index-pattern:logstash-*', - body: expect.objectContaining({ - upsert: expect.objectContaining({ - type: 'index-pattern', - 'index-pattern': { - title: 'foo', - description: 'bar', - }, - }), - }), - }), - expect.anything() - ); - }); - - it(`uses the 'upsertAttributes' option when specified for a multi-namespace type that does not exist`, async () => { - const options = { upsert: { title: 'foo', description: 'bar' } }; - mockUpdateResponse(MULTI_NAMESPACE_ISOLATED_TYPE, id, options); - await savedObjectsRepository.update(MULTI_NAMESPACE_ISOLATED_TYPE, id, attributes, options); - expect(client.get).toHaveBeenCalledTimes(1); - expect(client.update).toHaveBeenCalledWith( - expect.objectContaining({ - id: `${MULTI_NAMESPACE_ISOLATED_TYPE}:logstash-*`, - body: expect.objectContaining({ - upsert: expect.objectContaining({ - type: MULTI_NAMESPACE_ISOLATED_TYPE, - [MULTI_NAMESPACE_ISOLATED_TYPE]: { - title: 'foo', - description: 'bar', - }, - }), - }), - }), - expect.anything() - ); - }); - - it(`ignores use the 'upsertAttributes' option when specified for a multi-namespace type that already exists`, async () => { - const options = { upsert: { title: 'foo', description: 'bar' } }; - await updateSuccess(MULTI_NAMESPACE_ISOLATED_TYPE, id, attributes, options); - expect(client.update).toHaveBeenCalledWith( - expect.objectContaining({ - id: `${MULTI_NAMESPACE_ISOLATED_TYPE}:logstash-*`, - body: expect.not.objectContaining({ - upsert: expect.anything(), - }), - }), - expect.anything() - ); - }); - - it(`doesn't accept custom references if not an array`, async () => { - const test = async (references) => { - await updateSuccess(type, id, attributes, { references }); - expect(client.update).toHaveBeenCalledWith( - expect.objectContaining({ - body: { doc: expect.not.objectContaining({ references: expect.anything() }) }, - }), - expect.anything() - ); - client.update.mockClear(); - }; - await test('string'); - await test(123); - await test(true); - await test(null); - }); - - it(`defaults to a refresh setting of wait_for`, async () => { - await updateSuccess(type, id, { foo: 'bar' }); - expect(client.update).toHaveBeenCalledWith( - expect.objectContaining({ - refresh: 'wait_for', - }), - expect.anything() - ); - }); - - it(`defaults to the version of the existing document when type is multi-namespace`, async () => { - await updateSuccess(MULTI_NAMESPACE_ISOLATED_TYPE, id, attributes, { references }); - const versionProperties = { - if_seq_no: mockVersionProps._seq_no, - if_primary_term: mockVersionProps._primary_term, - }; - expect(client.update).toHaveBeenCalledWith( - expect.objectContaining(versionProperties), - expect.anything() - ); - }); - - it(`accepts version`, async () => { - await updateSuccess(type, id, attributes, { - version: encodeHitVersion({ _seq_no: 100, _primary_term: 200 }), - }); - expect(client.update).toHaveBeenCalledWith( - expect.objectContaining({ if_seq_no: 100, if_primary_term: 200 }), - expect.anything() - ); - }); - - it(`prepends namespace to the id when providing namespace for single-namespace type`, async () => { - await updateSuccess(type, id, attributes, { namespace }); - expect(client.update).toHaveBeenCalledWith( - expect.objectContaining({ id: expect.stringMatching(`${namespace}:${type}:${id}`) }), - expect.anything() - ); - }); - - it(`doesn't prepend namespace to the id when providing no namespace for single-namespace type`, async () => { - await updateSuccess(type, id, attributes, { references }); - expect(client.update).toHaveBeenCalledWith( - expect.objectContaining({ id: expect.stringMatching(`${type}:${id}`) }), - expect.anything() - ); - }); - - it(`normalizes options.namespace from 'default' to undefined`, async () => { - await updateSuccess(type, id, attributes, { references, namespace: 'default' }); - expect(client.update).toHaveBeenCalledWith( - expect.objectContaining({ id: expect.stringMatching(`${type}:${id}`) }), - expect.anything() - ); - }); - - it(`doesn't prepend namespace to the id when not using single-namespace type`, async () => { - await updateSuccess(NAMESPACE_AGNOSTIC_TYPE, id, attributes, { namespace }); - expect(client.update).toHaveBeenCalledWith( - expect.objectContaining({ - id: expect.stringMatching(`${NAMESPACE_AGNOSTIC_TYPE}:${id}`), - }), - expect.anything() - ); - - client.update.mockClear(); - await updateSuccess(MULTI_NAMESPACE_ISOLATED_TYPE, id, attributes, { namespace }); - expect(client.update).toHaveBeenCalledWith( - expect.objectContaining({ - id: expect.stringMatching(`${MULTI_NAMESPACE_ISOLATED_TYPE}:${id}`), - }), - expect.anything() - ); - }); - - it(`includes _source_includes when type is multi-namespace`, async () => { - await updateSuccess(MULTI_NAMESPACE_ISOLATED_TYPE, id, attributes); - expect(client.update).toHaveBeenCalledWith( - expect.objectContaining({ _source_includes: ['namespace', 'namespaces', 'originId'] }), - expect.anything() - ); - }); - - it(`includes _source_includes when type is not multi-namespace`, async () => { - await updateSuccess(type, id, attributes); - expect(client.update).toHaveBeenLastCalledWith( - expect.objectContaining({ - _source_includes: ['namespace', 'namespaces', 'originId'], - }), - expect.anything() - ); - }); - }); - - describe('errors', () => { - const expectNotFoundError = async (type, id) => { - await expect(savedObjectsRepository.update(type, id)).rejects.toThrowError( - createGenericNotFoundError(type, id) - ); - }; - - it(`throws when options.namespace is '*'`, async () => { - await expect( - savedObjectsRepository.update(type, id, attributes, { namespace: ALL_NAMESPACES_STRING }) - ).rejects.toThrowError(createBadRequestError('"options.namespace" cannot be "*"')); - }); - - it(`throws when type is invalid`, async () => { - await expectNotFoundError('unknownType', id); - expect(client.update).not.toHaveBeenCalled(); - }); - - it(`throws when type is hidden`, async () => { - await expectNotFoundError(HIDDEN_TYPE, id); - expect(client.update).not.toHaveBeenCalled(); - }); - - it(`throws when ES is unable to find the document during get`, async () => { - client.get.mockResolvedValueOnce( - elasticsearchClientMock.createSuccessTransportRequestPromise({ found: false }, undefined) - ); - await expectNotFoundError(MULTI_NAMESPACE_ISOLATED_TYPE, id); - expect(client.get).toHaveBeenCalledTimes(1); - }); - - it(`throws when ES is unable to find the index during get`, async () => { - client.get.mockResolvedValueOnce( - elasticsearchClientMock.createSuccessTransportRequestPromise({}, { statusCode: 404 }) - ); - await expectNotFoundError(MULTI_NAMESPACE_ISOLATED_TYPE, id); - expect(client.get).toHaveBeenCalledTimes(1); - }); - - it(`throws when type is multi-namespace and the document exists, but not in this namespace`, async () => { - const response = getMockGetResponse({ type: MULTI_NAMESPACE_ISOLATED_TYPE, id }, namespace); - client.get.mockResolvedValueOnce( - elasticsearchClientMock.createSuccessTransportRequestPromise(response) - ); - await expectNotFoundError(MULTI_NAMESPACE_ISOLATED_TYPE, id, { - namespace: 'bar-namespace', - }); - expect(client.get).toHaveBeenCalledTimes(1); - }); - - it(`throws when there is an alias conflict from preflightCheckForCreate`, async () => { - client.get.mockResolvedValueOnce( - elasticsearchClientMock.createSuccessTransportRequestPromise({ found: false }) - ); - mockPreflightCheckForCreate.mockResolvedValue([{ error: { type: 'aliasConflict' } }]); - await expect( - savedObjectsRepository.update(MULTI_NAMESPACE_ISOLATED_TYPE, id, {}, { upsert: true }) - ).rejects.toThrowError(createConflictError(MULTI_NAMESPACE_ISOLATED_TYPE, id)); - expect(client.get).toHaveBeenCalledTimes(1); - expect(mockPreflightCheckForCreate).toHaveBeenCalledTimes(1); - expect(client.update).not.toHaveBeenCalled(); - }); - - it(`does not throw when there is a different error from preflightCheckForCreate`, async () => { - mockPreflightCheckForCreate.mockResolvedValue([{ error: { type: 'something-else' } }]); - await updateSuccess( - MULTI_NAMESPACE_ISOLATED_TYPE, - id, - attributes, - { upsert: true }, - { mockGetResponseValue: { found: false } } - ); - expect(client.get).toHaveBeenCalledTimes(1); - expect(mockPreflightCheckForCreate).toHaveBeenCalledTimes(1); - expect(client.update).toHaveBeenCalledTimes(1); - }); - - it(`throws when ES is unable to find the document during update`, async () => { - const notFoundError = new EsErrors.ResponseError( - elasticsearchClientMock.createApiResponse({ - statusCode: 404, - body: { error: { type: 'es_type', reason: 'es_reason' } }, - }) - ); - client.update.mockResolvedValueOnce( - elasticsearchClientMock.createErrorTransportRequestPromise(notFoundError) - ); - await expectNotFoundError(type, id); - expect(client.update).toHaveBeenCalledTimes(1); - }); - }); - - describe('returns', () => { - it(`returns _seq_no and _primary_term encoded as version`, async () => { - const result = await updateSuccess(type, id, attributes, { - namespace, - references, - }); - expect(result).toEqual({ - id, - type, - ...mockTimestampFields, - version: mockVersion, - attributes, - references, - namespaces: [namespace], - }); - }); - - it(`includes namespaces if type is multi-namespace`, async () => { - const result = await updateSuccess(MULTI_NAMESPACE_ISOLATED_TYPE, id, attributes); - expect(result).toMatchObject({ - namespaces: expect.any(Array), - }); - }); - - it(`includes namespaces if type is not multi-namespace`, async () => { - const result = await updateSuccess(type, id, attributes); - expect(result).toMatchObject({ - namespaces: ['default'], - }); - }); - - it(`includes originId property if present in cluster call response`, async () => { - const result = await updateSuccess(type, id, attributes, {}, { includeOriginId: true }); - expect(result).toMatchObject({ originId }); - }); - }); - }); - - describe('#openPointInTimeForType', () => { - const type = 'index-pattern'; - - const generateResults = (id) => ({ id: id || null }); - const successResponse = async (type, options) => { - client.openPointInTime.mockResolvedValueOnce( - elasticsearchClientMock.createSuccessTransportRequestPromise(generateResults()) - ); - const result = await savedObjectsRepository.openPointInTimeForType(type, options); - expect(client.openPointInTime).toHaveBeenCalledTimes(1); - return result; - }; - - describe('client calls', () => { - it(`should use the ES PIT API`, async () => { - await successResponse(type); - expect(client.openPointInTime).toHaveBeenCalledTimes(1); - }); - - it(`accepts preference`, async () => { - await successResponse(type, { preference: 'pref' }); - expect(client.openPointInTime).toHaveBeenCalledWith( - expect.objectContaining({ - preference: 'pref', - }), - expect.anything() - ); - }); - - it(`accepts keepAlive`, async () => { - await successResponse(type, { keepAlive: '2m' }); - expect(client.openPointInTime).toHaveBeenCalledWith( - expect.objectContaining({ - keep_alive: '2m', - }), - expect.anything() - ); - }); - - it(`defaults keepAlive to 5m`, async () => { - await successResponse(type); - expect(client.openPointInTime).toHaveBeenCalledWith( - expect.objectContaining({ - keep_alive: '5m', - }), - expect.anything() - ); - }); - }); - - describe('errors', () => { - const expectNotFoundError = async (types) => { - await expect(savedObjectsRepository.openPointInTimeForType(types)).rejects.toThrowError( - createGenericNotFoundError() - ); - }; - - it(`throws when ES is unable to find the index`, async () => { - client.openPointInTime.mockResolvedValueOnce( - elasticsearchClientMock.createSuccessTransportRequestPromise({}, { statusCode: 404 }) - ); - await expectNotFoundError(type); - expect(client.openPointInTime).toHaveBeenCalledTimes(1); - }); - - it(`should return generic not found error when attempting to find only invalid or hidden types`, async () => { - const test = async (types) => { - await expectNotFoundError(types); - expect(client.openPointInTime).not.toHaveBeenCalled(); - }; - - await test('unknownType'); - await test(HIDDEN_TYPE); - await test(['unknownType', HIDDEN_TYPE]); - }); - }); - - describe('returns', () => { - it(`returns id in the expected format`, async () => { - const id = 'abc123'; - const results = generateResults(id); - client.openPointInTime.mockResolvedValueOnce( - elasticsearchClientMock.createSuccessTransportRequestPromise(results) - ); - const response = await savedObjectsRepository.openPointInTimeForType(type); - expect(response).toEqual({ id }); - }); - }); - }); - - describe('#closePointInTime', () => { - const generateResults = () => ({ succeeded: true, num_freed: 3 }); - const successResponse = async (id) => { - client.closePointInTime.mockResolvedValueOnce( - elasticsearchClientMock.createSuccessTransportRequestPromise(generateResults()) - ); - const result = await savedObjectsRepository.closePointInTime(id); - expect(client.closePointInTime).toHaveBeenCalledTimes(1); - return result; - }; - - describe('client calls', () => { - it(`should use the ES PIT API`, async () => { - await successResponse('abc123'); - expect(client.closePointInTime).toHaveBeenCalledTimes(1); - }); - - it(`accepts id`, async () => { - await successResponse('abc123'); - expect(client.closePointInTime).toHaveBeenCalledWith( - expect.objectContaining({ - body: expect.objectContaining({ - id: 'abc123', - }), - }), - expect.anything() - ); - }); - }); - - describe('returns', () => { - it(`returns response body from ES`, async () => { - const results = generateResults('abc123'); - client.closePointInTime.mockResolvedValueOnce( - elasticsearchClientMock.createSuccessTransportRequestPromise(results) - ); - const response = await savedObjectsRepository.closePointInTime('abc123'); - expect(response).toEqual(results); - }); - }); - }); - - describe('#createPointInTimeFinder', () => { - it('returns a new PointInTimeFinder instance', async () => { - const result = await savedObjectsRepository.createPointInTimeFinder({}, {}); - expect(result).toBeInstanceOf(PointInTimeFinder); - }); - - it('calls PointInTimeFinder with the provided options and dependencies', async () => { - const options = Symbol(); - const dependencies = { - client: { - find: Symbol(), - openPointInTimeForType: Symbol(), - closePointInTime: Symbol(), - }, - }; - - await savedObjectsRepository.createPointInTimeFinder(options, dependencies); - expect(pointInTimeFinderMock).toHaveBeenCalledWith( - options, - expect.objectContaining({ - ...dependencies, - logger, - }) - ); - }); - }); - - describe('#collectMultiNamespaceReferences', () => { - afterEach(() => { - mockCollectMultiNamespaceReferences.mockReset(); - }); - - it('passes arguments to the collectMultiNamespaceReferences module and returns the result', async () => { - const objects = Symbol(); - const expectedResult = Symbol(); - mockCollectMultiNamespaceReferences.mockResolvedValue(expectedResult); - - await expect( - savedObjectsRepository.collectMultiNamespaceReferences(objects) - ).resolves.toEqual(expectedResult); - expect(mockCollectMultiNamespaceReferences).toHaveBeenCalledTimes(1); - expect(mockCollectMultiNamespaceReferences).toHaveBeenCalledWith( - expect.objectContaining({ objects }) - ); - }); - - it('returns an error from the collectMultiNamespaceReferences module', async () => { - const expectedResult = new Error('Oh no!'); - mockCollectMultiNamespaceReferences.mockRejectedValue(expectedResult); - - await expect(savedObjectsRepository.collectMultiNamespaceReferences([])).rejects.toEqual( - expectedResult - ); - }); - }); - - describe('#updateObjectsSpaces', () => { - afterEach(() => { - mockUpdateObjectsSpaces.mockReset(); - }); - - it('passes arguments to the updateObjectsSpaces module and returns the result', async () => { - const objects = Symbol(); - const spacesToAdd = Symbol(); - const spacesToRemove = Symbol(); - const options = Symbol(); - const expectedResult = Symbol(); - mockUpdateObjectsSpaces.mockResolvedValue(expectedResult); - - await expect( - savedObjectsRepository.updateObjectsSpaces(objects, spacesToAdd, spacesToRemove, options) - ).resolves.toEqual(expectedResult); - expect(mockUpdateObjectsSpaces).toHaveBeenCalledTimes(1); - expect(mockUpdateObjectsSpaces).toHaveBeenCalledWith( - expect.objectContaining({ objects, spacesToAdd, spacesToRemove, options }) - ); - }); - - it('returns an error from the updateObjectsSpaces module', async () => { - const expectedResult = new Error('Oh no!'); - mockUpdateObjectsSpaces.mockRejectedValue(expectedResult); - - await expect(savedObjectsRepository.updateObjectsSpaces([], [], [])).rejects.toEqual( - expectedResult - ); - }); - }); -}); diff --git a/src/core/server/saved_objects/service/lib/repository.test.mock.ts b/src/core/server/saved_objects/service/lib/repository.test.mock.ts index 3ec2cb0a5d8b9..9ca156605d638 100644 --- a/src/core/server/saved_objects/service/lib/repository.test.mock.ts +++ b/src/core/server/saved_objects/service/lib/repository.test.mock.ts @@ -68,3 +68,6 @@ export const mockDeleteLegacyUrlAliases = jest.fn() as jest.MockedFunction< jest.mock('./legacy_url_aliases', () => ({ deleteLegacyUrlAliases: mockDeleteLegacyUrlAliases, })); + +export const mockGetSearchDsl = jest.fn(); +jest.mock('./search_dsl/search_dsl', () => ({ getSearchDsl: mockGetSearchDsl })); diff --git a/src/core/server/saved_objects/service/lib/repository.test.ts b/src/core/server/saved_objects/service/lib/repository.test.ts new file mode 100644 index 0000000000000..46a532cdefef4 --- /dev/null +++ b/src/core/server/saved_objects/service/lib/repository.test.ts @@ -0,0 +1,5023 @@ +/* + * Copyright 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 @typescript-eslint/no-shadow */ + +import { + pointInTimeFinderMock, + mockCollectMultiNamespaceReferences, + mockGetBulkOperationError, + mockInternalBulkResolve, + mockUpdateObjectsSpaces, + mockGetCurrentTime, + mockPreflightCheckForCreate, + mockDeleteLegacyUrlAliases, + mockGetSearchDsl, +} from './repository.test.mock'; + +import type { Payload } from '@hapi/boom'; +import * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; +import { + SavedObjectsType, + SavedObject, + SavedObjectReference, + SavedObjectsBaseOptions, + SavedObjectsFindOptions, +} from '../../types'; +import type { SavedObjectsUpdateObjectsSpacesResponse } from './update_objects_spaces'; +import { + SavedObjectsDeleteByNamespaceOptions, + SavedObjectsIncrementCounterField, + SavedObjectsIncrementCounterOptions, + SavedObjectsRepository, +} from './repository'; +import { SavedObjectsErrorHelpers } from './errors'; +import { + PointInTimeFinder, + SavedObjectsCreatePointInTimeFinderDependencies, + SavedObjectsCreatePointInTimeFinderOptions, +} from './point_in_time_finder'; +import { ALL_NAMESPACES_STRING } from './utils'; +import { loggerMock } from '../../../logging/logger.mock'; +import { + SavedObjectsRawDocSource, + SavedObjectsSerializer, + SavedObjectUnsanitizedDoc, +} from '../../serialization'; +import { encodeHitVersion } from '../../version'; +import { SavedObjectTypeRegistry } from '../../saved_objects_type_registry'; +import { DocumentMigrator } from '../../migrations/core/document_migrator'; +import { mockKibanaMigrator } from '../../migrations/kibana_migrator.mock'; +import { LEGACY_URL_ALIAS_TYPE } from '../../object_types'; +import { elasticsearchClientMock } from '../../../elasticsearch/client/mocks'; +import * as esKuery from '@kbn/es-query'; +import { errors as EsErrors } from '@elastic/elasticsearch'; +import { + SavedObjectsBulkCreateObject, + SavedObjectsBulkGetObject, + SavedObjectsBulkUpdateObject, + SavedObjectsBulkUpdateOptions, + SavedObjectsCreateOptions, + SavedObjectsDeleteOptions, + SavedObjectsOpenPointInTimeOptions, + SavedObjectsResolveResponse, + SavedObjectsUpdateOptions, +} from '../saved_objects_client'; +import { SavedObjectsMappingProperties, SavedObjectsTypeMappingDefinition } from '../../mappings'; +import { + SavedObjectsCollectMultiNamespaceReferencesObject, + SavedObjectsCollectMultiNamespaceReferencesResponse, + SavedObjectsUpdateObjectsSpacesObject, + SavedObjectsUpdateObjectsSpacesOptions, +} from 'kibana/server'; +import { InternalBulkResolveError } from './internal_bulk_resolve'; + +const { nodeTypes } = esKuery; + +// BEWARE: The SavedObjectClient depends on the implementation details of the SavedObjectsRepository +// so any breaking changes to this repository are considered breaking changes to the SavedObjectsClient. + +interface TypeIdTuple { + id: string; + type: string; +} + +interface ExpectedErrorResult { + type: string; + id: string; + error: Record; +} + +type ErrorPayload = Error & Payload; + +const createBadRequestError = (reason?: string) => + SavedObjectsErrorHelpers.createBadRequestError(reason).output.payload as ErrorPayload; +const createConflictError = (type: string, id: string, reason?: string) => + SavedObjectsErrorHelpers.createConflictError(type, id, reason).output.payload as ErrorPayload; +const createGenericNotFoundError = (type: string | null = null, id: string | null = null) => + SavedObjectsErrorHelpers.createGenericNotFoundError(type, id).output.payload as ErrorPayload; +const createUnsupportedTypeError = (type: string) => + SavedObjectsErrorHelpers.createUnsupportedTypeError(type).output.payload as ErrorPayload; + +describe('SavedObjectsRepository', () => { + let client: ReturnType; + let savedObjectsRepository: SavedObjectsRepository; + let migrator: ReturnType; + let logger: ReturnType; + let serializer: jest.Mocked; + + const mockTimestamp = '2017-08-14T15:49:14.886Z'; + const mockTimestampFields = { updated_at: mockTimestamp }; + const mockVersionProps = { _seq_no: 1, _primary_term: 1 }; + const mockVersion = encodeHitVersion(mockVersionProps); + + const KIBANA_VERSION = '2.0.0'; + const CUSTOM_INDEX_TYPE = 'customIndex'; + /** This type has namespaceType: 'agnostic'. */ + const NAMESPACE_AGNOSTIC_TYPE = 'globalType'; + /** + * This type has namespaceType: 'multiple'. + * + * That means that the object is serialized with a globally unique ID across namespaces. It also means that the object is shareable across + * namespaces. + **/ + const MULTI_NAMESPACE_TYPE = 'multiNamespaceType'; + /** + * This type has namespaceType: 'multiple-isolated'. + * + * That means that the object is serialized with a globally unique ID across namespaces. It also means that the object is NOT shareable + * across namespaces. This distinction only matters when using the `collectMultiNamespaceReferences` or `updateObjectsSpaces` APIs, or + * when using the `initialNamespaces` argument with the `create` and `bulkCreate` APIs. Those allow you to define or change what + * namespaces an object exists in. + * + * In a nutshell, this type is more restrictive than `MULTI_NAMESPACE_TYPE`, so we use `MULTI_NAMESPACE_ISOLATED_TYPE` for any test cases + * where `MULTI_NAMESPACE_TYPE` would also satisfy the test case. + **/ + const MULTI_NAMESPACE_ISOLATED_TYPE = 'multiNamespaceIsolatedType'; + /** This type has namespaceType: 'multiple', and it uses a custom index. */ + const MULTI_NAMESPACE_CUSTOM_INDEX_TYPE = 'multiNamespaceTypeCustomIndex'; + const HIDDEN_TYPE = 'hiddenType'; + + const mappings: SavedObjectsTypeMappingDefinition = { + properties: { + config: { + properties: { + otherField: { + type: 'keyword', + }, + }, + }, + 'index-pattern': { + properties: { + someField: { + type: 'keyword', + }, + }, + }, + dashboard: { + properties: { + otherField: { + type: 'keyword', + }, + }, + }, + [CUSTOM_INDEX_TYPE]: { + properties: { + otherField: { + type: 'keyword', + }, + }, + }, + [NAMESPACE_AGNOSTIC_TYPE]: { + properties: { + yetAnotherField: { + type: 'keyword', + }, + }, + }, + [MULTI_NAMESPACE_TYPE]: { + properties: { + evenYetAnotherField: { + type: 'keyword', + }, + }, + }, + [MULTI_NAMESPACE_ISOLATED_TYPE]: { + properties: { + evenYetAnotherField: { + type: 'keyword', + }, + }, + }, + [MULTI_NAMESPACE_CUSTOM_INDEX_TYPE]: { + properties: { + evenYetAnotherField: { + type: 'keyword', + }, + }, + }, + [HIDDEN_TYPE]: { + properties: { + someField: { + type: 'keyword', + }, + }, + }, + }, + }; + + const createType = (type: string, parts: Partial = {}): SavedObjectsType => ({ + name: type, + hidden: false, + namespaceType: 'single', + mappings: { + properties: mappings.properties[type].properties! as SavedObjectsMappingProperties, + }, + migrations: { '1.1.1': (doc) => doc }, + ...parts, + }); + + const registry = new SavedObjectTypeRegistry(); + registry.registerType(createType('config')); + registry.registerType(createType('index-pattern')); + registry.registerType(createType('dashboard')); + registry.registerType(createType(CUSTOM_INDEX_TYPE, { indexPattern: 'custom' })); + registry.registerType(createType(NAMESPACE_AGNOSTIC_TYPE, { namespaceType: 'agnostic' })); + registry.registerType(createType(MULTI_NAMESPACE_TYPE, { namespaceType: 'multiple' })); + registry.registerType( + createType(MULTI_NAMESPACE_ISOLATED_TYPE, { namespaceType: 'multiple-isolated' }) + ); + registry.registerType( + createType(MULTI_NAMESPACE_CUSTOM_INDEX_TYPE, { + namespaceType: 'multiple', + indexPattern: 'custom', + }) + ); + registry.registerType( + createType(HIDDEN_TYPE, { + hidden: true, + namespaceType: 'agnostic', + }) + ); + + const documentMigrator = new DocumentMigrator({ + typeRegistry: registry, + kibanaVersion: KIBANA_VERSION, + log: loggerMock.create(), + }); + + const getMockGetResponse = ( + { + type, + id, + references, + namespace: objectNamespace, + originId, + }: { + type: string; + id: string; + namespace?: string; + originId?: string; + references?: SavedObjectReference[]; + }, + namespace?: string | string[] + ) => { + let namespaces; + if (objectNamespace) { + namespaces = [objectNamespace]; + } else if (namespace) { + namespaces = Array.isArray(namespace) ? namespace : [namespace]; + } else { + namespaces = ['default']; + } + const namespaceId = namespaces[0] === 'default' ? undefined : namespaces[0]; + + return { + // NOTE: Elasticsearch returns more fields (_index, _type) but the SavedObjectsRepository method ignores these + found: true, + _id: `${ + registry.isSingleNamespace(type) && namespaceId ? `${namespaceId}:` : '' + }${type}:${id}`, + ...mockVersionProps, + _source: { + ...(registry.isSingleNamespace(type) && { namespace: namespaceId }), + ...(registry.isMultiNamespace(type) && { namespaces }), + ...(originId && { originId }), + type, + [type]: { title: 'Testing' }, + references, + specialProperty: 'specialValue', + ...mockTimestampFields, + } as SavedObjectsRawDocSource, + } as estypes.GetResponse; + }; + + const getMockMgetResponse = ( + objects: Array, + namespace?: string + ) => + ({ + docs: objects.map((obj) => + obj.found === false ? obj : getMockGetResponse(obj, obj.initialNamespaces ?? namespace) + ), + } as estypes.MgetResponse); + + expect.extend({ + toBeDocumentWithoutError(received, type, id) { + if (received.type === type && received.id === id && !received.error) { + return { message: () => `expected type and id not to match without error`, pass: true }; + } else { + return { message: () => `expected type and id to match without error`, pass: false }; + } + }, + }); + const expectSuccess = ({ type, id }: { type: string; id: string }) => { + // @ts-expect-error TS is not aware of the extension + return expect.toBeDocumentWithoutError(type, id); + }; + + const expectError = ({ type, id }: { type: string; id: string }) => ({ + type, + id, + error: expect.any(Object), + }); + + const expectErrorResult = ( + { type, id }: TypeIdTuple, + error: Record, + overrides: Record = {} + ): ExpectedErrorResult => ({ + type, + id, + error: { ...error, ...overrides }, + }); + const expectErrorNotFound = (obj: TypeIdTuple, overrides?: Record) => + expectErrorResult(obj, createGenericNotFoundError(obj.type, obj.id), overrides); + const expectErrorConflict = (obj: TypeIdTuple, overrides?: Record) => + expectErrorResult(obj, createConflictError(obj.type, obj.id), overrides); + const expectErrorInvalidType = (obj: TypeIdTuple, overrides?: Record) => + expectErrorResult(obj, createUnsupportedTypeError(obj.type), overrides); + + const expectMigrationArgs = (args: unknown, contains = true, n = 1) => { + const obj = contains ? expect.objectContaining(args) : expect.not.objectContaining(args); + expect(migrator.migrateDocument).toHaveBeenNthCalledWith(n, obj); + }; + + const createSpySerializer = () => { + const spyInstance = { + isRawSavedObject: jest.fn(), + rawToSavedObject: jest.fn(), + savedObjectToRaw: jest.fn(), + generateRawId: jest.fn(), + generateRawLegacyUrlAliasId: jest.fn(), + trimIdPrefix: jest.fn(), + }; + const realInstance = new SavedObjectsSerializer(registry); + Object.keys(spyInstance).forEach((key) => { + // @ts-expect-error no proper way to do this with typing support + spyInstance[key].mockImplementation((...args) => realInstance[key](...args)); + }); + + return spyInstance as unknown as jest.Mocked; + }; + + beforeEach(() => { + pointInTimeFinderMock.mockClear(); + client = elasticsearchClientMock.createElasticsearchClient(); + migrator = mockKibanaMigrator.create(); + documentMigrator.prepareMigrations(); + migrator.migrateDocument = jest.fn().mockImplementation(documentMigrator.migrate); + migrator.runMigrations = jest.fn().mockResolvedValue([{ status: 'skipped' }]); + logger = loggerMock.create(); + + // create a mock serializer "shim" so we can track function calls, but use the real serializer's implementation + serializer = createSpySerializer(); + + const allTypes = registry.getAllTypes().map((type) => type.name); + const allowedTypes = [...new Set(allTypes.filter((type) => !registry.isHidden(type)))]; + + // @ts-expect-error must use the private constructor to use the mocked serializer + savedObjectsRepository = new SavedObjectsRepository({ + index: '.kibana-test', + mappings, + client, + migrator, + typeRegistry: registry, + serializer, + allowedTypes, + logger, + }); + + mockGetCurrentTime.mockReturnValue(mockTimestamp); + mockGetSearchDsl.mockClear(); + }); + + const mockMigrationVersion = { foo: '2.3.4' }; + const mockMigrateDocument = (doc: SavedObjectUnsanitizedDoc) => ({ + ...doc, + attributes: { + ...doc.attributes, + ...(doc.attributes?.title && { title: `${doc.attributes.title}!!` }), + }, + migrationVersion: mockMigrationVersion, + references: [{ name: 'search_0', type: 'search', id: '123' }], + }); + + describe('#bulkCreate', () => { + beforeEach(() => { + mockPreflightCheckForCreate.mockReset(); + mockPreflightCheckForCreate.mockImplementation(({ objects }) => { + return Promise.resolve(objects.map(({ type, id }) => ({ type, id }))); // respond with no errors by default + }); + }); + + const obj1 = { + type: 'config', + id: '6.0.0-alpha1', + attributes: { title: 'Test One' }, + references: [{ name: 'ref_0', type: 'test', id: '1' }], + originId: 'some-origin-id', // only one of the object args has an originId, this is intentional to test both a positive and negative case + }; + const obj2 = { + type: 'index-pattern', + id: 'logstash-*', + attributes: { title: 'Test Two' }, + references: [{ name: 'ref_0', type: 'test', id: '2' }], + }; + const namespace = 'foo-namespace'; + + const getMockBulkCreateResponse = ( + objects: SavedObjectsBulkCreateObject[], + namespace?: string + ) => { + return { + errors: false, + took: 1, + items: objects.map(({ type, id, originId, attributes, references, migrationVersion }) => ({ + create: { + // status: 1, + // _index: '.kibana', + _id: `${namespace ? `${namespace}:` : ''}${type}:${id}`, + _source: { + [type]: attributes, + type, + namespace, + ...(originId && { originId }), + references, + ...mockTimestampFields, + migrationVersion: migrationVersion || { [type]: '1.1.1' }, + }, + ...mockVersionProps, + }, + })), + } as unknown as estypes.BulkResponse; + }; + + const bulkCreateSuccess = async ( + objects: SavedObjectsBulkCreateObject[], + options?: SavedObjectsCreateOptions + ) => { + const response = getMockBulkCreateResponse(objects, options?.namespace); + client.bulk.mockResolvedValue( + elasticsearchClientMock.createSuccessTransportRequestPromise(response) + ); + return await savedObjectsRepository.bulkCreate(objects, options); + }; + + // bulk create calls have two objects for each source -- the action, and the source + const expectClientCallArgsAction = ( + objects: Array<{ type: string; id?: string; if_primary_term?: string; if_seq_no?: string }>, + { + method, + _index = expect.any(String), + getId = () => expect.any(String), + }: { method: string; _index?: string; getId?: (type: string, id?: string) => string } + ) => { + const body = []; + for (const { type, id, if_primary_term: ifPrimaryTerm, if_seq_no: ifSeqNo } of objects) { + body.push({ + [method]: { + _index, + _id: getId(type, id), + ...(ifPrimaryTerm && ifSeqNo + ? { if_primary_term: expect.any(Number), if_seq_no: expect.any(Number) } + : {}), + }, + }); + body.push(expect.any(Object)); + } + expect(client.bulk).toHaveBeenCalledWith( + expect.objectContaining({ body }), + expect.anything() + ); + }; + + const expectObjArgs = ( + { + type, + attributes, + references, + }: { type: string; attributes: unknown; references?: SavedObjectReference[] }, + overrides: Record = {} + ) => [ + expect.any(Object), + expect.objectContaining({ + [type]: attributes, + references, + type, + ...overrides, + ...mockTimestampFields, + }), + ]; + + const expectSuccessResult = (obj: { + type: string; + namespace?: string; + namespaces?: string[]; + }) => ({ + ...obj, + migrationVersion: { [obj.type]: '1.1.1' }, + coreMigrationVersion: KIBANA_VERSION, + version: mockVersion, + namespaces: obj.namespaces ?? [obj.namespace ?? 'default'], + ...mockTimestampFields, + }); + + describe('client calls', () => { + it(`should use the ES bulk action by default`, async () => { + await bulkCreateSuccess([obj1, obj2]); + expect(client.bulk).toHaveBeenCalledTimes(1); + }); + + it(`should use the preflightCheckForCreate action before bulk action for any types that are multi-namespace, when id is defined`, async () => { + const objects = [obj1, { ...obj2, type: MULTI_NAMESPACE_ISOLATED_TYPE }]; + await bulkCreateSuccess(objects); + expect(client.bulk).toHaveBeenCalledTimes(1); + expect(mockPreflightCheckForCreate).toHaveBeenCalledTimes(1); + expect(mockPreflightCheckForCreate).toHaveBeenCalledWith( + expect.objectContaining({ + objects: [ + { + type: MULTI_NAMESPACE_ISOLATED_TYPE, + id: obj2.id, + overwrite: false, + namespaces: ['default'], + }, + ], + }) + ); + }); + + it(`should use the ES create method if ID is undefined and overwrite=true`, async () => { + const objects = [obj1, obj2].map((obj) => ({ ...obj, id: undefined })); + await bulkCreateSuccess(objects, { overwrite: true }); + expectClientCallArgsAction(objects, { method: 'create' }); + }); + + it(`should use the ES create method if ID is undefined and overwrite=false`, async () => { + const objects = [obj1, obj2].map((obj) => ({ ...obj, id: undefined })); + await bulkCreateSuccess(objects); + expectClientCallArgsAction(objects, { method: 'create' }); + }); + + it(`should use the ES index method if ID is defined and overwrite=true`, async () => { + await bulkCreateSuccess([obj1, obj2], { overwrite: true }); + expectClientCallArgsAction([obj1, obj2], { method: 'index' }); + }); + + it(`should use the ES index method with version if ID and version are defined and overwrite=true`, async () => { + await bulkCreateSuccess( + [ + { + ...obj1, + version: mockVersion, + }, + obj2, + ], + { overwrite: true } + ); + + const obj1WithSeq = { + ...obj1, + if_seq_no: mockVersionProps._seq_no, + if_primary_term: mockVersionProps._primary_term, + }; + + expectClientCallArgsAction([obj1WithSeq, obj2], { method: 'index' }); + }); + + it(`should use the ES create method if ID is defined and overwrite=false`, async () => { + await bulkCreateSuccess([obj1, obj2]); + expectClientCallArgsAction([obj1, obj2], { method: 'create' }); + }); + + it(`formats the ES request`, async () => { + await bulkCreateSuccess([obj1, obj2]); + const body = [...expectObjArgs(obj1), ...expectObjArgs(obj2)]; + expect(client.bulk).toHaveBeenCalledWith( + expect.objectContaining({ body }), + expect.anything() + ); + }); + + it(`adds namespace to request body for any types that are single-namespace`, async () => { + await bulkCreateSuccess([obj1, obj2], { namespace }); + const expected = expect.objectContaining({ namespace }); + const body = [expect.any(Object), expected, expect.any(Object), expected]; + expect(client.bulk).toHaveBeenCalledWith( + expect.objectContaining({ body }), + expect.anything() + ); + }); + + it(`normalizes options.namespace from 'default' to undefined`, async () => { + await bulkCreateSuccess([obj1, obj2], { namespace: 'default' }); + const expected = expect.not.objectContaining({ namespace: 'default' }); + const body = [expect.any(Object), expected, expect.any(Object), expected]; + expect(client.bulk).toHaveBeenCalledWith( + expect.objectContaining({ body }), + expect.anything() + ); + }); + + it(`doesn't add namespace to request body for any types that are not single-namespace`, async () => { + const objects = [ + { ...obj1, type: NAMESPACE_AGNOSTIC_TYPE }, + { ...obj2, type: MULTI_NAMESPACE_ISOLATED_TYPE }, + ]; + await bulkCreateSuccess(objects, { namespace }); + const expected = expect.not.objectContaining({ namespace: expect.anything() }); + const body = [expect.any(Object), expected, expect.any(Object), expected]; + expect(client.bulk).toHaveBeenCalledWith( + expect.objectContaining({ body }), + expect.anything() + ); + }); + + it(`adds namespaces to request body for any types that are multi-namespace`, async () => { + const test = async (namespace?: string) => { + const objects = [obj1, obj2].map((x) => ({ ...x, type: MULTI_NAMESPACE_ISOLATED_TYPE })); + const [o1, o2] = objects; + mockPreflightCheckForCreate.mockResolvedValueOnce([ + { type: o1.type, id: o1.id! }, // first object does not have an existing document to overwrite + { + type: o2.type, + id: o2.id!, + existingDocument: { _id: o2.id!, _source: { namespaces: ['*'], type: o2.type } }, // second object does have an existing document to overwrite + }, + ]); + await bulkCreateSuccess(objects, { namespace, overwrite: true }); + const expected1 = expect.objectContaining({ namespaces: [namespace ?? 'default'] }); + const expected2 = expect.objectContaining({ namespaces: ['*'] }); + const body = [expect.any(Object), expected1, expect.any(Object), expected2]; + expect(client.bulk).toHaveBeenCalledWith( + expect.objectContaining({ body }), + expect.anything() + ); + client.bulk.mockClear(); + mockPreflightCheckForCreate.mockReset(); + }; + await test(undefined); + await test(namespace); + }); + + it(`adds initialNamespaces instead of namespace`, async () => { + const test = async (namespace?: string) => { + const ns2 = 'bar-namespace'; + const ns3 = 'baz-namespace'; + const objects = [ + { ...obj1, type: 'dashboard', initialNamespaces: [ns2] }, + { ...obj1, type: MULTI_NAMESPACE_ISOLATED_TYPE, initialNamespaces: [ns2] }, + { ...obj1, type: MULTI_NAMESPACE_TYPE, initialNamespaces: [ns2, ns3] }, + ]; + const [o1, o2, o3] = objects; + mockPreflightCheckForCreate.mockResolvedValueOnce([ + // first object does not get passed in to preflightCheckForCreate at all + { type: o2.type, id: o2.id! }, // second object does not have an existing document to overwrite + { + type: o3.type, + id: o3.id!, + existingDocument: { + _id: o3.id!, + _source: { type: o3.type, namespaces: [namespace ?? 'default', 'something-else'] }, // third object does have an existing document to overwrite + }, + }, + ]); + await bulkCreateSuccess(objects, { namespace, overwrite: true }); + const body = [ + { index: expect.objectContaining({ _id: `${ns2}:dashboard:${o1.id}` }) }, + expect.objectContaining({ namespace: ns2 }), + { + index: expect.objectContaining({ + _id: `${MULTI_NAMESPACE_ISOLATED_TYPE}:${o2.id}`, + }), + }, + expect.objectContaining({ namespaces: [ns2] }), + { index: expect.objectContaining({ _id: `${MULTI_NAMESPACE_TYPE}:${o3.id}` }) }, + expect.objectContaining({ namespaces: [ns2, ns3] }), + ]; + expect(mockPreflightCheckForCreate).toHaveBeenCalledWith( + expect.objectContaining({ + objects: [ + // assert that the initialNamespaces fields were passed into preflightCheckForCreate instead of the current namespace + { type: o2.type, id: o2.id, overwrite: true, namespaces: o2.initialNamespaces }, + { type: o3.type, id: o3.id, overwrite: true, namespaces: o3.initialNamespaces }, + ], + }) + ); + expect(client.bulk).toHaveBeenCalledWith( + expect.objectContaining({ body }), + expect.anything() + ); + client.bulk.mockClear(); + mockPreflightCheckForCreate.mockReset(); + }; + await test(undefined); + await test(namespace); + }); + + it(`normalizes initialNamespaces from 'default' to undefined`, async () => { + const test = async (namespace?: string) => { + const objects = [{ ...obj1, type: 'dashboard', initialNamespaces: ['default'] }]; + await bulkCreateSuccess(objects, { namespace, overwrite: true }); + const body = [ + { index: expect.objectContaining({ _id: `dashboard:${obj1.id}` }) }, + expect.not.objectContaining({ namespace: 'default' }), + ]; + expect(client.bulk).toHaveBeenCalledWith( + expect.objectContaining({ body }), + expect.anything() + ); + client.bulk.mockClear(); + }; + await test(undefined); + await test(namespace); + }); + + it(`doesn't add namespaces to request body for any types that are not multi-namespace`, async () => { + const test = async (namespace?: string) => { + const objects = [obj1, { ...obj2, type: NAMESPACE_AGNOSTIC_TYPE }]; + await bulkCreateSuccess(objects, { namespace, overwrite: true }); + const expected = expect.not.objectContaining({ namespaces: expect.anything() }); + const body = [expect.any(Object), expected, expect.any(Object), expected]; + expect(client.bulk).toHaveBeenCalledWith( + expect.objectContaining({ body }), + expect.anything() + ); + client.bulk.mockClear(); + }; + await test(undefined); + await test(namespace); + }); + + it(`defaults to a refresh setting of wait_for`, async () => { + await bulkCreateSuccess([obj1, obj2]); + expect(client.bulk).toHaveBeenCalledWith( + expect.objectContaining({ refresh: 'wait_for' }), + expect.anything() + ); + }); + + it(`should use default index`, async () => { + await bulkCreateSuccess([obj1, obj2]); + expectClientCallArgsAction([obj1, obj2], { + method: 'create', + _index: '.kibana-test_8.0.0-testing', + }); + }); + + it(`should use custom index`, async () => { + await bulkCreateSuccess([obj1, obj2].map((x) => ({ ...x, type: CUSTOM_INDEX_TYPE }))); + expectClientCallArgsAction([obj1, obj2], { + method: 'create', + _index: 'custom_8.0.0-testing', + }); + }); + + it(`prepends namespace to the id when providing namespace for single-namespace type`, async () => { + const getId = (type: string, id: string = '') => `${namespace}:${type}:${id}`; // test that the raw document ID equals this (e.g., has a namespace prefix) + await bulkCreateSuccess([obj1, obj2], { namespace }); + expectClientCallArgsAction([obj1, obj2], { method: 'create', getId }); + }); + + it(`doesn't prepend namespace to the id when providing no namespace for single-namespace type`, async () => { + const getId = (type: string, id: string = '') => `${type}:${id}`; // test that the raw document ID equals this (e.g., does not have a namespace prefix) + await bulkCreateSuccess([obj1, obj2]); + expectClientCallArgsAction([obj1, obj2], { method: 'create', getId }); + }); + + it(`doesn't prepend namespace to the id when not using single-namespace type`, async () => { + const getId = (type: string, id: string = '') => `${type}:${id}`; // test that the raw document ID equals this (e.g., does not have a namespace prefix) + const objects = [ + { ...obj1, type: NAMESPACE_AGNOSTIC_TYPE }, + { ...obj2, type: MULTI_NAMESPACE_ISOLATED_TYPE }, + ]; + await bulkCreateSuccess(objects, { namespace }); + expectClientCallArgsAction(objects, { method: 'create', getId }); + }); + }); + + describe('errors', () => { + afterEach(() => { + mockGetBulkOperationError.mockReset(); + }); + + const obj3 = { + type: 'dashboard', + id: 'three', + attributes: { title: 'Test Three' }, + references: [{ name: 'ref_0', type: 'test', id: '2' }], + }; + + const bulkCreateError = async ( + obj: SavedObjectsBulkCreateObject, + isBulkError: boolean | undefined, + expectedErrorResult: ExpectedErrorResult + ) => { + let response; + if (isBulkError) { + // mock the bulk error for only the second object + mockGetBulkOperationError.mockReturnValueOnce(undefined); + mockGetBulkOperationError.mockReturnValueOnce(expectedErrorResult.error as Payload); + response = getMockBulkCreateResponse([obj1, obj, obj2]); + } else { + response = getMockBulkCreateResponse([obj1, obj2]); + } + client.bulk.mockResolvedValueOnce( + elasticsearchClientMock.createSuccessTransportRequestPromise(response) + ); + + const objects = [obj1, obj, obj2]; + const result = await savedObjectsRepository.bulkCreate(objects); + expect(client.bulk).toHaveBeenCalled(); + const objCall = isBulkError ? expectObjArgs(obj) : []; + const body = [...expectObjArgs(obj1), ...objCall, ...expectObjArgs(obj2)]; + expect(client.bulk).toHaveBeenCalledWith( + expect.objectContaining({ body }), + expect.anything() + ); + expect(result).toEqual({ + saved_objects: [expectSuccess(obj1), expectedErrorResult, expectSuccess(obj2)], + }); + }; + + it(`throws when options.namespace is '*'`, async () => { + await expect( + savedObjectsRepository.bulkCreate([obj3], { namespace: ALL_NAMESPACES_STRING }) + ).rejects.toThrowError(createBadRequestError('"options.namespace" cannot be "*"')); + }); + + it(`returns error when initialNamespaces is used with a space-agnostic object`, async () => { + const obj = { ...obj3, type: NAMESPACE_AGNOSTIC_TYPE, initialNamespaces: [] }; + await bulkCreateError( + obj, + undefined, + expectErrorResult( + obj, + createBadRequestError('"initialNamespaces" cannot be used on space-agnostic types') + ) + ); + }); + + it(`returns error when initialNamespaces is empty`, async () => { + const obj = { ...obj3, type: MULTI_NAMESPACE_TYPE, initialNamespaces: [] }; + await bulkCreateError( + obj, + undefined, + expectErrorResult( + obj, + createBadRequestError('"initialNamespaces" must be a non-empty array of strings') + ) + ); + }); + + it(`returns error when initialNamespaces is used with a space-isolated object and does not specify a single space`, async () => { + const doTest = async (objType: string, initialNamespaces: string[]) => { + const obj = { ...obj3, type: objType, initialNamespaces }; + await bulkCreateError( + obj, + undefined, + expectErrorResult( + obj, + createBadRequestError( + '"initialNamespaces" can only specify a single space when used with space-isolated types' + ) + ) + ); + }; + await doTest('dashboard', ['spacex', 'spacey']); + await doTest('dashboard', ['*']); + await doTest(MULTI_NAMESPACE_ISOLATED_TYPE, ['spacex', 'spacey']); + await doTest(MULTI_NAMESPACE_ISOLATED_TYPE, ['*']); + }); + + it(`returns error when type is invalid`, async () => { + const obj = { ...obj3, type: 'unknownType' }; + await bulkCreateError(obj, undefined, expectErrorInvalidType(obj)); + }); + + it(`returns error when type is hidden`, async () => { + const obj = { ...obj3, type: HIDDEN_TYPE }; + await bulkCreateError(obj, undefined, expectErrorInvalidType(obj)); + }); + + it(`returns error when there is a conflict from preflightCheckForCreate`, async () => { + const objects = [ + // only the second, third, and fourth objects are passed to preflightCheckForCreate and result in errors + obj1, + { ...obj1, type: MULTI_NAMESPACE_TYPE }, + { ...obj2, type: MULTI_NAMESPACE_TYPE }, + { ...obj3, type: MULTI_NAMESPACE_TYPE }, + obj2, + ]; + const [o1, o2, o3, o4, o5] = objects; + mockPreflightCheckForCreate.mockResolvedValueOnce([ + // first and last objects do not get passed in to preflightCheckForCreate at all + { type: o2.type, id: o2.id!, error: { type: 'conflict' } }, + { + type: o3.type, + id: o3.id!, + error: { type: 'unresolvableConflict', metadata: { isNotOverwritable: true } }, + }, + { + type: o4.type, + id: o4.id!, + error: { type: 'aliasConflict', metadata: { spacesWithConflictingAliases: ['foo'] } }, + }, + ]); + const bulkResponse = getMockBulkCreateResponse([o1, o5]); + client.bulk.mockResolvedValueOnce( + elasticsearchClientMock.createSuccessTransportRequestPromise(bulkResponse) + ); + + const options = { overwrite: true }; + const result = await savedObjectsRepository.bulkCreate(objects, options); + expect(mockPreflightCheckForCreate).toHaveBeenCalled(); + expect(mockPreflightCheckForCreate).toHaveBeenCalledWith( + expect.objectContaining({ + objects: [ + { type: o2.type, id: o2.id, overwrite: true, namespaces: ['default'] }, + { type: o3.type, id: o3.id, overwrite: true, namespaces: ['default'] }, + { type: o4.type, id: o4.id, overwrite: true, namespaces: ['default'] }, + ], + }) + ); + expect(client.bulk).toHaveBeenCalled(); + expect(client.bulk).toHaveBeenCalledWith( + expect.objectContaining({ body: [...expectObjArgs(o1), ...expectObjArgs(o5)] }), + expect.anything() + ); + expect(result).toEqual({ + saved_objects: [ + expectSuccess(o1), + expectErrorConflict(o2), + expectErrorConflict(o3, { metadata: { isNotOverwritable: true } }), + expectErrorConflict(o4, { metadata: { spacesWithConflictingAliases: ['foo'] } }), + expectSuccess(o5), + ], + }); + }); + + it(`returns bulk error`, async () => { + const expectedErrorResult = { + type: obj3.type, + id: obj3.id, + error: { error: 'Oh no, a bulk error!' }, + }; + await bulkCreateError(obj3, true, expectedErrorResult); + }); + }); + + describe('migration', () => { + it(`migrates the docs and serializes the migrated docs`, async () => { + migrator.migrateDocument.mockImplementation(mockMigrateDocument); + await bulkCreateSuccess([obj1, obj2]); + const docs = [obj1, obj2].map((x) => ({ ...x, ...mockTimestampFields })); + expectMigrationArgs(docs[0], true, 1); + expectMigrationArgs(docs[1], true, 2); + + const migratedDocs = docs.map((x) => migrator.migrateDocument(x)); + expect(serializer.savedObjectToRaw).toHaveBeenNthCalledWith(1, migratedDocs[0]); + expect(serializer.savedObjectToRaw).toHaveBeenNthCalledWith(2, migratedDocs[1]); + }); + + it(`adds namespace to body when providing namespace for single-namespace type`, async () => { + await bulkCreateSuccess([obj1, obj2], { namespace }); + expectMigrationArgs({ namespace }, true, 1); + expectMigrationArgs({ namespace }, true, 2); + }); + + it(`doesn't add namespace to body when providing no namespace for single-namespace type`, async () => { + await bulkCreateSuccess([obj1, obj2]); + expectMigrationArgs({ namespace: expect.anything() }, false, 1); + expectMigrationArgs({ namespace: expect.anything() }, false, 2); + }); + + it(`doesn't add namespace to body when not using single-namespace type`, async () => { + const objects = [ + { ...obj1, type: NAMESPACE_AGNOSTIC_TYPE }, + { ...obj2, type: MULTI_NAMESPACE_ISOLATED_TYPE }, + ]; + await bulkCreateSuccess(objects, { namespace }); + expectMigrationArgs({ namespace: expect.anything() }, false, 1); + expectMigrationArgs({ namespace: expect.anything() }, false, 2); + }); + + it(`adds namespaces to body when providing namespace for multi-namespace type`, async () => { + const objects = [obj1, obj2].map((obj) => ({ + ...obj, + type: MULTI_NAMESPACE_ISOLATED_TYPE, + })); + await bulkCreateSuccess(objects, { namespace }); + expectMigrationArgs({ namespaces: [namespace] }, true, 1); + expectMigrationArgs({ namespaces: [namespace] }, true, 2); + }); + + it(`adds default namespaces to body when providing no namespace for multi-namespace type`, async () => { + const objects = [obj1, obj2].map((obj) => ({ + ...obj, + type: MULTI_NAMESPACE_ISOLATED_TYPE, + })); + await bulkCreateSuccess(objects); + expectMigrationArgs({ namespaces: ['default'] }, true, 1); + expectMigrationArgs({ namespaces: ['default'] }, true, 2); + }); + + it(`doesn't add namespaces to body when not using multi-namespace type`, async () => { + const objects = [obj1, { ...obj2, type: NAMESPACE_AGNOSTIC_TYPE }]; + await bulkCreateSuccess(objects); + expectMigrationArgs({ namespaces: expect.anything() }, false, 1); + expectMigrationArgs({ namespaces: expect.anything() }, false, 2); + }); + }); + + describe('returns', () => { + it(`formats the ES response`, async () => { + const result = await bulkCreateSuccess([obj1, obj2]); + expect(result).toEqual({ + saved_objects: [obj1, obj2].map((x) => expectSuccessResult(x)), + }); + }); + + it.todo(`should return objects in the same order regardless of type`); + + it(`handles a mix of successful creates and errors`, async () => { + const obj = { + type: 'unknownType', + id: 'three', + attributes: {}, + }; + const objects = [obj1, obj, obj2]; + const response = getMockBulkCreateResponse([obj1, obj2]); + client.bulk.mockResolvedValueOnce( + elasticsearchClientMock.createSuccessTransportRequestPromise(response) + ); + const result = await savedObjectsRepository.bulkCreate(objects); + expect(client.bulk).toHaveBeenCalledTimes(1); + expect(result).toEqual({ + saved_objects: [expectSuccessResult(obj1), expectError(obj), expectSuccessResult(obj2)], + }); + }); + + it(`a deserialized saved object`, async () => { + // Test for fix to https://github.com/elastic/kibana/issues/65088 where + // we returned raw ID's when an object without an id was created. + const namespace = 'myspace'; + // FIXME: this test is based on a gigantic hack to have the bulk operation return the source + // of the document when it actually does not, forcing to cast to any as BulkResponse + // does not contains _source + const response = getMockBulkCreateResponse([obj1, obj2], namespace) as any; + client.bulk.mockResolvedValueOnce( + elasticsearchClientMock.createSuccessTransportRequestPromise(response) + ); + + // Bulk create one object with id unspecified, and one with id specified + const result = await savedObjectsRepository.bulkCreate([{ ...obj1, id: undefined }, obj2], { + namespace, + }); + + // Assert that both raw docs from the ES response are deserialized + expect(serializer.rawToSavedObject).toHaveBeenNthCalledWith(1, { + ...response.items[0].create, + _source: { + ...response.items[0].create._source, + coreMigrationVersion: '2.0.0', // the document migrator adds this to all objects before creation + namespaces: response.items[0].create._source.namespaces, + }, + _id: expect.stringMatching(/^myspace:config:[0-9a-f]{8}-([0-9a-f]{4}-){3}[0-9a-f]{12}$/), + }); + expect(serializer.rawToSavedObject).toHaveBeenNthCalledWith(2, { + ...response.items[1].create, + _source: { + ...response.items[1].create._source, + coreMigrationVersion: '2.0.0', // the document migrator adds this to all objects before creation + namespaces: response.items[1].create._source.namespaces, + }, + }); + + // Assert that ID's are deserialized to remove the type and namespace + expect(result.saved_objects[0].id).toEqual( + expect.stringMatching(/^[0-9a-f]{8}-([0-9a-f]{4}-){3}[0-9a-f]{12}$/) + ); + expect(result.saved_objects[1].id).toEqual(obj2.id); + }); + }); + }); + + describe('#bulkGet', () => { + const obj1: SavedObject = { + type: 'config', + id: '6.0.0-alpha1', + attributes: { title: 'Testing' }, + references: [ + { + name: 'ref_0', + type: 'test', + id: '1', + }, + ], + originId: 'some-origin-id', // only one of the results has an originId, this is intentional to test both a positive and negative case + }; + const obj2: SavedObject = { + type: 'index-pattern', + id: 'logstash-*', + attributes: { title: 'Testing' }, + references: [ + { + name: 'ref_0', + type: 'test', + id: '2', + }, + ], + }; + const namespace = 'foo-namespace'; + + const bulkGet = async ( + objects: SavedObjectsBulkGetObject[], + options?: SavedObjectsBaseOptions + ) => + savedObjectsRepository.bulkGet( + objects.map(({ type, id, namespaces }) => ({ type, id, namespaces })), // bulkGet only uses type, id, and optionally namespaces + options + ); + const bulkGetSuccess = async (objects: SavedObject[], options?: SavedObjectsBaseOptions) => { + const response = getMockMgetResponse(objects, options?.namespace); + client.mget.mockResolvedValueOnce( + elasticsearchClientMock.createSuccessTransportRequestPromise(response) + ); + const result = await bulkGet(objects, options); + expect(client.mget).toHaveBeenCalledTimes(1); + return result; + }; + + const _expectClientCallArgs = ( + objects: TypeIdTuple[], + { + _index = expect.any(String), + getId = () => expect.any(String), + }: { _index?: string; getId?: (type: string, id: string) => string } + ) => { + expect(client.mget).toHaveBeenCalledWith( + expect.objectContaining({ + body: { + docs: objects.map(({ type, id }) => + expect.objectContaining({ + _index, + _id: getId(type, id), + }) + ), + }, + }), + expect.anything() + ); + }; + + describe('client calls', () => { + it(`prepends namespace to the id when providing namespace for single-namespace type`, async () => { + const getId = (type: string, id: string) => `${namespace}:${type}:${id}`; // test that the raw document ID equals this (e.g., has a namespace prefix) + await bulkGetSuccess([obj1, obj2], { namespace }); + _expectClientCallArgs([obj1, obj2], { getId }); + }); + + it(`prepends namespace to the id when providing namespaces for single-namespace type`, async () => { + const getId = (type: string, id: string) => `${namespace}:${type}:${id}`; // test that the raw document ID equals this (e.g., has a namespace prefix) + const objects = [obj1, obj2].map((obj) => ({ ...obj, namespaces: [namespace] })); + await bulkGetSuccess(objects, { namespace: 'some-other-ns' }); + _expectClientCallArgs([obj1, obj2], { getId }); + }); + + it(`doesn't prepend namespace to the id when providing no namespace for single-namespace type`, async () => { + const getId = (type: string, id: string) => `${type}:${id}`; // test that the raw document ID equals this (e.g., does not have a namespace prefix) + await bulkGetSuccess([obj1, obj2]); + _expectClientCallArgs([obj1, obj2], { getId }); + }); + + it(`normalizes options.namespace from 'default' to undefined`, async () => { + const getId = (type: string, id: string) => `${type}:${id}`; // test that the raw document ID equals this (e.g., does not have a namespace prefix) + await bulkGetSuccess([obj1, obj2], { namespace: 'default' }); + _expectClientCallArgs([obj1, obj2], { getId }); + }); + + it(`doesn't prepend namespace to the id when not using single-namespace type`, async () => { + const getId = (type: string, id: string) => `${type}:${id}`; // test that the raw document ID equals this (e.g., does not have a namespace prefix) + let objects = [obj1, obj2].map((obj) => ({ ...obj, type: NAMESPACE_AGNOSTIC_TYPE })); + await bulkGetSuccess(objects, { namespace }); + _expectClientCallArgs(objects, { getId }); + + client.mget.mockClear(); + objects = [obj1, { ...obj2, namespaces: ['some-other-ns'] }].map((obj) => ({ + ...obj, + type: MULTI_NAMESPACE_ISOLATED_TYPE, + })); + await bulkGetSuccess(objects, { namespace }); + _expectClientCallArgs(objects, { getId }); + }); + }); + + describe('errors', () => { + const bulkGetError = async ( + obj: SavedObjectsBulkGetObject & { found?: boolean }, + isBulkError: boolean, + expectedErrorResult: ExpectedErrorResult + ) => { + let response; + if (isBulkError) { + // mock the bulk error for only the second object + mockGetBulkOperationError.mockReturnValueOnce(undefined); + mockGetBulkOperationError.mockReturnValueOnce(expectedErrorResult.error as Payload); + response = getMockMgetResponse([obj1, obj, obj2]); + } else { + response = getMockMgetResponse([obj1, obj2]); + } + client.mget.mockResolvedValueOnce( + elasticsearchClientMock.createSuccessTransportRequestPromise(response) + ); + + const objects = [obj1, obj, obj2]; + const result = await bulkGet(objects); + expect(client.mget).toHaveBeenCalled(); + expect(result).toEqual({ + saved_objects: [expectSuccess(obj1), expectedErrorResult, expectSuccess(obj2)], + }); + }; + + it(`throws when options.namespace is '*'`, async () => { + const obj = { type: 'dashboard', id: 'three' }; + await expect( + savedObjectsRepository.bulkGet([obj], { namespace: ALL_NAMESPACES_STRING }) + ).rejects.toThrowError(createBadRequestError('"options.namespace" cannot be "*"')); + }); + + it(`returns error when namespaces is used with a space-agnostic object`, async () => { + const obj = { type: NAMESPACE_AGNOSTIC_TYPE, id: 'three', namespaces: [] }; + await bulkGetError( + obj, + false, + expectErrorResult( + obj, + createBadRequestError('"namespaces" cannot be used on space-agnostic types') + ) + ); + }); + + it(`returns error when namespaces is used with a space-isolated object and does not specify a single space`, async () => { + const doTest = async (objType: string, namespaces?: string[]) => { + const obj = { type: objType, id: 'three', namespaces }; + await bulkGetError( + obj, + false, + expectErrorResult( + obj, + createBadRequestError( + '"namespaces" can only specify a single space when used with space-isolated types' + ) + ) + ); + }; + await doTest('dashboard', ['spacex', 'spacey']); + await doTest('dashboard', ['*']); + await doTest(MULTI_NAMESPACE_ISOLATED_TYPE, ['spacex', 'spacey']); + await doTest(MULTI_NAMESPACE_ISOLATED_TYPE, ['*']); + }); + + it(`returns error when type is invalid`, async () => { + const obj: SavedObjectsBulkGetObject = { type: 'unknownType', id: 'three' }; + await bulkGetError(obj, false, expectErrorInvalidType(obj)); + }); + + it(`returns error when type is hidden`, async () => { + const obj: SavedObjectsBulkGetObject = { type: HIDDEN_TYPE, id: 'three' }; + await bulkGetError(obj, false, expectErrorInvalidType(obj)); + }); + + it(`returns error when document is not found`, async () => { + const obj: SavedObjectsBulkGetObject & { found: boolean } = { + type: 'dashboard', + id: 'three', + found: false, + }; + await bulkGetError(obj, true, expectErrorNotFound(obj)); + }); + + it(`handles missing ids gracefully`, async () => { + const obj: SavedObjectsBulkGetObject & { found: boolean } = { + type: 'dashboard', + // @ts-expect-error id is undefined + id: undefined, + found: false, + }; + await bulkGetError(obj, true, expectErrorNotFound(obj)); + }); + + it(`returns error when type is multi-namespace and the document exists, but not in this namespace`, async () => { + const obj = { + type: MULTI_NAMESPACE_ISOLATED_TYPE, + id: 'three', + namespace: 'bar-namespace', + }; + await bulkGetError(obj, true, expectErrorNotFound(obj)); + }); + }); + + describe('returns', () => { + const expectSuccessResult = ( + { type, id }: TypeIdTuple, + doc: estypes.MgetHit + ) => ({ + type, + id, + namespaces: doc._source!.namespaces ?? ['default'], + ...(doc._source!.originId && { originId: doc._source!.originId }), + ...(doc._source!.updated_at && { updated_at: doc._source!.updated_at }), + version: encodeHitVersion(doc), + attributes: doc._source![type], + references: doc._source!.references || [], + migrationVersion: doc._source!.migrationVersion, + }); + + it(`returns early for empty objects argument`, async () => { + const result = await bulkGet([]); + expect(result).toEqual({ saved_objects: [] }); + expect(client.mget).not.toHaveBeenCalled(); + }); + + it(`formats the ES response`, async () => { + const response = getMockMgetResponse([obj1, obj2]); + client.mget.mockResolvedValueOnce( + elasticsearchClientMock.createSuccessTransportRequestPromise(response) + ); + const result = await bulkGet([obj1, obj2]); + expect(client.mget).toHaveBeenCalledTimes(1); + expect(result).toEqual({ + saved_objects: [ + expectSuccessResult(obj1, response.docs[0]), + expectSuccessResult(obj2, response.docs[1]), + ], + }); + }); + + it(`handles a mix of successful gets and errors`, async () => { + const response = getMockMgetResponse([obj1, obj2]); + client.mget.mockResolvedValueOnce( + elasticsearchClientMock.createSuccessTransportRequestPromise(response) + ); + const obj: SavedObject = { + type: 'unknownType', + id: 'three', + attributes: {}, + references: [], + }; + const result = await bulkGet([obj1, obj, obj2]); + expect(client.mget).toHaveBeenCalledTimes(1); + expect(result).toEqual({ + saved_objects: [ + expectSuccessResult(obj1, response.docs[0]), + expectError(obj), + expectSuccessResult(obj2, response.docs[1]), + ], + }); + }); + + it(`includes namespaces property for single-namespace and multi-namespace documents`, async () => { + const obj: SavedObject = { + type: MULTI_NAMESPACE_ISOLATED_TYPE, + id: 'three', + attributes: {}, + references: [], + }; + const result = await bulkGetSuccess([obj1, obj]); + expect(result).toEqual({ + saved_objects: [ + expect.objectContaining({ namespaces: ['default'] }), + expect.objectContaining({ namespaces: expect.any(Array) }), + ], + }); + }); + }); + }); + + describe('#bulkResolve', () => { + afterEach(() => { + mockInternalBulkResolve.mockReset(); + }); + + it('passes arguments to the internalBulkResolve module and returns the expected results', async () => { + mockInternalBulkResolve.mockResolvedValue({ + resolved_objects: [ + { + saved_object: { type: 'mock', id: 'mock-object', attributes: {}, references: [] }, + outcome: 'exactMatch', + }, + { + type: 'obj-type', + id: 'obj-id-2', + error: SavedObjectsErrorHelpers.createGenericNotFoundError('obj-type', 'obj-id-2'), + }, + ], + }); + + const objects = [ + { type: 'obj-type', id: 'obj-id-1' }, + { type: 'obj-type', id: 'obj-id-2' }, + ]; + await expect(savedObjectsRepository.bulkResolve(objects)).resolves.toEqual({ + resolved_objects: [ + { + saved_object: { type: 'mock', id: 'mock-object', attributes: {}, references: [] }, + outcome: 'exactMatch', + }, + { + saved_object: { + type: 'obj-type', + id: 'obj-id-2', + error: { + error: 'Not Found', + message: 'Saved object [obj-type/obj-id-2] not found', + statusCode: 404, + }, + }, + outcome: 'exactMatch', + }, + ], + }); + expect(mockInternalBulkResolve).toHaveBeenCalledTimes(1); + expect(mockInternalBulkResolve).toHaveBeenCalledWith(expect.objectContaining({ objects })); + }); + + it('throws when internalBulkResolve throws', async () => { + const error = new Error('Oh no!'); + mockInternalBulkResolve.mockRejectedValue(error); + + await expect(savedObjectsRepository.resolve('some-type', 'some-id')).rejects.toEqual(error); + }); + }); + + describe('#bulkUpdate', () => { + const obj1: SavedObjectsBulkUpdateObject = { + type: 'config', + id: '6.0.0-alpha1', + attributes: { title: 'Test One' }, + }; + const obj2: SavedObjectsBulkUpdateObject = { + type: 'index-pattern', + id: 'logstash-*', + attributes: { title: 'Test Two' }, + }; + const references = [{ name: 'ref_0', type: 'test', id: '1' }]; + const originId = 'some-origin-id'; + const namespace = 'foo-namespace'; + + const getMockBulkUpdateResponse = ( + objects: TypeIdTuple[], + options?: SavedObjectsBulkUpdateOptions, + includeOriginId?: boolean + ) => + ({ + items: objects.map(({ type, id }) => ({ + update: { + _id: `${ + registry.isSingleNamespace(type) && options?.namespace ? `${options?.namespace}:` : '' + }${type}:${id}`, + ...mockVersionProps, + get: { + _source: { + // "includeOriginId" is not an option for the operation; however, if the existing saved object contains an originId attribute, the + // operation will return it in the result. This flag is just used for test purposes to modify the mock cluster call response. + ...(includeOriginId && { originId }), + }, + }, + result: 'updated', + }, + })), + } as estypes.BulkResponse); + + const bulkUpdateSuccess = async ( + objects: SavedObjectsBulkUpdateObject[], + options?: SavedObjectsBulkUpdateOptions, + includeOriginId?: boolean + ) => { + const multiNamespaceObjects = objects.filter(({ type }) => registry.isMultiNamespace(type)); + if (multiNamespaceObjects?.length) { + const response = getMockMgetResponse(multiNamespaceObjects, options?.namespace); + client.mget.mockResolvedValueOnce( + elasticsearchClientMock.createSuccessTransportRequestPromise(response) + ); + } + const response = getMockBulkUpdateResponse(objects, options, includeOriginId); + client.bulk.mockResolvedValueOnce( + elasticsearchClientMock.createSuccessTransportRequestPromise(response) + ); + const result = await savedObjectsRepository.bulkUpdate(objects, options); + expect(client.mget).toHaveBeenCalledTimes(multiNamespaceObjects?.length ? 1 : 0); + return result; + }; + + // bulk create calls have two objects for each source -- the action, and the source + const expectClientCallArgsAction = ( + objects: TypeIdTuple[], + { + method, + _index = expect.any(String), + getId = () => expect.any(String), + overrides = {}, + }: { + method: string; + _index?: string; + getId?: (type: string, id: string) => string; + overrides?: Record; + } + ) => { + const body = []; + for (const { type, id } of objects) { + body.push({ + [method]: { + _index, + _id: getId(type, id), + ...overrides, + }, + }); + body.push(expect.any(Object)); + } + expect(client.bulk).toHaveBeenCalledWith( + expect.objectContaining({ body }), + expect.anything() + ); + }; + + const expectObjArgs = ({ type, attributes }: { type: string; attributes: unknown }) => [ + expect.any(Object), + { + doc: expect.objectContaining({ + [type]: attributes, + ...mockTimestampFields, + }), + }, + ]; + + describe('client calls', () => { + it(`should use the ES bulk action by default`, async () => { + await bulkUpdateSuccess([obj1, obj2]); + expect(client.bulk).toHaveBeenCalled(); + }); + + it(`should use the ES mget action before bulk action for any types that are multi-namespace`, async () => { + const objects = [obj1, { ...obj2, type: MULTI_NAMESPACE_ISOLATED_TYPE }]; + await bulkUpdateSuccess(objects); + expect(client.bulk).toHaveBeenCalled(); + expect(client.mget).toHaveBeenCalled(); + + const docs = [ + expect.objectContaining({ _id: `${MULTI_NAMESPACE_ISOLATED_TYPE}:${obj2.id}` }), + ]; + expect(client.mget).toHaveBeenCalledWith( + expect.objectContaining({ body: { docs } }), + expect.anything() + ); + }); + + it(`formats the ES request`, async () => { + await bulkUpdateSuccess([obj1, obj2]); + const body = [...expectObjArgs(obj1), ...expectObjArgs(obj2)]; + expect(client.bulk).toHaveBeenCalledWith( + expect.objectContaining({ body }), + expect.anything() + ); + }); + + it(`formats the ES request for any types that are multi-namespace`, async () => { + const _obj2 = { ...obj2, type: MULTI_NAMESPACE_ISOLATED_TYPE }; + await bulkUpdateSuccess([obj1, _obj2]); + const body = [...expectObjArgs(obj1), ...expectObjArgs(_obj2)]; + expect(client.bulk).toHaveBeenCalledWith( + expect.objectContaining({ body }), + expect.anything() + ); + }); + + it(`doesnt call Elasticsearch if there are no valid objects to update`, async () => { + const objects = [obj1, obj2].map((x) => ({ ...x, type: 'unknownType' })); + await savedObjectsRepository.bulkUpdate(objects); + expect(client.bulk).toHaveBeenCalledTimes(0); + }); + + it(`defaults to no references`, async () => { + await bulkUpdateSuccess([obj1, obj2]); + const expected = { doc: expect.not.objectContaining({ references: expect.anything() }) }; + const body = [expect.any(Object), expected, expect.any(Object), expected]; + expect(client.bulk).toHaveBeenCalledWith( + expect.objectContaining({ body }), + expect.anything() + ); + }); + + it(`accepts custom references array`, async () => { + const test = async (references: SavedObjectReference[]) => { + const objects = [obj1, obj2].map((obj) => ({ ...obj, references })); + await bulkUpdateSuccess(objects); + const expected = { doc: expect.objectContaining({ references }) }; + const body = [expect.any(Object), expected, expect.any(Object), expected]; + expect(client.bulk).toHaveBeenCalledWith( + expect.objectContaining({ body }), + expect.anything() + ); + client.bulk.mockClear(); + }; + await test(references); + await test([{ type: 'type', id: 'id', name: 'some ref' }]); + await test([]); + }); + + it(`doesn't accept custom references if not an array`, async () => { + const test = async (references: unknown) => { + const objects = [obj1, obj2].map((obj) => ({ ...obj, references })); + // @ts-expect-error references is unknown + await bulkUpdateSuccess(objects); + const expected = { doc: expect.not.objectContaining({ references: expect.anything() }) }; + const body = [expect.any(Object), expected, expect.any(Object), expected]; + expect(client.bulk).toHaveBeenCalledWith( + expect.objectContaining({ body }), + expect.anything() + ); + client.bulk.mockClear(); + }; + await test('string'); + await test(123); + await test(true); + await test(null); + }); + + it(`defaults to a refresh setting of wait_for`, async () => { + await bulkUpdateSuccess([obj1, obj2]); + expect(client.bulk).toHaveBeenCalledWith( + expect.objectContaining({ refresh: 'wait_for' }), + expect.anything() + ); + }); + + it(`defaults to the version of the existing document for multi-namespace types`, async () => { + // only multi-namespace documents are obtained using a pre-flight mget request + const objects = [ + { ...obj1, type: MULTI_NAMESPACE_ISOLATED_TYPE }, + { ...obj2, type: MULTI_NAMESPACE_ISOLATED_TYPE }, + ]; + await bulkUpdateSuccess(objects); + const overrides = { + if_seq_no: mockVersionProps._seq_no, + if_primary_term: mockVersionProps._primary_term, + }; + expectClientCallArgsAction(objects, { method: 'update', overrides }); + }); + + it(`defaults to no version for types that are not multi-namespace`, async () => { + const objects = [obj1, { ...obj2, type: NAMESPACE_AGNOSTIC_TYPE }]; + await bulkUpdateSuccess(objects); + expectClientCallArgsAction(objects, { method: 'update' }); + }); + + it(`accepts version`, async () => { + const version = encodeHitVersion({ _seq_no: 100, _primary_term: 200 }); + // test with both non-multi-namespace and multi-namespace types + const objects = [ + { ...obj1, version }, + { ...obj2, type: MULTI_NAMESPACE_ISOLATED_TYPE, version }, + ]; + await bulkUpdateSuccess(objects); + const overrides = { if_seq_no: 100, if_primary_term: 200 }; + expectClientCallArgsAction(objects, { method: 'update', overrides }); + }); + + it(`prepends namespace to the id when providing namespace for single-namespace type`, async () => { + const getId = (type: string, id: string) => `${namespace}:${type}:${id}`; // test that the raw document ID equals this (e.g., has a namespace prefix) + await bulkUpdateSuccess([obj1, obj2], { namespace }); + expectClientCallArgsAction([obj1, obj2], { method: 'update', getId }); + + jest.clearAllMocks(); + // test again with object namespace string that supersedes the operation's namespace ID + await bulkUpdateSuccess([ + { ...obj1, namespace }, + { ...obj2, namespace }, + ]); + expectClientCallArgsAction([obj1, obj2], { method: 'update', getId }); + }); + + it(`doesn't prepend namespace to the id when providing no namespace for single-namespace type`, async () => { + const getId = (type: string, id: string) => `${type}:${id}`; // test that the raw document ID equals this (e.g., does not have a namespace prefix) + await bulkUpdateSuccess([obj1, obj2]); + expectClientCallArgsAction([obj1, obj2], { method: 'update', getId }); + + jest.clearAllMocks(); + // test again with object namespace string that supersedes the operation's namespace ID + await bulkUpdateSuccess( + [ + { ...obj1, namespace: 'default' }, + { ...obj2, namespace: 'default' }, + ], + { namespace } + ); + expectClientCallArgsAction([obj1, obj2], { method: 'update', getId }); + }); + + it(`normalizes options.namespace from 'default' to undefined`, async () => { + const getId = (type: string, id: string) => `${type}:${id}`; + await bulkUpdateSuccess([obj1, obj2], { namespace: 'default' }); + expectClientCallArgsAction([obj1, obj2], { method: 'update', getId }); + }); + + it(`doesn't prepend namespace to the id when not using single-namespace type`, async () => { + const getId = (type: string, id: string) => `${type}:${id}`; // test that the raw document ID equals this (e.g., does not have a namespace prefix) + const overrides = { + // bulkUpdate uses a preflight `get` request for multi-namespace saved objects, and specifies that version on `update` + // we aren't testing for this here, but we need to include Jest assertions so this test doesn't fail + if_primary_term: expect.any(Number), + if_seq_no: expect.any(Number), + }; + const _obj1 = { ...obj1, type: NAMESPACE_AGNOSTIC_TYPE }; + const _obj2 = { ...obj2, type: MULTI_NAMESPACE_ISOLATED_TYPE }; + + await bulkUpdateSuccess([_obj1], { namespace }); + expectClientCallArgsAction([_obj1], { method: 'update', getId }); + client.bulk.mockClear(); + await bulkUpdateSuccess([_obj2], { namespace }); + expectClientCallArgsAction([_obj2], { method: 'update', getId, overrides }); + + jest.clearAllMocks(); + // test again with object namespace string that supersedes the operation's namespace ID + await bulkUpdateSuccess([{ ..._obj1, namespace }]); + expectClientCallArgsAction([_obj1], { method: 'update', getId }); + client.bulk.mockClear(); + await bulkUpdateSuccess([{ ..._obj2, namespace }]); + expectClientCallArgsAction([_obj2], { method: 'update', getId, overrides }); + }); + }); + + describe('errors', () => { + afterEach(() => { + mockGetBulkOperationError.mockReset(); + }); + + const obj: SavedObjectsBulkUpdateObject = { + type: 'dashboard', + id: 'three', + attributes: {}, + }; + + const bulkUpdateError = async ( + obj: SavedObjectsBulkUpdateObject, + isBulkError: boolean, + expectedErrorResult: ExpectedErrorResult + ) => { + const objects = [obj1, obj, obj2]; + const mockResponse = getMockBulkUpdateResponse(objects); + if (isBulkError) { + // mock the bulk error for only the second object + mockGetBulkOperationError.mockReturnValueOnce(undefined); + mockGetBulkOperationError.mockReturnValueOnce(expectedErrorResult.error as Payload); + } + client.bulk.mockResolvedValueOnce( + elasticsearchClientMock.createSuccessTransportRequestPromise(mockResponse) + ); + + const result = await savedObjectsRepository.bulkUpdate(objects); + expect(client.bulk).toHaveBeenCalled(); + const objCall = isBulkError ? expectObjArgs(obj) : []; + const body = [...expectObjArgs(obj1), ...objCall, ...expectObjArgs(obj2)]; + expect(client.bulk).toHaveBeenCalledWith( + expect.objectContaining({ body }), + expect.anything() + ); + expect(result).toEqual({ + saved_objects: [expectSuccess(obj1), expectedErrorResult, expectSuccess(obj2)], + }); + }; + + const bulkUpdateMultiError = async ( + [obj1, _obj, obj2]: SavedObjectsBulkUpdateObject[], + options: SavedObjectsBulkUpdateOptions | undefined, + mgetResponse: estypes.MgetResponse, + mgetOptions?: { statusCode?: number } + ) => { + client.mget.mockResolvedValueOnce( + elasticsearchClientMock.createSuccessTransportRequestPromise(mgetResponse, { + statusCode: mgetOptions?.statusCode, + }) + ); + + const bulkResponse = getMockBulkUpdateResponse([obj1, obj2], { namespace }); + client.bulk.mockResolvedValueOnce( + elasticsearchClientMock.createSuccessTransportRequestPromise(bulkResponse) + ); + + const result = await savedObjectsRepository.bulkUpdate([obj1, _obj, obj2], options); + expect(client.bulk).toHaveBeenCalled(); + expect(client.mget).toHaveBeenCalled(); + const body = [...expectObjArgs(obj1), ...expectObjArgs(obj2)]; + expect(client.bulk).toHaveBeenCalledWith( + expect.objectContaining({ body }), + expect.anything() + ); + + expect(result).toEqual({ + saved_objects: [expectSuccess(obj1), expectErrorNotFound(_obj), expectSuccess(obj2)], + }); + }; + + it(`throws when options.namespace is '*'`, async () => { + await expect( + savedObjectsRepository.bulkUpdate([obj], { namespace: ALL_NAMESPACES_STRING }) + ).rejects.toThrowError(createBadRequestError('"options.namespace" cannot be "*"')); + }); + + it(`returns error when type is invalid`, async () => { + const _obj = { ...obj, type: 'unknownType' }; + await bulkUpdateError(_obj, false, expectErrorNotFound(_obj)); + }); + + it(`returns error when type is hidden`, async () => { + const _obj = { ...obj, type: HIDDEN_TYPE }; + await bulkUpdateError(_obj, false, expectErrorNotFound(_obj)); + }); + + it(`returns error when object namespace is '*'`, async () => { + const _obj = { ...obj, namespace: '*' }; + await bulkUpdateError( + _obj, + false, + expectErrorResult(obj, createBadRequestError('"namespace" cannot be "*"')) + ); + }); + + it(`returns error when ES is unable to find the document (mget)`, async () => { + const _obj = { ...obj, type: MULTI_NAMESPACE_ISOLATED_TYPE, found: false }; + const mgetResponse = getMockMgetResponse([_obj]); + await bulkUpdateMultiError([obj1, _obj, obj2], undefined, mgetResponse); + }); + + it(`returns error when ES is unable to find the index (mget)`, async () => { + const _obj = { ...obj, type: MULTI_NAMESPACE_ISOLATED_TYPE }; + await bulkUpdateMultiError([obj1, _obj, obj2], { namespace }, {} as estypes.MgetResponse, { + statusCode: 404, + }); + }); + + it(`returns error when there is a conflict with an existing multi-namespace saved object (mget)`, async () => { + const _obj = { ...obj, type: MULTI_NAMESPACE_ISOLATED_TYPE }; + const mgetResponse = getMockMgetResponse([_obj], 'bar-namespace'); + await bulkUpdateMultiError([obj1, _obj, obj2], { namespace }, mgetResponse); + }); + + it(`returns bulk error`, async () => { + const expectedErrorResult = { + type: obj.type, + id: obj.id, + error: { message: 'Oh no, a bulk error!' }, + }; + await bulkUpdateError(obj, true, expectedErrorResult); + }); + }); + + describe('returns', () => { + const expectSuccessResult = ({ + type, + id, + attributes, + references, + }: SavedObjectsBulkUpdateObject) => ({ + type, + id, + attributes, + references, + version: mockVersion, + namespaces: ['default'], + ...mockTimestampFields, + }); + + it(`formats the ES response`, async () => { + const response = await bulkUpdateSuccess([obj1, obj2]); + expect(response).toEqual({ + saved_objects: [obj1, obj2].map(expectSuccessResult), + }); + }); + + it(`includes references`, async () => { + const objects = [obj1, obj2].map((obj) => ({ ...obj, references })); + const response = await bulkUpdateSuccess(objects); + expect(response).toEqual({ + saved_objects: objects.map(expectSuccessResult), + }); + }); + + it(`handles a mix of successful updates and errors`, async () => { + const obj: SavedObjectsBulkUpdateObject = { + type: 'unknownType', + id: 'three', + attributes: {}, + }; + const objects = [obj1, obj, obj2]; + const mockResponse = getMockBulkUpdateResponse(objects); + client.bulk.mockResolvedValueOnce( + elasticsearchClientMock.createSuccessTransportRequestPromise(mockResponse) + ); + + const result = await savedObjectsRepository.bulkUpdate(objects); + expect(client.bulk).toHaveBeenCalledTimes(1); + expect(result).toEqual({ + saved_objects: [expectSuccessResult(obj1), expectError(obj), expectSuccessResult(obj2)], + }); + }); + + it(`includes namespaces property for single-namespace and multi-namespace documents`, async () => { + const obj: SavedObjectsBulkUpdateObject = { + type: MULTI_NAMESPACE_ISOLATED_TYPE, + id: 'three', + attributes: {}, + }; + const result = await bulkUpdateSuccess([obj1, obj]); + expect(result).toEqual({ + saved_objects: [ + expect.objectContaining({ namespaces: expect.any(Array) }), + expect.objectContaining({ namespaces: expect.any(Array) }), + ], + }); + }); + + it(`includes originId property if present in cluster call response`, async () => { + const obj: SavedObjectsBulkUpdateObject = { + type: MULTI_NAMESPACE_ISOLATED_TYPE, + id: 'three', + attributes: {}, + }; + const result = await bulkUpdateSuccess([obj1, obj], {}, true); + expect(result).toEqual({ + saved_objects: [ + expect.objectContaining({ originId }), + expect.objectContaining({ originId }), + ], + }); + }); + }); + }); + + describe('#checkConflicts', () => { + const obj1 = { type: 'dashboard', id: 'one' }; + const obj2 = { type: 'dashboard', id: 'two' }; + const obj3 = { type: MULTI_NAMESPACE_ISOLATED_TYPE, id: 'three' }; + const obj4 = { type: MULTI_NAMESPACE_ISOLATED_TYPE, id: 'four' }; + const obj5 = { type: MULTI_NAMESPACE_ISOLATED_TYPE, id: 'five' }; + const obj6 = { type: NAMESPACE_AGNOSTIC_TYPE, id: 'six' }; + const obj7 = { type: NAMESPACE_AGNOSTIC_TYPE, id: 'seven' }; + const namespace = 'foo-namespace'; + + const checkConflicts = async (objects: TypeIdTuple[], options?: SavedObjectsBaseOptions) => + savedObjectsRepository.checkConflicts( + objects.map(({ type, id }) => ({ type, id })), // checkConflicts only uses type and id + options + ); + const checkConflictsSuccess = async ( + objects: TypeIdTuple[], + options?: SavedObjectsBaseOptions + ) => { + const response = getMockMgetResponse(objects, options?.namespace); + client.mget.mockResolvedValue( + elasticsearchClientMock.createSuccessTransportRequestPromise(response) + ); + const result = await checkConflicts(objects, options); + expect(client.mget).toHaveBeenCalledTimes(1); + return result; + }; + + const _expectClientCallArgs = ( + objects: TypeIdTuple[], + { + _index = expect.any(String), + getId = () => expect.any(String), + }: { _index?: string; getId?: (type: string, id: string) => string } + ) => { + expect(client.mget).toHaveBeenCalledWith( + expect.objectContaining({ + body: { + docs: objects.map(({ type, id }) => + expect.objectContaining({ + _index, + _id: getId(type, id), + }) + ), + }, + }), + expect.anything() + ); + }; + + describe('client calls', () => { + it(`doesn't make a cluster call if the objects array is empty`, async () => { + await checkConflicts([]); + expect(client.mget).not.toHaveBeenCalled(); + }); + + it(`prepends namespace to the id when providing namespace for single-namespace type`, async () => { + const getId = (type: string, id: string) => `${namespace}:${type}:${id}`; // test that the raw document ID equals this (e.g., has a namespace prefix) + await checkConflictsSuccess([obj1, obj2], { namespace }); + _expectClientCallArgs([obj1, obj2], { getId }); + }); + + it(`doesn't prepend namespace to the id when providing no namespace for single-namespace type`, async () => { + const getId = (type: string, id: string) => `${type}:${id}`; // test that the raw document ID equals this (e.g., does not have a namespace prefix) + await checkConflictsSuccess([obj1, obj2]); + _expectClientCallArgs([obj1, obj2], { getId }); + }); + + it(`normalizes options.namespace from 'default' to undefined`, async () => { + const getId = (type: string, id: string) => `${type}:${id}`; // test that the raw document ID equals this (e.g., does not have a namespace prefix) + await checkConflictsSuccess([obj1, obj2], { namespace: 'default' }); + _expectClientCallArgs([obj1, obj2], { getId }); + }); + + it(`doesn't prepend namespace to the id when not using single-namespace type`, async () => { + const getId = (type: string, id: string) => `${type}:${id}`; // test that the raw document ID equals this (e.g., does not have a namespace prefix) + // obj3 is multi-namespace, and obj6 is namespace-agnostic + await checkConflictsSuccess([obj3, obj6], { namespace }); + _expectClientCallArgs([obj3, obj6], { getId }); + }); + }); + + describe('errors', () => { + it(`throws when options.namespace is '*'`, async () => { + await expect( + savedObjectsRepository.checkConflicts([obj1], { namespace: ALL_NAMESPACES_STRING }) + ).rejects.toThrowError(createBadRequestError('"options.namespace" cannot be "*"')); + }); + }); + + describe('returns', () => { + it(`expected results`, async () => { + const unknownTypeObj = { type: 'unknownType', id: 'three' }; + const hiddenTypeObj = { type: HIDDEN_TYPE, id: 'three' }; + const objects = [unknownTypeObj, hiddenTypeObj, obj1, obj2, obj3, obj4, obj5, obj6, obj7]; + const response = { + docs: [ + getMockGetResponse(obj1), + { found: false }, + getMockGetResponse(obj3), + getMockGetResponse({ ...obj4, namespace: 'bar-namespace' }), + { found: false }, + getMockGetResponse(obj6), + { found: false }, + ], + } as estypes.MgetResponse; + client.mget.mockResolvedValue( + elasticsearchClientMock.createSuccessTransportRequestPromise(response) + ); + + const result = await checkConflicts(objects); + expect(client.mget).toHaveBeenCalledTimes(1); + expect(result).toEqual({ + errors: [ + { ...unknownTypeObj, error: createUnsupportedTypeError(unknownTypeObj.type) }, + { ...hiddenTypeObj, error: createUnsupportedTypeError(hiddenTypeObj.type) }, + { ...obj1, error: createConflictError(obj1.type, obj1.id) }, + // obj2 was not found so it does not result in a conflict error + { ...obj3, error: createConflictError(obj3.type, obj3.id) }, + { + ...obj4, + error: { + ...createConflictError(obj4.type, obj4.id), + metadata: { isNotOverwritable: true }, + }, + }, + // obj5 was not found so it does not result in a conflict error + { ...obj6, error: createConflictError(obj6.type, obj6.id) }, + // obj7 was not found so it does not result in a conflict error + ], + }); + }); + }); + }); + + describe('#create', () => { + beforeEach(() => { + mockPreflightCheckForCreate.mockReset(); + mockPreflightCheckForCreate.mockImplementation(({ objects }) => { + return Promise.resolve(objects.map(({ type, id }) => ({ type, id }))); // respond with no errors by default + }); + client.create.mockImplementation((params) => + elasticsearchClientMock.createSuccessTransportRequestPromise({ + _id: params.id, + ...mockVersionProps, + } as estypes.CreateResponse) + ); + }); + + const type = 'index-pattern'; + const attributes = { title: 'Logstash' }; + const id = 'logstash-*'; + const namespace = 'foo-namespace'; + const originId = 'some-origin-id'; + const references = [ + { + name: 'ref_0', + type: 'test', + id: '123', + }, + ]; + + const createSuccess = async ( + type: string, + attributes: T, + options?: SavedObjectsCreateOptions + ) => { + return await savedObjectsRepository.create(type, attributes, options); + }; + + describe('client calls', () => { + it(`should use the ES index action if ID is not defined and overwrite=true`, async () => { + await createSuccess(type, attributes, { overwrite: true }); + expect(mockPreflightCheckForCreate).not.toHaveBeenCalled(); + expect(client.index).toHaveBeenCalled(); + }); + + it(`should use the ES create action if ID is not defined and overwrite=false`, async () => { + await createSuccess(type, attributes); + expect(mockPreflightCheckForCreate).not.toHaveBeenCalled(); + expect(client.create).toHaveBeenCalled(); + }); + + it(`should use the ES index with version if ID and version are defined and overwrite=true`, async () => { + await createSuccess(type, attributes, { id, overwrite: true, version: mockVersion }); + expect(mockPreflightCheckForCreate).not.toHaveBeenCalled(); + expect(client.index).toHaveBeenCalled(); + expect(client.index.mock.calls[0][0]).toMatchObject({ + if_seq_no: mockVersionProps._seq_no, + if_primary_term: mockVersionProps._primary_term, + }); + }); + + it(`should use the ES create action if ID is defined and overwrite=false`, async () => { + await createSuccess(type, attributes, { id }); + expect(mockPreflightCheckForCreate).not.toHaveBeenCalled(); + expect(client.create).toHaveBeenCalled(); + }); + + it(`should use the preflightCheckForCreate action then create action if type is multi-namespace, ID is defined, and overwrite=false`, async () => { + await createSuccess(MULTI_NAMESPACE_TYPE, attributes, { id }); + expect(mockPreflightCheckForCreate).toHaveBeenCalled(); + expect(mockPreflightCheckForCreate).toHaveBeenCalledWith( + expect.objectContaining({ + objects: [ + { type: MULTI_NAMESPACE_TYPE, id, overwrite: false, namespaces: ['default'] }, + ], + }) + ); + expect(client.create).toHaveBeenCalled(); + }); + + it(`should use the preflightCheckForCreate action then index action if type is multi-namespace, ID is defined, and overwrite=true`, async () => { + await createSuccess(MULTI_NAMESPACE_ISOLATED_TYPE, attributes, { id, overwrite: true }); + expect(mockPreflightCheckForCreate).toHaveBeenCalled(); + expect(mockPreflightCheckForCreate).toHaveBeenCalledWith( + expect.objectContaining({ + objects: [ + { type: MULTI_NAMESPACE_ISOLATED_TYPE, id, overwrite: true, namespaces: ['default'] }, + ], + }) + ); + expect(client.index).toHaveBeenCalled(); + }); + + it(`defaults to empty references array`, async () => { + await createSuccess(type, attributes, { id }); + expect( + (client.create.mock.calls[0][0] as estypes.CreateRequest).body! + .references + ).toEqual([]); + }); + + it(`accepts custom references array`, async () => { + const test = async (references: SavedObjectReference[]) => { + await createSuccess(type, attributes, { id, references }); + expect( + (client.create.mock.calls[0][0] as estypes.CreateRequest) + .body!.references + ).toEqual(references); + client.create.mockClear(); + }; + await test(references); + await test([{ type: 'type', id: 'id', name: 'some ref' }]); + await test([]); + }); + + it(`doesn't accept custom references if not an array`, async () => { + const test = async (references: unknown) => { + // @ts-expect-error references is unknown + await createSuccess(type, attributes, { id, references }); + expect( + (client.create.mock.calls[0][0] as estypes.CreateRequest) + .body!.references + ).not.toBeDefined(); + client.create.mockClear(); + }; + await test('string'); + await test(123); + await test(true); + await test(null); + }); + + it(`defaults to no originId`, async () => { + await createSuccess(type, attributes, { id }); + expect(client.create).toHaveBeenCalledWith( + expect.objectContaining({ + body: expect.not.objectContaining({ originId: expect.anything() }), + }), + expect.anything() + ); + }); + + it(`accepts custom originId`, async () => { + await createSuccess(type, attributes, { id, originId }); + expect(client.create).toHaveBeenCalledWith( + expect.objectContaining({ + body: expect.objectContaining({ originId }), + }), + expect.anything() + ); + }); + + it(`defaults to a refresh setting of wait_for`, async () => { + await createSuccess(type, attributes); + expect(client.create).toHaveBeenCalledWith( + expect.objectContaining({ refresh: 'wait_for' }), + expect.anything() + ); + }); + + it(`should use default index`, async () => { + await createSuccess(type, attributes, { id }); + expect(client.create).toHaveBeenCalledWith( + expect.objectContaining({ index: '.kibana-test_8.0.0-testing' }), + expect.anything() + ); + }); + + it(`should use custom index`, async () => { + await createSuccess(CUSTOM_INDEX_TYPE, attributes, { id }); + expect(client.create).toHaveBeenCalledWith( + expect.objectContaining({ index: 'custom_8.0.0-testing' }), + expect.anything() + ); + }); + + it(`self-generates an id if none is provided`, async () => { + await createSuccess(type, attributes); + expect(client.create).toHaveBeenCalledWith( + expect.objectContaining({ + id: expect.objectContaining(/index-pattern:[0-9a-f]{8}-([0-9a-f]{4}-){3}[0-9a-f]{12}/), + }), + expect.anything() + ); + }); + + it(`prepends namespace to the id and adds namespace to the body when providing namespace for single-namespace type`, async () => { + await createSuccess(type, attributes, { id, namespace }); + expect(client.create).toHaveBeenCalledWith( + expect.objectContaining({ + id: `${namespace}:${type}:${id}`, + body: expect.objectContaining({ namespace }), + }), + expect.anything() + ); + }); + + it(`doesn't prepend namespace to the id or add namespace to the body when providing no namespace for single-namespace type`, async () => { + await createSuccess(type, attributes, { id }); + expect(client.create).toHaveBeenCalledWith( + expect.objectContaining({ + id: `${type}:${id}`, + body: expect.not.objectContaining({ namespace: expect.anything() }), + }), + expect.anything() + ); + }); + + it(`normalizes options.namespace from 'default' to undefined`, async () => { + await createSuccess(type, attributes, { id, namespace: 'default' }); + expect(client.create).toHaveBeenCalledWith( + expect.objectContaining({ + id: `${type}:${id}`, + body: expect.not.objectContaining({ namespace: expect.anything() }), + }), + expect.anything() + ); + }); + + it(`doesn't prepend namespace to the id and adds namespaces to body when using multi-namespace type`, async () => { + // first object does not have an existing document to overwrite + await createSuccess(MULTI_NAMESPACE_TYPE, attributes, { id, namespace }); + mockPreflightCheckForCreate.mockResolvedValueOnce([ + { + type: MULTI_NAMESPACE_TYPE, + id, + existingDocument: { + _id: id, + _source: { type: MULTI_NAMESPACE_TYPE, namespaces: ['*'] }, + }, // second object does have an existing document to overwrite + }, + ]); + await createSuccess(MULTI_NAMESPACE_ISOLATED_TYPE, attributes, { + id, + namespace, + overwrite: true, + }); + + expect(mockPreflightCheckForCreate).toHaveBeenCalledTimes(2); + expect(mockPreflightCheckForCreate).toHaveBeenNthCalledWith( + 1, + expect.objectContaining({ + objects: [ + { type: MULTI_NAMESPACE_TYPE, id, overwrite: false, namespaces: [namespace] }, + ], + }) + ); + expect(mockPreflightCheckForCreate).toHaveBeenNthCalledWith( + 2, + expect.objectContaining({ + objects: [ + { type: MULTI_NAMESPACE_ISOLATED_TYPE, id, overwrite: true, namespaces: [namespace] }, + ], + }) + ); + + expect(client.create).toHaveBeenCalledTimes(1); + expect(client.create).toHaveBeenCalledWith( + expect.objectContaining({ + id: `${MULTI_NAMESPACE_TYPE}:${id}`, + body: expect.objectContaining({ namespaces: [namespace] }), + }), + expect.anything() + ); + expect(client.index).toHaveBeenCalledTimes(1); + expect(client.index).toHaveBeenCalledWith( + expect.objectContaining({ + id: `${MULTI_NAMESPACE_ISOLATED_TYPE}:${id}`, + body: expect.objectContaining({ namespaces: ['*'] }), + }), + expect.anything() + ); + }); + + it(`adds initialNamespaces instead of namespace`, async () => { + const ns2 = 'bar-namespace'; + const ns3 = 'baz-namespace'; + // first object does not get passed in to preflightCheckForCreate at all + await savedObjectsRepository.create('dashboard', attributes, { + id, + namespace, + initialNamespaces: [ns2], + }); + // second object does not have an existing document to overwrite + await savedObjectsRepository.create(MULTI_NAMESPACE_TYPE, attributes, { + id, + namespace, + initialNamespaces: [ns2, ns3], + }); + mockPreflightCheckForCreate.mockResolvedValueOnce([ + { + type: MULTI_NAMESPACE_ISOLATED_TYPE, + id, + existingDocument: { + _id: id, + _source: { type: MULTI_NAMESPACE_ISOLATED_TYPE, namespaces: ['something-else'] }, + }, // third object does have an existing document to overwrite + }, + ]); + await savedObjectsRepository.create(MULTI_NAMESPACE_ISOLATED_TYPE, attributes, { + id, + namespace, + initialNamespaces: [ns2], + overwrite: true, + }); + + expect(mockPreflightCheckForCreate).toHaveBeenCalledTimes(2); + expect(mockPreflightCheckForCreate).toHaveBeenNthCalledWith( + 1, + expect.objectContaining({ + objects: [{ type: MULTI_NAMESPACE_TYPE, id, overwrite: false, namespaces: [ns2, ns3] }], + }) + ); + expect(mockPreflightCheckForCreate).toHaveBeenNthCalledWith( + 2, + expect.objectContaining({ + objects: [ + { type: MULTI_NAMESPACE_ISOLATED_TYPE, id, overwrite: true, namespaces: [ns2] }, + ], + }) + ); + + expect(client.create).toHaveBeenCalledTimes(2); + expect(client.create).toHaveBeenNthCalledWith( + 1, + expect.objectContaining({ + id: `${ns2}:dashboard:${id}`, + body: expect.objectContaining({ namespace: ns2 }), + }), + expect.anything() + ); + expect(client.create).toHaveBeenNthCalledWith( + 2, + expect.objectContaining({ + id: `${MULTI_NAMESPACE_TYPE}:${id}`, + body: expect.objectContaining({ namespaces: [ns2, ns3] }), + }), + expect.anything() + ); + expect(client.index).toHaveBeenCalledTimes(1); + expect(client.index).toHaveBeenCalledWith( + expect.objectContaining({ + id: `${MULTI_NAMESPACE_ISOLATED_TYPE}:${id}`, + body: expect.objectContaining({ namespaces: [ns2] }), + }), + expect.anything() + ); + }); + + it(`normalizes initialNamespaces from 'default' to undefined`, async () => { + await savedObjectsRepository.create('dashboard', attributes, { + id, + namespace, + initialNamespaces: ['default'], + }); + + expect(client.create).toHaveBeenCalledTimes(1); + expect(client.create).toHaveBeenNthCalledWith( + 1, + expect.objectContaining({ + id: `dashboard:${id}`, + body: expect.not.objectContaining({ namespace: 'default' }), + }), + expect.anything() + ); + }); + + it(`doesn't prepend namespace to the id or add namespace or namespaces fields when using namespace-agnostic type`, async () => { + await createSuccess(NAMESPACE_AGNOSTIC_TYPE, attributes, { id, namespace }); + expect(client.create).toHaveBeenCalledWith( + expect.objectContaining({ + id: `${NAMESPACE_AGNOSTIC_TYPE}:${id}`, + body: expect.not.objectContaining({ + namespace: expect.anything(), + namespaces: expect.anything(), + }), + }), + expect.anything() + ); + }); + }); + + describe('errors', () => { + it(`throws when options.initialNamespaces is used with a space-agnostic object`, async () => { + await expect( + savedObjectsRepository.create(NAMESPACE_AGNOSTIC_TYPE, attributes, { + initialNamespaces: [namespace], + }) + ).rejects.toThrowError( + createBadRequestError('"initialNamespaces" cannot be used on space-agnostic types') + ); + }); + + it(`throws when options.initialNamespaces is empty`, async () => { + await expect( + savedObjectsRepository.create(MULTI_NAMESPACE_TYPE, attributes, { initialNamespaces: [] }) + ).rejects.toThrowError( + createBadRequestError('"initialNamespaces" must be a non-empty array of strings') + ); + }); + + it(`throws when options.initialNamespaces is used with a space-isolated object and does not specify a single space`, async () => { + const doTest = async (objType: string, initialNamespaces?: string[]) => { + await expect( + savedObjectsRepository.create(objType, attributes, { initialNamespaces }) + ).rejects.toThrowError( + createBadRequestError( + '"initialNamespaces" can only specify a single space when used with space-isolated types' + ) + ); + }; + await doTest('dashboard', ['spacex', 'spacey']); + await doTest('dashboard', ['*']); + await doTest(MULTI_NAMESPACE_ISOLATED_TYPE, ['spacex', 'spacey']); + await doTest(MULTI_NAMESPACE_ISOLATED_TYPE, ['*']); + }); + + it(`throws when options.namespace is '*'`, async () => { + await expect( + savedObjectsRepository.create(type, attributes, { namespace: ALL_NAMESPACES_STRING }) + ).rejects.toThrowError(createBadRequestError('"options.namespace" cannot be "*"')); + }); + + it(`throws when type is invalid`, async () => { + await expect(savedObjectsRepository.create('unknownType', attributes)).rejects.toThrowError( + createUnsupportedTypeError('unknownType') + ); + expect(client.create).not.toHaveBeenCalled(); + }); + + it(`throws when type is hidden`, async () => { + await expect(savedObjectsRepository.create(HIDDEN_TYPE, attributes)).rejects.toThrowError( + createUnsupportedTypeError(HIDDEN_TYPE) + ); + expect(client.create).not.toHaveBeenCalled(); + }); + + it(`throws when there is a conflict from preflightCheckForCreate`, async () => { + mockPreflightCheckForCreate.mockResolvedValueOnce([ + { type: MULTI_NAMESPACE_ISOLATED_TYPE, id, error: { type: 'unresolvableConflict' } }, // error type and metadata dont matter + ]); + await expect( + savedObjectsRepository.create(MULTI_NAMESPACE_ISOLATED_TYPE, attributes, { + id, + overwrite: true, + namespace, + }) + ).rejects.toThrowError(createConflictError(MULTI_NAMESPACE_ISOLATED_TYPE, id)); + expect(mockPreflightCheckForCreate).toHaveBeenCalled(); + }); + + it.todo(`throws when automatic index creation fails`); + + it.todo(`throws when an unexpected failure occurs`); + }); + + describe('migration', () => { + beforeEach(() => { + migrator.migrateDocument.mockImplementation(mockMigrateDocument); + }); + + it(`migrates a document and serializes the migrated doc`, async () => { + const migrationVersion = mockMigrationVersion; + await createSuccess(type, attributes, { id, references, migrationVersion }); + const doc = { type, id, attributes, references, migrationVersion, ...mockTimestampFields }; + expectMigrationArgs(doc); + + const migratedDoc = migrator.migrateDocument(doc); + expect(serializer.savedObjectToRaw).toHaveBeenLastCalledWith(migratedDoc); + }); + + it(`adds namespace to body when providing namespace for single-namespace type`, async () => { + await createSuccess(type, attributes, { id, namespace }); + expectMigrationArgs({ namespace }); + }); + + it(`doesn't add namespace to body when providing no namespace for single-namespace type`, async () => { + await createSuccess(type, attributes, { id }); + expectMigrationArgs({ namespace: expect.anything() }, false); + }); + + it(`doesn't add namespace to body when not using single-namespace type`, async () => { + await createSuccess(NAMESPACE_AGNOSTIC_TYPE, attributes, { id, namespace }); + expectMigrationArgs({ namespace: expect.anything() }, false, 1); + + client.create.mockClear(); + await createSuccess(MULTI_NAMESPACE_ISOLATED_TYPE, attributes, { id }); + expectMigrationArgs({ namespace: expect.anything() }, false, 2); + }); + + it(`adds namespaces to body when providing namespace for multi-namespace type`, async () => { + await createSuccess(MULTI_NAMESPACE_ISOLATED_TYPE, attributes, { id, namespace }); + expectMigrationArgs({ namespaces: [namespace] }); + }); + + it(`adds default namespaces to body when providing no namespace for multi-namespace type`, async () => { + await createSuccess(MULTI_NAMESPACE_ISOLATED_TYPE, attributes, { id }); + expectMigrationArgs({ namespaces: ['default'] }); + }); + + it(`doesn't add namespaces to body when not using multi-namespace type`, async () => { + await createSuccess(type, attributes, { id }); + expectMigrationArgs({ namespaces: expect.anything() }, false, 1); + + client.create.mockClear(); + await createSuccess(NAMESPACE_AGNOSTIC_TYPE, attributes, { id }); + expectMigrationArgs({ namespaces: expect.anything() }, false, 2); + }); + }); + + describe('returns', () => { + it(`formats the ES response`, async () => { + const result = await createSuccess(type, attributes, { + id, + namespace, + references, + originId, + }); + expect(result).toEqual({ + type, + id, + originId, + ...mockTimestampFields, + version: mockVersion, + attributes, + references, + namespaces: [namespace ?? 'default'], + migrationVersion: { [type]: '1.1.1' }, + coreMigrationVersion: KIBANA_VERSION, + }); + }); + }); + }); + + describe('#delete', () => { + const type = 'index-pattern'; + const id = 'logstash-*'; + const namespace = 'foo-namespace'; + + const deleteSuccess = async ( + type: string, + id: string, + options?: SavedObjectsDeleteOptions, + internalOptions: { mockGetResponseValue?: estypes.GetResponse } = {} + ) => { + const { mockGetResponseValue } = internalOptions; + if (registry.isMultiNamespace(type)) { + const mockGetResponse = + mockGetResponseValue ?? getMockGetResponse({ type, id }, options?.namespace); + client.get.mockResolvedValueOnce( + elasticsearchClientMock.createSuccessTransportRequestPromise(mockGetResponse) + ); + } + client.delete.mockResolvedValueOnce( + elasticsearchClientMock.createSuccessTransportRequestPromise({ + result: 'deleted', + } as estypes.DeleteResponse) + ); + const result = await savedObjectsRepository.delete(type, id, options); + expect(client.get).toHaveBeenCalledTimes(registry.isMultiNamespace(type) ? 1 : 0); + return result; + }; + + beforeEach(() => { + mockDeleteLegacyUrlAliases.mockClear(); + mockDeleteLegacyUrlAliases.mockResolvedValue(); + }); + + describe('client calls', () => { + it(`should use the ES delete action when not using a multi-namespace type`, async () => { + await deleteSuccess(type, id); + expect(client.get).not.toHaveBeenCalled(); + expect(client.delete).toHaveBeenCalledTimes(1); + }); + + it(`should use ES get action then delete action when using a multi-namespace type`, async () => { + await deleteSuccess(MULTI_NAMESPACE_ISOLATED_TYPE, id); + expect(client.get).toHaveBeenCalledTimes(1); + expect(client.delete).toHaveBeenCalledTimes(1); + }); + + it(`includes the version of the existing document when using a multi-namespace type`, async () => { + await deleteSuccess(MULTI_NAMESPACE_ISOLATED_TYPE, id); + const versionProperties = { + if_seq_no: mockVersionProps._seq_no, + if_primary_term: mockVersionProps._primary_term, + }; + expect(client.delete).toHaveBeenCalledWith( + expect.objectContaining(versionProperties), + expect.anything() + ); + }); + + it(`defaults to a refresh setting of wait_for`, async () => { + await deleteSuccess(type, id); + expect(client.delete).toHaveBeenCalledWith( + expect.objectContaining({ refresh: 'wait_for' }), + expect.anything() + ); + }); + + it(`prepends namespace to the id when providing namespace for single-namespace type`, async () => { + await deleteSuccess(type, id, { namespace }); + expect(client.delete).toHaveBeenCalledWith( + expect.objectContaining({ id: `${namespace}:${type}:${id}` }), + expect.anything() + ); + }); + + it(`doesn't prepend namespace to the id when providing no namespace for single-namespace type`, async () => { + await deleteSuccess(type, id); + expect(client.delete).toHaveBeenCalledWith( + expect.objectContaining({ id: `${type}:${id}` }), + expect.anything() + ); + }); + + it(`normalizes options.namespace from 'default' to undefined`, async () => { + await deleteSuccess(type, id, { namespace: 'default' }); + expect(client.delete).toHaveBeenCalledWith( + expect.objectContaining({ id: `${type}:${id}` }), + expect.anything() + ); + }); + + it(`doesn't prepend namespace to the id when not using single-namespace type`, async () => { + await deleteSuccess(NAMESPACE_AGNOSTIC_TYPE, id, { namespace }); + expect(client.delete).toHaveBeenCalledWith( + expect.objectContaining({ id: `${NAMESPACE_AGNOSTIC_TYPE}:${id}` }), + expect.anything() + ); + + client.delete.mockClear(); + await deleteSuccess(MULTI_NAMESPACE_ISOLATED_TYPE, id, { namespace }); + expect(client.delete).toHaveBeenCalledWith( + expect.objectContaining({ id: `${MULTI_NAMESPACE_ISOLATED_TYPE}:${id}` }), + expect.anything() + ); + }); + }); + + describe('legacy URL aliases', () => { + it(`doesn't delete legacy URL aliases for single-namespace object types`, async () => { + await deleteSuccess(type, id, { namespace }); + expect(mockDeleteLegacyUrlAliases).not.toHaveBeenCalled(); + }); + + // We intentionally do not include a test case for a multi-namespace object with a "not found" preflight result, because that throws + // an error (without deleting aliases) and we already have a test case for that + + it(`deletes legacy URL aliases for multi-namespace object types (all spaces)`, async () => { + const internalOptions = { + mockGetResponseValue: getMockGetResponse( + { type: MULTI_NAMESPACE_TYPE, id }, + ALL_NAMESPACES_STRING + ), + }; + await deleteSuccess(MULTI_NAMESPACE_TYPE, id, { namespace, force: true }, internalOptions); + expect(mockDeleteLegacyUrlAliases).toHaveBeenCalledWith( + expect.objectContaining({ + type: MULTI_NAMESPACE_TYPE, + id, + namespaces: [], + deleteBehavior: 'exclusive', + }) + ); + }); + + it(`deletes legacy URL aliases for multi-namespace object types (specific spaces)`, async () => { + await deleteSuccess(MULTI_NAMESPACE_TYPE, id, { namespace }); // this function mocks a preflight response with the given namespace by default + expect(mockDeleteLegacyUrlAliases).toHaveBeenCalledWith( + expect.objectContaining({ + type: MULTI_NAMESPACE_TYPE, + id, + namespaces: [namespace], + deleteBehavior: 'inclusive', + }) + ); + }); + + it(`logs a message when deleteLegacyUrlAliases returns an error`, async () => { + client.get.mockResolvedValueOnce( + elasticsearchClientMock.createSuccessTransportRequestPromise( + getMockGetResponse({ type: MULTI_NAMESPACE_ISOLATED_TYPE, id, namespace }) + ) + ); + client.delete.mockResolvedValueOnce( + elasticsearchClientMock.createSuccessTransportRequestPromise({ + result: 'deleted', + } as estypes.DeleteResponse) + ); + mockDeleteLegacyUrlAliases.mockRejectedValueOnce(new Error('Oh no!')); + await savedObjectsRepository.delete(MULTI_NAMESPACE_ISOLATED_TYPE, id, { namespace }); + expect(client.get).toHaveBeenCalledTimes(1); + expect(logger.error).toHaveBeenCalledTimes(1); + expect(logger.error).toHaveBeenCalledWith( + 'Unable to delete aliases when deleting an object: Oh no!' + ); + }); + }); + + describe('errors', () => { + const expectNotFoundError = async ( + type: string, + id: string, + options?: SavedObjectsDeleteOptions + ) => { + await expect(savedObjectsRepository.delete(type, id, options)).rejects.toThrowError( + createGenericNotFoundError(type, id) + ); + }; + + it(`throws when options.namespace is '*'`, async () => { + await expect( + savedObjectsRepository.delete(type, id, { namespace: ALL_NAMESPACES_STRING }) + ).rejects.toThrowError(createBadRequestError('"options.namespace" cannot be "*"')); + }); + + it(`throws when type is invalid`, async () => { + await expectNotFoundError('unknownType', id); + expect(client.delete).not.toHaveBeenCalled(); + }); + + it(`throws when type is hidden`, async () => { + await expectNotFoundError(HIDDEN_TYPE, id); + expect(client.delete).not.toHaveBeenCalled(); + }); + + it(`throws when ES is unable to find the document during get`, async () => { + client.get.mockResolvedValueOnce( + elasticsearchClientMock.createSuccessTransportRequestPromise({ + found: false, + } as estypes.GetResponse) + ); + await expectNotFoundError(MULTI_NAMESPACE_ISOLATED_TYPE, id); + expect(client.get).toHaveBeenCalledTimes(1); + }); + + it(`throws when ES is unable to find the index during get`, async () => { + client.get.mockResolvedValueOnce( + elasticsearchClientMock.createSuccessTransportRequestPromise({} as estypes.GetResponse, { + statusCode: 404, + }) + ); + await expectNotFoundError(MULTI_NAMESPACE_ISOLATED_TYPE, id); + expect(client.get).toHaveBeenCalledTimes(1); + }); + + it(`throws when the type is multi-namespace and the document exists, but not in this namespace`, async () => { + const response = getMockGetResponse({ type: MULTI_NAMESPACE_ISOLATED_TYPE, id }, namespace); + client.get.mockResolvedValueOnce( + elasticsearchClientMock.createSuccessTransportRequestPromise(response) + ); + await expectNotFoundError(MULTI_NAMESPACE_ISOLATED_TYPE, id, { + namespace: 'bar-namespace', + }); + expect(client.get).toHaveBeenCalledTimes(1); + }); + + it(`throws when the type is multi-namespace and the document has multiple namespaces and the force option is not enabled`, async () => { + const response = getMockGetResponse({ type: MULTI_NAMESPACE_ISOLATED_TYPE, id, namespace }); + response._source!.namespaces = [namespace, 'bar-namespace']; + client.get.mockResolvedValueOnce( + elasticsearchClientMock.createSuccessTransportRequestPromise(response) + ); + await expect( + savedObjectsRepository.delete(MULTI_NAMESPACE_ISOLATED_TYPE, id, { namespace }) + ).rejects.toThrowError( + 'Unable to delete saved object that exists in multiple namespaces, use the `force` option to delete it anyway' + ); + expect(client.get).toHaveBeenCalledTimes(1); + }); + + it(`throws when the type is multi-namespace and the document has all namespaces and the force option is not enabled`, async () => { + const response = getMockGetResponse({ type: MULTI_NAMESPACE_ISOLATED_TYPE, id, namespace }); + response._source!.namespaces = ['*']; + client.get.mockResolvedValueOnce( + elasticsearchClientMock.createSuccessTransportRequestPromise(response) + ); + await expect( + savedObjectsRepository.delete(MULTI_NAMESPACE_ISOLATED_TYPE, id, { namespace }) + ).rejects.toThrowError( + 'Unable to delete saved object that exists in multiple namespaces, use the `force` option to delete it anyway' + ); + expect(client.get).toHaveBeenCalledTimes(1); + }); + + it(`throws when ES is unable to find the document during delete`, async () => { + client.delete.mockResolvedValueOnce( + elasticsearchClientMock.createSuccessTransportRequestPromise({ + result: 'not_found', + } as estypes.DeleteResponse) + ); + await expectNotFoundError(type, id); + expect(client.delete).toHaveBeenCalledTimes(1); + }); + + it(`throws when ES is unable to find the index during delete`, async () => { + client.delete.mockResolvedValueOnce( + elasticsearchClientMock.createSuccessTransportRequestPromise({ + // @elastic/elasticsearch doesn't declare error on DeleteResponse + error: { type: 'index_not_found_exception' }, + } as unknown as estypes.DeleteResponse) + ); + await expectNotFoundError(type, id); + expect(client.delete).toHaveBeenCalledTimes(1); + }); + + it(`throws when ES returns an unexpected response`, async () => { + client.delete.mockResolvedValueOnce( + elasticsearchClientMock.createSuccessTransportRequestPromise({ + result: 'something unexpected' as estypes.Result, + } as estypes.DeleteResponse) + ); + await expect(savedObjectsRepository.delete(type, id)).rejects.toThrowError( + 'Unexpected Elasticsearch DELETE response' + ); + expect(client.delete).toHaveBeenCalledTimes(1); + }); + }); + + describe('returns', () => { + it(`returns an empty object on success`, async () => { + const result = await deleteSuccess(type, id); + expect(result).toEqual({}); + }); + }); + }); + + describe('#deleteByNamespace', () => { + const namespace = 'foo-namespace'; + const mockUpdateResults = { + took: 15, + timed_out: false, + total: 3, + updated: 2, + deleted: 1, + batches: 1, + version_conflicts: 0, + noops: 0, + retries: { bulk: 0, search: 0 }, + throttled_millis: 0, + requests_per_second: -1.0, + throttled_until_millis: 0, + failures: [], + }; + + const deleteByNamespaceSuccess = async ( + namespace: string, + options?: SavedObjectsDeleteByNamespaceOptions + ) => { + client.updateByQuery.mockResolvedValueOnce( + elasticsearchClientMock.createSuccessTransportRequestPromise(mockUpdateResults) + ); + const result = await savedObjectsRepository.deleteByNamespace(namespace, options); + expect(mockGetSearchDsl).toHaveBeenCalledTimes(1); + expect(client.updateByQuery).toHaveBeenCalledTimes(1); + return result; + }; + + describe('client calls', () => { + it(`should use the ES updateByQuery action`, async () => { + await deleteByNamespaceSuccess(namespace); + expect(client.updateByQuery).toHaveBeenCalledTimes(1); + }); + + it(`should use all indices for types that are not namespace-agnostic`, async () => { + await deleteByNamespaceSuccess(namespace); + expect(client.updateByQuery).toHaveBeenCalledWith( + expect.objectContaining({ + index: ['.kibana-test_8.0.0-testing', 'custom_8.0.0-testing'], + }), + expect.anything() + ); + }); + }); + + describe('errors', () => { + it(`throws when namespace is not a string or is '*'`, async () => { + const test = async (namespace: unknown) => { + // @ts-expect-error namespace is unknown + await expect(savedObjectsRepository.deleteByNamespace(namespace)).rejects.toThrowError( + `namespace is required, and must be a string` + ); + expect(client.updateByQuery).not.toHaveBeenCalled(); + }; + await test(undefined); + await test(['namespace']); + await test(123); + await test(true); + await test(ALL_NAMESPACES_STRING); + }); + }); + + describe('returns', () => { + it(`returns the query results on success`, async () => { + const result = await deleteByNamespaceSuccess(namespace); + expect(result).toEqual(mockUpdateResults); + }); + }); + + describe('search dsl', () => { + it(`constructs a query using all multi-namespace types, and another using all single-namespace types`, async () => { + await deleteByNamespaceSuccess(namespace); + const allTypes = registry.getAllTypes().map((type) => type.name); + expect(mockGetSearchDsl).toHaveBeenCalledWith(mappings, registry, { + namespaces: [namespace], + type: [ + ...allTypes.filter((type) => !registry.isNamespaceAgnostic(type)), + LEGACY_URL_ALIAS_TYPE, + ], + kueryNode: expect.anything(), + }); + }); + }); + }); + + describe('#removeReferencesTo', () => { + const type = 'type'; + const id = 'id'; + const defaultOptions = {}; + + const updatedCount = 42; + + const removeReferencesToSuccess = async (options = defaultOptions) => { + client.updateByQuery.mockResolvedValueOnce( + elasticsearchClientMock.createSuccessTransportRequestPromise({ + updated: updatedCount, + }) + ); + return await savedObjectsRepository.removeReferencesTo(type, id, options); + }; + + describe('client calls', () => { + it('should use the ES updateByQuery action', async () => { + await removeReferencesToSuccess(); + expect(client.updateByQuery).toHaveBeenCalledTimes(1); + }); + + it('uses the correct default `refresh` value', async () => { + await removeReferencesToSuccess(); + expect(client.updateByQuery).toHaveBeenCalledWith( + expect.objectContaining({ + refresh: true, + }), + expect.any(Object) + ); + }); + + it('merges output of getSearchDsl into es request body', async () => { + const query = { query: 1, aggregations: 2 }; + mockGetSearchDsl.mockReturnValue(query); + await removeReferencesToSuccess({ type }); + + expect(client.updateByQuery).toHaveBeenCalledWith( + expect.objectContaining({ + body: expect.objectContaining({ ...query }), + }), + expect.anything() + ); + }); + + it('should set index to all known SO indices on the request', async () => { + await removeReferencesToSuccess(); + expect(client.updateByQuery).toHaveBeenCalledWith( + expect.objectContaining({ + index: ['.kibana-test_8.0.0-testing', 'custom_8.0.0-testing'], + }), + expect.anything() + ); + }); + + it('should use the `refresh` option in the request', async () => { + const refresh = Symbol(); + + await removeReferencesToSuccess({ refresh }); + expect(client.updateByQuery).toHaveBeenCalledWith( + expect.objectContaining({ + refresh, + }), + expect.anything() + ); + }); + + it('should pass the correct parameters to the update script', async () => { + await removeReferencesToSuccess(); + expect(client.updateByQuery).toHaveBeenCalledWith( + expect.objectContaining({ + body: expect.objectContaining({ + script: expect.objectContaining({ + params: { + type, + id, + }, + }), + }), + }), + expect.anything() + ); + }); + }); + + describe('search dsl', () => { + it(`passes mappings and registry to getSearchDsl`, async () => { + await removeReferencesToSuccess(); + expect(mockGetSearchDsl).toHaveBeenCalledWith(mappings, registry, expect.anything()); + }); + + it('passes namespace to getSearchDsl', async () => { + await removeReferencesToSuccess({ namespace: 'some-ns' }); + expect(mockGetSearchDsl).toHaveBeenCalledWith( + mappings, + registry, + expect.objectContaining({ + namespaces: ['some-ns'], + }) + ); + }); + + it('passes hasReference to getSearchDsl', async () => { + await removeReferencesToSuccess(); + expect(mockGetSearchDsl).toHaveBeenCalledWith( + mappings, + registry, + expect.objectContaining({ + hasReference: { + type, + id, + }, + }) + ); + }); + + it('passes all known types to getSearchDsl', async () => { + await removeReferencesToSuccess(); + expect(mockGetSearchDsl).toHaveBeenCalledWith( + mappings, + registry, + expect.objectContaining({ + type: registry.getAllTypes().map((type) => type.name), + }) + ); + }); + }); + + describe('returns', () => { + it('returns the updated count from the ES response', async () => { + const response = await removeReferencesToSuccess(); + expect(response.updated).toBe(updatedCount); + }); + }); + + describe('errors', () => { + it(`throws when ES returns failures`, async () => { + client.updateByQuery.mockResolvedValueOnce( + elasticsearchClientMock.createSuccessTransportRequestPromise({ + updated: 7, + failures: [ + { id: 'failure' } as estypes.BulkIndexByScrollFailure, + { id: 'another-failure' } as estypes.BulkIndexByScrollFailure, + ], + }) + ); + + await expect( + savedObjectsRepository.removeReferencesTo(type, id, defaultOptions) + ).rejects.toThrowError(createConflictError(type, id)); + }); + }); + }); + + describe('#find', () => { + const generateSearchResults = (namespace?: string) => { + return { + took: 1, + timed_out: false, + _shards: {} as any, + hits: { + total: 4, + hits: [ + { + _index: '.kibana', + _id: `${namespace ? `${namespace}:` : ''}index-pattern:logstash-*`, + _score: 1, + ...mockVersionProps, + _source: { + namespace, + originId: 'some-origin-id', // only one of the results has an originId, this is intentional to test both a positive and negative case + type: 'index-pattern', + ...mockTimestampFields, + 'index-pattern': { + title: 'logstash-*', + timeFieldName: '@timestamp', + notExpandable: true, + }, + }, + }, + { + _index: '.kibana', + _id: `${namespace ? `${namespace}:` : ''}config:6.0.0-alpha1`, + _score: 2, + ...mockVersionProps, + _source: { + namespace, + type: 'config', + ...mockTimestampFields, + config: { + buildNum: 8467, + defaultIndex: 'logstash-*', + }, + }, + }, + { + _index: '.kibana', + _id: `${namespace ? `${namespace}:` : ''}index-pattern:stocks-*`, + _score: 3, + ...mockVersionProps, + _source: { + namespace, + type: 'index-pattern', + ...mockTimestampFields, + 'index-pattern': { + title: 'stocks-*', + timeFieldName: '@timestamp', + notExpandable: true, + }, + }, + }, + { + _index: '.kibana', + _id: `${NAMESPACE_AGNOSTIC_TYPE}:something`, + _score: 4, + ...mockVersionProps, + _source: { + type: NAMESPACE_AGNOSTIC_TYPE, + ...mockTimestampFields, + [NAMESPACE_AGNOSTIC_TYPE]: { + name: 'bar', + }, + }, + }, + ], + }, + } as estypes.SearchResponse; + }; + + const type = 'index-pattern'; + const namespace = 'foo-namespace'; + + const findSuccess = async (options: SavedObjectsFindOptions, namespace?: string) => { + client.search.mockResolvedValueOnce( + elasticsearchClientMock.createSuccessTransportRequestPromise( + generateSearchResults(namespace) + ) + ); + const result = await savedObjectsRepository.find(options); + expect(mockGetSearchDsl).toHaveBeenCalledTimes(1); + expect(client.search).toHaveBeenCalledTimes(1); + return result; + }; + + describe('client calls', () => { + it(`should use the ES search action`, async () => { + await findSuccess({ type }); + expect(client.search).toHaveBeenCalledTimes(1); + }); + + it(`merges output of getSearchDsl into es request body`, async () => { + const query = { query: 1, aggregations: 2 }; + mockGetSearchDsl.mockReturnValue(query); + await findSuccess({ type }); + + expect(client.search).toHaveBeenCalledWith( + expect.objectContaining({ + body: expect.objectContaining({ ...query }), + }), + expect.anything() + ); + }); + + it(`accepts per_page/page`, async () => { + await findSuccess({ type, perPage: 10, page: 6 }); + expect(client.search).toHaveBeenCalledWith( + expect.objectContaining({ + size: 10, + from: 50, + }), + expect.anything() + ); + }); + + it(`accepts preference`, async () => { + await findSuccess({ type, preference: 'pref' }); + expect(client.search).toHaveBeenCalledWith( + expect.objectContaining({ + preference: 'pref', + }), + expect.anything() + ); + }); + + it(`can filter by fields`, async () => { + await findSuccess({ type, fields: ['title'] }); + expect(client.search).toHaveBeenCalledWith( + expect.objectContaining({ + body: expect.objectContaining({ + _source: [ + `${type}.title`, + 'namespace', + 'namespaces', + 'type', + 'references', + 'migrationVersion', + 'coreMigrationVersion', + 'updated_at', + 'originId', + 'title', + ], + }), + }), + expect.anything() + ); + }); + + it(`should set rest_total_hits_as_int to true on a request`, async () => { + await findSuccess({ type }); + expect(client.search).toHaveBeenCalledWith( + expect.objectContaining({ + rest_total_hits_as_int: true, + }), + expect.anything() + ); + }); + + it(`should not make a client call when attempting to find only invalid or hidden types`, async () => { + const test = async (types: string | string[]) => { + await savedObjectsRepository.find({ type: types }); + expect(client.search).not.toHaveBeenCalled(); + }; + + await test('unknownType'); + await test(HIDDEN_TYPE); + await test(['unknownType', HIDDEN_TYPE]); + }); + }); + + describe('errors', () => { + it(`throws when type is not defined`, async () => { + // @ts-expect-error type should be defined + await expect(savedObjectsRepository.find({})).rejects.toThrowError( + 'options.type must be a string or an array of strings' + ); + expect(client.search).not.toHaveBeenCalled(); + }); + + it(`throws when namespaces is an empty array`, async () => { + await expect( + savedObjectsRepository.find({ type: 'foo', namespaces: [] }) + ).rejects.toThrowError('options.namespaces cannot be an empty array'); + expect(client.search).not.toHaveBeenCalled(); + }); + + it(`throws when type is not falsy and typeToNamespacesMap is defined`, async () => { + await expect( + savedObjectsRepository.find({ type: 'foo', typeToNamespacesMap: new Map() }) + ).rejects.toThrowError( + 'options.type must be an empty string when options.typeToNamespacesMap is used' + ); + expect(client.search).not.toHaveBeenCalled(); + }); + + it(`throws when type is not an empty array and typeToNamespacesMap is defined`, async () => { + const test = async (args: SavedObjectsFindOptions) => { + await expect(savedObjectsRepository.find(args)).rejects.toThrowError( + 'options.namespaces must be an empty array when options.typeToNamespacesMap is used' + ); + expect(client.search).not.toHaveBeenCalled(); + }; + await test({ type: '', typeToNamespacesMap: new Map() }); + await test({ type: '', namespaces: ['some-ns'], typeToNamespacesMap: new Map() }); + }); + + it(`throws when searchFields is defined but not an array`, async () => { + await expect( + // @ts-expect-error searchFields is an array + savedObjectsRepository.find({ type, searchFields: 'string' }) + ).rejects.toThrowError('options.searchFields must be an array'); + expect(client.search).not.toHaveBeenCalled(); + }); + + it(`throws when fields is defined but not an array`, async () => { + // @ts-expect-error fields is an array + await expect(savedObjectsRepository.find({ type, fields: 'string' })).rejects.toThrowError( + 'options.fields must be an array' + ); + expect(client.search).not.toHaveBeenCalled(); + }); + + it(`throws when a preference is provided with pit`, async () => { + await expect( + savedObjectsRepository.find({ type: 'foo', pit: { id: 'abc123' }, preference: 'hi' }) + ).rejects.toThrowError('options.preference must be excluded when options.pit is used'); + expect(client.search).not.toHaveBeenCalled(); + }); + + it(`throws when KQL filter syntax is invalid`, async () => { + const findOpts: SavedObjectsFindOptions = { + namespaces: [namespace], + search: 'foo*', + searchFields: ['foo'], + type: ['dashboard'], + sortField: 'name', + sortOrder: 'desc', + defaultSearchOperator: 'AND', + hasReference: { + type: 'foo', + id: '1', + }, + filter: 'dashboard.attributes.otherField:<', + }; + + await expect(savedObjectsRepository.find(findOpts)).rejects.toMatchInlineSnapshot(` + [Error: KQLSyntaxError: Expected "(", "{", value, whitespace but "<" found. + dashboard.attributes.otherField:< + --------------------------------^: Bad Request] + `); + expect(mockGetSearchDsl).not.toHaveBeenCalled(); + expect(client.search).not.toHaveBeenCalled(); + }); + }); + + describe('returns', () => { + it(`formats the ES response when there is no namespace`, async () => { + const noNamespaceSearchResults = generateSearchResults(); + client.search.mockResolvedValueOnce( + elasticsearchClientMock.createSuccessTransportRequestPromise(noNamespaceSearchResults) + ); + const count = noNamespaceSearchResults.hits.hits.length; + + const response = await savedObjectsRepository.find({ type }); + + expect(response.total).toBe(count); + expect(response.saved_objects).toHaveLength(count); + + noNamespaceSearchResults.hits.hits.forEach((doc, i) => { + expect(response.saved_objects[i]).toEqual({ + id: doc._id.replace(/(index-pattern|config|globalType)\:/, ''), + type: doc._source!.type, + originId: doc._source!.originId, + ...mockTimestampFields, + version: mockVersion, + score: doc._score, + attributes: doc._source![doc._source!.type], + references: [], + namespaces: doc._source!.type === NAMESPACE_AGNOSTIC_TYPE ? undefined : ['default'], + }); + }); + }); + + it(`formats the ES response when there is a namespace`, async () => { + const namespacedSearchResults = generateSearchResults(namespace); + client.search.mockResolvedValueOnce( + elasticsearchClientMock.createSuccessTransportRequestPromise(namespacedSearchResults) + ); + const count = namespacedSearchResults.hits.hits.length; + + const response = await savedObjectsRepository.find({ type, namespaces: [namespace] }); + + expect(response.total).toBe(count); + expect(response.saved_objects).toHaveLength(count); + + namespacedSearchResults.hits.hits.forEach((doc, i) => { + expect(response.saved_objects[i]).toEqual({ + id: doc._id.replace(/(foo-namespace\:)?(index-pattern|config|globalType)\:/, ''), + type: doc._source!.type, + originId: doc._source!.originId, + ...mockTimestampFields, + version: mockVersion, + score: doc._score, + attributes: doc._source![doc._source!.type], + references: [], + namespaces: doc._source!.type === NAMESPACE_AGNOSTIC_TYPE ? undefined : [namespace], + }); + }); + }); + + it(`should return empty results when attempting to find only invalid or hidden types`, async () => { + const test = async (types: string | string[]) => { + const result = await savedObjectsRepository.find({ type: types }); + expect(result).toEqual(expect.objectContaining({ saved_objects: [] })); + expect(client.search).not.toHaveBeenCalled(); + }; + + await test('unknownType'); + await test(HIDDEN_TYPE); + await test(['unknownType', HIDDEN_TYPE]); + }); + + it(`should return empty results when attempting to find only invalid or hidden types using typeToNamespacesMap`, async () => { + const test = async (types: string[]) => { + const result = await savedObjectsRepository.find({ + typeToNamespacesMap: new Map(types.map((x) => [x, undefined])), + type: '', + namespaces: [], + }); + expect(result).toEqual(expect.objectContaining({ saved_objects: [] })); + expect(client.search).not.toHaveBeenCalled(); + }; + + await test(['unknownType']); + await test([HIDDEN_TYPE]); + await test(['unknownType', HIDDEN_TYPE]); + }); + }); + + describe('search dsl', () => { + const commonOptions: SavedObjectsFindOptions = { + type: [type], // cannot be used when `typeToNamespacesMap` is present + namespaces: [namespace], // cannot be used when `typeToNamespacesMap` is present + search: 'foo*', + searchFields: ['foo'], + sortField: 'name', + sortOrder: 'desc', + defaultSearchOperator: 'AND', + hasReference: { + type: 'foo', + id: '1', + }, + }; + + it(`passes mappings, registry, and search options to getSearchDsl`, async () => { + await findSuccess(commonOptions, namespace); + expect(mockGetSearchDsl).toHaveBeenCalledWith(mappings, registry, commonOptions); + }); + + it(`accepts typeToNamespacesMap`, async () => { + const relevantOpts = { + ...commonOptions, + type: '', + namespaces: [], + typeToNamespacesMap: new Map([[type, [namespace]]]), // can only be used when `type` is falsy and `namespaces` is an empty array + }; + + await findSuccess(relevantOpts, namespace); + expect(mockGetSearchDsl).toHaveBeenCalledWith(mappings, registry, { + ...relevantOpts, + type: [type], + }); + }); + + it(`accepts hasReferenceOperator`, async () => { + const relevantOpts: SavedObjectsFindOptions = { + ...commonOptions, + hasReferenceOperator: 'AND', + }; + + await findSuccess(relevantOpts, namespace); + expect(mockGetSearchDsl).toHaveBeenCalledWith(mappings, registry, { + ...relevantOpts, + hasReferenceOperator: 'AND', + }); + }); + + it(`accepts searchAfter`, async () => { + const relevantOpts: SavedObjectsFindOptions = { + ...commonOptions, + searchAfter: ['1', 'a'], + }; + + await findSuccess(relevantOpts, namespace); + expect(mockGetSearchDsl).toHaveBeenCalledWith(mappings, registry, { + ...relevantOpts, + searchAfter: ['1', 'a'], + }); + }); + + it(`accepts pit`, async () => { + const relevantOpts: SavedObjectsFindOptions = { + ...commonOptions, + pit: { id: 'abc123', keepAlive: '2m' }, + }; + + await findSuccess(relevantOpts, namespace); + expect(mockGetSearchDsl).toHaveBeenCalledWith(mappings, registry, { + ...relevantOpts, + pit: { id: 'abc123', keepAlive: '2m' }, + }); + }); + + it(`accepts KQL expression filter and passes KueryNode to getSearchDsl`, async () => { + const findOpts: SavedObjectsFindOptions = { + namespaces: [namespace], + search: 'foo*', + searchFields: ['foo'], + type: ['dashboard'], + sortField: 'name', + sortOrder: 'desc', + defaultSearchOperator: 'AND', + hasReference: { + type: 'foo', + id: '1', + }, + filter: 'dashboard.attributes.otherField: *', + }; + + await findSuccess(findOpts, namespace); + const { kueryNode } = mockGetSearchDsl.mock.calls[0][2]; + expect(kueryNode).toMatchInlineSnapshot(` + Object { + "arguments": Array [ + Object { + "type": "literal", + "value": "dashboard.otherField", + }, + Object { + "type": "wildcard", + "value": "@kuery-wildcard@", + }, + Object { + "type": "literal", + "value": false, + }, + ], + "function": "is", + "type": "function", + } + `); + }); + + it(`accepts KQL KueryNode filter and passes KueryNode to getSearchDsl`, async () => { + const findOpts: SavedObjectsFindOptions = { + namespaces: [namespace], + search: 'foo*', + searchFields: ['foo'], + type: ['dashboard'], + sortField: 'name', + sortOrder: 'desc', + defaultSearchOperator: 'AND', + hasReference: { + type: 'foo', + id: '1', + }, + filter: nodeTypes.function.buildNode('is', `dashboard.attributes.otherField`, '*'), + }; + + await findSuccess(findOpts, namespace); + const { kueryNode } = mockGetSearchDsl.mock.calls[0][2]; + expect(kueryNode).toMatchInlineSnapshot(` + Object { + "arguments": Array [ + Object { + "type": "literal", + "value": "dashboard.otherField", + }, + Object { + "type": "wildcard", + "value": "@kuery-wildcard@", + }, + Object { + "type": "literal", + "value": false, + }, + ], + "function": "is", + "type": "function", + } + `); + }); + + it(`supports multiple types`, async () => { + const types = ['config', 'index-pattern']; + await findSuccess({ type: types }); + + expect(mockGetSearchDsl).toHaveBeenCalledWith( + mappings, + registry, + expect.objectContaining({ + type: types, + }) + ); + }); + + it(`filters out invalid types`, async () => { + const types = ['config', 'unknownType', 'index-pattern']; + await findSuccess({ type: types }); + + expect(mockGetSearchDsl).toHaveBeenCalledWith( + mappings, + registry, + expect.objectContaining({ + type: ['config', 'index-pattern'], + }) + ); + }); + + it(`filters out hidden types`, async () => { + const types = ['config', HIDDEN_TYPE, 'index-pattern']; + await findSuccess({ type: types }); + + expect(mockGetSearchDsl).toHaveBeenCalledWith( + mappings, + registry, + expect.objectContaining({ + type: ['config', 'index-pattern'], + }) + ); + }); + }); + }); + + describe('#get', () => { + const type = 'index-pattern'; + const id = 'logstash-*'; + const namespace = 'foo-namespace'; + const originId = 'some-origin-id'; + + const getSuccess = async ( + type: string, + id: string, + options?: SavedObjectsBaseOptions, + includeOriginId?: boolean + ) => { + const response = getMockGetResponse( + { + type, + id, + // "includeOriginId" is not an option for the operation; however, if the existing saved object contains an originId attribute, the + // operation will return it in the result. This flag is just used for test purposes to modify the mock cluster call response. + ...(includeOriginId && { originId }), + }, + options?.namespace + ); + client.get.mockResolvedValueOnce( + elasticsearchClientMock.createSuccessTransportRequestPromise(response) + ); + const result = await savedObjectsRepository.get(type, id, options); + expect(client.get).toHaveBeenCalledTimes(1); + return result; + }; + + describe('client calls', () => { + it(`should use the ES get action`, async () => { + await getSuccess(type, id); + expect(client.get).toHaveBeenCalledTimes(1); + }); + + it(`prepends namespace to the id when providing namespace for single-namespace type`, async () => { + await getSuccess(type, id, { namespace }); + expect(client.get).toHaveBeenCalledWith( + expect.objectContaining({ + id: `${namespace}:${type}:${id}`, + }), + expect.anything() + ); + }); + + it(`doesn't prepend namespace to the id when providing no namespace for single-namespace type`, async () => { + await getSuccess(type, id); + expect(client.get).toHaveBeenCalledWith( + expect.objectContaining({ + id: `${type}:${id}`, + }), + expect.anything() + ); + }); + + it(`normalizes options.namespace from 'default' to undefined`, async () => { + await getSuccess(type, id, { namespace: 'default' }); + expect(client.get).toHaveBeenCalledWith( + expect.objectContaining({ + id: `${type}:${id}`, + }), + expect.anything() + ); + }); + + it(`doesn't prepend namespace to the id when not using single-namespace type`, async () => { + await getSuccess(NAMESPACE_AGNOSTIC_TYPE, id, { namespace }); + expect(client.get).toHaveBeenCalledWith( + expect.objectContaining({ + id: `${NAMESPACE_AGNOSTIC_TYPE}:${id}`, + }), + expect.anything() + ); + + client.get.mockClear(); + await getSuccess(MULTI_NAMESPACE_ISOLATED_TYPE, id, { namespace }); + expect(client.get).toHaveBeenCalledWith( + expect.objectContaining({ + id: `${MULTI_NAMESPACE_ISOLATED_TYPE}:${id}`, + }), + expect.anything() + ); + }); + }); + + describe('errors', () => { + const expectNotFoundError = async ( + type: string, + id: string, + options?: SavedObjectsBaseOptions + ) => { + await expect(savedObjectsRepository.get(type, id, options)).rejects.toThrowError( + createGenericNotFoundError(type, id) + ); + }; + + it(`throws when options.namespace is '*'`, async () => { + await expect( + savedObjectsRepository.get(type, id, { namespace: ALL_NAMESPACES_STRING }) + ).rejects.toThrowError(createBadRequestError('"options.namespace" cannot be "*"')); + }); + + it(`throws when type is invalid`, async () => { + await expectNotFoundError('unknownType', id); + expect(client.get).not.toHaveBeenCalled(); + }); + + it(`throws when type is hidden`, async () => { + await expectNotFoundError(HIDDEN_TYPE, id); + expect(client.get).not.toHaveBeenCalled(); + }); + + it(`throws when ES is unable to find the document during get`, async () => { + client.get.mockResolvedValueOnce( + elasticsearchClientMock.createSuccessTransportRequestPromise({ + found: false, + } as estypes.GetResponse) + ); + await expectNotFoundError(type, id); + expect(client.get).toHaveBeenCalledTimes(1); + }); + + it(`throws when ES is unable to find the index during get`, async () => { + client.get.mockResolvedValueOnce( + elasticsearchClientMock.createSuccessTransportRequestPromise({} as estypes.GetResponse, { + statusCode: 404, + }) + ); + await expectNotFoundError(type, id); + expect(client.get).toHaveBeenCalledTimes(1); + }); + + it(`throws when type is multi-namespace and the document exists, but not in this namespace`, async () => { + const response = getMockGetResponse({ type: MULTI_NAMESPACE_ISOLATED_TYPE, id }, namespace); + client.get.mockResolvedValueOnce( + elasticsearchClientMock.createSuccessTransportRequestPromise(response) + ); + await expectNotFoundError(MULTI_NAMESPACE_ISOLATED_TYPE, id, { + namespace: 'bar-namespace', + }); + expect(client.get).toHaveBeenCalledTimes(1); + }); + }); + + describe('returns', () => { + it(`formats the ES response`, async () => { + const result = await getSuccess(type, id); + expect(result).toEqual({ + id, + type, + updated_at: mockTimestamp, + version: mockVersion, + attributes: { + title: 'Testing', + }, + references: [], + namespaces: ['default'], + }); + }); + + it(`includes namespaces if type is multi-namespace`, async () => { + const result = await getSuccess(MULTI_NAMESPACE_ISOLATED_TYPE, id); + expect(result).toMatchObject({ + namespaces: expect.any(Array), + }); + }); + + it(`include namespaces if type is not multi-namespace`, async () => { + const result = await getSuccess(type, id); + expect(result).toMatchObject({ + namespaces: ['default'], + }); + }); + + it(`includes originId property if present in cluster call response`, async () => { + const result = await getSuccess(type, id, {}, true); + expect(result).toMatchObject({ originId }); + }); + }); + }); + + describe('#resolve', () => { + afterEach(() => { + mockInternalBulkResolve.mockReset(); + }); + + it('passes arguments to the internalBulkResolve module and returns the result', async () => { + const expectedResult: SavedObjectsResolveResponse = { + saved_object: { type: 'type', id: 'id', attributes: {}, references: [] }, + outcome: 'exactMatch', + }; + mockInternalBulkResolve.mockResolvedValue({ resolved_objects: [expectedResult] }); + + await expect(savedObjectsRepository.resolve('obj-type', 'obj-id')).resolves.toEqual( + expectedResult + ); + expect(mockInternalBulkResolve).toHaveBeenCalledTimes(1); + expect(mockInternalBulkResolve).toHaveBeenCalledWith( + expect.objectContaining({ objects: [{ type: 'obj-type', id: 'obj-id' }] }) + ); + }); + + it('throws when internalBulkResolve result is an error', async () => { + const error = SavedObjectsErrorHelpers.decorateBadRequestError(new Error('Oh no!')); + const expectedResult: InternalBulkResolveError = { type: 'obj-type', id: 'obj-id', error }; + mockInternalBulkResolve.mockResolvedValue({ resolved_objects: [expectedResult] }); + + await expect(savedObjectsRepository.resolve('foo', '2')).rejects.toEqual(error); + }); + + it('throws when internalBulkResolve throws', async () => { + const error = new Error('Oh no!'); + mockInternalBulkResolve.mockRejectedValue(error); + + await expect(savedObjectsRepository.resolve('foo', '2')).rejects.toEqual(error); + }); + }); + + describe('#incrementCounter', () => { + const type = 'config'; + const id = 'one'; + const counterFields = ['buildNum', 'apiCallsCount']; + const namespace = 'foo-namespace'; + const originId = 'some-origin-id'; + + const incrementCounterSuccess = async ( + type: string, + id: string, + fields: Array, + options?: SavedObjectsIncrementCounterOptions, + internalOptions: { mockGetResponseValue?: estypes.GetResponse } = {} + ) => { + const { mockGetResponseValue } = internalOptions; + const isMultiNamespace = registry.isMultiNamespace(type); + if (isMultiNamespace) { + const response = + mockGetResponseValue ?? getMockGetResponse({ type, id }, options?.namespace); + client.get.mockResolvedValueOnce( + elasticsearchClientMock.createSuccessTransportRequestPromise(response) + ); + } + client.update.mockImplementation((params) => + elasticsearchClientMock.createSuccessTransportRequestPromise({ + _id: params.id, + ...mockVersionProps, + _index: '.kibana', + get: { + found: true, + _source: { + type, + ...mockTimestampFields, + [type]: { + ...fields.reduce((acc, field) => { + acc[typeof field === 'string' ? field : field.fieldName] = 8468; + return acc; + }, {} as Record), + defaultIndex: 'logstash-*', + }, + }, + }, + } as estypes.UpdateResponse) + ); + + const result = await savedObjectsRepository.incrementCounter(type, id, fields, options); + expect(client.get).toHaveBeenCalledTimes(isMultiNamespace ? 1 : 0); + return result; + }; + + beforeEach(() => { + mockPreflightCheckForCreate.mockReset(); + mockPreflightCheckForCreate.mockImplementation(({ objects }) => { + return Promise.resolve(objects.map(({ type, id }) => ({ type, id }))); // respond with no errors by default + }); + }); + + describe('client calls', () => { + it(`should use the ES update action if type is not multi-namespace`, async () => { + await incrementCounterSuccess(type, id, counterFields, { namespace }); + expect(client.get).not.toHaveBeenCalled(); + expect(mockPreflightCheckForCreate).not.toHaveBeenCalled(); + expect(client.update).toHaveBeenCalledTimes(1); + }); + + it(`should use the ES get action then update action if type is multi-namespace, ID is defined, and overwrite=true`, async () => { + await incrementCounterSuccess(MULTI_NAMESPACE_ISOLATED_TYPE, id, counterFields, { + namespace, + }); + expect(client.get).toHaveBeenCalledTimes(1); + expect(mockPreflightCheckForCreate).not.toHaveBeenCalled(); + expect(client.update).toHaveBeenCalledTimes(1); + }); + + it(`should check for alias conflicts if a new multi-namespace object would be created`, async () => { + await incrementCounterSuccess( + MULTI_NAMESPACE_ISOLATED_TYPE, + id, + counterFields, + { namespace }, + { mockGetResponseValue: { found: false } as estypes.GetResponse } + ); + expect(client.get).toHaveBeenCalledTimes(1); + expect(mockPreflightCheckForCreate).toHaveBeenCalledTimes(1); + expect(client.update).toHaveBeenCalledTimes(1); + }); + + it(`defaults to a refresh setting of wait_for`, async () => { + await incrementCounterSuccess(type, id, counterFields, { namespace }); + expect(client.update).toHaveBeenCalledWith( + expect.objectContaining({ + refresh: 'wait_for', + }), + expect.anything() + ); + }); + + it(`uses the 'upsertAttributes' option when specified`, async () => { + const upsertAttributes = { + foo: 'bar', + hello: 'dolly', + }; + await incrementCounterSuccess(type, id, counterFields, { namespace, upsertAttributes }); + expect(client.update).toHaveBeenCalledWith( + expect.objectContaining({ + body: expect.objectContaining({ + upsert: expect.objectContaining({ + [type]: { + foo: 'bar', + hello: 'dolly', + ...counterFields.reduce((aggs, field) => { + return { + ...aggs, + [field]: 1, + }; + }, {}), + }, + }), + }), + }), + expect.anything() + ); + }); + + it(`prepends namespace to the id when providing namespace for single-namespace type`, async () => { + await incrementCounterSuccess(type, id, counterFields, { namespace }); + expect(client.update).toHaveBeenCalledWith( + expect.objectContaining({ + id: `${namespace}:${type}:${id}`, + }), + expect.anything() + ); + }); + + it(`doesn't prepend namespace to the id when providing no namespace for single-namespace type`, async () => { + await incrementCounterSuccess(type, id, counterFields); + expect(client.update).toHaveBeenCalledWith( + expect.objectContaining({ + id: `${type}:${id}`, + }), + expect.anything() + ); + }); + + it(`normalizes options.namespace from 'default' to undefined`, async () => { + await incrementCounterSuccess(type, id, counterFields, { namespace: 'default' }); + expect(client.update).toHaveBeenCalledWith( + expect.objectContaining({ + id: `${type}:${id}`, + }), + expect.anything() + ); + }); + + it(`doesn't prepend namespace to the id when not using single-namespace type`, async () => { + await incrementCounterSuccess(NAMESPACE_AGNOSTIC_TYPE, id, counterFields, { namespace }); + expect(client.update).toHaveBeenCalledWith( + expect.objectContaining({ + id: `${NAMESPACE_AGNOSTIC_TYPE}:${id}`, + }), + expect.anything() + ); + + client.update.mockClear(); + await incrementCounterSuccess(MULTI_NAMESPACE_ISOLATED_TYPE, id, counterFields, { + namespace, + }); + expect(client.update).toHaveBeenCalledWith( + expect.objectContaining({ + id: `${MULTI_NAMESPACE_ISOLATED_TYPE}:${id}`, + }), + expect.anything() + ); + }); + }); + + describe('errors', () => { + const expectUnsupportedTypeError = async ( + type: string, + id: string, + field: Array + ) => { + await expect(savedObjectsRepository.incrementCounter(type, id, field)).rejects.toThrowError( + createUnsupportedTypeError(type) + ); + }; + + it(`throws when options.namespace is '*'`, async () => { + await expect( + savedObjectsRepository.incrementCounter(type, id, counterFields, { + namespace: ALL_NAMESPACES_STRING, + }) + ).rejects.toThrowError(createBadRequestError('"options.namespace" cannot be "*"')); + }); + + it(`throws when type is not a string`, async () => { + const test = async (type: unknown) => { + await expect( + // @ts-expect-error type is supposed to be a string + savedObjectsRepository.incrementCounter(type, id, counterFields) + ).rejects.toThrowError(`"type" argument must be a string`); + expect(client.update).not.toHaveBeenCalled(); + }; + + await test(null); + await test(42); + await test(false); + await test({}); + }); + + it(`throws when counterField is not CounterField type`, async () => { + const test = async (field: unknown[]) => { + await expect( + // @ts-expect-error field is of wrong type + savedObjectsRepository.incrementCounter(type, id, field) + ).rejects.toThrowError( + `"counterFields" argument must be of type Array` + ); + expect(client.update).not.toHaveBeenCalled(); + }; + + await test([null]); + await test([42]); + await test([false]); + await test([{}]); + await test([{}, false, 42, null, 'string']); + await test([{ fieldName: 'string' }, false, null, 'string']); + }); + + it(`throws when type is invalid`, async () => { + await expectUnsupportedTypeError('unknownType', id, counterFields); + expect(client.update).not.toHaveBeenCalled(); + }); + + it(`throws when type is hidden`, async () => { + await expectUnsupportedTypeError(HIDDEN_TYPE, id, counterFields); + expect(client.update).not.toHaveBeenCalled(); + }); + + it(`throws when there is a conflict with an existing multi-namespace saved object (get)`, async () => { + const response = getMockGetResponse( + { type: MULTI_NAMESPACE_ISOLATED_TYPE, id }, + 'bar-namespace' + ); + client.get.mockResolvedValueOnce( + elasticsearchClientMock.createSuccessTransportRequestPromise(response) + ); + await expect( + savedObjectsRepository.incrementCounter( + MULTI_NAMESPACE_ISOLATED_TYPE, + id, + counterFields, + { + namespace, + } + ) + ).rejects.toThrowError(createConflictError(MULTI_NAMESPACE_ISOLATED_TYPE, id)); + expect(client.get).toHaveBeenCalledTimes(1); + expect(mockPreflightCheckForCreate).not.toHaveBeenCalled(); + expect(client.update).not.toHaveBeenCalled(); + }); + + it(`throws when there is an alias conflict from preflightCheckForCreate`, async () => { + client.get.mockResolvedValueOnce( + elasticsearchClientMock.createSuccessTransportRequestPromise({ + found: false, + } as estypes.GetResponse) + ); + mockPreflightCheckForCreate.mockResolvedValue([ + { type: 'foo', id: 'bar', error: { type: 'aliasConflict' } }, + ]); + await expect( + savedObjectsRepository.incrementCounter( + MULTI_NAMESPACE_ISOLATED_TYPE, + id, + counterFields, + { namespace } + ) + ).rejects.toThrowError(createConflictError(MULTI_NAMESPACE_ISOLATED_TYPE, id)); + expect(client.get).toHaveBeenCalledTimes(1); + expect(mockPreflightCheckForCreate).toHaveBeenCalledTimes(1); + expect(client.update).not.toHaveBeenCalled(); + }); + + it(`does not throw when there is a different error from preflightCheckForCreate`, async () => { + client.get.mockResolvedValueOnce( + elasticsearchClientMock.createSuccessTransportRequestPromise({ + found: false, + } as estypes.GetResponse) + ); + mockPreflightCheckForCreate.mockResolvedValue([ + { type: 'foo', id: 'bar', error: { type: 'conflict' } }, + ]); + await incrementCounterSuccess( + MULTI_NAMESPACE_ISOLATED_TYPE, + id, + counterFields, + { namespace }, + { mockGetResponseValue: { found: false } as estypes.GetResponse } + ); + expect(client.get).toHaveBeenCalledTimes(1); + expect(mockPreflightCheckForCreate).toHaveBeenCalledTimes(1); + expect(client.update).toHaveBeenCalledTimes(1); + }); + }); + + describe('migration', () => { + beforeEach(() => { + migrator.migrateDocument.mockImplementation(mockMigrateDocument); + }); + + it(`migrates a document and serializes the migrated doc`, async () => { + const migrationVersion = mockMigrationVersion; + await incrementCounterSuccess(type, id, counterFields, { migrationVersion }); + const attributes = { buildNum: 1, apiCallsCount: 1 }; // this is added by the incrementCounter function + const doc = { type, id, attributes, migrationVersion, ...mockTimestampFields }; + expectMigrationArgs(doc); + + const migratedDoc = migrator.migrateDocument(doc); + expect(serializer.savedObjectToRaw).toHaveBeenLastCalledWith(migratedDoc); + }); + }); + + describe('returns', () => { + it(`formats the ES response`, async () => { + client.update.mockImplementation((params) => + elasticsearchClientMock.createSuccessTransportRequestPromise({ + _id: params.id, + ...mockVersionProps, + _index: '.kibana', + get: { + found: true, + _source: { + type: 'config', + ...mockTimestampFields, + config: { + buildNum: 8468, + apiCallsCount: 100, + defaultIndex: 'logstash-*', + }, + originId, + }, + }, + } as estypes.UpdateResponse) + ); + + const response = await savedObjectsRepository.incrementCounter( + 'config', + '6.0.0-alpha1', + ['buildNum', 'apiCallsCount'], + { + namespace: 'foo-namespace', + } + ); + + expect(response).toEqual({ + type: 'config', + id: '6.0.0-alpha1', + ...mockTimestampFields, + version: mockVersion, + references: [], + attributes: { + buildNum: 8468, + apiCallsCount: 100, + defaultIndex: 'logstash-*', + }, + originId, + }); + }); + + it('increments counter by incrementBy config', async () => { + await incrementCounterSuccess(type, id, [{ fieldName: counterFields[0], incrementBy: 3 }]); + + expect(client.update).toBeCalledTimes(1); + expect(client.update).toBeCalledWith( + expect.objectContaining({ + body: expect.objectContaining({ + script: expect.objectContaining({ + params: expect.objectContaining({ + counterFieldNames: [counterFields[0]], + counts: [3], + }), + }), + }), + }), + expect.anything() + ); + }); + + it('does not increment counter when incrementBy is 0', async () => { + await incrementCounterSuccess(type, id, [{ fieldName: counterFields[0], incrementBy: 0 }]); + + expect(client.update).toBeCalledTimes(1); + expect(client.update).toBeCalledWith( + expect.objectContaining({ + body: expect.objectContaining({ + script: expect.objectContaining({ + params: expect.objectContaining({ + counterFieldNames: [counterFields[0]], + counts: [0], + }), + }), + }), + }), + expect.anything() + ); + }); + }); + }); + + describe('#update', () => { + const id = 'logstash-*'; + const type = 'index-pattern'; + const attributes = { title: 'Testing' }; + const namespace = 'foo-namespace'; + const references = [ + { + name: 'ref_0', + type: 'test', + id: '1', + }, + ]; + const originId = 'some-origin-id'; + + const mockUpdateResponse = ( + type: string, + id: string, + options?: SavedObjectsUpdateOptions, + includeOriginId?: boolean + ) => { + client.update.mockResolvedValueOnce( + elasticsearchClientMock.createSuccessTransportRequestPromise( + { + _id: `${type}:${id}`, + ...mockVersionProps, + result: 'updated', + // don't need the rest of the source for test purposes, just the namespace and namespaces attributes + get: { + _source: { + namespaces: [options?.namespace ?? 'default'], + namespace: options?.namespace, + + // "includeOriginId" is not an option for the operation; however, if the existing saved object contains an originId attribute, the + // operation will return it in the result. This flag is just used for test purposes to modify the mock cluster call response. + ...(includeOriginId && { originId }), + }, + }, + } as estypes.UpdateResponse, + { statusCode: 200 } + ) + ); + }; + + const updateSuccess = async ( + type: string, + id: string, + attributes: T, + options?: SavedObjectsUpdateOptions, + internalOptions: { + includeOriginId?: boolean; + mockGetResponseValue?: estypes.GetResponse; + } = {} + ) => { + const { mockGetResponseValue, includeOriginId } = internalOptions; + if (registry.isMultiNamespace(type)) { + const mockGetResponse = + mockGetResponseValue ?? getMockGetResponse({ type, id }, options?.namespace); + client.get.mockResolvedValueOnce( + elasticsearchClientMock.createSuccessTransportRequestPromise( + { ...mockGetResponse }, + { statusCode: 200 } + ) + ); + } + mockUpdateResponse(type, id, options, includeOriginId); + const result = await savedObjectsRepository.update(type, id, attributes, options); + expect(client.get).toHaveBeenCalledTimes(registry.isMultiNamespace(type) ? 1 : 0); + return result; + }; + + beforeEach(() => { + mockPreflightCheckForCreate.mockReset(); + mockPreflightCheckForCreate.mockImplementation(({ objects }) => { + return Promise.resolve(objects.map(({ type, id }) => ({ type, id }))); // respond with no errors by default + }); + }); + + describe('client calls', () => { + it(`should use the ES update action when type is not multi-namespace`, async () => { + await updateSuccess(type, id, attributes); + expect(client.get).not.toHaveBeenCalled(); + expect(mockPreflightCheckForCreate).not.toHaveBeenCalled(); + expect(client.update).toHaveBeenCalledTimes(1); + }); + + it(`should use the ES get action then update action when type is multi-namespace`, async () => { + await updateSuccess(MULTI_NAMESPACE_ISOLATED_TYPE, id, attributes); + expect(client.get).toHaveBeenCalledTimes(1); + expect(mockPreflightCheckForCreate).not.toHaveBeenCalled(); + expect(client.update).toHaveBeenCalledTimes(1); + }); + + it(`should check for alias conflicts if a new multi-namespace object would be created`, async () => { + await updateSuccess( + MULTI_NAMESPACE_ISOLATED_TYPE, + id, + attributes, + { upsert: true }, + { mockGetResponseValue: { found: false } as estypes.GetResponse } + ); + expect(client.get).toHaveBeenCalledTimes(1); + expect(mockPreflightCheckForCreate).toHaveBeenCalledTimes(1); + expect(client.update).toHaveBeenCalledTimes(1); + }); + + it(`defaults to no references array`, async () => { + await updateSuccess(type, id, attributes); + expect(client.update).toHaveBeenCalledWith( + expect.objectContaining({ + body: { doc: expect.not.objectContaining({ references: expect.anything() }) }, + }), + expect.anything() + ); + }); + + it(`accepts custom references array`, async () => { + const test = async (references: SavedObjectReference[]) => { + await updateSuccess(type, id, attributes, { references }); + expect(client.update).toHaveBeenCalledWith( + expect.objectContaining({ + body: { doc: expect.objectContaining({ references }) }, + }), + expect.anything() + ); + client.update.mockClear(); + }; + await test(references); + await test([{ type: 'foo', id: '42', name: 'some ref' }]); + await test([]); + }); + + it(`uses the 'upsertAttributes' option when specified for a single-namespace type`, async () => { + await updateSuccess(type, id, attributes, { + upsert: { + title: 'foo', + description: 'bar', + }, + }); + expect(client.update).toHaveBeenCalledWith( + expect.objectContaining({ + id: 'index-pattern:logstash-*', + body: expect.objectContaining({ + upsert: expect.objectContaining({ + type: 'index-pattern', + 'index-pattern': { + title: 'foo', + description: 'bar', + }, + }), + }), + }), + expect.anything() + ); + }); + + it(`uses the 'upsertAttributes' option when specified for a multi-namespace type that does not exist`, async () => { + const options = { upsert: { title: 'foo', description: 'bar' } }; + mockUpdateResponse(MULTI_NAMESPACE_ISOLATED_TYPE, id, options); + await savedObjectsRepository.update(MULTI_NAMESPACE_ISOLATED_TYPE, id, attributes, options); + expect(client.get).toHaveBeenCalledTimes(1); + expect(client.update).toHaveBeenCalledWith( + expect.objectContaining({ + id: `${MULTI_NAMESPACE_ISOLATED_TYPE}:logstash-*`, + body: expect.objectContaining({ + upsert: expect.objectContaining({ + type: MULTI_NAMESPACE_ISOLATED_TYPE, + [MULTI_NAMESPACE_ISOLATED_TYPE]: { + title: 'foo', + description: 'bar', + }, + }), + }), + }), + expect.anything() + ); + }); + + it(`ignores use the 'upsertAttributes' option when specified for a multi-namespace type that already exists`, async () => { + const options = { upsert: { title: 'foo', description: 'bar' } }; + await updateSuccess(MULTI_NAMESPACE_ISOLATED_TYPE, id, attributes, options); + expect(client.update).toHaveBeenCalledWith( + expect.objectContaining({ + id: `${MULTI_NAMESPACE_ISOLATED_TYPE}:logstash-*`, + body: expect.not.objectContaining({ + upsert: expect.anything(), + }), + }), + expect.anything() + ); + }); + + it(`doesn't accept custom references if not an array`, async () => { + const test = async (references: unknown) => { + // @ts-expect-error references is unknown + await updateSuccess(type, id, attributes, { references }); + expect(client.update).toHaveBeenCalledWith( + expect.objectContaining({ + body: { doc: expect.not.objectContaining({ references: expect.anything() }) }, + }), + expect.anything() + ); + client.update.mockClear(); + }; + await test('string'); + await test(123); + await test(true); + await test(null); + }); + + it(`defaults to a refresh setting of wait_for`, async () => { + await updateSuccess(type, id, { foo: 'bar' }); + expect(client.update).toHaveBeenCalledWith( + expect.objectContaining({ + refresh: 'wait_for', + }), + expect.anything() + ); + }); + + it(`defaults to the version of the existing document when type is multi-namespace`, async () => { + await updateSuccess(MULTI_NAMESPACE_ISOLATED_TYPE, id, attributes, { references }); + const versionProperties = { + if_seq_no: mockVersionProps._seq_no, + if_primary_term: mockVersionProps._primary_term, + }; + expect(client.update).toHaveBeenCalledWith( + expect.objectContaining(versionProperties), + expect.anything() + ); + }); + + it(`accepts version`, async () => { + await updateSuccess(type, id, attributes, { + version: encodeHitVersion({ _seq_no: 100, _primary_term: 200 }), + }); + expect(client.update).toHaveBeenCalledWith( + expect.objectContaining({ if_seq_no: 100, if_primary_term: 200 }), + expect.anything() + ); + }); + + it(`prepends namespace to the id when providing namespace for single-namespace type`, async () => { + await updateSuccess(type, id, attributes, { namespace }); + expect(client.update).toHaveBeenCalledWith( + expect.objectContaining({ id: expect.stringMatching(`${namespace}:${type}:${id}`) }), + expect.anything() + ); + }); + + it(`doesn't prepend namespace to the id when providing no namespace for single-namespace type`, async () => { + await updateSuccess(type, id, attributes, { references }); + expect(client.update).toHaveBeenCalledWith( + expect.objectContaining({ id: expect.stringMatching(`${type}:${id}`) }), + expect.anything() + ); + }); + + it(`normalizes options.namespace from 'default' to undefined`, async () => { + await updateSuccess(type, id, attributes, { references, namespace: 'default' }); + expect(client.update).toHaveBeenCalledWith( + expect.objectContaining({ id: expect.stringMatching(`${type}:${id}`) }), + expect.anything() + ); + }); + + it(`doesn't prepend namespace to the id when not using single-namespace type`, async () => { + await updateSuccess(NAMESPACE_AGNOSTIC_TYPE, id, attributes, { namespace }); + expect(client.update).toHaveBeenCalledWith( + expect.objectContaining({ + id: expect.stringMatching(`${NAMESPACE_AGNOSTIC_TYPE}:${id}`), + }), + expect.anything() + ); + + client.update.mockClear(); + await updateSuccess(MULTI_NAMESPACE_ISOLATED_TYPE, id, attributes, { namespace }); + expect(client.update).toHaveBeenCalledWith( + expect.objectContaining({ + id: expect.stringMatching(`${MULTI_NAMESPACE_ISOLATED_TYPE}:${id}`), + }), + expect.anything() + ); + }); + + it(`includes _source_includes when type is multi-namespace`, async () => { + await updateSuccess(MULTI_NAMESPACE_ISOLATED_TYPE, id, attributes); + expect(client.update).toHaveBeenCalledWith( + expect.objectContaining({ _source_includes: ['namespace', 'namespaces', 'originId'] }), + expect.anything() + ); + }); + + it(`includes _source_includes when type is not multi-namespace`, async () => { + await updateSuccess(type, id, attributes); + expect(client.update).toHaveBeenLastCalledWith( + expect.objectContaining({ + _source_includes: ['namespace', 'namespaces', 'originId'], + }), + expect.anything() + ); + }); + }); + + describe('errors', () => { + const expectNotFoundError = async (type: string, id: string) => { + await expect(savedObjectsRepository.update(type, id, {})).rejects.toThrowError( + createGenericNotFoundError(type, id) + ); + }; + + it(`throws when options.namespace is '*'`, async () => { + await expect( + savedObjectsRepository.update(type, id, attributes, { namespace: ALL_NAMESPACES_STRING }) + ).rejects.toThrowError(createBadRequestError('"options.namespace" cannot be "*"')); + }); + + it(`throws when type is invalid`, async () => { + await expectNotFoundError('unknownType', id); + expect(client.update).not.toHaveBeenCalled(); + }); + + it(`throws when type is hidden`, async () => { + await expectNotFoundError(HIDDEN_TYPE, id); + expect(client.update).not.toHaveBeenCalled(); + }); + + it(`throws when ES is unable to find the document during get`, async () => { + client.get.mockResolvedValueOnce( + elasticsearchClientMock.createSuccessTransportRequestPromise( + { found: false } as estypes.GetResponse, + undefined + ) + ); + await expectNotFoundError(MULTI_NAMESPACE_ISOLATED_TYPE, id); + expect(client.get).toHaveBeenCalledTimes(1); + }); + + it(`throws when ES is unable to find the index during get`, async () => { + client.get.mockResolvedValueOnce( + elasticsearchClientMock.createSuccessTransportRequestPromise({} as estypes.GetResponse, { + statusCode: 404, + }) + ); + await expectNotFoundError(MULTI_NAMESPACE_ISOLATED_TYPE, id); + expect(client.get).toHaveBeenCalledTimes(1); + }); + + it(`throws when type is multi-namespace and the document exists, but not in this namespace`, async () => { + const response = getMockGetResponse({ type: MULTI_NAMESPACE_ISOLATED_TYPE, id }, namespace); + client.get.mockResolvedValueOnce( + elasticsearchClientMock.createSuccessTransportRequestPromise(response) + ); + await expectNotFoundError(MULTI_NAMESPACE_ISOLATED_TYPE, id); + expect(client.get).toHaveBeenCalledTimes(1); + }); + + it(`throws when there is an alias conflict from preflightCheckForCreate`, async () => { + client.get.mockResolvedValueOnce( + elasticsearchClientMock.createSuccessTransportRequestPromise({ + found: false, + } as estypes.GetResponse) + ); + mockPreflightCheckForCreate.mockResolvedValue([ + { type: 'type', id: 'id', error: { type: 'aliasConflict' } }, + ]); + await expect( + savedObjectsRepository.update( + MULTI_NAMESPACE_ISOLATED_TYPE, + id, + { attr: 'value' }, + { + upsert: { + upsertAttr: 'val', + attr: 'value', + }, + } + ) + ).rejects.toThrowError(createConflictError(MULTI_NAMESPACE_ISOLATED_TYPE, id)); + expect(client.get).toHaveBeenCalledTimes(1); + expect(mockPreflightCheckForCreate).toHaveBeenCalledTimes(1); + expect(client.update).not.toHaveBeenCalled(); + }); + + it(`does not throw when there is a different error from preflightCheckForCreate`, async () => { + mockPreflightCheckForCreate.mockResolvedValue([ + { type: 'type', id: 'id', error: { type: 'conflict' } }, + ]); + await updateSuccess( + MULTI_NAMESPACE_ISOLATED_TYPE, + id, + attributes, + { upsert: true }, + { mockGetResponseValue: { found: false } as estypes.GetResponse } + ); + expect(client.get).toHaveBeenCalledTimes(1); + expect(mockPreflightCheckForCreate).toHaveBeenCalledTimes(1); + expect(client.update).toHaveBeenCalledTimes(1); + }); + + it(`throws when ES is unable to find the document during update`, async () => { + const notFoundError = new EsErrors.ResponseError( + elasticsearchClientMock.createApiResponse({ + statusCode: 404, + body: { error: { type: 'es_type', reason: 'es_reason' } }, + }) + ); + client.update.mockResolvedValueOnce( + elasticsearchClientMock.createErrorTransportRequestPromise(notFoundError) + ); + await expectNotFoundError(type, id); + expect(client.update).toHaveBeenCalledTimes(1); + }); + }); + + describe('returns', () => { + it(`returns _seq_no and _primary_term encoded as version`, async () => { + const result = await updateSuccess(type, id, attributes, { + namespace, + references, + }); + expect(result).toEqual({ + id, + type, + ...mockTimestampFields, + version: mockVersion, + attributes, + references, + namespaces: [namespace], + }); + }); + + it(`includes namespaces if type is multi-namespace`, async () => { + const result = await updateSuccess(MULTI_NAMESPACE_ISOLATED_TYPE, id, attributes); + expect(result).toMatchObject({ + namespaces: expect.any(Array), + }); + }); + + it(`includes namespaces if type is not multi-namespace`, async () => { + const result = await updateSuccess(type, id, attributes); + expect(result).toMatchObject({ + namespaces: ['default'], + }); + }); + + it(`includes originId property if present in cluster call response`, async () => { + const result = await updateSuccess(type, id, attributes, {}, { includeOriginId: true }); + expect(result).toMatchObject({ originId }); + }); + }); + }); + + describe('#openPointInTimeForType', () => { + const type = 'index-pattern'; + + const generateResults = (id?: string) => ({ id: id || 'id' }); + const successResponse = async (type: string, options?: SavedObjectsOpenPointInTimeOptions) => { + client.openPointInTime.mockResolvedValueOnce( + elasticsearchClientMock.createSuccessTransportRequestPromise(generateResults()) + ); + const result = await savedObjectsRepository.openPointInTimeForType(type, options); + expect(client.openPointInTime).toHaveBeenCalledTimes(1); + return result; + }; + + describe('client calls', () => { + it(`should use the ES PIT API`, async () => { + await successResponse(type); + expect(client.openPointInTime).toHaveBeenCalledTimes(1); + }); + + it(`accepts preference`, async () => { + await successResponse(type, { preference: 'pref' }); + expect(client.openPointInTime).toHaveBeenCalledWith( + expect.objectContaining({ + preference: 'pref', + }), + expect.anything() + ); + }); + + it(`accepts keepAlive`, async () => { + await successResponse(type, { keepAlive: '2m' }); + expect(client.openPointInTime).toHaveBeenCalledWith( + expect.objectContaining({ + keep_alive: '2m', + }), + expect.anything() + ); + }); + + it(`defaults keepAlive to 5m`, async () => { + await successResponse(type); + expect(client.openPointInTime).toHaveBeenCalledWith( + expect.objectContaining({ + keep_alive: '5m', + }), + expect.anything() + ); + }); + }); + + describe('errors', () => { + const expectNotFoundError = async (types: string | string[]) => { + await expect(savedObjectsRepository.openPointInTimeForType(types)).rejects.toThrowError( + createGenericNotFoundError() + ); + }; + + it(`throws when ES is unable to find the index`, async () => { + client.openPointInTime.mockResolvedValueOnce( + elasticsearchClientMock.createSuccessTransportRequestPromise( + { id: 'error' }, + { statusCode: 404 } + ) + ); + await expectNotFoundError(type); + expect(client.openPointInTime).toHaveBeenCalledTimes(1); + }); + + it(`should return generic not found error when attempting to find only invalid or hidden types`, async () => { + const test = async (types: string | string[]) => { + await expectNotFoundError(types); + expect(client.openPointInTime).not.toHaveBeenCalled(); + }; + + await test('unknownType'); + await test(HIDDEN_TYPE); + await test(['unknownType', HIDDEN_TYPE]); + }); + }); + + describe('returns', () => { + it(`returns id in the expected format`, async () => { + const id = 'abc123'; + const results = generateResults(id); + client.openPointInTime.mockResolvedValueOnce( + elasticsearchClientMock.createSuccessTransportRequestPromise(results) + ); + const response = await savedObjectsRepository.openPointInTimeForType(type); + expect(response).toEqual({ id }); + }); + }); + }); + + describe('#closePointInTime', () => { + const generateResults = () => ({ succeeded: true, num_freed: 3 }); + const successResponse = async (id: string) => { + client.closePointInTime.mockResolvedValueOnce( + elasticsearchClientMock.createSuccessTransportRequestPromise(generateResults()) + ); + const result = await savedObjectsRepository.closePointInTime(id); + expect(client.closePointInTime).toHaveBeenCalledTimes(1); + return result; + }; + + describe('client calls', () => { + it(`should use the ES PIT API`, async () => { + await successResponse('abc123'); + expect(client.closePointInTime).toHaveBeenCalledTimes(1); + }); + + it(`accepts id`, async () => { + await successResponse('abc123'); + expect(client.closePointInTime).toHaveBeenCalledWith( + expect.objectContaining({ + body: expect.objectContaining({ + id: 'abc123', + }), + }), + expect.anything() + ); + }); + }); + + describe('returns', () => { + it(`returns response body from ES`, async () => { + const results = generateResults(); + client.closePointInTime.mockResolvedValueOnce( + elasticsearchClientMock.createSuccessTransportRequestPromise(results) + ); + const response = await savedObjectsRepository.closePointInTime('abc123'); + expect(response).toEqual(results); + }); + }); + }); + + describe('#createPointInTimeFinder', () => { + it('returns a new PointInTimeFinder instance', async () => { + const result = await savedObjectsRepository.createPointInTimeFinder({ type: 'PIT' }); + expect(result).toBeInstanceOf(PointInTimeFinder); + }); + + it('calls PointInTimeFinder with the provided options and dependencies', async () => { + const options: SavedObjectsCreatePointInTimeFinderOptions = { + type: 'my-type', + }; + const dependencies: SavedObjectsCreatePointInTimeFinderDependencies = { + client: { + find: jest.fn(), + openPointInTimeForType: jest.fn(), + closePointInTime: jest.fn(), + }, + }; + + await savedObjectsRepository.createPointInTimeFinder(options, dependencies); + expect(pointInTimeFinderMock).toHaveBeenCalledWith( + options, + expect.objectContaining({ + ...dependencies, + logger, + }) + ); + }); + }); + + describe('#collectMultiNamespaceReferences', () => { + afterEach(() => { + mockCollectMultiNamespaceReferences.mockReset(); + }); + + it('passes arguments to the collectMultiNamespaceReferences module and returns the result', async () => { + const objects: SavedObjectsCollectMultiNamespaceReferencesObject[] = [ + { type: 'foo', id: 'bar' }, + ]; + const expectedResult: SavedObjectsCollectMultiNamespaceReferencesResponse = { + objects: [{ type: 'foo', id: 'bar', spaces: ['ns-1'], inboundReferences: [] }], + }; + mockCollectMultiNamespaceReferences.mockResolvedValue(expectedResult); + + await expect( + savedObjectsRepository.collectMultiNamespaceReferences(objects) + ).resolves.toEqual(expectedResult); + expect(mockCollectMultiNamespaceReferences).toHaveBeenCalledTimes(1); + expect(mockCollectMultiNamespaceReferences).toHaveBeenCalledWith( + expect.objectContaining({ objects }) + ); + }); + + it('returns an error from the collectMultiNamespaceReferences module', async () => { + const expectedResult = new Error('Oh no!'); + mockCollectMultiNamespaceReferences.mockRejectedValue(expectedResult); + + await expect(savedObjectsRepository.collectMultiNamespaceReferences([])).rejects.toEqual( + expectedResult + ); + }); + }); + + describe('#updateObjectsSpaces', () => { + afterEach(() => { + mockUpdateObjectsSpaces.mockReset(); + }); + + it('passes arguments to the updateObjectsSpaces module and returns the result', async () => { + const objects: SavedObjectsUpdateObjectsSpacesObject[] = [{ type: 'type', id: 'id' }]; + const spacesToAdd = ['to-add', 'also-to-add']; + const spacesToRemove = ['to-remove']; + const options: SavedObjectsUpdateObjectsSpacesOptions = { namespace: 'ns-1' }; + const expectedResult: SavedObjectsUpdateObjectsSpacesResponse = { + objects: [ + { + type: 'type', + id: 'id', + spaces: ['foo', 'bar'], + }, + ], + }; + mockUpdateObjectsSpaces.mockResolvedValue(expectedResult); + + await expect( + savedObjectsRepository.updateObjectsSpaces(objects, spacesToAdd, spacesToRemove, options) + ).resolves.toEqual(expectedResult); + expect(mockUpdateObjectsSpaces).toHaveBeenCalledTimes(1); + expect(mockUpdateObjectsSpaces).toHaveBeenCalledWith( + expect.objectContaining({ objects, spacesToAdd, spacesToRemove, options }) + ); + }); + + it('returns an error from the updateObjectsSpaces module', async () => { + const expectedResult = new Error('Oh no!'); + mockUpdateObjectsSpaces.mockRejectedValue(expectedResult); + + await expect(savedObjectsRepository.updateObjectsSpaces([], [], [])).rejects.toEqual( + expectedResult + ); + }); + }); +}); diff --git a/src/core/server/saved_objects/service/lib/repository_create_repository.test.ts b/src/core/server/saved_objects/service/lib/repository_create_repository.test.ts index a87f24a1eae14..2d03fff29df10 100644 --- a/src/core/server/saved_objects/service/lib/repository_create_repository.test.ts +++ b/src/core/server/saved_objects/service/lib/repository_create_repository.test.ts @@ -7,7 +7,7 @@ */ import { SavedObjectsRepository } from './repository'; -import { mockKibanaMigrator } from '../../migrations/kibana/kibana_migrator.mock'; +import { mockKibanaMigrator } from '../../migrations/kibana_migrator.mock'; import { KibanaMigrator } from '../../migrations'; import { SavedObjectTypeRegistry } from '../../saved_objects_type_registry'; import { loggerMock, MockedLogger } from '../../../logging/logger.mock'; diff --git a/src/core/server/saved_objects/service/lib/scoped_client_provider.test.js b/src/core/server/saved_objects/service/lib/scoped_client_provider.test.js deleted file mode 100644 index 506c354d5adf0..0000000000000 --- a/src/core/server/saved_objects/service/lib/scoped_client_provider.test.js +++ /dev/null @@ -1,178 +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 { SavedObjectsClientProvider } from './scoped_client_provider'; -import { typeRegistryMock } from '../../saved_objects_type_registry.mock'; - -test(`uses default client factory when one isn't set`, () => { - const returnValue = Symbol(); - const defaultClientFactoryMock = jest.fn().mockReturnValue(returnValue); - const request = Symbol(); - - const clientProvider = new SavedObjectsClientProvider({ - defaultClientFactory: defaultClientFactoryMock, - typeRegistry: typeRegistryMock.create(), - }); - const result = clientProvider.getClient(request); - - expect(result).toBe(returnValue); - expect(defaultClientFactoryMock).toHaveBeenCalledTimes(1); - expect(defaultClientFactoryMock).toHaveBeenCalledWith({ - request, - }); -}); - -test(`uses custom client factory when one is set`, () => { - const defaultClientFactoryMock = jest.fn(); - const request = Symbol(); - const returnValue = Symbol(); - const customClientFactoryMock = jest.fn().mockReturnValue(returnValue); - - const clientProvider = new SavedObjectsClientProvider({ - defaultClientFactory: defaultClientFactoryMock, - typeRegistry: typeRegistryMock.create(), - }); - clientProvider.setClientFactory(customClientFactoryMock); - const result = clientProvider.getClient(request); - - expect(result).toBe(returnValue); - expect(defaultClientFactoryMock).not.toHaveBeenCalled(); - expect(customClientFactoryMock).toHaveBeenCalledTimes(1); - expect(customClientFactoryMock).toHaveBeenCalledWith({ - request, - }); -}); - -test(`throws error when more than one scoped saved objects client factory is set`, () => { - const clientProvider = new SavedObjectsClientProvider({}); - clientProvider.setClientFactory(() => {}); - expect(() => { - clientProvider.setClientFactory(() => {}); - }).toThrowErrorMatchingSnapshot(); -}); - -test(`throws error when registering a wrapper with a duplicate id`, () => { - const defaultClientFactoryMock = jest.fn(); - const clientProvider = new SavedObjectsClientProvider({ - defaultClientFactory: defaultClientFactoryMock, - typeRegistry: typeRegistryMock.create(), - }); - const firstClientWrapperFactoryMock = jest.fn(); - const secondClientWrapperFactoryMock = jest.fn(); - - clientProvider.addClientWrapperFactory(1, 'foo', secondClientWrapperFactoryMock); - expect(() => - clientProvider.addClientWrapperFactory(0, 'foo', firstClientWrapperFactoryMock) - ).toThrowErrorMatchingInlineSnapshot(`"wrapper factory with id foo is already defined"`); -}); - -test(`invokes and uses wrappers in specified order`, () => { - const defaultClient = Symbol(); - const typeRegistry = typeRegistryMock.create(); - const defaultClientFactoryMock = jest.fn().mockReturnValue(defaultClient); - const clientProvider = new SavedObjectsClientProvider({ - defaultClientFactory: defaultClientFactoryMock, - typeRegistry, - }); - const firstWrappedClient = Symbol('first client'); - const firstClientWrapperFactoryMock = jest.fn().mockReturnValue(firstWrappedClient); - const secondWrapperClient = Symbol('second client'); - const secondClientWrapperFactoryMock = jest.fn().mockReturnValue(secondWrapperClient); - const request = Symbol(); - - clientProvider.addClientWrapperFactory(1, 'foo', secondClientWrapperFactoryMock); - clientProvider.addClientWrapperFactory(0, 'bar', firstClientWrapperFactoryMock); - const actualClient = clientProvider.getClient(request); - - expect(actualClient).toBe(firstWrappedClient); - expect(firstClientWrapperFactoryMock).toHaveBeenCalledWith({ - request, - client: secondWrapperClient, - typeRegistry, - }); - expect(secondClientWrapperFactoryMock).toHaveBeenCalledWith({ - request, - client: defaultClient, - typeRegistry, - }); -}); - -test(`does not invoke or use excluded wrappers`, () => { - const defaultClient = Symbol(); - const typeRegistry = typeRegistryMock.create(); - const defaultClientFactoryMock = jest.fn().mockReturnValue(defaultClient); - const clientProvider = new SavedObjectsClientProvider({ - defaultClientFactory: defaultClientFactoryMock, - typeRegistry, - }); - const firstWrappedClient = Symbol('first client'); - const firstClientWrapperFactoryMock = jest.fn().mockReturnValue(firstWrappedClient); - const secondWrapperClient = Symbol('second client'); - const secondClientWrapperFactoryMock = jest.fn().mockReturnValue(secondWrapperClient); - const request = Symbol(); - - clientProvider.addClientWrapperFactory(1, 'foo', secondClientWrapperFactoryMock); - clientProvider.addClientWrapperFactory(0, 'bar', firstClientWrapperFactoryMock); - - const actualClient = clientProvider.getClient(request, { - excludedWrappers: ['foo'], - }); - - expect(actualClient).toBe(firstWrappedClient); - expect(firstClientWrapperFactoryMock).toHaveBeenCalledWith({ - request, - client: defaultClient, - typeRegistry, - }); - expect(secondClientWrapperFactoryMock).not.toHaveBeenCalled(); -}); - -test(`allows all wrappers to be excluded`, () => { - const defaultClient = Symbol(); - const defaultClientFactoryMock = jest.fn().mockReturnValue(defaultClient); - const clientProvider = new SavedObjectsClientProvider({ - defaultClientFactory: defaultClientFactoryMock, - typeRegistry: typeRegistryMock.create(), - }); - const firstWrappedClient = Symbol('first client'); - const firstClientWrapperFactoryMock = jest.fn().mockReturnValue(firstWrappedClient); - const secondWrapperClient = Symbol('second client'); - const secondClientWrapperFactoryMock = jest.fn().mockReturnValue(secondWrapperClient); - const request = Symbol(); - - clientProvider.addClientWrapperFactory(1, 'foo', secondClientWrapperFactoryMock); - clientProvider.addClientWrapperFactory(0, 'bar', firstClientWrapperFactoryMock); - - const actualClient = clientProvider.getClient(request, { - excludedWrappers: ['foo', 'bar'], - }); - - expect(actualClient).toBe(defaultClient); - expect(firstClientWrapperFactoryMock).not.toHaveBeenCalled(); - expect(secondClientWrapperFactoryMock).not.toHaveBeenCalled(); -}); - -test(`allows hidden typed to be included`, () => { - const defaultClient = Symbol(); - const defaultClientFactoryMock = jest.fn().mockReturnValue(defaultClient); - const clientProvider = new SavedObjectsClientProvider({ - defaultClientFactory: defaultClientFactoryMock, - typeRegistry: typeRegistryMock.create(), - }); - const request = Symbol(); - - const actualClient = clientProvider.getClient(request, { - includedHiddenTypes: ['task'], - }); - - expect(actualClient).toBe(defaultClient); - expect(defaultClientFactoryMock).toHaveBeenCalledWith({ - request, - includedHiddenTypes: ['task'], - }); -}); diff --git a/src/core/server/saved_objects/service/lib/scoped_client_provider.test.ts b/src/core/server/saved_objects/service/lib/scoped_client_provider.test.ts new file mode 100644 index 0000000000000..f8a81e48bbbc3 --- /dev/null +++ b/src/core/server/saved_objects/service/lib/scoped_client_provider.test.ts @@ -0,0 +1,188 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 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 { SavedObjectsClientProvider } from './scoped_client_provider'; +import { httpServerMock } from '../../../http/http_server.mocks'; +import { typeRegistryMock } from '../../saved_objects_type_registry.mock'; + +test(`uses default client factory when one isn't set`, () => { + const returnValue = Symbol(); + const defaultClientFactoryMock = jest.fn().mockReturnValue(returnValue); + const request = httpServerMock.createKibanaRequest(); + + const clientProvider = new SavedObjectsClientProvider({ + defaultClientFactory: defaultClientFactoryMock, + typeRegistry: typeRegistryMock.create(), + }); + const result = clientProvider.getClient(request); + + expect(result).toBe(returnValue); + expect(defaultClientFactoryMock).toHaveBeenCalledTimes(1); + expect(defaultClientFactoryMock).toHaveBeenCalledWith({ + request, + }); +}); + +test(`uses custom client factory when one is set`, () => { + const defaultClientFactoryMock = jest.fn(); + const request = httpServerMock.createKibanaRequest(); + const returnValue = Symbol(); + const customClientFactoryMock = jest.fn().mockReturnValue(returnValue); + + const clientProvider = new SavedObjectsClientProvider({ + defaultClientFactory: defaultClientFactoryMock, + typeRegistry: typeRegistryMock.create(), + }); + clientProvider.setClientFactory(customClientFactoryMock); + const result = clientProvider.getClient(request); + + expect(result).toBe(returnValue); + expect(defaultClientFactoryMock).not.toHaveBeenCalled(); + expect(customClientFactoryMock).toHaveBeenCalledTimes(1); + expect(customClientFactoryMock).toHaveBeenCalledWith({ + request, + }); +}); + +test(`throws error when more than one scoped saved objects client factory is set`, () => { + const defaultClientFactory = jest.fn(); + const clientFactory = jest.fn(); + + const clientProvider = new SavedObjectsClientProvider({ + defaultClientFactory, + typeRegistry: typeRegistryMock.create(), + }); + + clientProvider.setClientFactory(clientFactory); + expect(() => { + clientProvider.setClientFactory(clientFactory); + }).toThrowErrorMatchingInlineSnapshot( + `"custom client factory is already set, unable to replace the current one"` + ); +}); + +test(`throws error when registering a wrapper with a duplicate id`, () => { + const defaultClientFactoryMock = jest.fn(); + const clientProvider = new SavedObjectsClientProvider({ + defaultClientFactory: defaultClientFactoryMock, + typeRegistry: typeRegistryMock.create(), + }); + const firstClientWrapperFactoryMock = jest.fn(); + const secondClientWrapperFactoryMock = jest.fn(); + + clientProvider.addClientWrapperFactory(1, 'foo', secondClientWrapperFactoryMock); + expect(() => + clientProvider.addClientWrapperFactory(0, 'foo', firstClientWrapperFactoryMock) + ).toThrowErrorMatchingInlineSnapshot(`"wrapper factory with id foo is already defined"`); +}); + +test(`invokes and uses wrappers in specified order`, () => { + const defaultClient = Symbol(); + const typeRegistry = typeRegistryMock.create(); + const defaultClientFactoryMock = jest.fn().mockReturnValue(defaultClient); + const clientProvider = new SavedObjectsClientProvider({ + defaultClientFactory: defaultClientFactoryMock, + typeRegistry, + }); + const firstWrappedClient = Symbol('first client'); + const firstClientWrapperFactoryMock = jest.fn().mockReturnValue(firstWrappedClient); + const secondWrapperClient = Symbol('second client'); + const secondClientWrapperFactoryMock = jest.fn().mockReturnValue(secondWrapperClient); + const request = httpServerMock.createKibanaRequest(); + + clientProvider.addClientWrapperFactory(1, 'foo', secondClientWrapperFactoryMock); + clientProvider.addClientWrapperFactory(0, 'bar', firstClientWrapperFactoryMock); + const actualClient = clientProvider.getClient(request); + + expect(actualClient).toBe(firstWrappedClient); + expect(firstClientWrapperFactoryMock).toHaveBeenCalledWith({ + request, + client: secondWrapperClient, + typeRegistry, + }); + expect(secondClientWrapperFactoryMock).toHaveBeenCalledWith({ + request, + client: defaultClient, + typeRegistry, + }); +}); + +test(`does not invoke or use excluded wrappers`, () => { + const defaultClient = Symbol(); + const typeRegistry = typeRegistryMock.create(); + const defaultClientFactoryMock = jest.fn().mockReturnValue(defaultClient); + const clientProvider = new SavedObjectsClientProvider({ + defaultClientFactory: defaultClientFactoryMock, + typeRegistry, + }); + const firstWrappedClient = Symbol('first client'); + const firstClientWrapperFactoryMock = jest.fn().mockReturnValue(firstWrappedClient); + const secondWrapperClient = Symbol('second client'); + const secondClientWrapperFactoryMock = jest.fn().mockReturnValue(secondWrapperClient); + const request = httpServerMock.createKibanaRequest(); + + clientProvider.addClientWrapperFactory(1, 'foo', secondClientWrapperFactoryMock); + clientProvider.addClientWrapperFactory(0, 'bar', firstClientWrapperFactoryMock); + + const actualClient = clientProvider.getClient(request, { + excludedWrappers: ['foo'], + }); + + expect(actualClient).toBe(firstWrappedClient); + expect(firstClientWrapperFactoryMock).toHaveBeenCalledWith({ + request, + client: defaultClient, + typeRegistry, + }); + expect(secondClientWrapperFactoryMock).not.toHaveBeenCalled(); +}); + +test(`allows all wrappers to be excluded`, () => { + const defaultClient = Symbol(); + const defaultClientFactoryMock = jest.fn().mockReturnValue(defaultClient); + const clientProvider = new SavedObjectsClientProvider({ + defaultClientFactory: defaultClientFactoryMock, + typeRegistry: typeRegistryMock.create(), + }); + const firstWrappedClient = Symbol('first client'); + const firstClientWrapperFactoryMock = jest.fn().mockReturnValue(firstWrappedClient); + const secondWrapperClient = Symbol('second client'); + const secondClientWrapperFactoryMock = jest.fn().mockReturnValue(secondWrapperClient); + const request = httpServerMock.createKibanaRequest(); + + clientProvider.addClientWrapperFactory(1, 'foo', secondClientWrapperFactoryMock); + clientProvider.addClientWrapperFactory(0, 'bar', firstClientWrapperFactoryMock); + + const actualClient = clientProvider.getClient(request, { + excludedWrappers: ['foo', 'bar'], + }); + + expect(actualClient).toBe(defaultClient); + expect(firstClientWrapperFactoryMock).not.toHaveBeenCalled(); + expect(secondClientWrapperFactoryMock).not.toHaveBeenCalled(); +}); + +test(`allows hidden typed to be included`, () => { + const defaultClient = Symbol(); + const defaultClientFactoryMock = jest.fn().mockReturnValue(defaultClient); + const clientProvider = new SavedObjectsClientProvider({ + defaultClientFactory: defaultClientFactoryMock, + typeRegistry: typeRegistryMock.create(), + }); + const request = httpServerMock.createKibanaRequest(); + + const actualClient = clientProvider.getClient(request, { + includedHiddenTypes: ['task'], + }); + + expect(actualClient).toBe(defaultClient); + expect(defaultClientFactoryMock).toHaveBeenCalledWith({ + request, + includedHiddenTypes: ['task'], + }); +}); diff --git a/src/core/server/saved_objects/service/saved_objects_client.test.js b/src/core/server/saved_objects/service/saved_objects_client.test.js deleted file mode 100644 index 736e6e06da905..0000000000000 --- a/src/core/server/saved_objects/service/saved_objects_client.test.js +++ /dev/null @@ -1,290 +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 { SavedObjectsClient } from './saved_objects_client'; - -test(`#create`, async () => { - const returnValue = Symbol(); - const mockRepository = { - create: jest.fn().mockResolvedValue(returnValue), - }; - const client = new SavedObjectsClient(mockRepository); - - const type = Symbol(); - const attributes = Symbol(); - const options = Symbol(); - const result = await client.create(type, attributes, options); - - expect(mockRepository.create).toHaveBeenCalledWith(type, attributes, options); - expect(result).toBe(returnValue); -}); - -test(`#checkConflicts`, async () => { - const returnValue = Symbol(); - const mockRepository = { - checkConflicts: jest.fn().mockResolvedValue(returnValue), - }; - const client = new SavedObjectsClient(mockRepository); - - const objects = Symbol(); - const options = Symbol(); - const result = await client.checkConflicts(objects, options); - - expect(mockRepository.checkConflicts).toHaveBeenCalledWith(objects, options); - expect(result).toBe(returnValue); -}); - -test(`#bulkCreate`, async () => { - const returnValue = Symbol(); - const mockRepository = { - bulkCreate: jest.fn().mockResolvedValue(returnValue), - }; - const client = new SavedObjectsClient(mockRepository); - - const objects = Symbol(); - const options = Symbol(); - const result = await client.bulkCreate(objects, options); - - expect(mockRepository.bulkCreate).toHaveBeenCalledWith(objects, options); - expect(result).toBe(returnValue); -}); - -describe(`#createPointInTimeFinder`, () => { - test(`calls repository with options and default dependencies`, () => { - const returnValue = Symbol(); - const mockRepository = { - createPointInTimeFinder: jest.fn().mockReturnValue(returnValue), - }; - const client = new SavedObjectsClient(mockRepository); - - const options = Symbol(); - const result = client.createPointInTimeFinder(options); - - expect(mockRepository.createPointInTimeFinder).toHaveBeenCalledWith(options, { - client, - }); - expect(result).toBe(returnValue); - }); - - test(`calls repository with options and custom dependencies`, () => { - const returnValue = Symbol(); - const mockRepository = { - createPointInTimeFinder: jest.fn().mockReturnValue(returnValue), - }; - const client = new SavedObjectsClient(mockRepository); - - const options = Symbol(); - const dependencies = { - client: { - find: Symbol(), - openPointInTimeForType: Symbol(), - closePointInTime: Symbol(), - }, - }; - const result = client.createPointInTimeFinder(options, dependencies); - - expect(mockRepository.createPointInTimeFinder).toHaveBeenCalledWith(options, dependencies); - expect(result).toBe(returnValue); - }); -}); - -test(`#delete`, async () => { - const returnValue = Symbol(); - const mockRepository = { - delete: jest.fn().mockResolvedValue(returnValue), - }; - const client = new SavedObjectsClient(mockRepository); - - const type = Symbol(); - const id = Symbol(); - const options = Symbol(); - const result = await client.delete(type, id, options); - - expect(mockRepository.delete).toHaveBeenCalledWith(type, id, options); - expect(result).toBe(returnValue); -}); - -test(`#find`, async () => { - const returnValue = Symbol(); - const mockRepository = { - find: jest.fn().mockResolvedValue(returnValue), - }; - const client = new SavedObjectsClient(mockRepository); - - const options = Symbol(); - const result = await client.find(options); - - expect(mockRepository.find).toHaveBeenCalledWith(options); - expect(result).toBe(returnValue); -}); - -test(`#bulkGet`, async () => { - const returnValue = Symbol(); - const mockRepository = { - bulkGet: jest.fn().mockResolvedValue(returnValue), - }; - const client = new SavedObjectsClient(mockRepository); - - const objects = Symbol(); - const options = Symbol(); - const result = await client.bulkGet(objects, options); - - expect(mockRepository.bulkGet).toHaveBeenCalledWith(objects, options); - expect(result).toBe(returnValue); -}); - -test(`#get`, async () => { - const returnValue = Symbol(); - const mockRepository = { - get: jest.fn().mockResolvedValue(returnValue), - }; - const client = new SavedObjectsClient(mockRepository); - - const type = Symbol(); - const id = Symbol(); - const options = Symbol(); - const result = await client.get(type, id, options); - - expect(mockRepository.get).toHaveBeenCalledWith(type, id, options); - expect(result).toBe(returnValue); -}); - -test(`#openPointInTimeForType`, async () => { - const returnValue = Symbol(); - const mockRepository = { - openPointInTimeForType: jest.fn().mockResolvedValue(returnValue), - }; - const client = new SavedObjectsClient(mockRepository); - - const type = Symbol(); - const options = Symbol(); - const result = await client.openPointInTimeForType(type, options); - - expect(mockRepository.openPointInTimeForType).toHaveBeenCalledWith(type, options); - expect(result).toBe(returnValue); -}); - -test(`#closePointInTime`, async () => { - const returnValue = Symbol(); - const mockRepository = { - closePointInTime: jest.fn().mockResolvedValue(returnValue), - }; - const client = new SavedObjectsClient(mockRepository); - - const id = Symbol(); - const options = Symbol(); - const result = await client.closePointInTime(id, options); - - expect(mockRepository.closePointInTime).toHaveBeenCalledWith(id, options); - expect(result).toBe(returnValue); -}); - -test(`#bulkResolve`, async () => { - const returnValue = Symbol(); - const mockRepository = { - bulkResolve: jest.fn().mockResolvedValue(returnValue), - }; - const client = new SavedObjectsClient(mockRepository); - - const objects = Symbol(); - const options = Symbol(); - const result = await client.bulkResolve(objects, options); - - expect(mockRepository.bulkResolve).toHaveBeenCalledWith(objects, options); - expect(result).toBe(returnValue); -}); - -test(`#resolve`, async () => { - const returnValue = Symbol(); - const mockRepository = { - resolve: jest.fn().mockResolvedValue(returnValue), - }; - const client = new SavedObjectsClient(mockRepository); - - const type = Symbol(); - const id = Symbol(); - const options = Symbol(); - const result = await client.resolve(type, id, options); - - expect(mockRepository.resolve).toHaveBeenCalledWith(type, id, options); - expect(result).toBe(returnValue); -}); - -test(`#update`, async () => { - const returnValue = Symbol(); - const mockRepository = { - update: jest.fn().mockResolvedValue(returnValue), - }; - const client = new SavedObjectsClient(mockRepository); - - const type = Symbol(); - const id = Symbol(); - const attributes = Symbol(); - const options = Symbol(); - const result = await client.update(type, id, attributes, options); - - expect(mockRepository.update).toHaveBeenCalledWith(type, id, attributes, options); - expect(result).toBe(returnValue); -}); - -test(`#bulkUpdate`, async () => { - const returnValue = Symbol(); - const mockRepository = { - bulkUpdate: jest.fn().mockResolvedValue(returnValue), - }; - const client = new SavedObjectsClient(mockRepository); - - const type = Symbol(); - const id = Symbol(); - const attributes = Symbol(); - const version = Symbol(); - const namespace = Symbol(); - const result = await client.bulkUpdate([{ type, id, attributes, version }], { namespace }); - - expect(mockRepository.bulkUpdate).toHaveBeenCalledWith([{ type, id, attributes, version }], { - namespace, - }); - expect(result).toBe(returnValue); -}); - -test(`#collectMultiNamespaceReferences`, async () => { - const returnValue = Symbol(); - const mockRepository = { - collectMultiNamespaceReferences: jest.fn().mockResolvedValue(returnValue), - }; - const client = new SavedObjectsClient(mockRepository); - - const objects = Symbol(); - const options = Symbol(); - const result = await client.collectMultiNamespaceReferences(objects, options); - - expect(mockRepository.collectMultiNamespaceReferences).toHaveBeenCalledWith(objects, options); - expect(result).toBe(returnValue); -}); - -test(`#updateObjectsSpaces`, async () => { - const returnValue = Symbol(); - const mockRepository = { - updateObjectsSpaces: jest.fn().mockResolvedValue(returnValue), - }; - const client = new SavedObjectsClient(mockRepository); - - const objects = Symbol(); - const spacesToAdd = Symbol(); - const spacesToRemove = Symbol(); - const options = Symbol(); - const result = await client.updateObjectsSpaces(objects, spacesToAdd, spacesToRemove, options); - - expect(mockRepository.updateObjectsSpaces).toHaveBeenCalledWith( - objects, - spacesToAdd, - spacesToRemove, - options - ); - expect(result).toBe(returnValue); -}); diff --git a/src/core/server/saved_objects/service/saved_objects_client.test.ts b/src/core/server/saved_objects/service/saved_objects_client.test.ts new file mode 100644 index 0000000000000..3c24df6dcc1a4 --- /dev/null +++ b/src/core/server/saved_objects/service/saved_objects_client.test.ts @@ -0,0 +1,314 @@ +/* + * Copyright 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 { + SavedObjectsBulkCreateObject, + SavedObjectsBulkGetObject, + SavedObjectsBulkResolveObject, + SavedObjectsCheckConflictsObject, + SavedObjectsClient, + SavedObjectsClosePointInTimeOptions, + SavedObjectsCreateOptions, + SavedObjectsDeleteOptions, + SavedObjectsOpenPointInTimeOptions, + SavedObjectsUpdateOptions, +} from './saved_objects_client'; +import { savedObjectsRepositoryMock } from './lib/repository.mock'; +import { savedObjectsClientMock } from './saved_objects_client.mock'; +import { + SavedObjectsBaseOptions, + SavedObjectsCollectMultiNamespaceReferencesObject, + SavedObjectsCollectMultiNamespaceReferencesOptions, + SavedObjectsCreatePointInTimeFinderOptions, + SavedObjectsFindOptions, + SavedObjectsUpdateObjectsSpacesObject, + SavedObjectsUpdateObjectsSpacesOptions, +} from 'kibana/server'; + +describe('', () => { + let mockRepository: ReturnType; + + beforeEach(() => { + mockRepository = savedObjectsRepositoryMock.create(); + }); + + test(`#create`, async () => { + const returnValue: any = Symbol(); + mockRepository.create.mockResolvedValueOnce(returnValue); + const client = new SavedObjectsClient(mockRepository); + + const type = 'foo'; + const attributes = { string: 'str', number: 12 }; + const options: SavedObjectsCreateOptions = { id: 'id', namespace: 'bar' }; + const result = await client.create(type, attributes, options); + + expect(mockRepository.create).toHaveBeenCalledWith(type, attributes, options); + expect(result).toBe(returnValue); + }); + + test(`#checkConflicts`, async () => { + const returnValue: any = Symbol(); + mockRepository.checkConflicts.mockResolvedValueOnce(returnValue); + const client = new SavedObjectsClient(mockRepository); + + const objects: SavedObjectsCheckConflictsObject[] = [ + { id: '1', type: 'foo' }, + { id: '2', type: 'bar' }, + ]; + const options: SavedObjectsBaseOptions = { namespace: 'ns-1' }; + const result = await client.checkConflicts(objects, options); + + expect(mockRepository.checkConflicts).toHaveBeenCalledWith(objects, options); + expect(result).toBe(returnValue); + }); + + test(`#bulkCreate`, async () => { + const returnValue: any = Symbol(); + mockRepository.bulkCreate.mockResolvedValueOnce(returnValue); + const client = new SavedObjectsClient(mockRepository); + + const objects: SavedObjectsBulkCreateObject[] = [ + { type: 'foo', attributes: { hello: 'dolly' } }, + { type: 'bar', attributes: { answer: 42 } }, + ]; + const options: SavedObjectsCreateOptions = { namespace: 'new-ns', refresh: true }; + const result = await client.bulkCreate(objects, options); + + expect(mockRepository.bulkCreate).toHaveBeenCalledWith(objects, options); + expect(result).toBe(returnValue); + }); + + describe(`#createPointInTimeFinder`, () => { + test(`calls repository with options and default dependencies`, () => { + const returnValue: any = Symbol(); + mockRepository.createPointInTimeFinder.mockReturnValue(returnValue); + const client = new SavedObjectsClient(mockRepository); + + const options: SavedObjectsCreatePointInTimeFinderOptions = { + perPage: 50, + search: 'candy', + type: 'foo', + }; + const result = client.createPointInTimeFinder(options); + + expect(mockRepository.createPointInTimeFinder).toHaveBeenCalledWith(options, { + client, + }); + expect(result).toBe(returnValue); + }); + + test(`calls repository with options and custom dependencies`, () => { + const returnValue: any = Symbol(); + mockRepository.createPointInTimeFinder.mockReturnValue(returnValue); + const client = new SavedObjectsClient(mockRepository); + + const options: SavedObjectsCreatePointInTimeFinderOptions = { + perPage: 50, + search: 'candy', + type: 'foo', + }; + const dependencies = { + client: savedObjectsClientMock.create(), + }; + const result = client.createPointInTimeFinder(options, dependencies); + + expect(mockRepository.createPointInTimeFinder).toHaveBeenCalledWith(options, dependencies); + expect(result).toBe(returnValue); + }); + }); + + test(`#delete`, async () => { + const returnValue: any = Symbol(); + mockRepository.delete.mockResolvedValueOnce(returnValue); + const client = new SavedObjectsClient(mockRepository); + + const type = 'foo'; + const id = '12'; + const options: SavedObjectsDeleteOptions = { force: true, refresh: false }; + const result = await client.delete(type, id, options); + + expect(mockRepository.delete).toHaveBeenCalledWith(type, id, options); + expect(result).toBe(returnValue); + }); + + test(`#find`, async () => { + const returnValue: any = Symbol(); + mockRepository.find.mockResolvedValueOnce(returnValue); + const client = new SavedObjectsClient(mockRepository); + + const options: SavedObjectsFindOptions = { search: 'something', type: ['a', 'b'], perPage: 42 }; + const result = await client.find(options); + + expect(mockRepository.find).toHaveBeenCalledWith(options); + expect(result).toBe(returnValue); + }); + + test(`#bulkGet`, async () => { + const returnValue: any = Symbol(); + mockRepository.bulkGet.mockResolvedValueOnce(returnValue); + const client = new SavedObjectsClient(mockRepository); + + const objects: SavedObjectsBulkGetObject[] = [ + { type: 'foo', id: '1' }, + { type: 'bar', id: '2' }, + ]; + const options: SavedObjectsBaseOptions = { namespace: 'ns-1' }; + const result = await client.bulkGet(objects, options); + + expect(mockRepository.bulkGet).toHaveBeenCalledWith(objects, options); + expect(result).toBe(returnValue); + }); + + test(`#get`, async () => { + const returnValue: any = Symbol(); + mockRepository.get.mockResolvedValueOnce(returnValue); + const client = new SavedObjectsClient(mockRepository); + + const type = 'foo'; + const id = '12'; + const options: SavedObjectsBaseOptions = { namespace: 'ns-1' }; + const result = await client.get(type, id, options); + + expect(mockRepository.get).toHaveBeenCalledWith(type, id, options); + expect(result).toBe(returnValue); + }); + + test(`#openPointInTimeForType`, async () => { + const returnValue: any = Symbol(); + mockRepository.openPointInTimeForType.mockResolvedValueOnce(returnValue); + const client = new SavedObjectsClient(mockRepository); + + const type = 'search'; + const options: SavedObjectsOpenPointInTimeOptions = { + namespaces: ['ns-1', 'ns-2'], + preference: 'pref', + }; + const result = await client.openPointInTimeForType(type, options); + + expect(mockRepository.openPointInTimeForType).toHaveBeenCalledWith(type, options); + expect(result).toBe(returnValue); + }); + + test(`#closePointInTime`, async () => { + const returnValue: any = Symbol(); + mockRepository.closePointInTime.mockResolvedValueOnce(returnValue); + const client = new SavedObjectsClient(mockRepository); + + const id = '42'; + const options: SavedObjectsClosePointInTimeOptions = { namespace: 'ns-42' }; + const result = await client.closePointInTime(id, options); + + expect(mockRepository.closePointInTime).toHaveBeenCalledWith(id, options); + expect(result).toBe(returnValue); + }); + + test(`#bulkResolve`, async () => { + const returnValue: any = Symbol(); + mockRepository.bulkResolve.mockResolvedValueOnce(returnValue); + const client = new SavedObjectsClient(mockRepository); + + const objects: SavedObjectsBulkResolveObject[] = [ + { type: 'foo', id: '1' }, + { type: 'bar', id: '2' }, + ]; + const options: SavedObjectsBaseOptions = { namespace: 'ns-1' }; + const result = await client.bulkResolve(objects, options); + + expect(mockRepository.bulkResolve).toHaveBeenCalledWith(objects, options); + expect(result).toBe(returnValue); + }); + + test(`#resolve`, async () => { + const returnValue: any = Symbol(); + mockRepository.resolve.mockResolvedValueOnce(returnValue); + const client = new SavedObjectsClient(mockRepository); + + const type = 'foo'; + const id = '9000'; + const options: SavedObjectsBaseOptions = { namespace: 'ns-3' }; + const result = await client.resolve(type, id, options); + + expect(mockRepository.resolve).toHaveBeenCalledWith(type, id, options); + expect(result).toBe(returnValue); + }); + + test(`#update`, async () => { + const returnValue: any = Symbol(); + mockRepository.update.mockResolvedValueOnce(returnValue); + const client = new SavedObjectsClient(mockRepository); + + const type = 'some-type'; + const id = '90'; + const attributes = { attr1: 91, attr2: 'some string' }; + const options: SavedObjectsUpdateOptions = { namespace: 'ns-1', version: '12' }; + const result = await client.update(type, id, attributes, options); + + expect(mockRepository.update).toHaveBeenCalledWith(type, id, attributes, options); + expect(result).toBe(returnValue); + }); + + test(`#bulkUpdate`, async () => { + const returnValue: any = Symbol(); + mockRepository.bulkUpdate.mockResolvedValueOnce(returnValue); + const client = new SavedObjectsClient(mockRepository); + + const type = 'some-type'; + const id = '42'; + const attributes = { attr1: 'value' }; + const version = '12.1'; + const namespace = 'ns-1'; + const result = await client.bulkUpdate([{ type, id, attributes, version }], { namespace }); + + expect(mockRepository.bulkUpdate).toHaveBeenCalledWith([{ type, id, attributes, version }], { + namespace, + }); + expect(result).toBe(returnValue); + }); + + test(`#collectMultiNamespaceReferences`, async () => { + const returnValue: any = Symbol(); + mockRepository.collectMultiNamespaceReferences.mockResolvedValueOnce(returnValue); + const client = new SavedObjectsClient(mockRepository); + + const objects: SavedObjectsCollectMultiNamespaceReferencesObject[] = [ + { type: 'foo', id: '1' }, + { type: 'bar', id: '2' }, + ]; + const options: SavedObjectsCollectMultiNamespaceReferencesOptions = { + namespace: 'ns-1', + purpose: 'collectMultiNamespaceReferences', + }; + const result = await client.collectMultiNamespaceReferences(objects, options); + + expect(mockRepository.collectMultiNamespaceReferences).toHaveBeenCalledWith(objects, options); + expect(result).toBe(returnValue); + }); + + test(`#updateObjectsSpaces`, async () => { + const returnValue: any = Symbol(); + mockRepository.updateObjectsSpaces.mockResolvedValueOnce(returnValue); + const client = new SavedObjectsClient(mockRepository); + + const objects: SavedObjectsUpdateObjectsSpacesObject[] = [ + { type: 'foo', id: '1' }, + { type: 'bar', id: '2' }, + ]; + const spacesToAdd = ['to-add', 'to-add-2']; + const spacesToRemove = ['to-remove', 'to-remove-2']; + const options: SavedObjectsUpdateObjectsSpacesOptions = { namespace: 'ns-1', refresh: false }; + const result = await client.updateObjectsSpaces(objects, spacesToAdd, spacesToRemove, options); + + expect(mockRepository.updateObjectsSpaces).toHaveBeenCalledWith( + objects, + spacesToAdd, + spacesToRemove, + options + ); + expect(result).toBe(returnValue); + }); +}); diff --git a/src/core/server/saved_objects/status.ts b/src/core/server/saved_objects/status.ts index 95bf6ddd9ff52..33cc344fc2b60 100644 --- a/src/core/server/saved_objects/status.ts +++ b/src/core/server/saved_objects/status.ts @@ -10,7 +10,7 @@ import { Observable, combineLatest } from 'rxjs'; import { startWith, map } from 'rxjs/operators'; import { ServiceStatus, ServiceStatusLevels } from '../status'; import { SavedObjectStatusMeta } from './types'; -import { KibanaMigratorStatus } from './migrations/kibana'; +import { KibanaMigratorStatus } from './migrations'; export const calculateStatus$ = ( rawMigratorStatus$: Observable, diff --git a/src/core/server/server.api.md b/src/core/server/server.api.md index f135d8caaf54e..18d1e479dddee 100644 --- a/src/core/server/server.api.md +++ b/src/core/server/server.api.md @@ -4,11 +4,14 @@ ```ts +/// + import { AddConfigDeprecation } from '@kbn/config'; import Boom from '@hapi/boom'; import { ByteSizeValue } from '@kbn/config-schema'; import { CliArgs } from '@kbn/config'; -import { ClientOptions } from '@elastic/elasticsearch/lib/client'; +import type { ClientOptions } from '@elastic/elasticsearch/lib/client'; +import { ConditionalType } from '@kbn/config-schema/target_types/types'; import { ConfigDeprecation } from '@kbn/config'; import { ConfigDeprecationContext } from '@kbn/config'; import { ConfigDeprecationFactory } from '@kbn/config'; @@ -26,13 +29,14 @@ import { EcsEventType } from '@kbn/logging'; import { EnvironmentMode } from '@kbn/config'; import * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; import { IncomingHttpHeaders } from 'http'; -import { KibanaClient } from '@elastic/elasticsearch/lib/api/kibana'; +import type { KibanaClient } from '@elastic/elasticsearch/lib/api/kibana'; import { Logger } from '@kbn/logging'; import { LoggerFactory } from '@kbn/logging'; -import { LogLevel } from '@kbn/logging'; +import { LogLevel as LogLevel_2 } from '@kbn/logging'; +import { LogLevelId } from '@kbn/logging'; import { LogMeta } from '@kbn/logging'; import { LogRecord } from '@kbn/logging'; -import { MaybePromise } from '@kbn/utility-types'; +import type { MaybePromise } from '@kbn/utility-types'; import { ObjectType } from '@kbn/config-schema'; import { Observable } from 'rxjs'; import { PackageInfo } from '@kbn/config'; @@ -41,20 +45,20 @@ import { PeerCertificate } from 'tls'; import { PublicMethodsOf } from '@kbn/utility-types'; import { Readable } from 'stream'; import { RecursiveReadonly } from '@kbn/utility-types'; -import { Request } from '@hapi/hapi'; -import { RequestHandlerContext as RequestHandlerContext_2 } from 'src/core/server'; +import { Request as Request_2 } from '@hapi/hapi'; +import type { RequestHandlerContext as RequestHandlerContext_2 } from 'src/core/server'; import { ResponseObject } from '@hapi/hapi'; import { ResponseToolkit } from '@hapi/hapi'; import { SchemaTypeError } from '@kbn/config-schema'; import { ShallowPromise } from '@kbn/utility-types'; import { Stream } from 'stream'; -import { TransportRequestOptions } from '@elastic/elasticsearch'; -import { TransportRequestParams } from '@elastic/elasticsearch'; -import { TransportResult } from '@elastic/elasticsearch'; +import type { TransportRequestOptions } from '@elastic/elasticsearch'; +import type { TransportRequestParams } from '@elastic/elasticsearch'; +import type { TransportResult } from '@elastic/elasticsearch'; import { Type } from '@kbn/config-schema'; import { TypeOf } from '@kbn/config-schema'; import { UiCounterMetricType } from '@kbn/analytics'; -import { URL } from 'url'; +import { URL as URL_2 } from 'url'; export { AddConfigDeprecation } @@ -223,42 +227,42 @@ export type CapabilitiesSwitcher = (request: KibanaRequest, uiCapabilities: Capa // @alpha export const config: { elasticsearch: { - schema: import("@kbn/config-schema").ObjectType<{ - sniffOnStart: Type; - sniffInterval: Type; - sniffOnConnectionFault: Type; - hosts: Type; - username: Type; - password: Type; - serviceAccountToken: Type; - requestHeadersWhitelist: Type; - customHeaders: Type>; - shardTimeout: Type; - requestTimeout: Type; - pingTimeout: Type; - logQueries: Type; - ssl: import("@kbn/config-schema").ObjectType<{ - verificationMode: Type<"none" | "certificate" | "full">; - certificateAuthorities: Type; - certificate: Type; - key: Type; - keyPassphrase: Type; - keystore: import("@kbn/config-schema").ObjectType<{ - path: Type; - password: Type; - }>; - truststore: import("@kbn/config-schema").ObjectType<{ - path: Type; - password: Type; - }>; - alwaysPresentCertificate: Type; - }>; - apiVersion: Type; - healthCheck: import("@kbn/config-schema").ObjectType<{ - delay: Type; - }>; - ignoreVersionMismatch: import("@kbn/config-schema/target_types/types").ConditionalType; - skipStartupConnectionCheck: import("@kbn/config-schema/target_types/types").ConditionalType; + schema: ObjectType< { + sniffOnStart: Type; + sniffInterval: Type; + sniffOnConnectionFault: Type; + hosts: Type; + username: Type; + password: Type; + serviceAccountToken: Type; + requestHeadersWhitelist: Type; + customHeaders: Type>; + shardTimeout: Type; + requestTimeout: Type; + pingTimeout: Type; + logQueries: Type; + ssl: ObjectType< { + verificationMode: Type<"none" | "certificate" | "full">; + certificateAuthorities: Type; + certificate: Type; + key: Type; + keyPassphrase: Type; + keystore: ObjectType< { + path: Type; + password: Type; + }>; + truststore: ObjectType< { + path: Type; + password: Type; + }>; + alwaysPresentCertificate: Type; + }>; + apiVersion: Type; + healthCheck: ObjectType< { + delay: Type; + }>; + ignoreVersionMismatch: ConditionalType; + skipStartupConnectionCheck: ConditionalType; }>; }; logging: { @@ -770,8 +774,6 @@ export interface CountResponse { // @public export class CspConfig implements ICspConfig { - // (undocumented) - #private; // Warning: (ae-forgotten-export) The symbol "CspConfigType" needs to be exported by the entry point index.d.ts // // @internal @@ -988,7 +990,7 @@ export type ExecutionContextStart = ExecutionContextSetup; // @public export interface FakeRequest { - headers: Headers; + headers: Headers_2; } // @public (undocumented) @@ -1046,11 +1048,12 @@ export type HandlerFunction = (context: T, ...args: any[]) => export type HandlerParameters> = T extends (context: any, ...args: infer U) => any ? U : never; // @public -export type Headers = { +type Headers_2 = { [header in KnownHeaders]?: string | string[] | undefined; } & { [header: string]: string | string[] | undefined; }; +export { Headers_2 as Headers } // @public (undocumented) export interface HttpAuth { @@ -1315,8 +1318,8 @@ export type KibanaExecutionContext = { // @public export class KibanaRequest { // @internal (undocumented) - protected readonly [requestSymbol]: Request; - constructor(request: Request, params: Params, query: Query, body: Body, withoutSecretHeaders: boolean); + protected readonly [requestSymbol]: Request_2; + constructor(request: Request_2, params: Params, query: Query, body: Body, withoutSecretHeaders: boolean); // (undocumented) readonly auth: { isAuthenticated: boolean; @@ -1327,21 +1330,21 @@ export class KibanaRequest(req: Request, routeSchemas?: RouteValidator | RouteValidatorFullConfig, withoutSecretHeaders?: boolean): KibanaRequest; - readonly headers: Headers; + static from(req: Request_2, routeSchemas?: RouteValidator | RouteValidatorFullConfig, withoutSecretHeaders?: boolean): KibanaRequest; + readonly headers: Headers_2; readonly id: string; readonly isSystemRequest: boolean; // (undocumented) readonly params: Params; // (undocumented) readonly query: Query; - readonly rewrittenUrl?: URL; + readonly rewrittenUrl?: URL_2; readonly route: RecursiveReadonly>; // (undocumented) readonly socket: IKibanaSocket; - readonly url: URL; + readonly url: URL_2; readonly uuid: string; - } +} // @public export interface KibanaRequestEvents { @@ -1415,7 +1418,7 @@ export interface LoggingServiceSetup { configure(config$: Observable): void; } -export { LogLevel } +export { LogLevel_2 as LogLevel } export { LogMeta } @@ -1597,7 +1600,7 @@ export interface OpsServerMetrics { export { PackageInfo } // @public -export interface Plugin { +interface Plugin_2 { // (undocumented) setup(core: CoreSetup, plugins: TPluginsSetup): TSetup; // (undocumented) @@ -1605,10 +1608,11 @@ export interface Plugin { - // Warning: (ae-unresolved-link) The @link reference could not be resolved: Reexported declarations are not supported + // Warning: (ae-unresolved-link) The @link reference could not be resolved: This type of declaration is not supported yet by the resolver deprecations?: ConfigDeprecationProvider; exposeToBrowser?: { [P in keyof T]?: boolean; @@ -1621,7 +1625,7 @@ export interface PluginConfigDescriptor { export type PluginConfigSchema = Type; // @public -export type PluginInitializer = (core: PluginInitializerContext) => Plugin | PrebootPlugin | AsyncPlugin; +export type PluginInitializer = (core: PluginInitializerContext) => Plugin_2 | PrebootPlugin | AsyncPlugin; // @public export interface PluginInitializerContext { @@ -1640,7 +1644,7 @@ export interface PluginInitializerContext { instanceUuid: string; configs: readonly string[]; }; - // Warning: (ae-unresolved-link) The @link reference could not be resolved: Reexported declarations are not supported + // Warning: (ae-unresolved-link) The @link reference could not be resolved: This type of declaration is not supported yet by the resolver logger: LoggerFactory; // (undocumented) opaqueId: PluginOpaqueId; @@ -1648,7 +1652,7 @@ export interface PluginInitializerContext { // @public export interface PluginManifest { - // Warning: (ae-unresolved-link) The @link reference could not be resolved: Reexported declarations are not supported + // Warning: (ae-unresolved-link) The @link reference could not be resolved: This type of declaration is not supported yet by the resolver readonly configPath: ConfigPath; readonly description?: string; // @deprecated @@ -2072,7 +2076,7 @@ export class SavedObjectsClient { removeReferencesTo(type: string, id: string, options?: SavedObjectsRemoveReferencesToOptions): Promise; resolve(type: string, id: string, options?: SavedObjectsBaseOptions): Promise>; update(type: string, id: string, attributes: Partial, options?: SavedObjectsUpdateOptions): Promise>; - updateObjectsSpaces(objects: SavedObjectsUpdateObjectsSpacesObject[], spacesToAdd: string[], spacesToRemove: string[], options?: SavedObjectsUpdateObjectsSpacesOptions): Promise; + updateObjectsSpaces(objects: SavedObjectsUpdateObjectsSpacesObject[], spacesToAdd: string[], spacesToRemove: string[], options?: SavedObjectsUpdateObjectsSpacesOptions): Promise; } // @public @@ -2251,17 +2255,15 @@ export interface SavedObjectsExportByTypeOptions extends SavedObjectExportBaseOp // @public (undocumented) export class SavedObjectsExporter { - // (undocumented) - #private; constructor({ savedObjectsClient, typeRegistry, exportSizeLimit, logger, }: { savedObjectsClient: SavedObjectsClientContract; typeRegistry: ISavedObjectTypeRegistry; exportSizeLimit: number; logger: Logger; }); - exportByObjects(options: SavedObjectsExportByObjectOptions): Promise; - exportByTypes(options: SavedObjectsExportByTypeOptions): Promise; - } + exportByObjects(options: SavedObjectsExportByObjectOptions): Promise; + exportByTypes(options: SavedObjectsExportByTypeOptions): Promise; +} // @public (undocumented) export class SavedObjectsExportError extends Error { @@ -2404,8 +2406,6 @@ export interface SavedObjectsImportConflictError { // @public (undocumented) export class SavedObjectsImporter { - // (undocumented) - #private; constructor({ savedObjectsClient, typeRegistry, importSizeLimit, }: { savedObjectsClient: SavedObjectsClientContract; typeRegistry: ISavedObjectTypeRegistry; @@ -2655,7 +2655,7 @@ export class SavedObjectsRepository { bulkUpdate(objects: Array>, options?: SavedObjectsBulkUpdateOptions): Promise>; checkConflicts(objects?: SavedObjectsCheckConflictsObject[], options?: SavedObjectsBaseOptions): Promise; closePointInTime(id: string, options?: SavedObjectsClosePointInTimeOptions): Promise; - collectMultiNamespaceReferences(objects: SavedObjectsCollectMultiNamespaceReferencesObject[], options?: SavedObjectsCollectMultiNamespaceReferencesOptions): Promise; + collectMultiNamespaceReferences(objects: SavedObjectsCollectMultiNamespaceReferencesObject[], options?: SavedObjectsCollectMultiNamespaceReferencesOptions): Promise; create(type: string, attributes: T, options?: SavedObjectsCreateOptions): Promise>; createPointInTimeFinder(findOptions: SavedObjectsCreatePointInTimeFinderOptions, dependencies?: SavedObjectsCreatePointInTimeFinderDependencies): ISavedObjectsPointInTimeFinder; // Warning: (ae-forgotten-export) The symbol "IKibanaMigrator" needs to be exported by the entry point index.d.ts @@ -2672,8 +2672,8 @@ export class SavedObjectsRepository { removeReferencesTo(type: string, id: string, options?: SavedObjectsRemoveReferencesToOptions): Promise; resolve(type: string, id: string, options?: SavedObjectsBaseOptions): Promise>; update(type: string, id: string, attributes: Partial, options?: SavedObjectsUpdateOptions): Promise>; - updateObjectsSpaces(objects: SavedObjectsUpdateObjectsSpacesObject[], spacesToAdd: string[], spacesToRemove: string[], options?: SavedObjectsUpdateObjectsSpacesOptions): Promise; - } + updateObjectsSpaces(objects: SavedObjectsUpdateObjectsSpacesObject[], spacesToAdd: string[], spacesToRemove: string[], options?: SavedObjectsUpdateObjectsSpacesOptions): Promise; +} // @public export interface SavedObjectsRepositoryFactory { @@ -2705,7 +2705,7 @@ export class SavedObjectsSerializer { isRawSavedObject(doc: SavedObjectsRawDoc, options?: SavedObjectsRawDocParseOptions): boolean; rawToSavedObject(doc: SavedObjectsRawDoc, options?: SavedObjectsRawDocParseOptions): SavedObjectSanitizedDoc; savedObjectToRaw(savedObj: SavedObjectSanitizedDoc): SavedObjectsRawDoc; - } +} // @public export interface SavedObjectsServiceSetup { @@ -2851,7 +2851,7 @@ export class SavedObjectTypeRegistry { isShareable(type: string): boolean; isSingleNamespace(type: string): boolean; registerType(type: SavedObjectsType): void; - } +} // @public export type SavedObjectUnsanitizedDoc = SavedObjectDoc & Partial; @@ -3053,7 +3053,6 @@ export interface UserProvidedValues { // @public export const validBodyOutput: readonly ["data", "stream"]; - // Warnings were encountered during analysis: // // src/core/server/elasticsearch/client/types.ts:93:7 - (ae-forgotten-export) The symbol "Explanation" needs to be exported by the entry point index.d.ts diff --git a/src/core/server/ui_settings/settings/misc.test.ts b/src/core/server/ui_settings/settings/misc.test.ts deleted file mode 100644 index 7b6788664c997..0000000000000 --- a/src/core/server/ui_settings/settings/misc.test.ts +++ /dev/null @@ -1,31 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import { UiSettingsParams } from '../../../types'; -import { getMiscUiSettings } from './misc'; - -describe('misc settings', () => { - const miscSettings = getMiscUiSettings(); - - const getValidationFn = (setting: UiSettingsParams) => (value: any) => - setting.schema.validate(value); - - describe('truncate:maxHeight', () => { - const validate = getValidationFn(miscSettings['truncate:maxHeight']); - - it('should only accept positive numeric values', () => { - expect(() => validate(127)).not.toThrow(); - expect(() => validate(-12)).toThrowErrorMatchingInlineSnapshot( - `"Value must be equal to or greater than [0]."` - ); - expect(() => validate('foo')).toThrowErrorMatchingInlineSnapshot( - `"expected value of type [number] but got [string]"` - ); - }); - }); -}); diff --git a/src/core/server/ui_settings/settings/misc.ts b/src/core/server/ui_settings/settings/misc.ts index cd9e43400d3c9..ad7411dfd12af 100644 --- a/src/core/server/ui_settings/settings/misc.ts +++ b/src/core/server/ui_settings/settings/misc.ts @@ -6,23 +6,11 @@ * Side Public License, v 1. */ -import { i18n } from '@kbn/i18n'; import { schema } from '@kbn/config-schema'; import { UiSettingsParams } from '../types'; export const getMiscUiSettings = (): Record => { return { - 'truncate:maxHeight': { - name: i18n.translate('core.ui_settings.params.maxCellHeightTitle', { - defaultMessage: 'Maximum table cell height', - }), - value: 115, - description: i18n.translate('core.ui_settings.params.maxCellHeightText', { - defaultMessage: - 'The maximum height that a cell in a table should occupy. Set to 0 to disable truncation', - }), - schema: schema.number({ min: 0 }), - }, buildNum: { readonly: true, schema: schema.maybe(schema.number()), diff --git a/src/dev/bazel/index.bzl b/src/dev/bazel/index.bzl index 313cc9f06236c..83d6361ff95f7 100644 --- a/src/dev/bazel/index.bzl +++ b/src/dev/bazel/index.bzl @@ -12,6 +12,8 @@ Please do not import from any other files when looking to use a custom rule load("//src/dev/bazel:jsts_transpiler.bzl", _jsts_transpiler = "jsts_transpiler") load("//src/dev/bazel:ts_project.bzl", _ts_project = "ts_project") +load("//src/dev/bazel:pkg_npm.bzl", _pkg_npm = "pkg_npm") jsts_transpiler = _jsts_transpiler +pkg_npm = _pkg_npm ts_project = _ts_project diff --git a/src/dev/bazel/pkg_npm.bzl b/src/dev/bazel/pkg_npm.bzl new file mode 100644 index 0000000000000..263d941d4b435 --- /dev/null +++ b/src/dev/bazel/pkg_npm.bzl @@ -0,0 +1,16 @@ +"Simple wrapper over the general pkg_npm rule from rules_nodejs so we can override some configs" + +load("@build_bazel_rules_nodejs//internal/pkg_npm:pkg_npm.bzl", _pkg_npm = "pkg_npm_macro") + +def pkg_npm(validate = False, **kwargs): + """A macro around the upstream pkg_npm rule. + + Args: + validate: boolean; Whether to check that the attributes match the package.json. Defaults to false + **kwargs: the rest + """ + + _pkg_npm( + validate = validate, + **kwargs + ) diff --git a/src/dev/build/args.test.ts b/src/dev/build/args.test.ts index 64d89a650e62e..9ac4c5ec3b236 100644 --- a/src/dev/build/args.test.ts +++ b/src/dev/build/args.test.ts @@ -36,6 +36,7 @@ it('build default and oss dist for current platform, without packages, by defaul "createGenericFolders": true, "createPlatformFolders": true, "createRpmPackage": false, + "downloadCloudDependencies": true, "downloadFreshNode": true, "initialize": true, "isRelease": false, @@ -56,13 +57,14 @@ it('builds packages if --all-platforms is passed', () => { "createArchives": true, "createDebPackage": true, "createDockerCentOS": true, - "createDockerCloud": false, + "createDockerCloud": true, "createDockerContexts": true, "createDockerUBI": true, "createExamplePlugins": false, "createGenericFolders": true, "createPlatformFolders": true, "createRpmPackage": true, + "downloadCloudDependencies": true, "downloadFreshNode": true, "initialize": true, "isRelease": false, @@ -90,6 +92,7 @@ it('limits packages if --rpm passed with --all-platforms', () => { "createGenericFolders": true, "createPlatformFolders": true, "createRpmPackage": true, + "downloadCloudDependencies": true, "downloadFreshNode": true, "initialize": true, "isRelease": false, @@ -117,6 +120,7 @@ it('limits packages if --deb passed with --all-platforms', () => { "createGenericFolders": true, "createPlatformFolders": true, "createRpmPackage": false, + "downloadCloudDependencies": true, "downloadFreshNode": true, "initialize": true, "isRelease": false, @@ -138,13 +142,14 @@ it('limits packages if --docker passed with --all-platforms', () => { "createArchives": true, "createDebPackage": false, "createDockerCentOS": true, - "createDockerCloud": false, + "createDockerCloud": true, "createDockerContexts": true, "createDockerUBI": true, "createExamplePlugins": false, "createGenericFolders": true, "createPlatformFolders": true, "createRpmPackage": false, + "downloadCloudDependencies": true, "downloadFreshNode": true, "initialize": true, "isRelease": false, @@ -173,13 +178,14 @@ it('limits packages if --docker passed with --skip-docker-ubi and --all-platform "createArchives": true, "createDebPackage": false, "createDockerCentOS": true, - "createDockerCloud": false, + "createDockerCloud": true, "createDockerContexts": true, "createDockerUBI": false, "createExamplePlugins": false, "createGenericFolders": true, "createPlatformFolders": true, "createRpmPackage": false, + "downloadCloudDependencies": true, "downloadFreshNode": true, "initialize": true, "isRelease": false, @@ -201,13 +207,14 @@ it('limits packages if --all-platforms passed with --skip-docker-centos', () => "createArchives": true, "createDebPackage": true, "createDockerCentOS": false, - "createDockerCloud": false, + "createDockerCloud": true, "createDockerContexts": true, "createDockerUBI": true, "createExamplePlugins": false, "createGenericFolders": true, "createPlatformFolders": true, "createRpmPackage": true, + "downloadCloudDependencies": true, "downloadFreshNode": true, "initialize": true, "isRelease": false, diff --git a/src/dev/build/args.ts b/src/dev/build/args.ts index 1124d90be89c6..e7fca2a2a3d7b 100644 --- a/src/dev/build/args.ts +++ b/src/dev/build/args.ts @@ -26,9 +26,10 @@ export function readCliArgs(argv: string[]) { 'skip-docker-contexts', 'skip-docker-ubi', 'skip-docker-centos', - 'docker-cloud', + 'skip-docker-cloud', 'release', 'skip-node-download', + 'skip-cloud-dependencies-download', 'verbose', 'debug', 'all-platforms', @@ -96,6 +97,7 @@ export function readCliArgs(argv: string[]) { versionQualifier: flags['version-qualifier'], initialize: !Boolean(flags['skip-initialize']), downloadFreshNode: !Boolean(flags['skip-node-download']), + downloadCloudDependencies: !Boolean(flags['skip-cloud-dependencies-download']), createGenericFolders: !Boolean(flags['skip-generic-folders']), createPlatformFolders: !Boolean(flags['skip-platform-folders']), createArchives: !Boolean(flags['skip-archives']), @@ -104,7 +106,7 @@ export function readCliArgs(argv: string[]) { createDebPackage: isOsPackageDesired('deb'), createDockerCentOS: isOsPackageDesired('docker-images') && !Boolean(flags['skip-docker-centos']), - createDockerCloud: isOsPackageDesired('docker-images') && Boolean(flags['docker-cloud']), + createDockerCloud: isOsPackageDesired('docker-images') && !Boolean(flags['skip-docker-cloud']), createDockerUBI: isOsPackageDesired('docker-images') && !Boolean(flags['skip-docker-ubi']), createDockerContexts: !Boolean(flags['skip-docker-contexts']), targetAllPlatforms: Boolean(flags['all-platforms']), diff --git a/src/dev/build/build_distributables.ts b/src/dev/build/build_distributables.ts index 39a62c1fd35dc..8912b05a16943 100644 --- a/src/dev/build/build_distributables.ts +++ b/src/dev/build/build_distributables.ts @@ -14,6 +14,7 @@ import * as Tasks from './tasks'; export interface BuildOptions { isRelease: boolean; downloadFreshNode: boolean; + downloadCloudDependencies: boolean; initialize: boolean; createGenericFolders: boolean; createPlatformFolders: boolean; @@ -129,7 +130,11 @@ export async function buildDistributables(log: ToolingLog, options: BuildOptions } if (options.createDockerCloud) { - // control w/ --docker-images and --docker-cloud + // control w/ --docker-images and --skip-docker-cloud + if (options.downloadCloudDependencies) { + // control w/ --skip-cloud-dependencies-download + await run(Tasks.DownloadCloudDependencies); + } await run(Tasks.CreateDockerCloud); } diff --git a/src/dev/build/lib/download.ts b/src/dev/build/lib/download.ts index ce2bdbd33e8c1..9293854bfb2bd 100644 --- a/src/dev/build/lib/download.ts +++ b/src/dev/build/lib/download.ts @@ -34,14 +34,15 @@ interface DownloadOptions { log: ToolingLog; url: string; destination: string; - sha256: string; + shaChecksum: string; + shaAlgorithm: string; retries?: number; } export async function download(options: DownloadOptions): Promise { - const { log, url, destination, sha256, retries = 0 } = options; + const { log, url, destination, shaChecksum, shaAlgorithm, retries = 0 } = options; - if (!sha256) { - throw new Error(`sha256 checksum of ${url} not provided, refusing to download.`); + if (!shaChecksum) { + throw new Error(`${shaAlgorithm} checksum of ${url} not provided, refusing to download.`); } // mkdirp and open file outside of try/catch, we don't retry for those errors @@ -50,7 +51,7 @@ export async function download(options: DownloadOptions): Promise { let error; try { - log.debug(`Attempting download of ${url}`, chalk.dim(sha256)); + log.debug(`Attempting download of ${url}`, chalk.dim(shaAlgorithm)); const response = await Axios.request({ url, @@ -62,7 +63,7 @@ export async function download(options: DownloadOptions): Promise { throw new Error(`Unexpected status code ${response.status} when downloading ${url}`); } - const hash = createHash('sha256'); + const hash = createHash(shaAlgorithm); await new Promise((resolve, reject) => { response.data.on('data', (chunk: Buffer) => { hash.update(chunk); @@ -73,10 +74,10 @@ export async function download(options: DownloadOptions): Promise { response.data.on('end', resolve); }); - const downloadedSha256 = hash.digest('hex'); - if (downloadedSha256 !== sha256) { + const downloadedSha = hash.digest('hex'); + if (downloadedSha !== shaChecksum) { throw new Error( - `Downloaded checksum ${downloadedSha256} does not match the expected sha256 checksum.` + `Downloaded checksum ${downloadedSha} does not match the expected ${shaAlgorithm} checksum.` ); } } catch (_error) { diff --git a/src/dev/build/lib/integration_tests/download.test.ts b/src/dev/build/lib/integration_tests/download.test.ts index 9003e678e98a8..173682ef05d71 100644 --- a/src/dev/build/lib/integration_tests/download.test.ts +++ b/src/dev/build/lib/integration_tests/download.test.ts @@ -93,7 +93,8 @@ it('downloads from URL and checks that content matches sha256', async () => { log, url: serverUrl, destination: TMP_DESTINATION, - sha256: FOO_SHA256, + shaChecksum: FOO_SHA256, + shaAlgorithm: 'sha256', }); expect(readFileSync(TMP_DESTINATION, 'utf8')).toBe('foo'); }); @@ -106,7 +107,8 @@ it('rejects and deletes destination if sha256 does not match', async () => { log, url: serverUrl, destination: TMP_DESTINATION, - sha256: 'bar', + shaChecksum: 'bar', + shaAlgorithm: 'sha256', }); throw new Error('Expected download() to reject'); } catch (error) { @@ -141,7 +143,8 @@ describe('reties download retries: number of times', () => { log, url: serverUrl, destination: TMP_DESTINATION, - sha256: FOO_SHA256, + shaChecksum: FOO_SHA256, + shaAlgorithm: 'sha256', retries: 2, }); @@ -167,7 +170,8 @@ describe('reties download retries: number of times', () => { log, url: serverUrl, destination: TMP_DESTINATION, - sha256: FOO_SHA256, + shaChecksum: FOO_SHA256, + shaAlgorithm: 'sha256', retries: 2, }); }); @@ -185,7 +189,8 @@ describe('reties download retries: number of times', () => { log, url: serverUrl, destination: TMP_DESTINATION, - sha256: FOO_SHA256, + shaChecksum: FOO_SHA256, + shaAlgorithm: 'sha256', retries: 5, }); throw new Error('Expected download() to reject'); diff --git a/src/dev/build/tasks/clean_tasks.ts b/src/dev/build/tasks/clean_tasks.ts index f9fcbc74b0efc..19747ce72b5a6 100644 --- a/src/dev/build/tasks/clean_tasks.ts +++ b/src/dev/build/tasks/clean_tasks.ts @@ -196,6 +196,7 @@ export const CleanEmptyFolders: Task = { await deleteEmptyFolders(log, build.resolvePath('.'), [ build.resolvePath('plugins'), build.resolvePath('data'), + build.resolvePath('logs'), ]); }, }; diff --git a/src/dev/build/tasks/create_empty_dirs_and_files_task.ts b/src/dev/build/tasks/create_empty_dirs_and_files_task.ts index 26ed25e801475..dd4cea350ba00 100644 --- a/src/dev/build/tasks/create_empty_dirs_and_files_task.ts +++ b/src/dev/build/tasks/create_empty_dirs_and_files_task.ts @@ -12,6 +12,10 @@ export const CreateEmptyDirsAndFiles: Task = { description: 'Creating some empty directories and files to prevent file-permission issues', async run(config, log, build) { - await Promise.all([mkdirp(build.resolvePath('plugins')), mkdirp(build.resolvePath('data'))]); + await Promise.all([ + mkdirp(build.resolvePath('plugins')), + mkdirp(build.resolvePath('data')), + mkdirp(build.resolvePath('logs')), + ]); }, }; diff --git a/src/dev/build/tasks/download_cloud_dependencies.ts b/src/dev/build/tasks/download_cloud_dependencies.ts new file mode 100644 index 0000000000000..5b5ba2a9ff625 --- /dev/null +++ b/src/dev/build/tasks/download_cloud_dependencies.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 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 axios from 'axios'; +import Path from 'path'; +import del from 'del'; +import { Task, download } from '../lib'; + +export const DownloadCloudDependencies: Task = { + description: 'Downloading cloud dependencies', + + async run(config, log, build) { + const downloadBeat = async (beat: string) => { + const subdomain = config.isRelease ? 'artifacts' : 'snapshots'; + const version = config.getBuildVersion(); + const architecture = process.arch === 'arm64' ? 'arm64' : 'x86_64'; + const url = `https://${subdomain}-no-kpi.elastic.co/downloads/beats/${beat}/${beat}-${version}-linux-${architecture}.tar.gz`; + const checksumRes = await axios.get(url + '.sha512'); + if (checksumRes.status !== 200) { + throw new Error(`Unexpected status code ${checksumRes.status} when downloading ${url}`); + } + const destination = config.resolveFromRepo('.beats', Path.basename(url)); + return download({ + log, + url, + destination, + shaChecksum: checksumRes.data.split(' ')[0], + shaAlgorithm: 'sha512', + retries: 3, + }); + }; + + await del([config.resolveFromRepo('.beats')]); + await downloadBeat('metricbeat'); + await downloadBeat('filebeat'); + }, +}; diff --git a/src/dev/build/tasks/index.ts b/src/dev/build/tasks/index.ts index 35d35023399db..5043be288928e 100644 --- a/src/dev/build/tasks/index.ts +++ b/src/dev/build/tasks/index.ts @@ -16,6 +16,7 @@ export * from './create_archives_sources_task'; export * from './create_archives_task'; export * from './create_empty_dirs_and_files_task'; export * from './create_readme_task'; +export * from './download_cloud_dependencies'; export * from './generate_packages_optimized_assets'; export * from './install_dependencies_task'; export * from './license_file_task'; diff --git a/src/dev/build/tasks/nodejs/download_node_builds_task.test.ts b/src/dev/build/tasks/nodejs/download_node_builds_task.test.ts index 31374d2050971..ec82caac273cf 100644 --- a/src/dev/build/tasks/nodejs/download_node_builds_task.test.ts +++ b/src/dev/build/tasks/nodejs/download_node_builds_task.test.ts @@ -76,7 +76,8 @@ it('downloads node builds for each platform', async () => { "destination": "linux:downloadPath", "log": , "retries": 3, - "sha256": "linux:sha256", + "shaAlgorithm": "sha256", + "shaChecksum": "linux:sha256", "url": "linux:url", }, ], @@ -85,7 +86,8 @@ it('downloads node builds for each platform', async () => { "destination": "linux:downloadPath", "log": , "retries": 3, - "sha256": "linux:sha256", + "shaAlgorithm": "sha256", + "shaChecksum": "linux:sha256", "url": "linux:url", }, ], @@ -94,7 +96,8 @@ it('downloads node builds for each platform', async () => { "destination": "darwin:downloadPath", "log": , "retries": 3, - "sha256": "darwin:sha256", + "shaAlgorithm": "sha256", + "shaChecksum": "darwin:sha256", "url": "darwin:url", }, ], @@ -103,7 +106,8 @@ it('downloads node builds for each platform', async () => { "destination": "darwin:downloadPath", "log": , "retries": 3, - "sha256": "darwin:sha256", + "shaAlgorithm": "sha256", + "shaChecksum": "darwin:sha256", "url": "darwin:url", }, ], @@ -112,7 +116,8 @@ it('downloads node builds for each platform', async () => { "destination": "win32:downloadPath", "log": , "retries": 3, - "sha256": "win32:sha256", + "shaAlgorithm": "sha256", + "shaChecksum": "win32:sha256", "url": "win32:url", }, ], diff --git a/src/dev/build/tasks/nodejs/download_node_builds_task.ts b/src/dev/build/tasks/nodejs/download_node_builds_task.ts index c0c7a399f84cf..f19195092d964 100644 --- a/src/dev/build/tasks/nodejs/download_node_builds_task.ts +++ b/src/dev/build/tasks/nodejs/download_node_builds_task.ts @@ -21,7 +21,8 @@ export const DownloadNodeBuilds: GlobalTask = { await download({ log, url, - sha256: shasums[downloadName], + shaChecksum: shasums[downloadName], + shaAlgorithm: 'sha256', destination: downloadPath, retries: 3, }); diff --git a/src/dev/build/tasks/os_packages/docker_generator/templates/kibana_yml.template.ts b/src/dev/build/tasks/os_packages/docker_generator/templates/kibana_yml.template.ts index 340a035adea4c..96d66b111b062 100644 --- a/src/dev/build/tasks/os_packages/docker_generator/templates/kibana_yml.template.ts +++ b/src/dev/build/tasks/os_packages/docker_generator/templates/kibana_yml.template.ts @@ -22,7 +22,7 @@ function generator({ imageFlavor }: TemplateContext) { server.host: "0.0.0.0" server.shutdownTimeout: "5s" elasticsearch.hosts: [ "http://elasticsearch:9200" ] - ${!imageFlavor ? 'monitoring.ui.container.elasticsearch.enabled: true' : ''} + monitoring.ui.container.elasticsearch.enabled: true `); } diff --git a/src/dev/build/tasks/os_packages/run_fpm.ts b/src/dev/build/tasks/os_packages/run_fpm.ts index c7d9f6997cdf2..9c3f370ba7e98 100644 --- a/src/dev/build/tasks/os_packages/run_fpm.ts +++ b/src/dev/build/tasks/os_packages/run_fpm.ts @@ -113,6 +113,8 @@ export async function runFpm( '--exclude', `usr/share/kibana/data`, '--exclude', + `usr/share/kibana/logs`, + '--exclude', 'run/kibana/.gitempty', // flags specific to the package we are building, supplied by tasks below @@ -129,6 +131,9 @@ export async function runFpm( // copy the data directory at /var/lib/kibana `${resolveWithTrailingSlash(fromBuild('data'))}=/var/lib/kibana/`, + // copy the logs directory at /var/log/kibana + `${resolveWithTrailingSlash(fromBuild('logs'))}=/var/log/kibana/`, + // copy package configurations `${resolveWithTrailingSlash(__dirname, 'service_templates/systemd/')}=/`, diff --git a/src/dev/build/tasks/patch_native_modules_task.ts b/src/dev/build/tasks/patch_native_modules_task.ts index 37cb729053785..fe9743533b901 100644 --- a/src/dev/build/tasks/patch_native_modules_task.ts +++ b/src/dev/build/tasks/patch_native_modules_task.ts @@ -104,7 +104,8 @@ async function patchModule( log, url: archive.url, destination: downloadPath, - sha256: archive.sha256, + shaChecksum: archive.sha256, + shaAlgorithm: 'sha256', retries: 3, }); switch (pkg.extractMethod) { diff --git a/src/dev/ci_setup/.bazelrc-ci b/src/dev/ci_setup/.bazelrc-ci deleted file mode 100644 index a0a0c3de73405..0000000000000 --- a/src/dev/ci_setup/.bazelrc-ci +++ /dev/null @@ -1,5 +0,0 @@ -# Generated by .buildkite/scripts/common/setup_bazel.sh - -import %workspace%/.bazelrc.common - -build --build_metadata=ROLE=CI diff --git a/src/dev/ci_setup/setup.sh b/src/dev/ci_setup/setup.sh index 62e1b24d6d559..18f11fa7f16e4 100755 --- a/src/dev/ci_setup/setup.sh +++ b/src/dev/ci_setup/setup.sh @@ -10,17 +10,6 @@ echo " -- PARENT_DIR='$PARENT_DIR'" echo " -- KIBANA_PKG_BRANCH='$KIBANA_PKG_BRANCH'" echo " -- TEST_ES_SNAPSHOT_VERSION='$TEST_ES_SNAPSHOT_VERSION'" -### -### copy .bazelrc-ci into $HOME/.bazelrc -### -cp "src/dev/ci_setup/.bazelrc-ci" "$HOME/.bazelrc"; - -### -### append auth token to buildbuddy into "$HOME/.bazelrc"; -### -echo "# Appended by src/dev/ci_setup/setup.sh" >> "$HOME/.bazelrc" -echo "build --remote_header=x-buildbuddy-api-key=$KIBANA_BUILDBUDDY_CI_API_KEY" >> "$HOME/.bazelrc" - ### ### install dependencies ### diff --git a/src/dev/ci_setup/setup_env.sh b/src/dev/ci_setup/setup_env.sh index b9898960135fc..4bbc7235e5cb5 100644 --- a/src/dev/ci_setup/setup_env.sh +++ b/src/dev/ci_setup/setup_env.sh @@ -181,24 +181,4 @@ if [[ -d "$ES_DIR" && -f "$ES_JAVA_PROP_PATH" ]]; then export JAVA_HOME=$HOME/.java/$ES_BUILD_JAVA fi -### -### copy .bazelrc-ci into $HOME/.bazelrc -### -cp -f "$KIBANA_DIR/src/dev/ci_setup/.bazelrc-ci" "$HOME/.bazelrc"; - -### -### remove write permissions on buildbuddy remote cache for prs -### -if [[ "$ghprbPullId" ]] ; then - echo "# Appended by $KIBANA_DIR/src/dev/ci_setup/setup.sh" >> "$HOME/.bazelrc" - echo "# Uploads logs & artifacts without writing to cache" >> "$HOME/.bazelrc" - echo "build --noremote_upload_local_results" >> "$HOME/.bazelrc" -fi - -### -### append auth token to buildbuddy into "$HOME/.bazelrc"; -### -echo "# Appended by $KIBANA_DIR/src/dev/ci_setup/setup.sh" >> "$HOME/.bazelrc" -echo "build --remote_header=x-buildbuddy-api-key=$KIBANA_BUILDBUDDY_CI_API_KEY" >> "$HOME/.bazelrc" - export CI_ENV_SETUP=true diff --git a/src/dev/notice/bundled_notices.js b/src/dev/notice/bundled_notices.js index 7ab2a5b3f03fe..00b044e9053f7 100644 --- a/src/dev/notice/bundled_notices.js +++ b/src/dev/notice/bundled_notices.js @@ -7,18 +7,20 @@ */ import { resolve } from 'path'; -import { readFile } from 'fs'; +import { readFile } from 'fs/promises'; +import { promisify } from 'util'; -import { fromNode as fcb } from 'bluebird'; import glob from 'glob'; +const globAsync = promisify(glob); + export async function getBundledNotices(packageDirectory) { const pattern = resolve(packageDirectory, '*{LICENSE,NOTICE}*'); - const paths = await fcb((cb) => glob(pattern, cb)); + const paths = await globAsync(pattern); return Promise.all( paths.map(async (path) => ({ path, - text: await fcb((cb) => readFile(path, 'utf8', cb)), + text: await readFile(path, 'utf8'), })) ); } diff --git a/src/dev/precommit_hook/get_files_for_commit.js b/src/dev/precommit_hook/get_files_for_commit.js index 44c8c9d5e6bc0..366575ad15ae7 100644 --- a/src/dev/precommit_hook/get_files_for_commit.js +++ b/src/dev/precommit_hook/get_files_for_commit.js @@ -6,8 +6,7 @@ * Side Public License, v 1. */ -import SimpleGit from 'simple-git'; -import { fromNode as fcb } from 'bluebird'; +import SimpleGit from 'simple-git/promise'; import { REPO_ROOT } from '@kbn/utils'; import { File } from '../file'; @@ -22,7 +21,7 @@ import { File } from '../file'; export async function getFilesForCommit(gitRef) { const simpleGit = new SimpleGit(REPO_ROOT); const gitRefForDiff = gitRef ? gitRef : '--cached'; - const output = await fcb((cb) => simpleGit.diff(['--name-status', gitRefForDiff], cb)); + const output = await simpleGit.diff(['--name-status', gitRefForDiff]); return ( output diff --git a/src/dev/run_build_docs_cli.ts b/src/dev/run_build_docs_cli.ts new file mode 100644 index 0000000000000..aad524b4437d3 --- /dev/null +++ b/src/dev/run_build_docs_cli.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 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 Path from 'path'; + +import dedent from 'dedent'; +import { run, REPO_ROOT, createFailError } from '@kbn/dev-utils'; + +const DEFAULT_DOC_REPO_PATH = Path.resolve(REPO_ROOT, '..', 'docs'); + +const rel = (path: string) => Path.relative(process.cwd(), path); + +export function runBuildDocsCli() { + run( + async ({ flags, procRunner }) => { + const docRepoPath = + typeof flags.docrepo === 'string' && flags.docrepo + ? Path.resolve(process.cwd(), flags.docrepo) + : DEFAULT_DOC_REPO_PATH; + + try { + await procRunner.run('build_docs', { + cmd: rel(Path.resolve(docRepoPath, 'build_docs')), + args: [ + ['--doc', rel(Path.resolve(REPO_ROOT, 'docs/index.asciidoc'))], + ['--chunk', '1'], + flags.open ? ['--open'] : [], + ].flat(), + cwd: REPO_ROOT, + wait: true, + }); + } catch (error) { + if (error.code === 'ENOENT') { + throw createFailError(dedent` + Unable to run "build_docs" script from docs repo. + Does it exist at [${rel(docRepoPath)}]? + Do you need to pass --docrepo to specify the correct path or clone it there? + `); + } + + throw error; + } + }, + { + description: 'Build the docs and serve them from a docker container', + flags: { + string: ['docrepo'], + boolean: ['open'], + default: { + docrepo: DEFAULT_DOC_REPO_PATH, + }, + help: ` + --docrepo [path] Path to the doc repo, defaults to ${rel(DEFAULT_DOC_REPO_PATH)} + --open Automatically open the built docs in your default browser after building + `, + }, + } + ); +} diff --git a/src/docs/cli.js b/src/docs/cli.js deleted file mode 100644 index ac17c3908f0ca..0000000000000 --- a/src/docs/cli.js +++ /dev/null @@ -1,30 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import { execFileSync } from 'child_process'; -import { Command } from 'commander'; - -import { defaultDocsRepoPath, buildDocsScript, buildDocsArgs } from './docs_repo'; - -const cmd = new Command('node scripts/docs'); -cmd - .option('--docrepo [path]', 'local path to the docs repo', defaultDocsRepoPath()) - .option('--open', 'open the docs in the browser', false) - .parse(process.argv); - -try { - execFileSync(buildDocsScript(cmd), buildDocsArgs(cmd)); -} catch (err) { - if (err.code === 'ENOENT') { - console.error(`elastic/docs repo must be cloned to ${cmd.docrepo}`); - } else { - console.error(err.stack); - } - - process.exit(1); -} diff --git a/src/docs/docs_repo.js b/src/docs/docs_repo.js deleted file mode 100644 index 2d3589c444b34..0000000000000 --- a/src/docs/docs_repo.js +++ /dev/null @@ -1,28 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import { resolve } from 'path'; - -const kibanaDir = resolve(__dirname, '..', '..'); - -export function buildDocsScript(cmd) { - return resolve(process.cwd(), cmd.docrepo, 'build_docs'); -} - -export function buildDocsArgs(cmd) { - const docsIndexFile = resolve(kibanaDir, 'docs', 'index.asciidoc'); - let args = ['--doc', docsIndexFile, '--direct_html', '--chunk=1']; - if (cmd.open) { - args = [...args, '--open']; - } - return args; -} - -export function defaultDocsRepoPath() { - return resolve(kibanaDir, '..', 'docs'); -} diff --git a/src/plugins/chart_expressions/expression_metric/.storybook/main.js b/src/plugins/chart_expressions/expression_metric/.storybook/main.js index cb483d5394285..f73918da64596 100644 --- a/src/plugins/chart_expressions/expression_metric/.storybook/main.js +++ b/src/plugins/chart_expressions/expression_metric/.storybook/main.js @@ -12,7 +12,10 @@ import { resolve } from 'path'; const mockConfig = { resolve: { alias: { - '../format_service': resolve(__dirname, '../public/__mocks__/format_service.ts'), + '../../../expression_metric/public/services': resolve( + __dirname, + '../public/__mocks__/services.ts' + ), }, }, }; diff --git a/src/plugins/chart_expressions/expression_metric/common/expression_functions/__snapshots__/metric_vis_function.test.ts.snap b/src/plugins/chart_expressions/expression_metric/common/expression_functions/__snapshots__/metric_vis_function.test.ts.snap index 03055764cc4a4..c502c9efa2beb 100644 --- a/src/plugins/chart_expressions/expression_metric/common/expression_functions/__snapshots__/metric_vis_function.test.ts.snap +++ b/src/plugins/chart_expressions/expression_metric/common/expression_functions/__snapshots__/metric_vis_function.test.ts.snap @@ -6,7 +6,8 @@ Object { Object { "id": "col-0-1", "meta": Object { - "dimensionName": undefined, + "dimensionName": "Metric", + "type": "number", }, "name": "Count", }, @@ -27,31 +28,55 @@ Object { "value": Object { "visConfig": Object { "dimensions": Object { - "metrics": undefined, + "metrics": Array [ + Object { + "accessor": 0, + "format": Object { + "id": "number", + "params": Object {}, + }, + "type": "vis_dimension", + }, + ], }, "metric": Object { - "colorSchema": "Green to Red", - "colorsRange": "{range from=0 to=10000}", - "invertColors": false, "labels": Object { "show": true, }, - "metricColorMode": "\\"None\\"", + "metricColorMode": "None", + "palette": Object { + "colors": Array [ + "rgb(0, 0, 0, 0)", + "rgb(112, 38, 231)", + ], + "gradient": false, + "range": "number", + "rangeMax": 150, + "rangeMin": 0, + "stops": Array [ + 0, + 10000, + ], + }, "percentageMode": false, "style": Object { "bgColor": false, - "bgFill": "\\"#000\\"", - "fontSize": 60, + "css": "", "labelColor": false, - "subText": "\\"\\"", + "spec": Object { + "fontSize": "60px", + }, + "type": "style", }, - "useRanges": false, }, }, "visData": Object { "columns": Array [ Object { "id": "col-0-1", + "meta": Object { + "type": "number", + }, "name": "Count", }, ], diff --git a/src/plugins/chart_expressions/expression_metric/common/expression_functions/metric_vis_function.test.ts b/src/plugins/chart_expressions/expression_metric/common/expression_functions/metric_vis_function.test.ts index 1f90322e703b8..faf2f93e4d188 100644 --- a/src/plugins/chart_expressions/expression_metric/common/expression_functions/metric_vis_function.test.ts +++ b/src/plugins/chart_expressions/expression_metric/common/expression_functions/metric_vis_function.test.ts @@ -13,45 +13,39 @@ import { Datatable } from '../../../../expressions/common/expression_types/specs describe('interpreter/functions#metric', () => { const fn = functionWrapper(metricVisFunction()); - const context = { + const context: Datatable = { type: 'datatable', rows: [{ 'col-0-1': 0 }], - columns: [{ id: 'col-0-1', name: 'Count' }], - } as unknown as Datatable; - const args = { + columns: [{ id: 'col-0-1', name: 'Count', meta: { type: 'number' } }], + }; + const args: MetricArguments = { percentageMode: false, - useRanges: false, - colorSchema: 'Green to Red', - metricColorMode: 'None', - colorsRange: [ - { - from: 0, - to: 10000, + colorMode: 'None', + palette: { + type: 'palette', + name: '', + params: { + colors: ['rgb(0, 0, 0, 0)', 'rgb(112, 38, 231)'], + stops: [0, 10000], + gradient: false, + rangeMin: 0, + rangeMax: 150, + range: 'number', }, - ], - labels: { - show: true, - }, - invertColors: false, - style: { - bgFill: '#000', - bgColor: false, - labelColor: false, - subText: '', - fontSize: 60, }, - font: { spec: { fontSize: 60 } }, - metrics: [ + showLabels: true, + font: { spec: { fontSize: '60px' }, type: 'style', css: '' }, + metric: [ { + type: 'vis_dimension', accessor: 0, format: { id: 'number', + params: {}, }, - params: {}, - aggType: 'count', }, ], - } as unknown as MetricArguments; + }; it('returns an object with the correct structure', () => { const actual = fn(context, args, undefined); diff --git a/src/plugins/chart_expressions/expression_metric/common/expression_functions/metric_vis_function.ts b/src/plugins/chart_expressions/expression_metric/common/expression_functions/metric_vis_function.ts index 31f5b8421b3a6..ac3b4f5cc4576 100644 --- a/src/plugins/chart_expressions/expression_metric/common/expression_functions/metric_vis_function.ts +++ b/src/plugins/chart_expressions/expression_metric/common/expression_functions/metric_vis_function.ts @@ -10,7 +10,7 @@ import { i18n } from '@kbn/i18n'; import { visType } from '../types'; import { prepareLogTable, Dimension } from '../../../../visualizations/common/prepare_log_table'; -import { vislibColorMaps, ColorMode } from '../../../../charts/common'; +import { ColorMode } from '../../../../charts/common'; import { MetricVisExpressionFunctionDefinition } from '../types'; import { EXPRESSION_METRIC_NAME } from '../constants'; @@ -29,43 +29,18 @@ export const metricVisFunction = (): MetricVisExpressionFunctionDefinition => ({ defaultMessage: 'Shows metric in percentage mode. Requires colorRange to be set.', }), }, - colorSchema: { - types: ['string'], - default: '"Green to Red"', - options: Object.values(vislibColorMaps).map((value: any) => value.id), - help: i18n.translate('expressionMetricVis.function.colorSchema.help', { - defaultMessage: 'Color schema to use', - }), - }, colorMode: { types: ['string'], - default: '"None"', + default: `"${ColorMode.None}"`, options: [ColorMode.None, ColorMode.Labels, ColorMode.Background], help: i18n.translate('expressionMetricVis.function.colorMode.help', { defaultMessage: 'Which part of metric to color', }), }, - colorRange: { - types: ['range'], - multi: true, - default: '{range from=0 to=10000}', - help: i18n.translate('expressionMetricVis.function.colorRange.help', { - defaultMessage: - 'A range object specifying groups of values to which different colors should be applied.', - }), - }, - useRanges: { - types: ['boolean'], - default: false, - help: i18n.translate('expressionMetricVis.function.useRanges.help', { - defaultMessage: 'Enabled color ranges.', - }), - }, - invertColors: { - types: ['boolean'], - default: false, - help: i18n.translate('expressionMetricVis.function.invertColors.help', { - defaultMessage: 'Inverts the color ranges', + palette: { + types: ['palette'], + help: i18n.translate('expressionMetricVis.function.palette.help', { + defaultMessage: 'Provides colors for the values, based on the bounds.', }), }, showLabels: { @@ -75,29 +50,12 @@ export const metricVisFunction = (): MetricVisExpressionFunctionDefinition => ({ defaultMessage: 'Shows labels under the metric values.', }), }, - bgFill: { - types: ['string'], - default: '"#000"', - aliases: ['backgroundFill', 'bgColor', 'backgroundColor'], - help: i18n.translate('expressionMetricVis.function.bgFill.help', { - defaultMessage: - 'Color as html hex code (#123456), html color (red, blue) or rgba value (rgba(255,255,255,1)).', - }), - }, font: { types: ['style'], help: i18n.translate('expressionMetricVis.function.font.help', { defaultMessage: 'Font settings.', }), - default: '{font size=60}', - }, - subText: { - types: ['string'], - aliases: ['label', 'text', 'description'], - default: '""', - help: i18n.translate('expressionMetricVis.function.subText.help', { - defaultMessage: 'Custom text to show under the metric', - }), + default: `{font size=60 align="center"}`, }, metric: { types: ['vis_dimension'], @@ -115,12 +73,10 @@ export const metricVisFunction = (): MetricVisExpressionFunctionDefinition => ({ }, }, fn(input, args, handlers) { - if (args.percentageMode && (!args.colorRange || args.colorRange.length === 0)) { - throw new Error('colorRange must be provided when using percentageMode'); + if (args.percentageMode && !args.palette?.params) { + throw new Error('Palette must be provided when using percentageMode'); } - const fontSize = Number.parseInt(args.font.spec.fontSize || '', 10); - if (handlers?.inspectorAdapters?.tables) { const argsTable: Dimension[] = [ [ @@ -150,21 +106,16 @@ export const metricVisFunction = (): MetricVisExpressionFunctionDefinition => ({ visType, visConfig: { metric: { + palette: args.palette?.params, percentageMode: args.percentageMode, - useRanges: args.useRanges, - colorSchema: args.colorSchema, metricColorMode: args.colorMode, - colorsRange: args.colorRange, labels: { show: args.showLabels, }, - invertColors: args.invertColors, style: { - bgFill: args.bgFill, bgColor: args.colorMode === ColorMode.Background, labelColor: args.colorMode === ColorMode.Labels, - subText: args.subText, - fontSize, + ...args.font, }, }, dimensions: { diff --git a/src/plugins/chart_expressions/expression_metric/common/types/expression_functions.ts b/src/plugins/chart_expressions/expression_metric/common/types/expression_functions.ts index 5e8b01ec93005..88bc0310a6a04 100644 --- a/src/plugins/chart_expressions/expression_metric/common/types/expression_functions.ts +++ b/src/plugins/chart_expressions/expression_metric/common/types/expression_functions.ts @@ -9,35 +9,29 @@ import { Datatable, ExpressionFunctionDefinition, - Range, ExpressionValueRender, Style, } from '../../../../expressions'; import { ExpressionValueVisDimension } from '../../../../visualizations/common'; -import { ColorSchemas, ColorMode } from '../../../../charts/common'; +import { ColorMode, CustomPaletteState, PaletteOutput } from '../../../../charts/common'; import { VisParams, visType } from './expression_renderers'; import { EXPRESSION_METRIC_NAME } from '../constants'; export interface MetricArguments { percentageMode: boolean; - colorSchema: ColorSchemas; colorMode: ColorMode; - useRanges: boolean; - invertColors: boolean; showLabels: boolean; - bgFill: string; - subText: string; - colorRange: Range[]; + palette?: PaletteOutput; font: Style; metric: ExpressionValueVisDimension[]; - bucket: ExpressionValueVisDimension; + bucket?: ExpressionValueVisDimension; } export type MetricInput = Datatable; export interface MetricVisRenderConfig { visType: typeof visType; - visData: MetricInput; + visData: Datatable; visConfig: Pick; } diff --git a/src/plugins/chart_expressions/expression_metric/common/types/expression_renderers.ts b/src/plugins/chart_expressions/expression_metric/common/types/expression_renderers.ts index 2cc7ce853f8bf..eb7573183894c 100644 --- a/src/plugins/chart_expressions/expression_metric/common/types/expression_renderers.ts +++ b/src/plugins/chart_expressions/expression_metric/common/types/expression_renderers.ts @@ -5,9 +5,15 @@ * in compliance with, at your election, the Elastic License 2.0 or the Server * Side Public License, v 1. */ -import { Range } from '../../../../expressions/common'; + import { ExpressionValueVisDimension } from '../../../../visualizations/common'; -import { ColorMode, Labels, Style, ColorSchemas } from '../../../../charts/common'; +import { + ColorMode, + Labels, + CustomPaletteState, + Style as ChartStyle, +} from '../../../../charts/common'; +import { Style } from '../../../../expressions/common'; export const visType = 'metric'; @@ -16,16 +22,14 @@ export interface DimensionsVisParam { bucket?: ExpressionValueVisDimension; } +export type MetricStyle = Style & Pick; export interface MetricVisParam { percentageMode: boolean; percentageFormatPattern?: string; - useRanges: boolean; - colorSchema: ColorSchemas; metricColorMode: ColorMode; - colorsRange: Range[]; + palette?: CustomPaletteState; labels: Labels; - invertColors: boolean; - style: Style; + style: MetricStyle; } export interface VisParams { @@ -42,5 +46,4 @@ export interface MetricOptions { color?: string; bgColor?: string; lightText: boolean; - rowIndex: number; } diff --git a/src/plugins/chart_expressions/expression_metric/public/__mocks__/palette_service.ts b/src/plugins/chart_expressions/expression_metric/public/__mocks__/palette_service.ts new file mode 100644 index 0000000000000..89872b4461be3 --- /dev/null +++ b/src/plugins/chart_expressions/expression_metric/public/__mocks__/palette_service.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 + * 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 { CustomPaletteState } from 'src/plugins/charts/common'; + +export const getPaletteService = () => { + return { + get: (paletteName: string) => ({ + getColorForValue: (value: number, params: CustomPaletteState) => { + const { colors = [], stops = [] } = params ?? {}; + const lessThenValueIndex = stops.findIndex((stop) => value <= stop); + return colors[lessThenValueIndex]; + }, + }), + }; +}; diff --git a/src/plugins/chart_expressions/expression_metric/public/__mocks__/services.ts b/src/plugins/chart_expressions/expression_metric/public/__mocks__/services.ts new file mode 100644 index 0000000000000..c87fa71aa5862 --- /dev/null +++ b/src/plugins/chart_expressions/expression_metric/public/__mocks__/services.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 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 { getFormatService } from './format_service'; +export { getPaletteService } from './palette_service'; diff --git a/src/plugins/chart_expressions/expression_metric/public/__stories__/metric_renderer.stories.tsx b/src/plugins/chart_expressions/expression_metric/public/__stories__/metric_renderer.stories.tsx index b22616af01c91..748ef15a6c9c9 100644 --- a/src/plugins/chart_expressions/expression_metric/public/__stories__/metric_renderer.stories.tsx +++ b/src/plugins/chart_expressions/expression_metric/public/__stories__/metric_renderer.stories.tsx @@ -9,11 +9,31 @@ import React from 'react'; import { storiesOf } from '@storybook/react'; import { ExpressionValueVisDimension } from '../../../../visualizations/common'; -import { DatatableColumn, Range } from '../../../../expressions'; +import { Datatable, DatatableColumn } from '../../../../expressions'; import { Render } from '../../../../presentation_util/public/__stories__'; -import { ColorMode, ColorSchemas } from '../../../../charts/common'; +import { ColorMode, CustomPaletteState } from '../../../../charts/common'; import { metricVisRenderer } from '../expression_renderers'; -import { MetricVisRenderConfig, visType } from '../../common/types'; +import { MetricStyle, MetricVisRenderConfig, visType } from '../../common/types'; + +const palette: CustomPaletteState = { + colors: ['rgb(219 231 38)', 'rgb(112 38 231)', 'rgb(38 124 231)'], + stops: [0, 50, 150], + gradient: false, + rangeMin: 0, + rangeMax: 150, + range: 'number', +}; + +const style: MetricStyle = { + spec: { fontSize: '12px' }, + + /* stylelint-disable */ + type: 'style', + css: '', + bgColor: false, + labelColor: false, + /* stylelint-enable */ +}; const config: MetricVisRenderConfig = { visType, @@ -35,20 +55,10 @@ const config: MetricVisRenderConfig = { }, visConfig: { metric: { - percentageMode: false, - useRanges: false, - colorSchema: ColorSchemas.GreenToRed, metricColorMode: ColorMode.None, - colorsRange: [], labels: { show: true }, - invertColors: false, - style: { - bgColor: false, - bgFill: '#000', - fontSize: 60, - labelColor: false, - subText: '', - }, + percentageMode: false, + style, }, dimensions: { metrics: [ @@ -102,11 +112,6 @@ const dataWithBuckets = [ { 'col-0-1': 56, 'col-0-2': 52, 'col-0-3': 'Wednesday' }, ]; -const colorsRange: Range[] = [ - { type: 'range', from: 0, to: 50 }, - { type: 'range', from: 51, to: 150 }, -]; - const containerSize = { width: '700px', height: '700px', @@ -141,7 +146,10 @@ storiesOf('renderers/visMetric', module) ...config.visConfig, metric: { ...config.visConfig.metric, - style: { ...config.visConfig.metric.style, fontSize: 120 }, + style: { + ...config.visConfig.metric.style, + spec: { ...config.visConfig.metric.style.spec, fontSize: '120px' }, + }, }, }, }} @@ -159,7 +167,7 @@ storiesOf('renderers/visMetric', module) ...config.visConfig, metric: { ...config.visConfig.metric, - colorsRange, + palette, metricColorMode: ColorMode.Background, style: { ...config.visConfig.metric.style, @@ -182,7 +190,7 @@ storiesOf('renderers/visMetric', module) ...config.visConfig, metric: { ...config.visConfig.metric, - colorsRange, + palette, metricColorMode: ColorMode.Labels, style: { ...config.visConfig.metric.style, @@ -205,13 +213,12 @@ storiesOf('renderers/visMetric', module) ...config.visConfig, metric: { ...config.visConfig.metric, - colorsRange, + palette, metricColorMode: ColorMode.Labels, style: { ...config.visConfig.metric.style, labelColor: true, }, - invertColors: true, }, }, }} @@ -226,8 +233,8 @@ storiesOf('renderers/visMetric', module) config={{ ...config, visData: { - ...config.visData, - columns: [...config.visData.columns, dayColumn], + ...(config.visData as Datatable), + columns: [...(config.visData as Datatable).columns, dayColumn], rows: dataWithBuckets, }, visConfig: { @@ -243,7 +250,7 @@ storiesOf('renderers/visMetric', module) return ( ); diff --git a/src/plugins/chart_expressions/expression_metric/public/components/__snapshots__/metric_component.test.tsx.snap b/src/plugins/chart_expressions/expression_metric/public/components/__snapshots__/metric_component.test.tsx.snap index f07fdfa682d87..5f856f3154d58 100644 --- a/src/plugins/chart_expressions/expression_metric/public/components/__snapshots__/metric_component.test.tsx.snap +++ b/src/plugins/chart_expressions/expression_metric/public/components/__snapshots__/metric_component.test.tsx.snap @@ -15,6 +15,15 @@ Array [ } } showLabel={true} + style={ + Object { + "bgColor": false, + "css": "", + "labelColor": false, + "spec": Object {}, + "type": "style", + } + } />, , ] `; @@ -47,5 +65,14 @@ exports[`MetricVisComponent should render correct structure for single metric 1` } } showLabel={true} + style={ + Object { + "bgColor": false, + "css": "", + "labelColor": false, + "spec": Object {}, + "type": "style", + } + } /> `; diff --git a/src/plugins/chart_expressions/expression_metric/public/components/metric.scss b/src/plugins/chart_expressions/expression_metric/public/components/metric.scss index 5665ba8e8d099..24c5c05129882 100644 --- a/src/plugins/chart_expressions/expression_metric/public/components/metric.scss +++ b/src/plugins/chart_expressions/expression_metric/public/components/metric.scss @@ -6,6 +6,7 @@ // mtrChart__legend-isLoading .mtrVis { + height: 100%; width: 100%; display: flex; flex-direction: row; diff --git a/src/plugins/chart_expressions/expression_metric/public/components/metric_component.test.tsx b/src/plugins/chart_expressions/expression_metric/public/components/metric_component.test.tsx index ec3b9aee8583c..033cdd629b8eb 100644 --- a/src/plugins/chart_expressions/expression_metric/public/components/metric_component.test.tsx +++ b/src/plugins/chart_expressions/expression_metric/public/components/metric_component.test.tsx @@ -8,49 +8,64 @@ import React from 'react'; import { shallow } from 'enzyme'; - +import { Datatable } from '../../../../expressions/common'; import MetricVisComponent, { MetricVisComponentProps } from './metric_component'; -jest.mock('../format_service', () => ({ - getFormatService: () => ({ - deserialize: () => { - return { - convert: (x: unknown) => x, - }; - }, - }), +jest.mock('../../../expression_metric/public/services', () => ({ + getFormatService: () => { + // eslint-disable-next-line @typescript-eslint/no-var-requires + const { getFormatService } = require('../__mocks__/services'); + return getFormatService(); + }, + getPaletteService: () => { + // eslint-disable-next-line @typescript-eslint/no-var-requires + const { getPaletteService } = require('../__mocks__/services'); + return getPaletteService(); + }, })); type Props = MetricVisComponentProps; -const baseVisData = { - columns: [{ id: 'col-0', name: 'Count' }], +const visData: Datatable = { + type: 'datatable', + columns: [{ id: 'col-0', name: 'Count', meta: { type: 'number' } }], rows: [{ 'col-0': 4301021 }], -} as any; +}; describe('MetricVisComponent', function () { - const visParams = { - type: 'metric', - addTooltip: false, - addLegend: false, + const visParams: Props['visParams'] = { metric: { - colorSchema: 'Green to Red', - colorsRange: [{ from: 0, to: 1000 }], - style: {}, + metricColorMode: 'None', + percentageMode: false, + palette: { + colors: ['rgb(0, 0, 0, 0)', 'rgb(112, 38, 231)'], + stops: [0, 10000], + gradient: false, + rangeMin: 0, + rangeMax: 1000, + range: 'number', + }, + style: { + type: 'style', + spec: {}, + css: '', + bgColor: false, + labelColor: false, + }, labels: { show: true, }, }, dimensions: { - metrics: [{ accessor: 0 } as any], + metrics: [{ accessor: 0, type: 'vis_dimension', format: { params: {}, id: 'number' } }], bucket: undefined, }, }; const getComponent = (propOverrides: Partial = {} as Partial) => { const props: Props = { - visParams: visParams as any, - visData: baseVisData, + visParams, + visData, renderComplete: jest.fn(), fireEvent: jest.fn(), ...propOverrides, @@ -70,9 +85,10 @@ describe('MetricVisComponent', function () { it('should render correct structure for multi-value metrics', function () { const component = getComponent({ visData: { + type: 'datatable', columns: [ - { id: 'col-0', name: '1st percentile of bytes' }, - { id: 'col-1', name: '99th percentile of bytes' }, + { id: 'col-0', name: '1st percentile of bytes', meta: { type: 'number' } }, + { id: 'col-1', name: '99th percentile of bytes', meta: { type: 'number' } }, ], rows: [{ 'col-0': 182, 'col-1': 445842.4634666484 }], }, @@ -80,10 +96,13 @@ describe('MetricVisComponent', function () { ...visParams, dimensions: { ...visParams.dimensions, - metrics: [{ accessor: 0 }, { accessor: 1 }], + metrics: [ + { accessor: 0, type: 'vis_dimension', format: { id: 'number', params: {} } }, + { accessor: 1, type: 'vis_dimension', format: { id: 'number', params: {} } }, + ], }, }, - } as any); + }); expect(component).toMatchSnapshot(); }); diff --git a/src/plugins/chart_expressions/expression_metric/public/components/metric_component.tsx b/src/plugins/chart_expressions/expression_metric/public/components/metric_component.tsx index 4efdefc7d28ee..245fdf0a37170 100644 --- a/src/plugins/chart_expressions/expression_metric/public/components/metric_component.tsx +++ b/src/plugins/chart_expressions/expression_metric/public/components/metric_component.tsx @@ -6,161 +6,92 @@ * Side Public License, v 1. */ -import { last, findIndex, isNaN } from 'lodash'; import React, { Component } from 'react'; -import { isColorDark } from '@elastic/eui'; import { MetricVisValue } from './metric_value'; -import { MetricInput, VisParams, MetricOptions } from '../../common/types'; -import type { FieldFormatsContentType, IFieldFormat } from '../../../../field_formats/common'; +import { VisParams, MetricOptions } from '../../common/types'; +import type { IFieldFormat } from '../../../../field_formats/common'; import { Datatable } from '../../../../expressions/public'; -import { getHeatmapColors } from '../../../../charts/public'; -import { getFormatService } from '../format_service'; +import { CustomPaletteState } from '../../../../charts/public'; +import { getFormatService, getPaletteService } from '../../../expression_metric/public/services'; import { ExpressionValueVisDimension } from '../../../../visualizations/public'; +import { formatValue, shouldApplyColor } from '../utils'; +import { getColumnByAccessor } from '../utils/accessor'; +import { needsLightText } from '../utils/palette'; import './metric.scss'; export interface MetricVisComponentProps { visParams: Pick; - visData: MetricInput; + visData: Datatable; fireEvent: (event: any) => void; renderComplete: () => void; } class MetricVisComponent extends Component { - private getLabels() { - const config = this.props.visParams.metric; - const isPercentageMode = config.percentageMode; - const colorsRange = config.colorsRange; - const max = last(colorsRange)?.to ?? 1; - const labels: string[] = []; - - colorsRange.forEach((range: any) => { - const from = isPercentageMode ? Math.round((100 * range.from) / max) : range.from; - const to = isPercentageMode ? Math.round((100 * range.to) / max) : range.to; - labels.push(`${from} - ${to}`); + private getColor(value: number, paletteParams: CustomPaletteState) { + return getPaletteService().get('custom')?.getColorForValue?.(value, paletteParams, { + min: paletteParams.rangeMin, + max: paletteParams.rangeMax, }); - return labels; - } - - private getColors() { - const config = this.props.visParams.metric; - const invertColors = config.invertColors; - const colorSchema = config.colorSchema; - const colorsRange = config.colorsRange; - const labels = this.getLabels(); - const colors: any = {}; - for (let i = 0; i < labels.length; i += 1) { - const divider = Math.max(colorsRange.length - 1, 1); - const val = invertColors ? 1 - i / divider : i / divider; - colors[labels[i]] = getHeatmapColors(val, colorSchema); - } - return colors; - } - - private getBucket(val: number) { - const config = this.props.visParams.metric; - let bucket = findIndex(config.colorsRange, (range: any) => { - return range.from <= val && range.to > val; - }); - - if (bucket === -1) { - if (config.colorsRange?.[0] && val < config.colorsRange?.[0].from) bucket = 0; - else bucket = config.colorsRange.length - 1; - } - - return bucket; - } - - private getColor(val: number, labels: string[], colors: { [label: string]: string }) { - const bucket = this.getBucket(val); - const label = labels[bucket]; - return colors[label]; - } - - private needsLightText(bgColor: string) { - const colors = /rgb\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*\)/.exec(bgColor); - if (!colors) { - return false; - } - - const [red, green, blue] = colors.slice(1).map((c) => parseInt(c, 10)); - return isColorDark(red, green, blue); - } - - private getFormattedValue = ( - fieldFormatter: IFieldFormat, - value: any, - format: FieldFormatsContentType = 'text' - ) => { - if (isNaN(value)) return '-'; - return fieldFormatter.convert(value, format); - }; - - private getColumn( - accessor: ExpressionValueVisDimension['accessor'], - columns: Datatable['columns'] = [] - ) { - if (typeof accessor === 'number') { - return columns[accessor]; - } - return columns.filter(({ id }) => accessor.id === id)[0]; } private processTableGroups(table: Datatable) { const { metric: metricConfig, dimensions } = this.props.visParams; - const { percentageMode: isPercentageMode, colorsRange, style } = metricConfig; - const min = colorsRange?.[0]?.from; - const max = last(colorsRange)?.to; - const colors = this.getColors(); - const labels = this.getLabels(); - const metrics: MetricOptions[] = []; + const { percentageMode: isPercentageMode, style, palette } = metricConfig; + const { stops = [] } = palette ?? {}; + const min = stops[0]; + const max = stops[stops.length - 1]; let bucketColumnId: string; let bucketFormatter: IFieldFormat; if (dimensions.bucket) { - bucketColumnId = this.getColumn(dimensions.bucket.accessor, table.columns).id; + bucketColumnId = getColumnByAccessor(dimensions.bucket.accessor, table.columns).id; bucketFormatter = getFormatService().deserialize(dimensions.bucket.format); } - dimensions.metrics.forEach((metric: ExpressionValueVisDimension) => { - const column = this.getColumn(metric.accessor, table?.columns); - const formatter = getFormatService().deserialize(metric.format); - table.rows.forEach((row, rowIndex) => { - let title = column.name; - let value: number = row[column.id]; - const color = this.getColor(value, labels, colors); - - if (isPercentageMode && colorsRange?.length && max !== undefined && min !== undefined) { - value = (value - min) / (max - min); - } - const formattedValue = this.getFormattedValue(formatter, value, 'html'); - if (bucketColumnId) { - const bucketValue = this.getFormattedValue(bucketFormatter, row[bucketColumnId]); - title = `${bucketValue} - ${title}`; - } - - const shouldColor = colorsRange && colorsRange.length > 1; - - metrics.push({ - label: title, - value: formattedValue, - color: shouldColor && style.labelColor ? color : undefined, - bgColor: shouldColor && style.bgColor ? color : undefined, - lightText: shouldColor && style.bgColor && this.needsLightText(color), - rowIndex, + return dimensions.metrics.reduce( + (acc: MetricOptions[], metric: ExpressionValueVisDimension) => { + const column = getColumnByAccessor(metric.accessor, table?.columns); + const formatter = getFormatService().deserialize(metric.format); + const metrics = table.rows.map((row, rowIndex) => { + let title = column.name; + let value: number = row[column.id]; + const color = palette ? this.getColor(value, palette) : undefined; + + if (isPercentageMode && stops.length) { + value = (value - min) / (max - min); + } + + const formattedValue = formatValue(value, formatter, 'html'); + if (bucketColumnId) { + const bucketValue = formatValue(row[bucketColumnId], bucketFormatter); + title = `${bucketValue} - ${title}`; + } + + const shouldBrush = stops.length > 1 && shouldApplyColor(color ?? ''); + return { + label: title, + value: formattedValue, + color: shouldBrush && (style.labelColor ?? false) ? color : undefined, + bgColor: shouldBrush && (style.bgColor ?? false) ? color : undefined, + lightText: shouldBrush && (style.bgColor ?? false) && needsLightText(color), + rowIndex, + }; }); - }); - }); - return metrics; + return [...acc, ...metrics]; + }, + [] + ); } - private filterBucket = (metric: MetricOptions) => { - const dimensions = this.props.visParams.dimensions; + private filterBucket = (row: number) => { + const { dimensions } = this.props.visParams; if (!dimensions.bucket) { return; } + const table = this.props.visData; this.props.fireEvent({ name: 'filterBucket', @@ -169,7 +100,7 @@ class MetricVisComponent extends Component { { table, column: dimensions.bucket.accessor, - row: metric.rowIndex, + row, }, ], }, @@ -181,8 +112,10 @@ class MetricVisComponent extends Component { this.filterBucket(index) : undefined + } showLabel={this.props.visParams.metric.labels.show} /> ); diff --git a/src/plugins/chart_expressions/expression_metric/public/components/metric_value.test.tsx b/src/plugins/chart_expressions/expression_metric/public/components/metric_value.test.tsx index db145f85a0d4a..9a9e0eef5df97 100644 --- a/src/plugins/chart_expressions/expression_metric/public/components/metric_value.test.tsx +++ b/src/plugins/chart_expressions/expression_metric/public/components/metric_value.test.tsx @@ -10,33 +10,44 @@ import React from 'react'; import { shallow } from 'enzyme'; import { MetricVisValue } from './metric_value'; +import { MetricOptions, MetricStyle } from '../../common/types'; -const baseMetric = { label: 'Foo', value: 'foo' } as any; +const baseMetric: MetricOptions = { label: 'Foo', value: 'foo', lightText: false }; +const font: MetricStyle = { + spec: { fontSize: '12px' }, + + /* stylelint-disable */ + type: 'style', + css: '', + bgColor: false, + labelColor: false, + /* stylelint-enable */ +}; describe('MetricVisValue', () => { it('should be wrapped in button if having a click listener', () => { const component = shallow( - {}} /> + {}} /> ); expect(component.find('button').exists()).toBe(true); }); it('should not be wrapped in button without having a click listener', () => { - const component = shallow(); + const component = shallow(); expect(component.find('button').exists()).toBe(false); }); it('should add -isfilterable class if onFilter is provided', () => { const onFilter = jest.fn(); const component = shallow( - + ); component.simulate('click'); expect(component.find('.mtrVis__container-isfilterable')).toHaveLength(1); }); it('should not add -isfilterable class if onFilter is not provided', () => { - const component = shallow(); + const component = shallow(); component.simulate('click'); expect(component.find('.mtrVis__container-isfilterable')).toHaveLength(0); }); @@ -44,9 +55,9 @@ describe('MetricVisValue', () => { it('should call onFilter callback if provided', () => { const onFilter = jest.fn(); const component = shallow( - + ); component.simulate('click'); - expect(onFilter).toHaveBeenCalledWith(baseMetric); + expect(onFilter).toHaveBeenCalled(); }); }); diff --git a/src/plugins/chart_expressions/expression_metric/public/components/metric_value.tsx b/src/plugins/chart_expressions/expression_metric/public/components/metric_value.tsx index 9554c7ab13616..54662ee647b6a 100644 --- a/src/plugins/chart_expressions/expression_metric/public/components/metric_value.tsx +++ b/src/plugins/chart_expressions/expression_metric/public/components/metric_value.tsx @@ -6,19 +6,18 @@ * Side Public License, v 1. */ -import React from 'react'; +import React, { CSSProperties } from 'react'; import classNames from 'classnames'; - -import type { MetricOptions } from '../../common/types'; +import type { MetricOptions, MetricStyle } from '../../common/types'; interface MetricVisValueProps { metric: MetricOptions; - fontSize: number; - onFilter?: (metric: MetricOptions) => void; + onFilter?: () => void; showLabel?: boolean; + style: MetricStyle; } -export const MetricVisValue = ({ fontSize, metric, onFilter, showLabel }: MetricVisValueProps) => { +export const MetricVisValue = ({ style, metric, onFilter, showLabel }: MetricVisValueProps) => { const containerClassName = classNames('mtrVis__container', { // eslint-disable-next-line @typescript-eslint/naming-convention 'mtrVis__container--light': metric.lightText, @@ -31,8 +30,8 @@ export const MetricVisValue = ({ fontSize, metric, onFilter, showLabel }: Metric
    onFilter(metric)}> + ); diff --git a/src/plugins/chart_expressions/expression_metric/public/plugin.ts b/src/plugins/chart_expressions/expression_metric/public/plugin.ts index 3ac338380a398..6053cba597b4b 100644 --- a/src/plugins/chart_expressions/expression_metric/public/plugin.ts +++ b/src/plugins/chart_expressions/expression_metric/public/plugin.ts @@ -6,16 +6,18 @@ * Side Public License, v 1. */ +import { ChartsPluginSetup } from '../../../charts/public'; import { CoreSetup, CoreStart, Plugin } from '../../../../core/public'; import { Plugin as ExpressionsPublicPlugin } from '../../../expressions/public'; import { metricVisFunction } from '../common'; -import { setFormatService } from './format_service'; +import { setFormatService, setPaletteService } from './services'; import { metricVisRenderer } from './expression_renderers'; import { FieldFormatsStart } from '../../../field_formats/public'; /** @internal */ export interface ExpressionMetricPluginSetup { expressions: ReturnType; + charts: ChartsPluginSetup; } /** @internal */ @@ -25,9 +27,12 @@ export interface ExpressionMetricPluginStart { /** @internal */ export class ExpressionMetricPlugin implements Plugin { - public setup(core: CoreSetup, { expressions }: ExpressionMetricPluginSetup) { + public setup(core: CoreSetup, { expressions, charts }: ExpressionMetricPluginSetup) { expressions.registerFunction(metricVisFunction); expressions.registerRenderer(metricVisRenderer); + charts.palettes.getPalettes().then((palettes) => { + setPaletteService(palettes); + }); } public start(core: CoreStart, { fieldFormats }: ExpressionMetricPluginStart) { diff --git a/src/plugins/chart_expressions/expression_metric/public/services/format_service.ts b/src/plugins/chart_expressions/expression_metric/public/services/format_service.ts new file mode 100644 index 0000000000000..73b66341c4d9a --- /dev/null +++ b/src/plugins/chart_expressions/expression_metric/public/services/format_service.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 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 { createGetterSetter } from '../../../../kibana_utils/public'; +import { FieldFormatsStart } from '../../../../field_formats/public'; + +export const [getFormatService, setFormatService] = + createGetterSetter('fieldFormats'); diff --git a/src/plugins/chart_expressions/expression_metric/public/services/index.ts b/src/plugins/chart_expressions/expression_metric/public/services/index.ts new file mode 100644 index 0000000000000..0b445d9c10b72 --- /dev/null +++ b/src/plugins/chart_expressions/expression_metric/public/services/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 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 { getFormatService, setFormatService } from './format_service'; +export { getPaletteService, setPaletteService } from './palette_service'; diff --git a/src/plugins/chart_expressions/expression_metric/public/services/palette_service.ts b/src/plugins/chart_expressions/expression_metric/public/services/palette_service.ts new file mode 100644 index 0000000000000..cfcf2a818c5bc --- /dev/null +++ b/src/plugins/chart_expressions/expression_metric/public/services/palette_service.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 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 { createGetterSetter } from '../../../../kibana_utils/public'; +import { PaletteRegistry } from '../../../../charts/public'; + +export const [getPaletteService, setPaletteService] = + createGetterSetter('palette'); diff --git a/src/plugins/chart_expressions/expression_metric/public/utils/accessor.ts b/src/plugins/chart_expressions/expression_metric/public/utils/accessor.ts new file mode 100644 index 0000000000000..679a1ca01affb --- /dev/null +++ b/src/plugins/chart_expressions/expression_metric/public/utils/accessor.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 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 { Datatable } from '../../../../expressions'; +import { ExpressionValueVisDimension } from '../../../../visualizations/common'; + +export const getColumnByAccessor = ( + accessor: ExpressionValueVisDimension['accessor'], + columns: Datatable['columns'] = [] +) => { + if (typeof accessor === 'number') { + return columns[accessor]; + } + return columns.filter(({ id }) => accessor.id === id)[0]; +}; diff --git a/src/plugins/chart_expressions/expression_metric/public/utils/format.ts b/src/plugins/chart_expressions/expression_metric/public/utils/format.ts new file mode 100644 index 0000000000000..0339baec9e904 --- /dev/null +++ b/src/plugins/chart_expressions/expression_metric/public/utils/format.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 + * 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 { FieldFormatsContentType, IFieldFormat } from '../../../../field_formats/common'; + +export const formatValue = ( + value: number | string, + fieldFormatter: IFieldFormat, + format: FieldFormatsContentType = 'text' +) => { + if (typeof value === 'number' && isNaN(value)) { + return '-'; + } + + return fieldFormatter.convert(value, format); +}; diff --git a/src/plugins/chart_expressions/expression_metric/public/utils/index.ts b/src/plugins/chart_expressions/expression_metric/public/utils/index.ts new file mode 100644 index 0000000000000..66c305a14c460 --- /dev/null +++ b/src/plugins/chart_expressions/expression_metric/public/utils/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 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 { parseRgbString, shouldApplyColor, needsLightText } from './palette'; +export { formatValue } from './format'; diff --git a/src/plugins/chart_expressions/expression_metric/public/utils/palette.ts b/src/plugins/chart_expressions/expression_metric/public/utils/palette.ts new file mode 100644 index 0000000000000..7f588aa552385 --- /dev/null +++ b/src/plugins/chart_expressions/expression_metric/public/utils/palette.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 { isColorDark } from '@elastic/eui'; + +export const parseRgbString = (rgb: string) => { + const groups = rgb.match(/rgb\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*?(,\s*(\d+)\s*)?\)/) ?? []; + if (!groups) { + return null; + } + + const red = parseFloat(groups[1]); + const green = parseFloat(groups[2]); + const blue = parseFloat(groups[3]); + const opacity = groups[5] ? parseFloat(groups[5]) : undefined; + + return { red, green, blue, opacity }; +}; + +export const shouldApplyColor = (color: string) => { + const rgb = parseRgbString(color); + const { opacity } = rgb ?? {}; + + // if opacity === 0, it means there is no color to apply to the metric + return !rgb || (rgb && opacity !== 0); +}; + +export const needsLightText = (bgColor: string = '') => { + const rgb = parseRgbString(bgColor); + if (!rgb) { + return false; + } + + const { red, green, blue, opacity } = rgb; + return isColorDark(red, green, blue) && opacity !== 0; +}; diff --git a/src/plugins/charts/common/palette.ts b/src/plugins/charts/common/palette.ts index 1faeb4df7788e..8cd449fe99f99 100644 --- a/src/plugins/charts/common/palette.ts +++ b/src/plugins/charts/common/palette.ts @@ -8,6 +8,7 @@ import { ExpressionFunctionDefinition } from 'src/plugins/expressions/common'; import { i18n } from '@kbn/i18n'; +import { last } from 'lodash'; import { paletteIds } from './constants'; export interface CustomPaletteArguments { @@ -141,21 +142,24 @@ export function palette(): ExpressionFunctionDefinition< }, }, fn: (input, args) => { - const { - color, - continuity, - reverse, - gradient, - stop, - range, - rangeMin = 0, - rangeMax = 100, - } = args; + const { color, continuity, reverse, gradient, stop, range, rangeMin, rangeMax } = args; const colors = ([] as string[]).concat(color || defaultCustomColors); const stops = ([] as number[]).concat(stop || []); if (stops.length > 0 && colors.length !== stops.length) { throw Error('When stop is used, each color must have an associated stop value.'); } + + // If the user has defined stops, choose rangeMin/Max, provided by user or range, + // taken from first/last element of ranges or default range (0 or 100). + const calculateRange = ( + userRange: number | undefined, + stopsRange: number | undefined, + defaultRange: number + ) => userRange ?? stopsRange ?? defaultRange; + + const rangeMinDefault = 0; + const rangeMaxDefault = 100; + return { type: 'palette', name: 'custom', @@ -165,8 +169,8 @@ export function palette(): ExpressionFunctionDefinition< range: range ?? 'percent', gradient, continuity, - rangeMin, - rangeMax, + rangeMin: calculateRange(rangeMin, stops[0], rangeMinDefault), + rangeMax: calculateRange(rangeMax, last(stops), rangeMaxDefault), }, }; }, diff --git a/src/plugins/charts/public/static/components/current_time.tsx b/src/plugins/charts/public/static/components/current_time.tsx index 9cc261bf3ed86..ad05f451b607f 100644 --- a/src/plugins/charts/public/static/components/current_time.tsx +++ b/src/plugins/charts/public/static/components/current_time.tsx @@ -10,8 +10,10 @@ import moment, { Moment } from 'moment'; import React, { FC } from 'react'; import { LineAnnotation, AnnotationDomainType, LineAnnotationStyle } from '@elastic/charts'; -import lightEuiTheme from '@elastic/eui/dist/eui_theme_light.json'; -import darkEuiTheme from '@elastic/eui/dist/eui_theme_dark.json'; +import { + euiLightVars as lightEuiTheme, + euiDarkVars as darkEuiTheme, +} from '@kbn/ui-shared-deps-src/theme'; interface CurrentTimeProps { isDarkMode: boolean; diff --git a/src/plugins/charts/public/static/components/endzones.tsx b/src/plugins/charts/public/static/components/endzones.tsx index 85a020e54eb37..695b51c9702d2 100644 --- a/src/plugins/charts/public/static/components/endzones.tsx +++ b/src/plugins/charts/public/static/components/endzones.tsx @@ -17,8 +17,10 @@ import { } from '@elastic/charts'; import { i18n } from '@kbn/i18n'; import { EuiFlexGroup, EuiFlexItem, EuiIcon, EuiSpacer } from '@elastic/eui'; -import lightEuiTheme from '@elastic/eui/dist/eui_theme_light.json'; -import darkEuiTheme from '@elastic/eui/dist/eui_theme_dark.json'; +import { + euiLightVars as lightEuiTheme, + euiDarkVars as darkEuiTheme, +} from '@kbn/ui-shared-deps-src/theme'; interface EndzonesProps { isDarkMode: boolean; diff --git a/src/plugins/dashboard/public/application/embeddable/viewport/dashboard_viewport.tsx b/src/plugins/dashboard/public/application/embeddable/viewport/dashboard_viewport.tsx index 1f4cd3952e7a5..611a426dd4d71 100644 --- a/src/plugins/dashboard/public/application/embeddable/viewport/dashboard_viewport.tsx +++ b/src/plugins/dashboard/public/application/embeddable/viewport/dashboard_viewport.tsx @@ -8,7 +8,7 @@ import React from 'react'; import { Subscription } from 'rxjs'; -import { PanelState, ViewMode } from '../../../services/embeddable'; +import { ViewMode } from '../../../services/embeddable'; import { DashboardContainer, DashboardReactContextValue } from '../dashboard_container'; import { DashboardGrid } from '../grid'; import { context } from '../../../services/kibana_react'; @@ -26,7 +26,7 @@ interface State { useMargins: boolean; title: string; description?: string; - panels: { [key: string]: PanelState }; + panelCount: number; isEmbeddedExternally?: boolean; } @@ -48,7 +48,7 @@ export class DashboardViewport extends React.Component { - const { isFullScreenMode, useMargins, title, description, isEmbeddedExternally } = + const { isFullScreenMode, useMargins, title, description, isEmbeddedExternally, panels } = this.props.container.getInput(); if (this.mounted) { this.setState({ + panelCount: Object.values(panels).length, + isEmbeddedExternally, isFullScreenMode, description, useMargins, title, - isEmbeddedExternally, }); } }); @@ -94,13 +95,13 @@ export class DashboardViewport extends React.Component
    { - if (nextUrl.includes(DASHBOARD_STATE_STORAGE_KEY)) { - return replaceUrlHashQuery(nextUrl, (query) => { - delete query[DASHBOARD_STATE_STORAGE_KEY]; - return query; - }); - } - return nextUrl; - }, true); + if (!awaitingRemoval) { + awaitingRemoval = true; + kbnUrlStateStorage.kbnUrlControls.updateAsync((nextUrl) => { + if (nextUrl.includes(DASHBOARD_STATE_STORAGE_KEY)) { + return replaceUrlHashQuery(nextUrl, (query) => { + delete query[DASHBOARD_STATE_STORAGE_KEY]; + return query; + }); + } + awaitingRemoval = false; + return nextUrl; + }, true); + } return { ..._.omit(rawAppStateInUrl, ['panels', 'query']), diff --git a/src/plugins/dashboard/public/application/listing/__snapshots__/dashboard_listing.test.tsx.snap b/src/plugins/dashboard/public/application/listing/__snapshots__/dashboard_listing.test.tsx.snap index 2e37dc61fe851..2f383adb3f5c3 100644 --- a/src/plugins/dashboard/public/application/listing/__snapshots__/dashboard_listing.test.tsx.snap +++ b/src/plugins/dashboard/public/application/listing/__snapshots__/dashboard_listing.test.tsx.snap @@ -34,13 +34,13 @@ exports[`after fetch When given a title that matches multiple dashboards, filter iconType="plusInCircle" onClick={[Function]} > - Create new dashboard + Create a dashboard } body={

    - You can combine data views from any Kibana app into one dashboard and see everything in one place. + Analyze all of your Elastic data in one place by creating a dashboard and adding visualizations.

    - Install some sample data + Add some sample data , } } @@ -146,13 +146,13 @@ exports[`after fetch initialFilter 1`] = ` iconType="plusInCircle" onClick={[Function]} > - Create new dashboard + Create a dashboard } body={

    - You can combine data views from any Kibana app into one dashboard and see everything in one place. + Analyze all of your Elastic data in one place by creating a dashboard and adding visualizations.

    - Install some sample data + Add some sample data , } } @@ -257,13 +257,13 @@ exports[`after fetch renders all table rows 1`] = ` iconType="plusInCircle" onClick={[Function]} > - Create new dashboard + Create a dashboard } body={

    - You can combine data views from any Kibana app into one dashboard and see everything in one place. + Analyze all of your Elastic data in one place by creating a dashboard and adding visualizations.

    - Install some sample data + Add some sample data , } } @@ -368,13 +368,13 @@ exports[`after fetch renders call to action when no dashboards exist 1`] = ` iconType="plusInCircle" onClick={[Function]} > - Create new dashboard + Create a dashboard } body={

    - You can combine data views from any Kibana app into one dashboard and see everything in one place. + Analyze all of your Elastic data in one place by creating a dashboard and adding visualizations.

    - Install some sample data + Add some sample data , } } @@ -446,6 +446,128 @@ exports[`after fetch renders call to action when no dashboards exist 1`] = ` `; +exports[`after fetch renders call to action with continue when no dashboards exist but one is in progress 1`] = ` + + + + + Discard changes + + + + + Continue editing + + + + } + body={ + +

    + Analyze all of your Elastic data in one place by creating a dashboard and adding visualizations. +

    +
    + } + iconType="dashboardApp" + title={ +

    + Dashboard in progress +

    + } + /> + } + entityName="dashboard" + entityNamePlural="dashboards" + findItems={[Function]} + headingId="dashboardListingHeading" + initialFilter="" + initialPageSize={20} + listingLimit={100} + rowHeader="title" + searchFilters={Array []} + tableCaption="Dashboards" + tableColumns={ + Array [ + Object { + "field": "title", + "name": "Title", + "render": [Function], + "sortable": true, + }, + Object { + "field": "description", + "name": "Description", + "render": [Function], + "sortable": true, + }, + ] + } + tableListTitle="Dashboards" + toastNotifications={ + Object { + "add": [MockFunction], + "addDanger": [MockFunction], + "addError": [MockFunction], + "addInfo": [MockFunction], + "addSuccess": [MockFunction], + "addWarning": [MockFunction], + "get$": [MockFunction], + "remove": [MockFunction], + } + } + /> + +`; + exports[`after fetch renders warning when listingLimit is exceeded 1`] = ` - Create new dashboard + Create a dashboard } body={

    - You can combine data views from any Kibana app into one dashboard and see everything in one place. + Analyze all of your Elastic data in one place by creating a dashboard and adding visualizations.

    - Install some sample data + Add some sample data , } } diff --git a/src/plugins/dashboard/public/application/listing/dashboard_listing.test.tsx b/src/plugins/dashboard/public/application/listing/dashboard_listing.test.tsx index 37ee0ec13d7c9..ff34a63bdce19 100644 --- a/src/plugins/dashboard/public/application/listing/dashboard_listing.test.tsx +++ b/src/plugins/dashboard/public/application/listing/dashboard_listing.test.tsx @@ -16,6 +16,7 @@ import { KibanaContextProvider } from '../../services/kibana_react'; import { createKbnUrlStateStorage } from '../../services/kibana_utils'; import { DashboardListing, DashboardListingProps } from './dashboard_listing'; import { makeDefaultServices } from '../test_helpers'; +import { DASHBOARD_PANELS_UNSAVED_ID } from '../lib/dashboard_session_storage'; function makeDefaultProps(): DashboardListingProps { return { @@ -72,6 +73,25 @@ describe('after fetch', () => { expect(component).toMatchSnapshot(); }); + test('renders call to action with continue when no dashboards exist but one is in progress', async () => { + const services = makeDefaultServices(); + services.savedDashboards.find = () => { + return Promise.resolve({ + total: 0, + hits: [], + }); + }; + services.dashboardSessionStorage.getDashboardIdsWithUnsavedChanges = () => [ + DASHBOARD_PANELS_UNSAVED_ID, + ]; + const { component } = mountWith({ services }); + // Ensure all promises resolve + await new Promise((resolve) => process.nextTick(resolve)); + // Ensure the state changes are reflected + component.update(); + expect(component).toMatchSnapshot(); + }); + test('initialFilter', async () => { const props = makeDefaultProps(); props.initialFilter = 'testFilter'; diff --git a/src/plugins/dashboard/public/application/listing/dashboard_listing.tsx b/src/plugins/dashboard/public/application/listing/dashboard_listing.tsx index 827e5abf2bd6a..8b99b5c51598a 100644 --- a/src/plugins/dashboard/public/application/listing/dashboard_listing.tsx +++ b/src/plugins/dashboard/public/application/listing/dashboard_listing.tsx @@ -7,7 +7,15 @@ */ import { FormattedMessage } from '@kbn/i18n/react'; -import { EuiLink, EuiButton, EuiEmptyPrompt, EuiBasicTableColumn } from '@elastic/eui'; +import { + EuiLink, + EuiButton, + EuiEmptyPrompt, + EuiBasicTableColumn, + EuiFlexGroup, + EuiFlexItem, + EuiButtonEmpty, +} from '@elastic/eui'; import React, { useCallback, useEffect, useMemo, useState } from 'react'; import { attemptLoadDashboardByTitle } from '../lib'; import { DashboardAppServices, DashboardRedirect } from '../../types'; @@ -15,6 +23,8 @@ import { getDashboardBreadcrumb, dashboardListingTable, noItemsStrings, + dashboardUnsavedListingStrings, + getNewDashboardTitle, } from '../../dashboard_strings'; import { ApplicationStart, SavedObjectsFindOptionsReference } from '../../../../../core/public'; import { syncQueryStateWithUrl } from '../../services/data'; @@ -22,8 +32,9 @@ import { IKbnUrlStateStorage } from '../../services/kibana_utils'; import { TableListView, useKibana } from '../../services/kibana_react'; import { SavedObjectsTaggingApi } from '../../services/saved_objects_tagging_oss'; import { DashboardUnsavedListing } from './dashboard_unsaved_listing'; -import { confirmCreateWithUnsaved } from './confirm_overlays'; +import { confirmCreateWithUnsaved, confirmDiscardUnsavedChanges } from './confirm_overlays'; import { getDashboardListItemLink } from './get_dashboard_list_item_link'; +import { DASHBOARD_PANELS_UNSAVED_ID } from '../lib/dashboard_session_storage'; export interface DashboardListingProps { kbnUrlStateStorage: IKbnUrlStateStorage; @@ -117,10 +128,109 @@ export const DashboardListing = ({ } }, [dashboardSessionStorage, redirectTo, core.overlays]); - const emptyPrompt = useMemo( - () => getNoItemsMessage(showWriteControls, core.application, createItem), - [createItem, core.application, showWriteControls] - ); + const emptyPrompt = useMemo(() => { + if (!showWriteControls) { + return ( + {noItemsStrings.getReadonlyTitle()}} + body={

    {noItemsStrings.getReadonlyBody()}

    } + /> + ); + } + + const isEditingFirstDashboard = unsavedDashboardIds.length === 1; + + const emptyAction = isEditingFirstDashboard ? ( + + + + confirmDiscardUnsavedChanges(core.overlays, () => { + dashboardSessionStorage.clearState(DASHBOARD_PANELS_UNSAVED_ID); + setUnsavedDashboardIds(dashboardSessionStorage.getDashboardIdsWithUnsavedChanges()); + }) + } + data-test-subj="discardDashboardPromptButton" + aria-label={dashboardUnsavedListingStrings.getDiscardAriaLabel(getNewDashboardTitle())} + > + {dashboardUnsavedListingStrings.getDiscardTitle()} + + + + redirectTo({ destination: 'dashboard' })} + data-test-subj="createDashboardPromptButton" + aria-label={dashboardUnsavedListingStrings.getEditAriaLabel(getNewDashboardTitle())} + > + {dashboardUnsavedListingStrings.getEditTitle()} + + + + ) : ( + + {noItemsStrings.getCreateNewDashboardText()} + + ); + + return ( + + {isEditingFirstDashboard + ? noItemsStrings.getReadEditInProgressTitle() + : noItemsStrings.getReadEditTitle()} + + } + body={ + <> +

    {noItemsStrings.getReadEditDashboardDescription()}

    + {!isEditingFirstDashboard && ( +

    + + core.application.navigateToApp('home', { + path: '#/tutorial_directory/sampleData', + }) + } + > + {noItemsStrings.getSampleDataLinkText()} + + ), + }} + /> +

    + )} + + } + actions={emptyAction} + /> + ); + }, [ + redirectTo, + createItem, + core.overlays, + core.application, + showWriteControls, + unsavedDashboardIds, + dashboardSessionStorage, + ]); const fetchItems = useCallback( (filter: string) => { @@ -233,60 +343,3 @@ const getTableColumns = ( ...(savedObjectsTagging ? [savedObjectsTagging.ui.getTableColumnDefinition()] : []), ] as unknown as Array>>; }; - -const getNoItemsMessage = ( - showWriteControls: boolean, - application: ApplicationStart, - createItem: () => void -) => { - if (!showWriteControls) { - return ( - {noItemsStrings.getReadonlyTitle()}} - body={

    {noItemsStrings.getReadonlyBody()}

    } - /> - ); - } - - return ( - {noItemsStrings.getReadEditTitle()}} - body={ - <> -

    {noItemsStrings.getReadEditDashboardDescription()}

    -

    - - application.navigateToApp('home', { - path: '#/tutorial_directory/sampleData', - }) - } - > - {noItemsStrings.getSampleDataLinkText()} - - ), - }} - /> -

    - - } - actions={ - - {noItemsStrings.getCreateNewDashboardText()} - - } - /> - ); -}; diff --git a/src/plugins/dashboard/public/application/top_nav/editor_menu.tsx b/src/plugins/dashboard/public/application/top_nav/editor_menu.tsx index 8a46a16c1bf0c..effbf8ce980d7 100644 --- a/src/plugins/dashboard/public/application/top_nav/editor_menu.tsx +++ b/src/plugins/dashboard/public/application/top_nav/editor_menu.tsx @@ -231,7 +231,7 @@ export const EditorMenu = ({ dashboardContainer, createNewVisType }: Props) => { i18n.translate('dashboard.createConfirmModal.unsavedChangesSubtitle', { - defaultMessage: 'You can continue editing or start with a blank dashboard.', + defaultMessage: 'Continue editing or start over with a blank dashboard.', }), getStartOverButtonText: () => i18n.translate('dashboard.createConfirmModal.confirmButtonLabel', { @@ -420,7 +420,7 @@ export const dashboardListingTable = { export const dashboardUnsavedListingStrings = { getUnsavedChangesTitle: (plural = false) => i18n.translate('dashboard.listing.unsaved.unsavedChangesTitle', { - defaultMessage: 'You have unsaved changes in the following {dash}.', + defaultMessage: 'You have unsaved changes in the following {dash}:', values: { dash: plural ? dashboardListingTable.getEntityNamePlural() @@ -469,17 +469,21 @@ export const noItemsStrings = { i18n.translate('dashboard.listing.createNewDashboard.title', { defaultMessage: 'Create your first dashboard', }), + getReadEditInProgressTitle: () => + i18n.translate('dashboard.listing.createNewDashboard.inProgressTitle', { + defaultMessage: 'Dashboard in progress', + }), getReadEditDashboardDescription: () => i18n.translate('dashboard.listing.createNewDashboard.combineDataViewFromKibanaAppDescription', { defaultMessage: - 'You can combine data views from any Kibana app into one dashboard and see everything in one place.', + 'Analyze all of your Elastic data in one place by creating a dashboard and adding visualizations.', }), getSampleDataLinkText: () => i18n.translate('dashboard.listing.createNewDashboard.sampleDataInstallLinkText', { - defaultMessage: `Install some sample data`, + defaultMessage: `Add some sample data`, }), getCreateNewDashboardText: () => i18n.translate('dashboard.listing.createNewDashboard.createButtonLabel', { - defaultMessage: `Create new dashboard`, + defaultMessage: `Create a dashboard`, }), }; diff --git a/src/plugins/data/common/search/aggs/agg_type.ts b/src/plugins/data/common/search/aggs/agg_type.ts index 917f80d3b7819..3f91eadd19eb4 100644 --- a/src/plugins/data/common/search/aggs/agg_type.ts +++ b/src/plugins/data/common/search/aggs/agg_type.ts @@ -53,6 +53,7 @@ export interface AggTypeConfig< json?: boolean; decorateAggConfig?: () => any; postFlightRequest?: PostFlightRequestFn; + hasPrecisionError?: (aggBucket: Record) => boolean; getSerializedFormat?: (agg: TAggConfig) => SerializedFieldFormat; getValue?: (agg: TAggConfig, bucket: any) => any; getKey?: (bucket: any, key: any, agg: TAggConfig) => any; @@ -180,6 +181,9 @@ export class AggType< * is created, giving the agg type a chance to modify the agg config */ decorateAggConfig: () => any; + + hasPrecisionError?: (aggBucket: Record) => boolean; + /** * A function that needs to be called after the main request has been made * and should return an updated response @@ -283,6 +287,7 @@ export class AggType< this.getResponseAggs = config.getResponseAggs || (() => {}); this.decorateAggConfig = config.decorateAggConfig || (() => ({})); this.postFlightRequest = config.postFlightRequest || identity; + this.hasPrecisionError = config.hasPrecisionError; this.getSerializedFormat = config.getSerializedFormat || diff --git a/src/plugins/data/common/search/aggs/buckets/terms.test.ts b/src/plugins/data/common/search/aggs/buckets/terms.test.ts index 50aa4eb2b0357..524606f7c562f 100644 --- a/src/plugins/data/common/search/aggs/buckets/terms.test.ts +++ b/src/plugins/data/common/search/aggs/buckets/terms.test.ts @@ -286,7 +286,19 @@ describe('Terms Agg', () => { { typesRegistry: mockAggTypesRegistry() } ); const { [BUCKET_TYPES.TERMS]: params } = aggConfigs.aggs[0].toDsl(); + expect(params.order).toEqual({ 'test-orderAgg.50': 'desc' }); }); + + test('should override "hasPrecisionError" for the "terms" bucket type', () => { + const aggConfigs = getAggConfigs(); + const { type } = aggConfigs.aggs[0]; + + expect(type.hasPrecisionError).toBeInstanceOf(Function); + + expect(type.hasPrecisionError!({})).toBeFalsy(); + expect(type.hasPrecisionError!({ doc_count_error_upper_bound: 0 })).toBeFalsy(); + expect(type.hasPrecisionError!({ doc_count_error_upper_bound: -1 })).toBeTruthy(); + }); }); }); diff --git a/src/plugins/data/common/search/aggs/buckets/terms.ts b/src/plugins/data/common/search/aggs/buckets/terms.ts index b9329bcb25af3..b3872d29beaac 100644 --- a/src/plugins/data/common/search/aggs/buckets/terms.ts +++ b/src/plugins/data/common/search/aggs/buckets/terms.ts @@ -85,6 +85,7 @@ export const getTermsBucketAgg = () => }; }, createFilter: createFilterTerms, + hasPrecisionError: (aggBucket) => Boolean(aggBucket?.doc_count_error_upper_bound), postFlightRequest: async ( resp, aggConfigs, diff --git a/src/plugins/data/common/search/search_source/mocks.ts b/src/plugins/data/common/search/search_source/mocks.ts index a0897a2dacf2a..dee5c09a6b858 100644 --- a/src/plugins/data/common/search/search_source/mocks.ts +++ b/src/plugins/data/common/search/search_source/mocks.ts @@ -14,7 +14,7 @@ import { SearchSource } from './search_source'; import { ISearchStartSearchSource, ISearchSource, SearchSourceFields } from './types'; export const searchSourceInstanceMock: MockedKeys = { - setPreferredSearchStrategyId: jest.fn(), + setOverwriteDataViewType: jest.fn(), setFields: jest.fn().mockReturnThis(), setField: jest.fn().mockReturnThis(), removeField: jest.fn().mockReturnThis(), diff --git a/src/plugins/data/common/search/search_source/search_source.ts b/src/plugins/data/common/search/search_source/search_source.ts index 50752523403cf..a3979ffa6e943 100644 --- a/src/plugins/data/common/search/search_source/search_source.ts +++ b/src/plugins/data/common/search/search_source/search_source.ts @@ -124,7 +124,8 @@ export interface SearchSourceDependencies extends FetchHandlers { /** @public **/ export class SearchSource { private id: string = uniqueId('data_source'); - private searchStrategyId?: string; + private shouldOverwriteDataViewType: boolean = false; + private overwriteDataViewType?: string; private parent?: SearchSource; private requestStartHandlers: Array< (searchSource: SearchSource, options?: ISearchOptions) => Promise @@ -149,11 +150,22 @@ export class SearchSource { *****/ /** - * internal, dont use - * @param searchStrategyId + * Used to make the search source overwrite the actual data view type for the + * specific requests done. This should only be needed very rarely, since it means + * e.g. we'd be treating a rollup index pattern as a regular one. Be very sure + * you understand the consequences of using this method before using it. + * + * @param overwriteType If `false` is passed in it will disable the overwrite, otherwise + * the passed in value will be used as the data view type for this search source. */ - setPreferredSearchStrategyId(searchStrategyId: string) { - this.searchStrategyId = searchStrategyId; + setOverwriteDataViewType(overwriteType: string | undefined | false) { + if (overwriteType === false) { + this.shouldOverwriteDataViewType = false; + this.overwriteDataViewType = undefined; + } else { + this.shouldOverwriteDataViewType = true; + this.overwriteDataViewType = overwriteType; + } } /** @@ -609,11 +621,7 @@ export class SearchSource { } private getIndexType(index?: IIndexPattern) { - if (this.searchStrategyId) { - return this.searchStrategyId === 'default' ? undefined : this.searchStrategyId; - } else { - return index?.type; - } + return this.shouldOverwriteDataViewType ? this.overwriteDataViewType : index?.type; } private readonly getFieldName = (fld: string | Record): string => diff --git a/src/plugins/data/common/search/tabify/index.ts b/src/plugins/data/common/search/tabify/index.ts index 279ff705f231c..3a4b094826e78 100644 --- a/src/plugins/data/common/search/tabify/index.ts +++ b/src/plugins/data/common/search/tabify/index.ts @@ -9,3 +9,4 @@ export { tabifyDocs, flattenHit } from './tabify_docs'; export { tabifyAggResponse } from './tabify'; export { tabifyGetColumns } from './get_columns'; +export { checkColumnForPrecisionError } from './utils'; diff --git a/src/plugins/data/common/search/tabify/response_writer.test.ts b/src/plugins/data/common/search/tabify/response_writer.test.ts index 603ccc0f493c7..cee297d255db3 100644 --- a/src/plugins/data/common/search/tabify/response_writer.test.ts +++ b/src/plugins/data/common/search/tabify/response_writer.test.ts @@ -166,6 +166,7 @@ describe('TabbedAggResponseWriter class', () => { field: 'geo.src', source: 'esaggs', sourceParams: { + hasPrecisionError: false, enabled: true, id: '1', indexPatternId: '1234', @@ -193,6 +194,7 @@ describe('TabbedAggResponseWriter class', () => { }, source: 'esaggs', sourceParams: { + hasPrecisionError: false, appliedTimeRange: undefined, enabled: true, id: '2', @@ -227,6 +229,7 @@ describe('TabbedAggResponseWriter class', () => { field: 'geo.src', source: 'esaggs', sourceParams: { + hasPrecisionError: false, enabled: true, id: '1', indexPatternId: '1234', @@ -254,6 +257,7 @@ describe('TabbedAggResponseWriter class', () => { }, source: 'esaggs', sourceParams: { + hasPrecisionError: false, appliedTimeRange: undefined, enabled: true, id: '2', diff --git a/src/plugins/data/common/search/tabify/response_writer.ts b/src/plugins/data/common/search/tabify/response_writer.ts index a0ba07598e53a..6af0576b9ed4d 100644 --- a/src/plugins/data/common/search/tabify/response_writer.ts +++ b/src/plugins/data/common/search/tabify/response_writer.ts @@ -10,7 +10,7 @@ import { isEmpty } from 'lodash'; import { IAggConfigs } from '../aggs'; import { tabifyGetColumns } from './get_columns'; -import { TabbedResponseWriterOptions, TabbedAggColumn, TabbedAggRow } from './types'; +import type { TabbedResponseWriterOptions, TabbedAggColumn, TabbedAggRow } from './types'; import { Datatable, DatatableColumn } from '../../../../expressions/common/expression_types/specs'; interface BufferColumn { @@ -80,6 +80,7 @@ export class TabbedAggResponseWriter { params: column.aggConfig.toSerializedFieldFormat(), source: 'esaggs', sourceParams: { + hasPrecisionError: Boolean(column.hasPrecisionError), indexPatternId: column.aggConfig.getIndexPattern()?.id, appliedTimeRange: column.aggConfig.params.field?.name && diff --git a/src/plugins/data/common/search/tabify/tabify.ts b/src/plugins/data/common/search/tabify/tabify.ts index a4d9551da75d5..d3273accff974 100644 --- a/src/plugins/data/common/search/tabify/tabify.ts +++ b/src/plugins/data/common/search/tabify/tabify.ts @@ -42,8 +42,14 @@ export function tabifyAggResponse( switch (agg.type.type) { case AggGroupNames.Buckets: - const aggBucket = get(bucket, agg.id); + const aggBucket = get(bucket, agg.id) as Record; const tabifyBuckets = new TabifyBuckets(aggBucket, agg.params, respOpts?.timeRange); + const precisionError = agg.type.hasPrecisionError?.(aggBucket); + + if (precisionError) { + // "сolumn" mutation, we have to do this here as this value is filled in based on aggBucket value + column.hasPrecisionError = true; + } if (tabifyBuckets.length) { tabifyBuckets.forEach((subBucket, tabifyBucketKey) => { diff --git a/src/plugins/data/common/search/tabify/types.ts b/src/plugins/data/common/search/tabify/types.ts index 758a2dfb181f2..9fadb0ef860e3 100644 --- a/src/plugins/data/common/search/tabify/types.ts +++ b/src/plugins/data/common/search/tabify/types.ts @@ -41,6 +41,7 @@ export interface TabbedAggColumn { aggConfig: IAggConfig; id: string; name: string; + hasPrecisionError?: boolean; } /** @public **/ diff --git a/src/plugins/data/common/search/tabify/utils.test.ts b/src/plugins/data/common/search/tabify/utils.test.ts new file mode 100644 index 0000000000000..ed29ef58ec0bf --- /dev/null +++ b/src/plugins/data/common/search/tabify/utils.test.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 { checkColumnForPrecisionError } from './utils'; +import type { DatatableColumn } from '../../../../expressions'; + +describe('tabify utils', () => { + describe('checkDatatableForPrecisionError', () => { + test('should return true if there is a precision error in the column', () => { + expect( + checkColumnForPrecisionError({ + meta: { + sourceParams: { + hasPrecisionError: true, + }, + }, + } as unknown as DatatableColumn) + ).toBeTruthy(); + }); + test('should return false if there is no precision error in the column', () => { + expect( + checkColumnForPrecisionError({ + meta: { + sourceParams: { + hasPrecisionError: false, + }, + }, + } as unknown as DatatableColumn) + ).toBeFalsy(); + }); + test('should return false if precision error is not defined', () => { + expect( + checkColumnForPrecisionError({ + meta: { + sourceParams: {}, + }, + } as unknown as DatatableColumn) + ).toBeFalsy(); + }); + }); +}); diff --git a/src/plugins/data/common/search/tabify/utils.ts b/src/plugins/data/common/search/tabify/utils.ts new file mode 100644 index 0000000000000..1a4f87e2fed73 --- /dev/null +++ b/src/plugins/data/common/search/tabify/utils.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 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 { DatatableColumn } from '../../../../expressions'; + +/** @public **/ +export const checkColumnForPrecisionError = (column: DatatableColumn) => + column.meta.sourceParams?.hasPrecisionError; diff --git a/src/plugins/data/common/search/types.ts b/src/plugins/data/common/search/types.ts index 68a25d4c4d69d..e9b6160c4a75a 100644 --- a/src/plugins/data/common/search/types.ts +++ b/src/plugins/data/common/search/types.ts @@ -71,7 +71,7 @@ export interface IKibanaSearchResponse { isRestored?: boolean; /** - * Optional warnings that should be surfaced to the end user + * Optional warnings returned from Elasticsearch (for example, deprecation warnings) */ warning?: string; diff --git a/src/plugins/data/public/index.ts b/src/plugins/data/public/index.ts index 0b749d90f7152..a54a9c7f35e3f 100644 --- a/src/plugins/data/public/index.ts +++ b/src/plugins/data/public/index.ts @@ -139,6 +139,7 @@ import { // tabify tabifyAggResponse, tabifyGetColumns, + checkColumnForPrecisionError, } from '../common'; export { AggGroupLabels, AggGroupNames, METRIC_TYPES, BUCKET_TYPES } from '../common'; @@ -246,6 +247,7 @@ export const search = { getResponseInspectorStats, tabifyAggResponse, tabifyGetColumns, + checkColumnForPrecisionError, }; /* diff --git a/src/plugins/data/public/search/fetch/handle_response.tsx b/src/plugins/data/public/search/fetch/handle_response.tsx index f4acebfb36060..9e68209af2b92 100644 --- a/src/plugins/data/public/search/fetch/handle_response.tsx +++ b/src/plugins/data/public/search/fetch/handle_response.tsx @@ -16,17 +16,7 @@ import { getNotifications } from '../../services'; import { SearchRequest } from '..'; export function handleResponse(request: SearchRequest, response: IKibanaSearchResponse) { - const { rawResponse, warning } = response; - if (warning) { - getNotifications().toasts.addWarning({ - title: i18n.translate('data.search.searchSource.fetch.warningMessage', { - defaultMessage: 'Warning: {warning}', - values: { - warning, - }, - }), - }); - } + const { rawResponse } = response; if (rawResponse.timed_out) { getNotifications().toasts.addWarning({ diff --git a/src/plugins/data/public/search/index.ts b/src/plugins/data/public/search/index.ts index 821f16e0cf68a..2cd7993e3b183 100644 --- a/src/plugins/data/public/search/index.ts +++ b/src/plugins/data/public/search/index.ts @@ -36,6 +36,7 @@ export { parseSearchSourceJSON, SearchSource, SortDirection, + checkColumnForPrecisionError, } from '../../common/search'; export type { ISessionService, diff --git a/src/plugins/data/public/ui/query_string_input/query_bar_top_row.test.tsx b/src/plugins/data/public/ui/query_string_input/query_bar_top_row.test.tsx index 0541e12cf8172..453e74c9fad5b 100644 --- a/src/plugins/data/public/ui/query_string_input/query_bar_top_row.test.tsx +++ b/src/plugins/data/public/ui/query_string_input/query_bar_top_row.test.tsx @@ -10,10 +10,9 @@ import { mockPersistedLogFactory } from './query_string_input.test.mocks'; import React from 'react'; import { mount } from 'enzyme'; -import { waitFor } from '@testing-library/dom'; import { render } from '@testing-library/react'; -import { QueryBarTopRow } from './'; +import QueryBarTopRow from './query_bar_top_row'; import { coreMock } from '../../../../../core/public/mocks'; import { dataPluginMock } from '../../mocks'; @@ -103,8 +102,7 @@ function wrapQueryBarTopRowInContext(testProps: any) { ); } -// Failing: See https://github.com/elastic/kibana/issues/92528 -describe.skip('QueryBarTopRowTopRow', () => { +describe('QueryBarTopRowTopRow', () => { const QUERY_INPUT_SELECTOR = 'QueryStringInputUI'; const TIMEPICKER_SELECTOR = 'EuiSuperDatePicker'; const TIMEPICKER_DURATION = '[data-shared-timefilter-duration]'; @@ -113,7 +111,7 @@ describe.skip('QueryBarTopRowTopRow', () => { jest.clearAllMocks(); }); - it('Should render query and time picker', async () => { + it('Should render query and time picker', () => { const { getByText, getByTestId } = render( wrapQueryBarTopRowInContext({ query: kqlQuery, @@ -124,8 +122,8 @@ describe.skip('QueryBarTopRowTopRow', () => { }) ); - await waitFor(() => getByText(kqlQuery.query)); - await waitFor(() => getByTestId('superDatePickerShowDatesButton')); + expect(getByText(kqlQuery.query)).toBeInTheDocument(); + expect(getByTestId('superDatePickerShowDatesButton')).toBeInTheDocument(); }); it('Should create a unique PersistedLog based on the appName and query language', () => { diff --git a/src/plugins/data_views/common/data_views/data_views.ts b/src/plugins/data_views/common/data_views/data_views.ts index 7882f8d7ca43e..b54bcf3471d4b 100644 --- a/src/plugins/data_views/common/data_views/data_views.ts +++ b/src/plugins/data_views/common/data_views/data_views.ts @@ -230,7 +230,7 @@ export class DataViewsService { * @param force */ setDefault = async (id: string | null, force = false) => { - if (force || !this.config.get('defaultIndex')) { + if (force || !(await this.config.get('defaultIndex'))) { await this.config.set('defaultIndex', id); } }; diff --git a/src/plugins/data_views/server/fetcher/index_patterns_fetcher.test.ts b/src/plugins/data_views/server/fetcher/index_patterns_fetcher.test.ts index a65d4d551cf7c..1a8b705480258 100644 --- a/src/plugins/data_views/server/fetcher/index_patterns_fetcher.test.ts +++ b/src/plugins/data_views/server/fetcher/index_patterns_fetcher.test.ts @@ -5,7 +5,6 @@ * in compliance with, at your election, the Elastic License 2.0 or the Server * Side Public License, v 1. */ - import { IndexPatternsFetcher } from '.'; import { ElasticsearchClient } from 'kibana/server'; import * as indexNotFoundException from './index_not_found_exception.json'; @@ -15,36 +14,36 @@ describe('Index Pattern Fetcher - server', () => { let esClient: ElasticsearchClient; const emptyResponse = { body: { - count: 0, + indices: [], }, }; const response = { body: { - count: 1115, + indices: ['b'], + fields: [{ name: 'foo' }, { name: 'bar' }, { name: 'baz' }], }, }; const patternList = ['a', 'b', 'c']; beforeEach(() => { + jest.clearAllMocks(); esClient = { - count: jest.fn().mockResolvedValueOnce(emptyResponse).mockResolvedValue(response), + fieldCaps: jest.fn().mockResolvedValueOnce(emptyResponse).mockResolvedValue(response), } as unknown as ElasticsearchClient; indexPatterns = new IndexPatternsFetcher(esClient); }); - it('Removes pattern without matching indices', async () => { const result = await indexPatterns.validatePatternListActive(patternList); expect(result).toEqual(['b', 'c']); }); - it('Returns all patterns when all match indices', async () => { esClient = { - count: jest.fn().mockResolvedValue(response), + fieldCaps: jest.fn().mockResolvedValue(response), } as unknown as ElasticsearchClient; indexPatterns = new IndexPatternsFetcher(esClient); const result = await indexPatterns.validatePatternListActive(patternList); expect(result).toEqual(patternList); }); - it('Removes pattern when "index_not_found_exception" error is thrown', async () => { + it('Removes pattern when error is thrown', async () => { class ServerError extends Error { public body?: Record; constructor( @@ -56,9 +55,8 @@ describe('Index Pattern Fetcher - server', () => { this.body = errBody; } } - esClient = { - count: jest + fieldCaps: jest .fn() .mockResolvedValueOnce(response) .mockRejectedValue( @@ -69,4 +67,22 @@ describe('Index Pattern Fetcher - server', () => { const result = await indexPatterns.validatePatternListActive(patternList); expect(result).toEqual([patternList[0]]); }); + it('When allowNoIndices is false, run validatePatternListActive', async () => { + const fieldCapsMock = jest.fn(); + esClient = { + fieldCaps: fieldCapsMock.mockResolvedValue(response), + } as unknown as ElasticsearchClient; + indexPatterns = new IndexPatternsFetcher(esClient); + await indexPatterns.getFieldsForWildcard({ pattern: patternList }); + expect(fieldCapsMock.mock.calls).toHaveLength(4); + }); + it('When allowNoIndices is true, do not run validatePatternListActive', async () => { + const fieldCapsMock = jest.fn(); + esClient = { + fieldCaps: fieldCapsMock.mockResolvedValue(response), + } as unknown as ElasticsearchClient; + indexPatterns = new IndexPatternsFetcher(esClient, true); + await indexPatterns.getFieldsForWildcard({ pattern: patternList }); + expect(fieldCapsMock.mock.calls).toHaveLength(1); + }); }); diff --git a/src/plugins/data_views/server/fetcher/index_patterns_fetcher.ts b/src/plugins/data_views/server/fetcher/index_patterns_fetcher.ts index 7dae85c920ebf..c054d547e956f 100644 --- a/src/plugins/data_views/server/fetcher/index_patterns_fetcher.ts +++ b/src/plugins/data_views/server/fetcher/index_patterns_fetcher.ts @@ -36,12 +36,10 @@ interface FieldSubType { export class IndexPatternsFetcher { private elasticsearchClient: ElasticsearchClient; private allowNoIndices: boolean; - constructor(elasticsearchClient: ElasticsearchClient, allowNoIndices: boolean = false) { this.elasticsearchClient = elasticsearchClient; this.allowNoIndices = allowNoIndices; } - /** * Get a list of field objects for an index pattern that may contain wildcards * @@ -60,23 +58,22 @@ export class IndexPatternsFetcher { }): Promise { const { pattern, metaFields, fieldCapsOptions, type, rollupIndex } = options; const patternList = Array.isArray(pattern) ? pattern : pattern.split(','); + const allowNoIndices = fieldCapsOptions + ? fieldCapsOptions.allow_no_indices + : this.allowNoIndices; let patternListActive: string[] = patternList; // if only one pattern, don't bother with validation. We let getFieldCapabilities fail if the single pattern is bad regardless - if (patternList.length > 1) { + if (patternList.length > 1 && !allowNoIndices) { patternListActive = await this.validatePatternListActive(patternList); } const fieldCapsResponse = await getFieldCapabilities( this.elasticsearchClient, - // if none of the patterns are active, pass the original list to get an error - patternListActive.length > 0 ? patternListActive : patternList, + patternListActive, metaFields, { - allow_no_indices: fieldCapsOptions - ? fieldCapsOptions.allow_no_indices - : this.allowNoIndices, + allow_no_indices: allowNoIndices, } ); - if (type === 'rollup' && rollupIndex) { const rollupFields: FieldDescriptor[] = []; const rollupIndexCapabilities = getCapabilitiesForRollupIndices( @@ -87,13 +84,11 @@ export class IndexPatternsFetcher { ).body )[rollupIndex].aggs; const fieldCapsResponseObj = keyBy(fieldCapsResponse, 'name'); - // Keep meta fields metaFields!.forEach( (field: string) => fieldCapsResponseObj[field] && rollupFields.push(fieldCapsResponseObj[field]) ); - return mergeCapabilitiesWithFields( rollupIndexCapabilities, fieldCapsResponseObj, @@ -137,23 +132,20 @@ export class IndexPatternsFetcher { async validatePatternListActive(patternList: string[]) { const result = await Promise.all( patternList - .map((pattern) => - this.elasticsearchClient.count({ - index: pattern, - }) - ) - .map((p) => - p.catch((e) => { - if (e.body.error.type === 'index_not_found_exception') { - return { body: { count: 0 } }; - } - throw e; - }) - ) + .map(async (index) => { + const searchResponse = await this.elasticsearchClient.fieldCaps({ + index, + fields: '_id', + ignore_unavailable: true, + allow_no_indices: false, + }); + return searchResponse.body.indices.length > 0; + }) + .map((p) => p.catch(() => false)) ); return result.reduce( - (acc: string[], { body: { count } }, patternListIndex) => - count > 0 ? [...acc, patternList[patternListIndex]] : acc, + (acc: string[], isValid, patternListIndex) => + isValid ? [...acc, patternList[patternListIndex]] : acc, [] ); } diff --git a/src/plugins/discover/common/index.ts b/src/plugins/discover/common/index.ts index 32704d95423f7..6262855409b29 100644 --- a/src/plugins/discover/common/index.ts +++ b/src/plugins/discover/common/index.ts @@ -21,4 +21,5 @@ export const SEARCH_FIELDS_FROM_SOURCE = 'discover:searchFieldsFromSource'; export const MAX_DOC_FIELDS_DISPLAYED = 'discover:maxDocFieldsDisplayed'; export const SHOW_FIELD_STATISTICS = 'discover:showFieldStatistics'; export const SHOW_MULTIFIELDS = 'discover:showMultiFields'; +export const TRUNCATE_MAX_HEIGHT = 'truncate:maxHeight'; export const SEARCH_EMBEDDABLE_TYPE = 'search'; diff --git a/src/plugins/discover/kibana.json b/src/plugins/discover/kibana.json index 791ce54a0cb1b..92871ca6d5e17 100644 --- a/src/plugins/discover/kibana.json +++ b/src/plugins/discover/kibana.json @@ -9,7 +9,6 @@ "embeddable", "inspector", "fieldFormats", - "kibanaLegacy", "urlForwarding", "navigation", "uiActions", diff --git a/src/plugins/discover/public/__mocks__/saved_search.ts b/src/plugins/discover/public/__mocks__/saved_search.ts index a488fe7e04c50..04c880e7e1928 100644 --- a/src/plugins/discover/public/__mocks__/saved_search.ts +++ b/src/plugins/discover/public/__mocks__/saved_search.ts @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import { SavedSearch } from '../saved_searches'; +import { SavedSearch } from '../services/saved_searches'; import { createSearchSourceMock } from '../../../data/public/mocks'; import { indexPatternMock } from './index_pattern'; import { indexPatternWithTimefieldMock } from './index_pattern_with_timefield'; diff --git a/src/plugins/discover/public/__mocks__/search_session.ts b/src/plugins/discover/public/__mocks__/search_session.ts index a9037217a303a..78a7a7f90a39e 100644 --- a/src/plugins/discover/public/__mocks__/search_session.ts +++ b/src/plugins/discover/public/__mocks__/search_session.ts @@ -8,7 +8,7 @@ import { createMemoryHistory } from 'history'; import { dataPluginMock } from '../../../data/public/mocks'; import { DataPublicPluginStart } from '../../../data/public'; -import { DiscoverSearchSessionManager } from '../application/apps/main/services/discover_search_session'; +import { DiscoverSearchSessionManager } from '../application/main/services/discover_search_session'; export function createSearchSessionMock() { const history = createMemoryHistory(); diff --git a/src/plugins/discover/public/application/apps/main/components/doc_table/actions/columns.test.ts b/src/plugins/discover/public/application/apps/main/components/doc_table/actions/columns.test.ts deleted file mode 100644 index 93b38cfc6519c..0000000000000 --- a/src/plugins/discover/public/application/apps/main/components/doc_table/actions/columns.test.ts +++ /dev/null @@ -1,78 +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 { getStateColumnActions } from './columns'; -import { configMock } from '../../../../../../__mocks__/config'; -import { indexPatternMock } from '../../../../../../__mocks__/index_pattern'; -import { indexPatternsMock } from '../../../../../../__mocks__/index_patterns'; -import { Capabilities } from '../../../../../../../../../core/types'; -import { AppState } from '../../../services/discover_state'; - -function getStateColumnAction(state: {}, setAppState: (state: Partial) => void) { - return getStateColumnActions({ - capabilities: { - discover: { - save: false, - }, - } as unknown as Capabilities, - config: configMock, - indexPattern: indexPatternMock, - indexPatterns: indexPatternsMock, - useNewFieldsApi: true, - setAppState, - state, - }); -} - -describe('Test column actions', () => { - test('getStateColumnActions with empty state', () => { - const setAppState = jest.fn(); - const actions = getStateColumnAction({}, setAppState); - - actions.onAddColumn('_score'); - expect(setAppState).toHaveBeenCalledWith({ columns: ['_score'], sort: [['_score', 'desc']] }); - actions.onAddColumn('test'); - expect(setAppState).toHaveBeenCalledWith({ columns: ['test'] }); - }); - test('getStateColumnActions with columns and sort in state', () => { - const setAppState = jest.fn(); - const actions = getStateColumnAction( - { columns: ['first', 'second'], sort: [['first', 'desc']] }, - setAppState - ); - - actions.onAddColumn('_score'); - expect(setAppState).toHaveBeenCalledWith({ - columns: ['first', 'second', '_score'], - sort: [['first', 'desc']], - }); - setAppState.mockClear(); - actions.onAddColumn('third'); - expect(setAppState).toHaveBeenCalledWith({ - columns: ['first', 'second', 'third'], - sort: [['first', 'desc']], - }); - setAppState.mockClear(); - actions.onRemoveColumn('first'); - expect(setAppState).toHaveBeenCalledWith({ - columns: ['second'], - sort: [], - }); - setAppState.mockClear(); - actions.onSetColumns(['first', 'second', 'third'], true); - expect(setAppState).toHaveBeenCalledWith({ - columns: ['first', 'second', 'third'], - }); - setAppState.mockClear(); - - actions.onMoveColumn('second', 0); - expect(setAppState).toHaveBeenCalledWith({ - columns: ['second', 'first'], - }); - }); -}); diff --git a/src/plugins/discover/public/application/apps/main/components/doc_table/actions/columns.ts b/src/plugins/discover/public/application/apps/main/components/doc_table/actions/columns.ts deleted file mode 100644 index 2fc82e25634bd..0000000000000 --- a/src/plugins/discover/public/application/apps/main/components/doc_table/actions/columns.ts +++ /dev/null @@ -1,120 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ -import { Capabilities, IUiSettingsClient } from 'kibana/public'; -import { SORT_DEFAULT_ORDER_SETTING } from '../../../../../../../common'; -import { - AppState as DiscoverState, - GetStateReturn as DiscoverGetStateReturn, -} from '../../../../../../application/apps/main/services/discover_state'; -import { - AppState as ContextState, - GetStateReturn as ContextGetStateReturn, -} from '../../../../context/services/context_state'; -import { IndexPattern, IndexPatternsContract } from '../../../../../../../../data/public'; -import { popularizeField } from '../../../../../helpers/popularize_field'; - -/** - * Helper function to provide a fallback to a single _source column if the given array of columns - * is empty, and removes _source if there are more than 1 columns given - * @param columns - * @param useNewFieldsApi should a new fields API be used - */ -function buildColumns(columns: string[], useNewFieldsApi = false) { - if (columns.length > 1 && columns.indexOf('_source') !== -1) { - return columns.filter((col) => col !== '_source'); - } else if (columns.length !== 0) { - return columns; - } - return useNewFieldsApi ? [] : ['_source']; -} - -export function addColumn(columns: string[], columnName: string, useNewFieldsApi?: boolean) { - if (columns.includes(columnName)) { - return columns; - } - return buildColumns([...columns, columnName], useNewFieldsApi); -} - -export function removeColumn(columns: string[], columnName: string, useNewFieldsApi?: boolean) { - if (!columns.includes(columnName)) { - return columns; - } - return buildColumns( - columns.filter((col) => col !== columnName), - useNewFieldsApi - ); -} - -export function moveColumn(columns: string[], columnName: string, newIndex: number) { - if (newIndex < 0 || newIndex >= columns.length || !columns.includes(columnName)) { - return columns; - } - const modifiedColumns = [...columns]; - modifiedColumns.splice(modifiedColumns.indexOf(columnName), 1); // remove at old index - modifiedColumns.splice(newIndex, 0, columnName); // insert before new index - return modifiedColumns; -} - -export function getStateColumnActions({ - capabilities, - config, - indexPattern, - indexPatterns, - useNewFieldsApi, - setAppState, - state, -}: { - capabilities: Capabilities; - config: IUiSettingsClient; - indexPattern: IndexPattern; - indexPatterns: IndexPatternsContract; - useNewFieldsApi: boolean; - setAppState: DiscoverGetStateReturn['setAppState'] | ContextGetStateReturn['setAppState']; - state: DiscoverState | ContextState; -}) { - function onAddColumn(columnName: string) { - popularizeField(indexPattern, columnName, indexPatterns, capabilities); - const columns = addColumn(state.columns || [], columnName, useNewFieldsApi); - const defaultOrder = config.get(SORT_DEFAULT_ORDER_SETTING); - const sort = - columnName === '_score' && !state.sort?.length ? [['_score', defaultOrder]] : state.sort; - setAppState({ columns, sort }); - } - - function onRemoveColumn(columnName: string) { - popularizeField(indexPattern, columnName, indexPatterns, capabilities); - const columns = removeColumn(state.columns || [], columnName, useNewFieldsApi); - // The state's sort property is an array of [sortByColumn,sortDirection] - const sort = - state.sort && state.sort.length - ? state.sort.filter((subArr) => subArr[0] !== columnName) - : []; - setAppState({ columns, sort }); - } - - function onMoveColumn(columnName: string, newIndex: number) { - const columns = moveColumn(state.columns || [], columnName, newIndex); - setAppState({ columns }); - } - - function onSetColumns(columns: string[], hideTimeColumn: boolean) { - // The next line should gone when classic table will be removed - const actualColumns = - !hideTimeColumn && indexPattern.timeFieldName && indexPattern.timeFieldName === columns[0] - ? columns.slice(1) - : columns; - - setAppState({ columns: actualColumns }); - } - return { - onAddColumn, - onRemoveColumn, - onMoveColumn, - onSetColumns, - }; -} diff --git a/src/plugins/discover/public/application/apps/main/components/layout/types.ts b/src/plugins/discover/public/application/apps/main/components/layout/types.ts deleted file mode 100644 index e4a780cadceae..0000000000000 --- a/src/plugins/discover/public/application/apps/main/components/layout/types.ts +++ /dev/null @@ -1,38 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 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, - IndexPatternAttributes, - Query, - SavedObject, - TimeRange, -} from '../../../../../../../data/common'; -import { ISearchSource } from '../../../../../../../data/public'; -import { AppState, GetStateReturn } from '../../services/discover_state'; -import { DataRefetch$, SavedSearchData } from '../../services/use_saved_search'; -import { DiscoverServices } from '../../../../../build_services'; -import { SavedSearch } from '../../../../../saved_searches'; -import { RequestAdapter } from '../../../../../../../inspector'; - -export interface DiscoverLayoutProps { - indexPattern: IndexPattern; - indexPatternList: Array>; - inspectorAdapters: { requests: RequestAdapter }; - navigateTo: (url: string) => void; - onChangeIndexPattern: (id: string) => void; - onUpdateQuery: (payload: { dateRange: TimeRange; query?: Query }, isUpdate?: boolean) => void; - resetSavedSearch: () => void; - savedSearch: SavedSearch; - savedSearchData$: SavedSearchData; - savedSearchRefetch$: DataRefetch$; - searchSource: ISearchSource; - services: DiscoverServices; - state: AppState; - stateContainer: GetStateReturn; -} diff --git a/src/plugins/discover/public/application/apps/main/components/top_nav/on_save_search.test.tsx b/src/plugins/discover/public/application/apps/main/components/top_nav/on_save_search.test.tsx deleted file mode 100644 index 1f0cee0b75672..0000000000000 --- a/src/plugins/discover/public/application/apps/main/components/top_nav/on_save_search.test.tsx +++ /dev/null @@ -1,36 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import { showSaveModal } from '../../../../../../../saved_objects/public'; -jest.mock('../../../../../../../saved_objects/public'); - -import { onSaveSearch } from './on_save_search'; -import { indexPatternMock } from '../../../../../__mocks__/index_pattern'; -import { savedSearchMock } from '../../../../../__mocks__/saved_search'; -import { DiscoverServices } from '../../../../../build_services'; -import { GetStateReturn } from '../../services/discover_state'; -import { i18nServiceMock } from '../../../../../../../../core/public/mocks'; - -test('onSaveSearch', async () => { - const serviceMock = { - core: { - i18n: i18nServiceMock.create(), - }, - } as unknown as DiscoverServices; - const stateMock = {} as unknown as GetStateReturn; - - await onSaveSearch({ - indexPattern: indexPatternMock, - navigateTo: jest.fn(), - savedSearch: savedSearchMock, - services: serviceMock, - state: stateMock, - }); - - expect(showSaveModal).toHaveBeenCalled(); -}); diff --git a/src/plugins/discover/public/application/apps/main/components/top_nav/open_options_popover.scss b/src/plugins/discover/public/application/apps/main/components/top_nav/open_options_popover.scss deleted file mode 100644 index f68b2bfe74a9d..0000000000000 --- a/src/plugins/discover/public/application/apps/main/components/top_nav/open_options_popover.scss +++ /dev/null @@ -1,5 +0,0 @@ -$dscOptionsPopoverWidth: $euiSizeL * 12; - -.dscOptionsPopover { - width: $dscOptionsPopoverWidth; -} \ No newline at end of file diff --git a/src/plugins/discover/public/application/apps/main/components/top_nav/open_options_popover.tsx b/src/plugins/discover/public/application/apps/main/components/top_nav/open_options_popover.tsx deleted file mode 100644 index 6e90c702c2bfd..0000000000000 --- a/src/plugins/discover/public/application/apps/main/components/top_nav/open_options_popover.tsx +++ /dev/null @@ -1,134 +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 ReactDOM from 'react-dom'; -import { I18nStart } from 'kibana/public'; -import { i18n } from '@kbn/i18n'; -import { FormattedMessage } from '@kbn/i18n/react'; -import { - EuiSpacer, - EuiButton, - EuiText, - EuiWrappingPopover, - EuiCode, - EuiHorizontalRule, - EuiButtonEmpty, - EuiTextAlign, -} from '@elastic/eui'; -import './open_options_popover.scss'; -import { DOC_TABLE_LEGACY } from '../../../../../../common'; -import { getServices } from '../../../../../kibana_services'; - -const container = document.createElement('div'); -let isOpen = false; - -interface OptionsPopoverProps { - onClose: () => void; - anchorElement: HTMLElement; -} - -export function OptionsPopover(props: OptionsPopoverProps) { - const { - core: { uiSettings }, - addBasePath, - } = getServices(); - const isLegacy = uiSettings.get(DOC_TABLE_LEGACY); - - const mode = isLegacy - ? i18n.translate('discover.openOptionsPopover.legacyTableText', { - defaultMessage: 'Classic table', - }) - : i18n.translate('discover.openOptionsPopover.dataGridText', { - defaultMessage: 'New table', - }); - - return ( - -
    - -

    - - - - ), - currentViewMode: {mode}, - }} - /> -

    -
    - - - - - - - {i18n.translate('discover.openOptionsPopover.goToAdvancedSettings', { - defaultMessage: 'Get started', - })} - - - - - {i18n.translate('discover.openOptionsPopover.gotToAllSettings', { - defaultMessage: 'All Discover options', - })} - - -
    -
    - ); -} - -function onClose() { - ReactDOM.unmountComponentAtNode(container); - document.body.removeChild(container); - isOpen = false; -} - -export function openOptionsPopover({ - I18nContext, - anchorElement, -}: { - I18nContext: I18nStart['Context']; - anchorElement: HTMLElement; -}) { - if (isOpen) { - onClose(); - return; - } - - isOpen = true; - document.body.appendChild(container); - - const element = ( - - - - ); - ReactDOM.render(element, container); -} diff --git a/src/plugins/discover/public/application/apps/main/discover_main_app.test.tsx b/src/plugins/discover/public/application/apps/main/discover_main_app.test.tsx deleted file mode 100644 index db29da87b4641..0000000000000 --- a/src/plugins/discover/public/application/apps/main/discover_main_app.test.tsx +++ /dev/null @@ -1,43 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ -import React from 'react'; -import { mountWithIntl } from '@kbn/test/jest'; -import { indexPatternMock } from '../../../__mocks__/index_pattern'; -import { DiscoverMainApp } from './discover_main_app'; -import { discoverServiceMock } from '../../../__mocks__/services'; -import { savedSearchMock } from '../../../__mocks__/saved_search'; -import { createSearchSessionMock } from '../../../__mocks__/search_session'; -import { SavedObject } from '../../../../../../core/types'; -import { IndexPatternAttributes } from '../../../../../data/common'; -import { setHeaderActionMenuMounter } from '../../../kibana_services'; -import { findTestSubject } from '@elastic/eui/lib/test'; - -setHeaderActionMenuMounter(jest.fn()); - -describe('DiscoverMainApp', () => { - test('renders', () => { - const { history } = createSearchSessionMock(); - const indexPatternList = [indexPatternMock].map((ip) => { - return { ...ip, ...{ attributes: { title: ip.title } } }; - }) as unknown as Array>; - - const props = { - indexPatternList, - services: discoverServiceMock, - savedSearch: savedSearchMock, - navigateTo: jest.fn(), - history, - }; - - const component = mountWithIntl(); - - expect(findTestSubject(component, 'indexPattern-switch-link').text()).toBe( - indexPatternMock.title - ); - }); -}); diff --git a/src/plugins/discover/public/application/apps/main/utils/fetch_all.ts b/src/plugins/discover/public/application/apps/main/utils/fetch_all.ts deleted file mode 100644 index e9d9335abcda0..0000000000000 --- a/src/plugins/discover/public/application/apps/main/utils/fetch_all.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 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 { forkJoin, of } from 'rxjs'; -import { - sendCompleteMsg, - sendErrorMsg, - sendLoadingMsg, - sendPartialMsg, - sendResetMsg, -} from '../services/use_saved_search_messages'; -import { updateSearchSource } from './update_search_source'; -import type { SortOrder } from '../../../../saved_searches'; -import { fetchDocuments } from './fetch_documents'; -import { fetchTotalHits } from './fetch_total_hits'; -import { fetchChart } from './fetch_chart'; -import { ISearchSource } from '../../../../../../data/common'; -import { Adapters } from '../../../../../../inspector'; -import { AppState } from '../services/discover_state'; -import { FetchStatus } from '../../../types'; -import { DataPublicPluginStart } from '../../../../../../data/public'; -import { SavedSearchData } from '../services/use_saved_search'; -import { DiscoverServices } from '../../../../build_services'; -import { ReduxLikeStateContainer } from '../../../../../../kibana_utils/common'; - -export function fetchAll( - dataSubjects: SavedSearchData, - searchSource: ISearchSource, - reset = false, - fetchDeps: { - abortController: AbortController; - appStateContainer: ReduxLikeStateContainer; - inspectorAdapters: Adapters; - data: DataPublicPluginStart; - initialFetchStatus: FetchStatus; - searchSessionId: string; - services: DiscoverServices; - useNewFieldsApi: boolean; - } -) { - const { initialFetchStatus, appStateContainer, services, useNewFieldsApi, data } = fetchDeps; - - const indexPattern = searchSource.getField('index')!; - - if (reset) { - sendResetMsg(dataSubjects, initialFetchStatus); - } - - sendLoadingMsg(dataSubjects.main$); - - const { hideChart, sort } = appStateContainer.getState(); - // Update the base searchSource, base for all child fetches - updateSearchSource(searchSource, false, { - indexPattern, - services, - sort: sort as SortOrder[], - useNewFieldsApi, - }); - - const subFetchDeps = { - ...fetchDeps, - onResults: (foundDocuments: boolean) => { - if (!foundDocuments) { - sendCompleteMsg(dataSubjects.main$, foundDocuments); - } else { - sendPartialMsg(dataSubjects.main$); - } - }, - }; - - const all = forkJoin({ - documents: fetchDocuments(dataSubjects, searchSource.createCopy(), subFetchDeps), - totalHits: - hideChart || !indexPattern.timeFieldName - ? fetchTotalHits(dataSubjects, searchSource.createCopy(), subFetchDeps) - : of(null), - chart: - !hideChart && indexPattern.timeFieldName - ? fetchChart(dataSubjects, searchSource.createCopy(), subFetchDeps) - : of(null), - }); - - all.subscribe( - () => sendCompleteMsg(dataSubjects.main$, true), - (error) => { - if (error instanceof Error && error.name === 'AbortError') return; - data.search.showError(error); - sendErrorMsg(dataSubjects.main$, error); - } - ); - return all; -} diff --git a/src/plugins/discover/public/application/apps/main/utils/update_search_source.test.ts b/src/plugins/discover/public/application/apps/main/utils/update_search_source.test.ts deleted file mode 100644 index 22f3b6ad86f6c..0000000000000 --- a/src/plugins/discover/public/application/apps/main/utils/update_search_source.test.ts +++ /dev/null @@ -1,78 +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 { updateSearchSource } from './update_search_source'; -import { createSearchSourceMock } from '../../../../../../data/common/search/search_source/mocks'; -import { indexPatternMock } from '../../../../__mocks__/index_pattern'; -import type { SortOrder } from '../../../../saved_searches'; -import { discoverServiceMock } from '../../../../__mocks__/services'; - -describe('updateSearchSource', () => { - test('updates a given search source', async () => { - const persistentSearchSourceMock = createSearchSourceMock({}); - const volatileSearchSourceMock = createSearchSourceMock({}); - volatileSearchSourceMock.setParent(persistentSearchSourceMock); - updateSearchSource(volatileSearchSourceMock, false, { - indexPattern: indexPatternMock, - services: discoverServiceMock, - sort: [] as SortOrder[], - useNewFieldsApi: false, - }); - expect(persistentSearchSourceMock.getField('index')).toEqual(indexPatternMock); - expect(volatileSearchSourceMock.getField('fields')).toBe(undefined); - }); - - test('updates a given search source with the usage of the new fields api', async () => { - const persistentSearchSourceMock = createSearchSourceMock({}); - const volatileSearchSourceMock = createSearchSourceMock({}); - volatileSearchSourceMock.setParent(persistentSearchSourceMock); - updateSearchSource(volatileSearchSourceMock, false, { - indexPattern: indexPatternMock, - services: discoverServiceMock, - sort: [] as SortOrder[], - useNewFieldsApi: true, - }); - expect(persistentSearchSourceMock.getField('index')).toEqual(indexPatternMock); - expect(volatileSearchSourceMock.getField('fields')).toEqual([ - { field: '*', include_unmapped: 'true' }, - ]); - expect(volatileSearchSourceMock.getField('fieldsFromSource')).toBe(undefined); - }); - - test('updates a given search source when showUnmappedFields option is set to true', async () => { - const persistentSearchSourceMock = createSearchSourceMock({}); - const volatileSearchSourceMock = createSearchSourceMock({}); - volatileSearchSourceMock.setParent(persistentSearchSourceMock); - updateSearchSource(volatileSearchSourceMock, false, { - indexPattern: indexPatternMock, - services: discoverServiceMock, - sort: [] as SortOrder[], - useNewFieldsApi: true, - }); - expect(persistentSearchSourceMock.getField('index')).toEqual(indexPatternMock); - expect(volatileSearchSourceMock.getField('fields')).toEqual([ - { field: '*', include_unmapped: 'true' }, - ]); - expect(volatileSearchSourceMock.getField('fieldsFromSource')).toBe(undefined); - }); - - test('does not explicitly request fieldsFromSource when not using fields API', async () => { - const persistentSearchSourceMock = createSearchSourceMock({}); - const volatileSearchSourceMock = createSearchSourceMock({}); - volatileSearchSourceMock.setParent(persistentSearchSourceMock); - updateSearchSource(volatileSearchSourceMock, false, { - indexPattern: indexPatternMock, - services: discoverServiceMock, - sort: [] as SortOrder[], - useNewFieldsApi: false, - }); - expect(persistentSearchSourceMock.getField('index')).toEqual(indexPatternMock); - expect(volatileSearchSourceMock.getField('fields')).toEqual(undefined); - expect(volatileSearchSourceMock.getField('fieldsFromSource')).toBe(undefined); - }); -}); diff --git a/src/plugins/discover/public/application/apps/main/utils/update_search_source.ts b/src/plugins/discover/public/application/apps/main/utils/update_search_source.ts deleted file mode 100644 index 6d592e176afe5..0000000000000 --- a/src/plugins/discover/public/application/apps/main/utils/update_search_source.ts +++ /dev/null @@ -1,70 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import { SORT_DEFAULT_ORDER_SETTING } from '../../../../../common'; -import { IndexPattern, ISearchSource } from '../../../../../../data/common'; -import type { SortOrder } from '../../../../saved_searches'; -import { DiscoverServices } from '../../../../build_services'; -import { getSortForSearchSource } from '../components/doc_table'; - -/** - * Helper function to update the given searchSource before fetching/sharing/persisting - */ -export function updateSearchSource( - searchSource: ISearchSource, - persist = true, - { - indexPattern, - services, - sort, - useNewFieldsApi, - }: { - indexPattern: IndexPattern; - services: DiscoverServices; - sort: SortOrder[]; - useNewFieldsApi: boolean; - } -) { - const { uiSettings, data } = services; - const parentSearchSource = persist ? searchSource : searchSource.getParent()!; - - parentSearchSource - .setField('index', indexPattern) - .setField('query', data.query.queryString.getQuery() || null) - .setField('filter', data.query.filterManager.getFilters()); - - if (!persist) { - const usedSort = getSortForSearchSource( - sort, - indexPattern, - uiSettings.get(SORT_DEFAULT_ORDER_SETTING) - ); - searchSource - .setField('trackTotalHits', true) - .setField('sort', usedSort) - // Even when searching rollups, we want to use the default strategy so that we get back a - // document-like response. - .setPreferredSearchStrategyId('default'); - - if (indexPattern.type !== 'rollup') { - // Set the date range filter fields from timeFilter using the absolute format. Search sessions requires that it be converted from a relative range - searchSource.setField('filter', data.query.timefilter.timefilter.createFilter(indexPattern)); - } - - if (useNewFieldsApi) { - searchSource.removeField('fieldsFromSource'); - const fields: Record = { field: '*' }; - - fields.include_unmapped = 'true'; - - searchSource.setField('fields', [fields]); - } else { - searchSource.removeField('fields'); - } - } -} diff --git a/src/plugins/discover/public/application/components/data_visualizer_grid/data_visualizer_grid.tsx b/src/plugins/discover/public/application/components/data_visualizer_grid/data_visualizer_grid.tsx deleted file mode 100644 index 5492fac014b74..0000000000000 --- a/src/plugins/discover/public/application/components/data_visualizer_grid/data_visualizer_grid.tsx +++ /dev/null @@ -1,196 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import React, { useEffect, useMemo, useRef, useState } from 'react'; -import { Filter } from '@kbn/es-query'; -import { IndexPatternField, IndexPattern, DataView, Query } from '../../../../../data/common'; -import { DiscoverServices } from '../../../build_services'; -import { - EmbeddableInput, - EmbeddableOutput, - ErrorEmbeddable, - IEmbeddable, - isErrorEmbeddable, -} from '../../../../../embeddable/public'; -import { SavedSearch } from '../../../saved_searches'; -import { GetStateReturn } from '../../apps/main/services/discover_state'; - -export interface DataVisualizerGridEmbeddableInput extends EmbeddableInput { - indexPattern: IndexPattern; - savedSearch?: SavedSearch; - query?: Query; - visibleFieldNames?: string[]; - filters?: Filter[]; - showPreviewByDefault?: boolean; - /** - * Callback to add a filter to filter bar - */ - onAddFilter?: (field: IndexPatternField | string, value: string, type: '+' | '-') => void; -} -export interface DataVisualizerGridEmbeddableOutput extends EmbeddableOutput { - showDistributions?: boolean; -} - -export interface DiscoverDataVisualizerGridProps { - /** - * Determines which columns are displayed - */ - columns: string[]; - /** - * The used index pattern - */ - indexPattern: DataView; - /** - * Saved search description - */ - searchDescription?: string; - /** - * Saved search title - */ - searchTitle?: string; - /** - * Discover plugin services - */ - services: DiscoverServices; - /** - * Optional saved search - */ - savedSearch?: SavedSearch; - /** - * Optional query to update the table content - */ - query?: Query; - /** - * Filters query to update the table content - */ - filters?: Filter[]; - stateContainer?: GetStateReturn; - /** - * Callback to add a filter to filter bar - */ - onAddFilter?: (field: IndexPatternField | string, value: string, type: '+' | '-') => void; -} - -export const DiscoverDataVisualizerGrid = (props: DiscoverDataVisualizerGridProps) => { - const { - services, - indexPattern, - savedSearch, - query, - columns, - filters, - stateContainer, - onAddFilter, - } = props; - const { uiSettings } = services; - - const [embeddable, setEmbeddable] = useState< - | ErrorEmbeddable - | IEmbeddable - | undefined - >(); - const embeddableRoot: React.RefObject = useRef(null); - - const showPreviewByDefault = useMemo( - () => - stateContainer ? !stateContainer.appStateContainer.getState().hideAggregatedPreview : true, - [stateContainer] - ); - - useEffect(() => { - const sub = embeddable?.getOutput$().subscribe((output: DataVisualizerGridEmbeddableOutput) => { - if (output.showDistributions !== undefined && stateContainer) { - stateContainer.setAppState({ hideAggregatedPreview: !output.showDistributions }); - } - }); - - return () => { - sub?.unsubscribe(); - }; - }, [embeddable, stateContainer]); - - useEffect(() => { - if (embeddable && !isErrorEmbeddable(embeddable)) { - // Update embeddable whenever one of the important input changes - embeddable.updateInput({ - indexPattern, - savedSearch, - query, - filters, - visibleFieldNames: columns, - onAddFilter, - }); - embeddable.reload(); - } - }, [embeddable, indexPattern, savedSearch, query, columns, filters, onAddFilter]); - - useEffect(() => { - if (showPreviewByDefault && embeddable && !isErrorEmbeddable(embeddable)) { - // Update embeddable whenever one of the important input changes - embeddable.updateInput({ - showPreviewByDefault, - }); - embeddable.reload(); - } - }, [showPreviewByDefault, uiSettings, embeddable]); - - useEffect(() => { - return () => { - // Clean up embeddable upon unmounting - embeddable?.destroy(); - }; - }, [embeddable]); - - useEffect(() => { - let unmounted = false; - const loadEmbeddable = async () => { - if (services.embeddable) { - const factory = services.embeddable.getEmbeddableFactory< - DataVisualizerGridEmbeddableInput, - DataVisualizerGridEmbeddableOutput - >('data_visualizer_grid'); - if (factory) { - // Initialize embeddable with information available at mount - const initializedEmbeddable = await factory.create({ - id: 'discover_data_visualizer_grid', - indexPattern, - savedSearch, - query, - showPreviewByDefault, - onAddFilter, - }); - if (!unmounted) { - setEmbeddable(initializedEmbeddable); - } - } - } - }; - loadEmbeddable(); - return () => { - unmounted = true; - }; - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [services.embeddable, showPreviewByDefault]); - - // We can only render after embeddable has already initialized - useEffect(() => { - if (embeddableRoot.current && embeddable) { - embeddable.render(embeddableRoot.current); - } - }, [embeddable, embeddableRoot, uiSettings]); - - return ( -
    - ); -}; diff --git a/src/plugins/discover/public/application/components/data_visualizer_grid/field_stats_table_embeddable.tsx b/src/plugins/discover/public/application/components/data_visualizer_grid/field_stats_table_embeddable.tsx deleted file mode 100644 index 099f45bf988cc..0000000000000 --- a/src/plugins/discover/public/application/components/data_visualizer_grid/field_stats_table_embeddable.tsx +++ /dev/null @@ -1,31 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import React from 'react'; -import { I18nProvider } from '@kbn/i18n/react'; -import { - DiscoverDataVisualizerGrid, - DiscoverDataVisualizerGridProps, -} from './data_visualizer_grid'; - -export function FieldStatsTableEmbeddable(renderProps: DiscoverDataVisualizerGridProps) { - return ( - - - - ); -} diff --git a/src/plugins/discover/public/application/components/data_visualizer_grid/index.ts b/src/plugins/discover/public/application/components/data_visualizer_grid/index.ts deleted file mode 100644 index dc85495a7c2ec..0000000000000 --- a/src/plugins/discover/public/application/components/data_visualizer_grid/index.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 { DiscoverDataVisualizerGrid } from './data_visualizer_grid'; diff --git a/src/plugins/discover/public/application/components/field_stats_table/constants.ts b/src/plugins/discover/public/application/components/field_stats_table/constants.ts new file mode 100644 index 0000000000000..bf1a36da59ecf --- /dev/null +++ b/src/plugins/discover/public/application/components/field_stats_table/constants.ts @@ -0,0 +1,12 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 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. + */ + +/** Telemetry related to field statistics table **/ +export const FIELD_STATISTICS_LOADED = 'field_statistics_loaded'; +export const FIELD_STATISTICS_VIEW_CLICK = 'field_statistics_view_click'; +export const DOCUMENTS_VIEW_CLICK = 'documents_view_click'; diff --git a/src/plugins/discover/public/application/components/field_stats_table/field_stats_table.tsx b/src/plugins/discover/public/application/components/field_stats_table/field_stats_table.tsx new file mode 100644 index 0000000000000..5061ab0ba3746 --- /dev/null +++ b/src/plugins/discover/public/application/components/field_stats_table/field_stats_table.tsx @@ -0,0 +1,217 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import React, { useEffect, useMemo, useRef, useState } from 'react'; +import type { Filter } from '@kbn/es-query'; +import { METRIC_TYPE, UiCounterMetricType } from '@kbn/analytics'; +import { IndexPatternField, IndexPattern, DataView, Query } from '../../../../../data/common'; +import type { DiscoverServices } from '../../../build_services'; +import { + EmbeddableInput, + EmbeddableOutput, + ErrorEmbeddable, + IEmbeddable, + isErrorEmbeddable, +} from '../../../../../embeddable/public'; +import { FIELD_STATISTICS_LOADED } from './constants'; +import type { SavedSearch } from '../../../services/saved_searches'; +import type { GetStateReturn } from '../../main/services/discover_state'; +import { DataRefetch$ } from '../../main/utils/use_saved_search'; + +export interface DataVisualizerGridEmbeddableInput extends EmbeddableInput { + indexPattern: IndexPattern; + savedSearch?: SavedSearch; + query?: Query; + visibleFieldNames?: string[]; + filters?: Filter[]; + showPreviewByDefault?: boolean; + /** + * Callback to add a filter to filter bar + */ + onAddFilter?: (field: IndexPatternField | string, value: string, type: '+' | '-') => void; +} +export interface DataVisualizerGridEmbeddableOutput extends EmbeddableOutput { + showDistributions?: boolean; +} + +export interface FieldStatisticsTableProps { + /** + * Determines which columns are displayed + */ + columns: string[]; + /** + * The used index pattern + */ + indexPattern: DataView; + /** + * Saved search description + */ + searchDescription?: string; + /** + * Saved search title + */ + searchTitle?: string; + /** + * Discover plugin services + */ + services: DiscoverServices; + /** + * Optional saved search + */ + savedSearch?: SavedSearch; + /** + * Optional query to update the table content + */ + query?: Query; + /** + * Filters query to update the table content + */ + filters?: Filter[]; + /** + * State container with persisted settings + */ + stateContainer?: GetStateReturn; + /** + * Callback to add a filter to filter bar + */ + onAddFilter?: (field: IndexPatternField | string, value: string, type: '+' | '-') => void; + /** + * Metric tracking function + * @param metricType + * @param eventName + */ + trackUiMetric?: (metricType: UiCounterMetricType, eventName: string | string[]) => void; + savedSearchRefetch$?: DataRefetch$; +} + +export const FieldStatisticsTable = (props: FieldStatisticsTableProps) => { + const { + services, + indexPattern, + savedSearch, + query, + columns, + filters, + stateContainer, + onAddFilter, + trackUiMetric, + savedSearchRefetch$, + } = props; + const { uiSettings } = services; + const [embeddable, setEmbeddable] = useState< + | ErrorEmbeddable + | IEmbeddable + | undefined + >(); + const embeddableRoot: React.RefObject = useRef(null); + + const showPreviewByDefault = useMemo( + () => + stateContainer ? !stateContainer.appStateContainer.getState().hideAggregatedPreview : true, + [stateContainer] + ); + + useEffect(() => { + const sub = embeddable?.getOutput$().subscribe((output: DataVisualizerGridEmbeddableOutput) => { + if (output.showDistributions !== undefined && stateContainer) { + stateContainer.setAppState({ hideAggregatedPreview: !output.showDistributions }); + } + }); + + const refetch = savedSearchRefetch$?.subscribe(() => { + if (embeddable && !isErrorEmbeddable(embeddable)) { + embeddable.updateInput({ lastReloadRequestTime: Date.now() }); + } + }); + return () => { + sub?.unsubscribe(); + refetch?.unsubscribe(); + }; + }, [embeddable, stateContainer, savedSearchRefetch$]); + + useEffect(() => { + if (embeddable && !isErrorEmbeddable(embeddable)) { + // Update embeddable whenever one of the important input changes + embeddable.updateInput({ + indexPattern, + savedSearch, + query, + filters, + visibleFieldNames: columns, + onAddFilter, + }); + embeddable.reload(); + } + }, [embeddable, indexPattern, savedSearch, query, columns, filters, onAddFilter]); + + useEffect(() => { + if (showPreviewByDefault && embeddable && !isErrorEmbeddable(embeddable)) { + // Update embeddable whenever one of the important input changes + embeddable.updateInput({ + showPreviewByDefault, + }); + + embeddable.reload(); + } + }, [showPreviewByDefault, uiSettings, embeddable]); + + useEffect(() => { + let unmounted = false; + const loadEmbeddable = async () => { + if (services.embeddable) { + const factory = services.embeddable.getEmbeddableFactory< + DataVisualizerGridEmbeddableInput, + DataVisualizerGridEmbeddableOutput + >('data_visualizer_grid'); + if (factory) { + // Initialize embeddable with information available at mount + const initializedEmbeddable = await factory.create({ + id: 'discover_data_visualizer_grid', + indexPattern, + savedSearch, + query, + showPreviewByDefault, + onAddFilter, + }); + if (!unmounted) { + setEmbeddable(initializedEmbeddable); + } + } + } + }; + loadEmbeddable(); + return () => { + unmounted = true; + }; + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [services.embeddable, showPreviewByDefault]); + + // We can only render after embeddable has already initialized + useEffect(() => { + if (embeddableRoot.current && embeddable) { + embeddable.render(embeddableRoot.current); + + trackUiMetric?.(METRIC_TYPE.LOADED, FIELD_STATISTICS_LOADED); + } + + return () => { + // Clean up embeddable upon unmounting + embeddable?.destroy(); + }; + }, [embeddable, embeddableRoot, uiSettings, trackUiMetric]); + + return ( +
    + ); +}; diff --git a/src/plugins/discover/public/application/components/field_stats_table/field_stats_table_saved_search_embeddable.tsx b/src/plugins/discover/public/application/components/field_stats_table/field_stats_table_saved_search_embeddable.tsx new file mode 100644 index 0000000000000..9c0c6f4e11609 --- /dev/null +++ b/src/plugins/discover/public/application/components/field_stats_table/field_stats_table_saved_search_embeddable.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 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 { I18nProvider } from '@kbn/i18n/react'; +import { FieldStatisticsTable, FieldStatisticsTableProps } from './field_stats_table'; + +export function FieldStatsTableSavedSearchEmbeddable(renderProps: FieldStatisticsTableProps) { + return ( + + + + ); +} diff --git a/src/plugins/discover/public/application/components/field_stats_table/index.ts b/src/plugins/discover/public/application/components/field_stats_table/index.ts new file mode 100644 index 0000000000000..39f3dd81e74e6 --- /dev/null +++ b/src/plugins/discover/public/application/components/field_stats_table/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 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 { FieldStatisticsTable } from './field_stats_table'; +export { FieldStatsTableSavedSearchEmbeddable } from './field_stats_table_saved_search_embeddable'; diff --git a/src/plugins/discover/public/application/components/source_viewer/__snapshots__/source_viewer.test.tsx.snap b/src/plugins/discover/public/application/components/source_viewer/__snapshots__/source_viewer.test.tsx.snap deleted file mode 100644 index 761263ee861b9..0000000000000 --- a/src/plugins/discover/public/application/components/source_viewer/__snapshots__/source_viewer.test.tsx.snap +++ /dev/null @@ -1,777 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`Source Viewer component renders error state 1`] = ` - - - Could not fetch data at this time. Refresh the tab to try again. - - - Refresh - -
    - } - iconType="alert" - title={ -

    - An Error Occurred -

    - } - > -
    - - - - -
    - - -

    - An Error Occurred -

    -
    - - - -
    - - -
    -
    - Could not fetch data at this time. Refresh the tab to try again. - -
    - - - - - - -
    -
    - - - -
    - - -`; - -exports[`Source Viewer component renders json code editor 1`] = ` - - - - -
    - -
    - -
    - -
    - - - - - - - - - -
    -
    - - -
    - - - } - > - -
    - -
    -
    -
    -
    -
    -
    -
    -
    - - - - -`; - -exports[`Source Viewer component renders loading state 1`] = ` - -
    - - - - -
    - -
    - - Loading JSON - -
    -
    -
    -
    -
    -
    -`; diff --git a/src/plugins/discover/public/application/components/source_viewer/source_viewer.test.tsx b/src/plugins/discover/public/application/components/source_viewer/source_viewer.test.tsx deleted file mode 100644 index a98c2de6197d8..0000000000000 --- a/src/plugins/discover/public/application/components/source_viewer/source_viewer.test.tsx +++ /dev/null @@ -1,117 +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 type { IndexPattern } from 'src/plugins/data/common'; -import { mountWithIntl } from '@kbn/test/jest'; -import { SourceViewer } from './source_viewer'; -import * as hooks from '../../services/use_es_doc_search'; -import * as useUiSettingHook from 'src/plugins/kibana_react/public/ui_settings/use_ui_setting'; -import { EuiButton, EuiEmptyPrompt, EuiLoadingSpinner } from '@elastic/eui'; -import { JsonCodeEditorCommon } from '../json_code_editor/json_code_editor_common'; - -jest.mock('../../../kibana_services', () => ({ - getServices: jest.fn(), -})); - -import { getServices } from '../../../kibana_services'; - -const mockIndexPattern = { - getComputedFields: () => [], -} as never; -const getMock = jest.fn(() => Promise.resolve(mockIndexPattern)); -const mockIndexPatternService = { - get: getMock, -} as unknown as IndexPattern; - -(getServices as jest.Mock).mockImplementation(() => ({ - uiSettings: { - get: (key: string) => { - if (key === 'discover:useNewFieldsApi') { - return true; - } - }, - }, - data: { - indexPatternService: mockIndexPatternService, - }, -})); -describe('Source Viewer component', () => { - test('renders loading state', () => { - jest.spyOn(hooks, 'useEsDocSearch').mockImplementation(() => [0, null, () => {}]); - - const comp = mountWithIntl( - - ); - expect(comp).toMatchSnapshot(); - const loadingIndicator = comp.find(EuiLoadingSpinner); - expect(loadingIndicator).not.toBe(null); - }); - - test('renders error state', () => { - jest.spyOn(hooks, 'useEsDocSearch').mockImplementation(() => [3, null, () => {}]); - - const comp = mountWithIntl( - - ); - expect(comp).toMatchSnapshot(); - const errorPrompt = comp.find(EuiEmptyPrompt); - expect(errorPrompt.length).toBe(1); - const refreshButton = comp.find(EuiButton); - expect(refreshButton.length).toBe(1); - }); - - test('renders json code editor', () => { - const mockHit = { - _index: 'logstash-2014.09.09', - _type: 'doc', - _id: 'id123', - _score: 1, - _source: { - message: 'Lorem ipsum dolor sit amet', - extension: 'html', - not_mapped: 'yes', - bytes: 100, - objectArray: [{ foo: true }], - relatedContent: { - test: 1, - }, - scripted: 123, - _underscore: 123, - }, - } as never; - jest.spyOn(hooks, 'useEsDocSearch').mockImplementation(() => [2, mockHit, () => {}]); - jest.spyOn(useUiSettingHook, 'useUiSetting').mockImplementation(() => { - return false; - }); - const comp = mountWithIntl( - - ); - expect(comp).toMatchSnapshot(); - const jsonCodeEditor = comp.find(JsonCodeEditorCommon); - expect(jsonCodeEditor).not.toBe(null); - }); -}); diff --git a/src/plugins/discover/public/application/components/source_viewer/source_viewer.tsx b/src/plugins/discover/public/application/components/source_viewer/source_viewer.tsx deleted file mode 100644 index 31d4d866df21e..0000000000000 --- a/src/plugins/discover/public/application/components/source_viewer/source_viewer.tsx +++ /dev/null @@ -1,128 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 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 './source_viewer.scss'; - -import React, { useEffect, useState } from 'react'; -import { FormattedMessage } from '@kbn/i18n/react'; -import { monaco } from '@kbn/monaco'; -import { EuiButton, EuiEmptyPrompt, EuiLoadingSpinner, EuiSpacer, EuiText } from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; -import { JSONCodeEditorCommonMemoized } from '../json_code_editor/json_code_editor_common'; -import { getServices } from '../../../kibana_services'; -import { SEARCH_FIELDS_FROM_SOURCE } from '../../../../common'; -import { ElasticRequestState } from '../../apps/doc/types'; -import { useEsDocSearch } from '../../services/use_es_doc_search'; -import { IndexPattern } from '../../../../../data_views/common'; - -interface SourceViewerProps { - id: string; - index: string; - indexPattern: IndexPattern; - hasLineNumbers: boolean; - width?: number; -} - -export const SourceViewer = ({ - id, - index, - indexPattern, - width, - hasLineNumbers, -}: SourceViewerProps) => { - const [editor, setEditor] = useState(); - const [jsonValue, setJsonValue] = useState(''); - const useNewFieldsApi = !getServices().uiSettings.get(SEARCH_FIELDS_FROM_SOURCE); - const [reqState, hit, requestData] = useEsDocSearch({ - id, - index, - indexPattern, - requestSource: useNewFieldsApi, - }); - - useEffect(() => { - if (reqState === ElasticRequestState.Found && hit) { - setJsonValue(JSON.stringify(hit, undefined, 2)); - } - }, [reqState, hit]); - - // setting editor height based on lines height and count to stretch and fit its content - useEffect(() => { - if (!editor) { - return; - } - const editorElement = editor.getDomNode(); - - if (!editorElement) { - return; - } - - const lineHeight = editor.getOption(monaco.editor.EditorOption.lineHeight); - const lineCount = editor.getModel()?.getLineCount() || 1; - const height = editor.getTopForLineNumber(lineCount + 1) + lineHeight; - if (!jsonValue || jsonValue === '') { - editorElement.style.height = '0px'; - } else { - editorElement.style.height = `${height}px`; - } - editor.layout(); - }, [editor, jsonValue]); - - const loadingState = ( -
    - - - - -
    - ); - - const errorMessageTitle = ( -

    - {i18n.translate('discover.sourceViewer.errorMessageTitle', { - defaultMessage: 'An Error Occurred', - })} -

    - ); - const errorMessage = ( -
    - {i18n.translate('discover.sourceViewer.errorMessage', { - defaultMessage: 'Could not fetch data at this time. Refresh the tab to try again.', - })} - - - {i18n.translate('discover.sourceViewer.refresh', { - defaultMessage: 'Refresh', - })} - -
    - ); - const errorState = ( - - ); - - if (reqState === ElasticRequestState.Error || reqState === ElasticRequestState.NotFound) { - return errorState; - } - - if (reqState === ElasticRequestState.Loading || jsonValue === '') { - return loadingState; - } - - return ( - setEditor(editorNode)} - /> - ); -}; - -// Required for usage in React.lazy -// eslint-disable-next-line import/no-default-export -export default SourceViewer; diff --git a/src/plugins/discover/public/application/apps/context/__mocks__/top_nav_menu.tsx b/src/plugins/discover/public/application/context/__mocks__/top_nav_menu.tsx similarity index 100% rename from src/plugins/discover/public/application/apps/context/__mocks__/top_nav_menu.tsx rename to src/plugins/discover/public/application/context/__mocks__/top_nav_menu.tsx diff --git a/src/plugins/discover/public/application/apps/context/__mocks__/use_context_app_fetch.tsx b/src/plugins/discover/public/application/context/__mocks__/use_context_app_fetch.tsx similarity index 100% rename from src/plugins/discover/public/application/apps/context/__mocks__/use_context_app_fetch.tsx rename to src/plugins/discover/public/application/context/__mocks__/use_context_app_fetch.tsx diff --git a/src/plugins/discover/public/application/apps/context/components/action_bar/_action_bar.scss b/src/plugins/discover/public/application/context/components/action_bar/_action_bar.scss similarity index 100% rename from src/plugins/discover/public/application/apps/context/components/action_bar/_action_bar.scss rename to src/plugins/discover/public/application/context/components/action_bar/_action_bar.scss diff --git a/src/plugins/discover/public/application/apps/context/components/action_bar/action_bar.test.tsx b/src/plugins/discover/public/application/context/components/action_bar/action_bar.test.tsx similarity index 97% rename from src/plugins/discover/public/application/apps/context/components/action_bar/action_bar.test.tsx rename to src/plugins/discover/public/application/context/components/action_bar/action_bar.test.tsx index de6d01b6a5273..cce6c7fbe6053 100644 --- a/src/plugins/discover/public/application/apps/context/components/action_bar/action_bar.test.tsx +++ b/src/plugins/discover/public/application/context/components/action_bar/action_bar.test.tsx @@ -10,7 +10,7 @@ import React from 'react'; import { mountWithIntl } from '@kbn/test/jest'; import { ActionBar, ActionBarProps } from './action_bar'; import { findTestSubject } from '@elastic/eui/lib/test'; -import { MAX_CONTEXT_SIZE, MIN_CONTEXT_SIZE } from '../../utils/constants'; +import { MAX_CONTEXT_SIZE, MIN_CONTEXT_SIZE } from '../../services/constants'; import { SurrDocType } from '../../services/context'; describe('Test Discover Context ActionBar for successor | predecessor records', () => { diff --git a/src/plugins/discover/public/application/apps/context/components/action_bar/action_bar.tsx b/src/plugins/discover/public/application/context/components/action_bar/action_bar.tsx similarity index 98% rename from src/plugins/discover/public/application/apps/context/components/action_bar/action_bar.tsx rename to src/plugins/discover/public/application/context/components/action_bar/action_bar.tsx index d9d56964358f8..4c1f7857e2b42 100644 --- a/src/plugins/discover/public/application/apps/context/components/action_bar/action_bar.tsx +++ b/src/plugins/discover/public/application/context/components/action_bar/action_bar.tsx @@ -20,7 +20,7 @@ import { } from '@elastic/eui'; import { ActionBarWarning } from './action_bar_warning'; import { SurrDocType } from '../../services/context'; -import { MAX_CONTEXT_SIZE, MIN_CONTEXT_SIZE } from '../../utils/constants'; +import { MAX_CONTEXT_SIZE, MIN_CONTEXT_SIZE } from '../../services/constants'; export interface ActionBarProps { /** diff --git a/src/plugins/discover/public/application/apps/context/components/action_bar/action_bar_warning.tsx b/src/plugins/discover/public/application/context/components/action_bar/action_bar_warning.tsx similarity index 100% rename from src/plugins/discover/public/application/apps/context/components/action_bar/action_bar_warning.tsx rename to src/plugins/discover/public/application/context/components/action_bar/action_bar_warning.tsx diff --git a/src/plugins/discover/public/application/apps/context/components/context_error_message/context_error_message.test.tsx b/src/plugins/discover/public/application/context/components/context_error_message/context_error_message.test.tsx similarity index 100% rename from src/plugins/discover/public/application/apps/context/components/context_error_message/context_error_message.test.tsx rename to src/plugins/discover/public/application/context/components/context_error_message/context_error_message.test.tsx diff --git a/src/plugins/discover/public/application/apps/context/components/context_error_message/context_error_message.tsx b/src/plugins/discover/public/application/context/components/context_error_message/context_error_message.tsx similarity index 100% rename from src/plugins/discover/public/application/apps/context/components/context_error_message/context_error_message.tsx rename to src/plugins/discover/public/application/context/components/context_error_message/context_error_message.tsx diff --git a/src/plugins/discover/public/application/apps/context/components/context_error_message/index.ts b/src/plugins/discover/public/application/context/components/context_error_message/index.ts similarity index 100% rename from src/plugins/discover/public/application/apps/context/components/context_error_message/index.ts rename to src/plugins/discover/public/application/context/components/context_error_message/index.ts diff --git a/src/plugins/discover/public/application/apps/context/context_app.scss b/src/plugins/discover/public/application/context/context_app.scss similarity index 100% rename from src/plugins/discover/public/application/apps/context/context_app.scss rename to src/plugins/discover/public/application/context/context_app.scss diff --git a/src/plugins/discover/public/application/apps/context/context_app.test.tsx b/src/plugins/discover/public/application/context/context_app.test.tsx similarity index 88% rename from src/plugins/discover/public/application/apps/context/context_app.test.tsx rename to src/plugins/discover/public/application/context/context_app.test.tsx index d1c557f2839bc..7f78bb1c698ab 100644 --- a/src/plugins/discover/public/application/apps/context/context_app.test.tsx +++ b/src/plugins/discover/public/application/context/context_app.test.tsx @@ -9,16 +9,16 @@ import React from 'react'; import { waitFor } from '@testing-library/react'; import { mountWithIntl } from '@kbn/test/jest'; -import { createFilterManagerMock } from '../../../../../data/public/query/filter_manager/filter_manager.mock'; +import { createFilterManagerMock } from '../../../../data/public/query/filter_manager/filter_manager.mock'; import { mockTopNavMenu } from './__mocks__/top_nav_menu'; import { ContextAppContent } from './context_app_content'; -import { indexPatternMock } from '../../../__mocks__/index_pattern'; +import { indexPatternMock } from '../../__mocks__/index_pattern'; import { ContextApp } from './context_app'; -import { setServices } from '../../../kibana_services'; -import { DiscoverServices } from '../../../build_services'; -import { indexPatternsMock } from '../../../__mocks__/index_patterns'; +import { setServices } from '../../kibana_services'; +import { DiscoverServices } from '../../build_services'; +import { indexPatternsMock } from '../../__mocks__/index_patterns'; import { act } from 'react-dom/test-utils'; -import { uiSettingsMock } from '../../../__mocks__/ui_settings'; +import { uiSettingsMock } from '../../__mocks__/ui_settings'; const mockFilterManager = createFilterManagerMock(); const mockNavigationPlugin = { ui: { TopNavMenu: mockTopNavMenu } }; diff --git a/src/plugins/discover/public/application/apps/context/context_app.tsx b/src/plugins/discover/public/application/context/context_app.tsx similarity index 89% rename from src/plugins/discover/public/application/apps/context/context_app.tsx rename to src/plugins/discover/public/application/context/context_app.tsx index bfc13aac90e4b..1bda31bd7bd27 100644 --- a/src/plugins/discover/public/application/apps/context/context_app.tsx +++ b/src/plugins/discover/public/application/context/context_app.tsx @@ -12,20 +12,20 @@ import classNames from 'classnames'; import { FormattedMessage } from '@kbn/i18n/react'; import { EuiText, EuiPageContent, EuiPage, EuiSpacer } from '@elastic/eui'; import { cloneDeep } from 'lodash'; -import { esFilters } from '../../../../../data/public'; -import { DOC_TABLE_LEGACY, SEARCH_FIELDS_FROM_SOURCE } from '../../../../common'; +import { esFilters } from '../../../../data/public'; +import { DOC_TABLE_LEGACY, SEARCH_FIELDS_FROM_SOURCE } from '../../../common'; import { ContextErrorMessage } from './components/context_error_message'; -import { IndexPattern, IndexPatternField } from '../../../../../data/common'; +import { IndexPattern, IndexPatternField } from '../../../../data/common'; import { LoadingStatus } from './services/context_query_state'; -import { getServices } from '../../../kibana_services'; +import { getServices } from '../../kibana_services'; import { AppState, isEqualFilters } from './services/context_state'; -import { useColumns } from '../../helpers/use_data_grid_columns'; +import { useColumns } from '../../utils/use_data_grid_columns'; import { useContextAppState } from './utils/use_context_app_state'; import { useContextAppFetch } from './utils/use_context_app_fetch'; -import { popularizeField } from '../../helpers/popularize_field'; +import { popularizeField } from '../../utils/popularize_field'; import { ContextAppContent } from './context_app_content'; import { SurrDocType } from './services/context'; -import { DocViewFilterFn } from '../../doc_views/doc_views_types'; +import { DocViewFilterFn } from '../../services/doc_views/doc_views_types'; const ContextAppContentMemoized = memo(ContextAppContent); @@ -50,21 +50,29 @@ export const ContextApp = ({ indexPattern, anchorId }: ContextAppProps) => { /** * Context fetched state */ - const { fetchedState, fetchContextRows, fetchAllRows, fetchSurroundingRows } = useContextAppFetch( - { + const { fetchedState, fetchContextRows, fetchAllRows, fetchSurroundingRows, resetFetchedState } = + useContextAppFetch({ anchorId, indexPattern, appState, useNewFieldsApi, services, + }); + /** + * Reset state when anchor changes + */ + useEffect(() => { + if (prevAppState.current) { + prevAppState.current = undefined; + resetFetchedState(); } - ); + }, [anchorId, resetFetchedState]); /** * Fetch docs on ui changes */ useEffect(() => { - if (!prevAppState.current || fetchedState.anchor._id !== anchorId) { + if (!prevAppState.current) { fetchAllRows(); } else if (prevAppState.current.predecessorCount !== appState.predecessorCount) { fetchSurroundingRows(SurrDocType.PREDECESSORS); diff --git a/src/plugins/discover/public/application/apps/context/context_app_content.test.tsx b/src/plugins/discover/public/application/context/context_app_content.test.tsx similarity index 91% rename from src/plugins/discover/public/application/apps/context/context_app_content.test.tsx rename to src/plugins/discover/public/application/context/context_app_content.test.tsx index 9b1c47d37203f..5ec89c8e267ea 100644 --- a/src/plugins/discover/public/application/apps/context/context_app_content.test.tsx +++ b/src/plugins/discover/public/application/context/context_app_content.test.tsx @@ -13,13 +13,13 @@ import { ActionBar } from './components/action_bar/action_bar'; import { AppState, GetStateReturn } from './services/context_state'; import { SortDirection } from 'src/plugins/data/common'; import { ContextAppContent, ContextAppContentProps } from './context_app_content'; -import { getServices, setServices } from '../../../kibana_services'; +import { getServices, setServices } from '../../kibana_services'; import { LoadingStatus } from './services/context_query_state'; -import { indexPatternMock } from '../../../__mocks__/index_pattern'; +import { indexPatternMock } from '../../__mocks__/index_pattern'; import { DiscoverGrid } from '../../components/discover_grid/discover_grid'; -import { discoverServiceMock } from '../../../__mocks__/services'; -import { DocTableWrapper } from '../main/components/doc_table/doc_table_wrapper'; -import { EsHitRecordList } from '../../types'; +import { discoverServiceMock } from '../../__mocks__/services'; +import { DocTableWrapper } from '../../components/doc_table/doc_table_wrapper'; +import { EsHitRecordList } from '../types'; describe('ContextAppContent test', () => { let hit; diff --git a/src/plugins/discover/public/application/apps/context/context_app_content.tsx b/src/plugins/discover/public/application/context/context_app_content.tsx similarity index 91% rename from src/plugins/discover/public/application/apps/context/context_app_content.tsx rename to src/plugins/discover/public/application/context/context_app_content.tsx index c8f3cfe0a568f..0d24e8129a8dd 100644 --- a/src/plugins/discover/public/application/apps/context/context_app_content.tsx +++ b/src/plugins/discover/public/application/context/context_app_content.tsx @@ -9,20 +9,20 @@ import React, { useState, Fragment, useMemo, useCallback } from 'react'; import { FormattedMessage } from '@kbn/i18n/react'; import { EuiHorizontalRule, EuiText } from '@elastic/eui'; -import { CONTEXT_STEP_SETTING, DOC_HIDE_TIME_COLUMN_SETTING } from '../../../../common'; -import { IndexPattern } from '../../../../../data/common'; -import { SortDirection } from '../../../../../data/public'; +import { CONTEXT_STEP_SETTING, DOC_HIDE_TIME_COLUMN_SETTING } from '../../../common'; +import { IndexPattern } from '../../../../data/common'; +import { SortDirection } from '../../../../data/public'; import { LoadingStatus } from './services/context_query_state'; import { ActionBar } from './components/action_bar/action_bar'; import { DiscoverGrid } from '../../components/discover_grid/discover_grid'; -import { DocViewFilterFn, ElasticSearchHit } from '../../doc_views/doc_views_types'; +import { DocViewFilterFn, ElasticSearchHit } from '../../services/doc_views/doc_views_types'; import { AppState } from './services/context_state'; import { SurrDocType } from './services/context'; -import { DiscoverServices } from '../../../build_services'; -import { MAX_CONTEXT_SIZE, MIN_CONTEXT_SIZE } from './utils/constants'; -import { DocTableContext } from '../main/components/doc_table/doc_table_context'; -import { EsHitRecordList } from '../../types'; -import { SortPairArr } from '../main/components/doc_table/lib/get_sort'; +import { DiscoverServices } from '../../build_services'; +import { MAX_CONTEXT_SIZE, MIN_CONTEXT_SIZE } from './services/constants'; +import { DocTableContext } from '../../components/doc_table/doc_table_context'; +import { EsHitRecordList } from '../types'; +import { SortPairArr } from '../../components/doc_table/lib/get_sort'; export interface ContextAppContentProps { columns: string[]; diff --git a/src/plugins/discover/public/application/apps/context/context_app_route.tsx b/src/plugins/discover/public/application/context/context_app_route.tsx similarity index 91% rename from src/plugins/discover/public/application/apps/context/context_app_route.tsx rename to src/plugins/discover/public/application/context/context_app_route.tsx index 6c4722418be14..9d47d211489b0 100644 --- a/src/plugins/discover/public/application/apps/context/context_app_route.tsx +++ b/src/plugins/discover/public/application/context/context_app_route.tsx @@ -10,11 +10,11 @@ import { useParams } from 'react-router-dom'; import { i18n } from '@kbn/i18n'; import { EuiEmptyPrompt } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; -import { DiscoverServices } from '../../../build_services'; +import { DiscoverServices } from '../../build_services'; import { ContextApp } from './context_app'; -import { getRootBreadcrumbs } from '../../helpers/breadcrumbs'; +import { getRootBreadcrumbs } from '../../utils/breadcrumbs'; import { LoadingIndicator } from '../../components/common/loading_indicator'; -import { useIndexPattern } from '../../helpers/use_index_pattern'; +import { useIndexPattern } from '../../utils/use_index_pattern'; export interface ContextAppProps { /** diff --git a/src/plugins/discover/public/application/apps/context/index.ts b/src/plugins/discover/public/application/context/index.ts similarity index 100% rename from src/plugins/discover/public/application/apps/context/index.ts rename to src/plugins/discover/public/application/context/index.ts diff --git a/src/plugins/discover/public/application/apps/context/services/__snapshots__/context.test.ts.snap b/src/plugins/discover/public/application/context/services/__snapshots__/context.test.ts.snap similarity index 100% rename from src/plugins/discover/public/application/apps/context/services/__snapshots__/context.test.ts.snap rename to src/plugins/discover/public/application/context/services/__snapshots__/context.test.ts.snap diff --git a/src/plugins/discover/public/application/apps/context/services/_stubs.ts b/src/plugins/discover/public/application/context/services/_stubs.ts similarity index 98% rename from src/plugins/discover/public/application/apps/context/services/_stubs.ts rename to src/plugins/discover/public/application/context/services/_stubs.ts index e8d09e548c07a..df53523c367ae 100644 --- a/src/plugins/discover/public/application/apps/context/services/_stubs.ts +++ b/src/plugins/discover/public/application/context/services/_stubs.ts @@ -9,7 +9,7 @@ import sinon from 'sinon'; import moment from 'moment'; -import { EsHitRecordList } from '../../../types'; +import { EsHitRecordList } from '../../types'; type SortHit = { [key in string]: number; // timeField name diff --git a/src/plugins/discover/public/application/apps/context/services/anchor.test.ts b/src/plugins/discover/public/application/context/services/anchor.test.ts similarity index 96% rename from src/plugins/discover/public/application/apps/context/services/anchor.test.ts rename to src/plugins/discover/public/application/context/services/anchor.test.ts index 8886c8ab11f64..69c962b0c857e 100644 --- a/src/plugins/discover/public/application/apps/context/services/anchor.test.ts +++ b/src/plugins/discover/public/application/context/services/anchor.test.ts @@ -6,12 +6,12 @@ * Side Public License, v 1. */ -import { IndexPattern, SortDirection } from '../../../../../../data/public'; +import { IndexPattern, SortDirection } from '../../../../../data/public'; import { createSearchSourceStub } from './_stubs'; import { fetchAnchor, updateSearchSource } from './anchor'; -import { indexPatternMock } from '../../../../__mocks__/index_pattern'; -import { savedSearchMock } from '../../../../__mocks__/saved_search'; -import { EsHitRecordList } from '../../../types'; +import { indexPatternMock } from '../../../__mocks__/index_pattern'; +import { savedSearchMock } from '../../../__mocks__/saved_search'; +import { EsHitRecordList } from '../../types'; describe('context app', function () { // eslint-disable-next-line @typescript-eslint/no-explicit-any diff --git a/src/plugins/discover/public/application/apps/context/services/anchor.ts b/src/plugins/discover/public/application/context/services/anchor.ts similarity index 96% rename from src/plugins/discover/public/application/apps/context/services/anchor.ts rename to src/plugins/discover/public/application/context/services/anchor.ts index f262d440b8a28..a42c0285bc197 100644 --- a/src/plugins/discover/public/application/apps/context/services/anchor.ts +++ b/src/plugins/discover/public/application/context/services/anchor.ts @@ -8,8 +8,8 @@ import { i18n } from '@kbn/i18n'; -import { ISearchSource, EsQuerySortValue, IndexPattern } from '../../../../../../data/public'; -import { EsHitRecord } from '../../../types'; +import { ISearchSource, EsQuerySortValue, IndexPattern } from '../../../../../data/public'; +import { EsHitRecord } from '../../types'; export async function fetchAnchor( anchorId: string, diff --git a/src/plugins/discover/public/application/apps/context/utils/constants.ts b/src/plugins/discover/public/application/context/services/constants.ts similarity index 100% rename from src/plugins/discover/public/application/apps/context/utils/constants.ts rename to src/plugins/discover/public/application/context/services/constants.ts diff --git a/src/plugins/discover/public/application/apps/context/services/context.predecessors.test.ts b/src/plugins/discover/public/application/context/services/context.predecessors.test.ts similarity index 97% rename from src/plugins/discover/public/application/apps/context/services/context.predecessors.test.ts rename to src/plugins/discover/public/application/context/services/context.predecessors.test.ts index 9bcf6f9c90d2c..1d343a2765da6 100644 --- a/src/plugins/discover/public/application/apps/context/services/context.predecessors.test.ts +++ b/src/plugins/discover/public/application/context/services/context.predecessors.test.ts @@ -11,10 +11,10 @@ import { get, last } from 'lodash'; import { IndexPattern, SortDirection } from 'src/plugins/data/common'; import { createContextSearchSourceStub } from './_stubs'; import { fetchSurroundingDocs, SurrDocType } from './context'; -import { setServices } from '../../../../kibana_services'; -import { Query } from '../../../../../../data/public'; -import { DiscoverServices } from '../../../../build_services'; -import { EsHitRecord, EsHitRecordList } from '../../../types'; +import { setServices } from '../../../kibana_services'; +import { Query } from '../../../../../data/public'; +import { DiscoverServices } from '../../../build_services'; +import { EsHitRecord, EsHitRecordList } from '../../types'; const MS_PER_DAY = 24 * 60 * 60 * 1000; const ANCHOR_TIMESTAMP = new Date(MS_PER_DAY).toJSON(); diff --git a/src/plugins/discover/public/application/apps/context/services/context.successors.test.ts b/src/plugins/discover/public/application/context/services/context.successors.test.ts similarity index 97% rename from src/plugins/discover/public/application/apps/context/services/context.successors.test.ts rename to src/plugins/discover/public/application/context/services/context.successors.test.ts index 169d969753645..975d951f49ef3 100644 --- a/src/plugins/discover/public/application/apps/context/services/context.successors.test.ts +++ b/src/plugins/discover/public/application/context/services/context.successors.test.ts @@ -10,11 +10,11 @@ import moment from 'moment'; import { get, last } from 'lodash'; import { IndexPattern, SortDirection } from 'src/plugins/data/common'; import { createContextSearchSourceStub } from './_stubs'; -import { setServices } from '../../../../kibana_services'; -import { Query } from '../../../../../../data/public'; +import { setServices } from '../../../kibana_services'; +import { Query } from '../../../../../data/public'; import { fetchSurroundingDocs, SurrDocType } from './context'; -import { DiscoverServices } from '../../../../build_services'; -import { EsHitRecord, EsHitRecordList } from '../../../types'; +import { DiscoverServices } from '../../../build_services'; +import { EsHitRecord, EsHitRecordList } from '../../types'; const MS_PER_DAY = 24 * 60 * 60 * 1000; const ANCHOR_TIMESTAMP = new Date(MS_PER_DAY).toJSON(); diff --git a/src/plugins/discover/public/application/apps/context/services/context.test.ts b/src/plugins/discover/public/application/context/services/context.test.ts similarity index 87% rename from src/plugins/discover/public/application/apps/context/services/context.test.ts rename to src/plugins/discover/public/application/context/services/context.test.ts index 5ad9c02871dca..df5a279dfe927 100644 --- a/src/plugins/discover/public/application/apps/context/services/context.test.ts +++ b/src/plugins/discover/public/application/context/services/context.test.ts @@ -7,8 +7,8 @@ */ import { updateSearchSource } from './context'; -import { indexPatternMock } from '../../../../__mocks__/index_pattern'; -import { createSearchSourceMock } from '../../../../../../data/public/mocks'; +import { indexPatternMock } from '../../../__mocks__/index_pattern'; +import { createSearchSourceMock } from '../../../../../data/public/mocks'; describe('context api', function () { test('createSearchSource when useFieldsApi is true', () => { diff --git a/src/plugins/discover/public/application/apps/context/services/context.ts b/src/plugins/discover/public/application/context/services/context.ts similarity index 86% rename from src/plugins/discover/public/application/apps/context/services/context.ts rename to src/plugins/discover/public/application/context/services/context.ts index 257ae2dcce834..f8af5468cbad5 100644 --- a/src/plugins/discover/public/application/apps/context/services/context.ts +++ b/src/plugins/discover/public/application/context/services/context.ts @@ -6,14 +6,14 @@ * Side Public License, v 1. */ import { Filter, IndexPattern, ISearchSource } from 'src/plugins/data/public'; -import { reverseSortDir, SortDirection } from './utils/sorting'; -import { convertIsoToMillis, extractNanos } from './utils/date_conversion'; -import { fetchHitsInInterval } from './utils/fetch_hits_in_interval'; -import { generateIntervals } from './utils/generate_intervals'; -import { getEsQuerySearchAfter } from './utils/get_es_query_search_after'; -import { getEsQuerySort } from './utils/get_es_query_sort'; -import { getServices } from '../../../../kibana_services'; -import { EsHitRecord, EsHitRecordList } from '../../../types'; +import { reverseSortDir, SortDirection } from '../utils/sorting'; +import { convertIsoToMillis, extractNanos } from '../utils/date_conversion'; +import { fetchHitsInInterval } from '../utils/fetch_hits_in_interval'; +import { generateIntervals } from '../utils/generate_intervals'; +import { getEsQuerySearchAfter } from '../utils/get_es_query_search_after'; +import { getEsQuerySort } from '../utils/get_es_query_sort'; +import { getServices } from '../../../kibana_services'; +import { EsHitRecord, EsHitRecordList } from '../../types'; export enum SurrDocType { SUCCESSORS = 'successors', diff --git a/src/plugins/discover/public/application/apps/context/services/context_query_state.ts b/src/plugins/discover/public/application/context/services/context_query_state.ts similarity index 95% rename from src/plugins/discover/public/application/apps/context/services/context_query_state.ts rename to src/plugins/discover/public/application/context/services/context_query_state.ts index 132b74647f66e..3a6a4c0959ea6 100644 --- a/src/plugins/discover/public/application/apps/context/services/context_query_state.ts +++ b/src/plugins/discover/public/application/context/services/context_query_state.ts @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import { EsHitRecord, EsHitRecordList } from '../../../types'; +import { EsHitRecord, EsHitRecordList } from '../../types'; export interface ContextFetchState { /** diff --git a/src/plugins/discover/public/application/apps/context/services/context_state.test.ts b/src/plugins/discover/public/application/context/services/context_state.test.ts similarity index 96% rename from src/plugins/discover/public/application/apps/context/services/context_state.test.ts rename to src/plugins/discover/public/application/context/services/context_state.test.ts index 3df8ab710729f..8f564d56c1042 100644 --- a/src/plugins/discover/public/application/apps/context/services/context_state.test.ts +++ b/src/plugins/discover/public/application/context/services/context_state.test.ts @@ -9,9 +9,9 @@ import { IUiSettingsClient } from 'kibana/public'; import { getState } from './context_state'; import { createBrowserHistory, History } from 'history'; -import { FilterManager, Filter } from '../../../../../../data/public'; -import { coreMock } from '../../../../../../../core/public/mocks'; -import { SEARCH_FIELDS_FROM_SOURCE } from '../../../../../common'; +import { FilterManager, Filter } from '../../../../../data/public'; +import { coreMock } from '../../../../../../core/public/mocks'; +import { SEARCH_FIELDS_FROM_SOURCE } from '../../../../common'; const setupMock = coreMock.createSetup(); diff --git a/src/plugins/discover/public/application/apps/context/services/context_state.ts b/src/plugins/discover/public/application/context/services/context_state.ts similarity index 98% rename from src/plugins/discover/public/application/apps/context/services/context_state.ts rename to src/plugins/discover/public/application/context/services/context_state.ts index 87f7cf00bafcf..8fb79f6f011c7 100644 --- a/src/plugins/discover/public/application/apps/context/services/context_state.ts +++ b/src/plugins/discover/public/application/context/services/context_state.ts @@ -15,9 +15,9 @@ import { syncStates, withNotifyOnErrors, ReduxLikeStateContainer, -} from '../../../../../../kibana_utils/public'; -import { esFilters, FilterManager, Filter } from '../../../../../../data/public'; -import { handleSourceColumnState } from '../../../helpers/state_helpers'; +} from '../../../../../kibana_utils/public'; +import { esFilters, FilterManager, Filter } from '../../../../../data/public'; +import { handleSourceColumnState } from '../../../utils/state_helpers'; export interface AppState { /** diff --git a/src/plugins/discover/public/application/apps/context/services/utils/date_conversion.test.ts b/src/plugins/discover/public/application/context/utils/date_conversion.test.ts similarity index 100% rename from src/plugins/discover/public/application/apps/context/services/utils/date_conversion.test.ts rename to src/plugins/discover/public/application/context/utils/date_conversion.test.ts diff --git a/src/plugins/discover/public/application/apps/context/services/utils/date_conversion.ts b/src/plugins/discover/public/application/context/utils/date_conversion.ts similarity index 100% rename from src/plugins/discover/public/application/apps/context/services/utils/date_conversion.ts rename to src/plugins/discover/public/application/context/utils/date_conversion.ts diff --git a/src/plugins/discover/public/application/apps/context/services/utils/fetch_hits_in_interval.ts b/src/plugins/discover/public/application/context/utils/fetch_hits_in_interval.ts similarity index 95% rename from src/plugins/discover/public/application/apps/context/services/utils/fetch_hits_in_interval.ts rename to src/plugins/discover/public/application/context/utils/fetch_hits_in_interval.ts index 4cfbee2dc02bc..3fc2716114513 100644 --- a/src/plugins/discover/public/application/apps/context/services/utils/fetch_hits_in_interval.ts +++ b/src/plugins/discover/public/application/context/utils/fetch_hits_in_interval.ts @@ -6,11 +6,11 @@ * Side Public License, v 1. */ -import { ISearchSource, EsQuerySortValue, SortDirection } from '../../../../../../../data/public'; +import { ISearchSource, EsQuerySortValue, SortDirection } from '../../../../../data/public'; import { convertTimeValueToIso } from './date_conversion'; import { IntervalValue } from './generate_intervals'; import { EsQuerySearchAfter } from './get_es_query_search_after'; -import { EsHitRecord, EsHitRecordList } from '../../../../types'; +import { EsHitRecord, EsHitRecordList } from '../../types'; interface RangeQuery { format: string; diff --git a/src/plugins/discover/public/application/apps/context/services/utils/generate_intervals.ts b/src/plugins/discover/public/application/context/utils/generate_intervals.ts similarity index 93% rename from src/plugins/discover/public/application/apps/context/services/utils/generate_intervals.ts rename to src/plugins/discover/public/application/context/utils/generate_intervals.ts index 47952f4f84759..5adeb31444ee7 100644 --- a/src/plugins/discover/public/application/apps/context/services/utils/generate_intervals.ts +++ b/src/plugins/discover/public/application/context/utils/generate_intervals.ts @@ -6,8 +6,8 @@ * Side Public License, v 1. */ -import { SortDirection } from '../../../../../../../data/public'; -import { SurrDocType } from '../context'; +import { SortDirection } from '../../../../../data/public'; +import { SurrDocType } from '../services/context'; export type IntervalValue = number | null; diff --git a/src/plugins/discover/public/application/apps/context/services/utils/get_es_query_search_after.ts b/src/plugins/discover/public/application/context/utils/get_es_query_search_after.ts similarity index 94% rename from src/plugins/discover/public/application/apps/context/services/utils/get_es_query_search_after.ts rename to src/plugins/discover/public/application/context/utils/get_es_query_search_after.ts index 721459fee08f8..85a68376fe43b 100644 --- a/src/plugins/discover/public/application/apps/context/services/utils/get_es_query_search_after.ts +++ b/src/plugins/discover/public/application/context/utils/get_es_query_search_after.ts @@ -6,8 +6,8 @@ * Side Public License, v 1. */ -import { SurrDocType } from '../context'; -import { EsHitRecord, EsHitRecordList } from '../../../../types'; +import { SurrDocType } from '../services/context'; +import { EsHitRecord, EsHitRecordList } from '../../types'; export type EsQuerySearchAfter = [string | number, string | number]; diff --git a/src/plugins/discover/public/application/apps/context/services/utils/get_es_query_sort.ts b/src/plugins/discover/public/application/context/utils/get_es_query_sort.ts similarity index 100% rename from src/plugins/discover/public/application/apps/context/services/utils/get_es_query_sort.ts rename to src/plugins/discover/public/application/context/utils/get_es_query_sort.ts diff --git a/src/plugins/discover/public/application/apps/context/services/utils/sorting.test.ts b/src/plugins/discover/public/application/context/utils/sorting.test.ts similarity index 100% rename from src/plugins/discover/public/application/apps/context/services/utils/sorting.test.ts rename to src/plugins/discover/public/application/context/utils/sorting.test.ts diff --git a/src/plugins/discover/public/application/apps/context/services/utils/sorting.ts b/src/plugins/discover/public/application/context/utils/sorting.ts similarity index 100% rename from src/plugins/discover/public/application/apps/context/services/utils/sorting.ts rename to src/plugins/discover/public/application/context/utils/sorting.ts diff --git a/src/plugins/discover/public/application/apps/context/utils/use_context_app_fetch.test.ts b/src/plugins/discover/public/application/context/utils/use_context_app_fetch.test.ts similarity index 94% rename from src/plugins/discover/public/application/apps/context/utils/use_context_app_fetch.test.ts rename to src/plugins/discover/public/application/context/utils/use_context_app_fetch.test.ts index b3626f9c06f10..cd7bcd810dc39 100644 --- a/src/plugins/discover/public/application/apps/context/utils/use_context_app_fetch.test.ts +++ b/src/plugins/discover/public/application/context/utils/use_context_app_fetch.test.ts @@ -7,10 +7,10 @@ */ import { act, renderHook } from '@testing-library/react-hooks'; -import { setServices, getServices } from '../../../../kibana_services'; -import { createFilterManagerMock } from '../../../../../../data/public/query/filter_manager/filter_manager.mock'; -import { CONTEXT_TIE_BREAKER_FIELDS_SETTING } from '../../../../../common'; -import { DiscoverServices } from '../../../../build_services'; +import { setServices, getServices } from '../../../kibana_services'; +import { createFilterManagerMock } from '../../../../../data/public/query/filter_manager/filter_manager.mock'; +import { CONTEXT_TIE_BREAKER_FIELDS_SETTING } from '../../../../common'; +import { DiscoverServices } from '../../../build_services'; import { FailureReason, LoadingStatus } from '../services/context_query_state'; import { ContextAppFetchProps, useContextAppFetch } from './use_context_app_fetch'; import { @@ -18,9 +18,9 @@ import { mockPredecessorHits, mockSuccessorHits, } from '../__mocks__/use_context_app_fetch'; -import { indexPatternWithTimefieldMock } from '../../../../__mocks__/index_pattern_with_timefield'; +import { indexPatternWithTimefieldMock } from '../../../__mocks__/index_pattern_with_timefield'; import { createContextSearchSourceStub } from '../services/_stubs'; -import { IndexPattern } from '../../../../../../data_views/common'; +import { IndexPattern } from '../../../../../data_views/common'; const mockFilterManager = createFilterManagerMock(); diff --git a/src/plugins/discover/public/application/apps/context/utils/use_context_app_fetch.tsx b/src/plugins/discover/public/application/context/utils/use_context_app_fetch.tsx similarity index 89% rename from src/plugins/discover/public/application/apps/context/utils/use_context_app_fetch.tsx rename to src/plugins/discover/public/application/context/utils/use_context_app_fetch.tsx index ed3b4e8ed5b5a..e5ed24d475497 100644 --- a/src/plugins/discover/public/application/apps/context/utils/use_context_app_fetch.tsx +++ b/src/plugins/discover/public/application/context/utils/use_context_app_fetch.tsx @@ -7,12 +7,12 @@ */ import React, { useCallback, useMemo, useState } from 'react'; import { i18n } from '@kbn/i18n'; -import { CONTEXT_TIE_BREAKER_FIELDS_SETTING } from '../../../../../common'; -import { DiscoverServices } from '../../../../build_services'; +import { CONTEXT_TIE_BREAKER_FIELDS_SETTING } from '../../../../common'; +import { DiscoverServices } from '../../../build_services'; import { fetchAnchor } from '../services/anchor'; import { fetchSurroundingDocs, SurrDocType } from '../services/context'; -import { MarkdownSimple, toMountPoint } from '../../../../../../kibana_react/public'; -import { IndexPattern, SortDirection } from '../../../../../../data/public'; +import { MarkdownSimple, toMountPoint } from '../../../../../kibana_react/public'; +import { IndexPattern, SortDirection } from '../../../../../data/public'; import { ContextFetchState, FailureReason, @@ -20,8 +20,8 @@ import { LoadingStatus, } from '../services/context_query_state'; import { AppState } from '../services/context_state'; -import { getFirstSortableField } from '../services/utils/sorting'; -import { EsHitRecord } from '../../../types'; +import { getFirstSortableField } from './sorting'; +import { EsHitRecord } from '../../types'; const createError = (statusKey: string, reason: FailureReason, error?: Error) => ({ [statusKey]: { value: LoadingStatus.FAILED, error, reason }, @@ -160,15 +160,19 @@ export function useContextAppFetch({ [fetchSurroundingRows] ); - const fetchAllRows = useCallback( - () => fetchAnchorRow().then((anchor) => anchor && fetchContextRows(anchor)), - [fetchAnchorRow, fetchContextRows] - ); + const fetchAllRows = useCallback(() => { + fetchAnchorRow().then((anchor) => anchor && fetchContextRows(anchor)); + }, [fetchAnchorRow, fetchContextRows]); + + const resetFetchedState = useCallback(() => { + setFetchedState(getInitialContextQueryState()); + }, []); return { fetchedState, fetchAllRows, fetchContextRows, fetchSurroundingRows, + resetFetchedState, }; } diff --git a/src/plugins/discover/public/application/apps/context/utils/use_context_app_state.ts b/src/plugins/discover/public/application/context/utils/use_context_app_state.ts similarity index 94% rename from src/plugins/discover/public/application/apps/context/utils/use_context_app_state.ts rename to src/plugins/discover/public/application/context/utils/use_context_app_state.ts index 56701f17c7a63..9accdb363af92 100644 --- a/src/plugins/discover/public/application/apps/context/utils/use_context_app_state.ts +++ b/src/plugins/discover/public/application/context/utils/use_context_app_state.ts @@ -8,8 +8,8 @@ import { useEffect, useMemo, useState } from 'react'; import { cloneDeep } from 'lodash'; -import { CONTEXT_DEFAULT_SIZE_SETTING } from '../../../../../common'; -import { DiscoverServices } from '../../../../build_services'; +import { CONTEXT_DEFAULT_SIZE_SETTING } from '../../../../common'; +import { DiscoverServices } from '../../../build_services'; import { AppState, getState } from '../services/context_state'; export function useContextAppState({ services }: { services: DiscoverServices }) { diff --git a/src/plugins/discover/public/application/discover_router.test.tsx b/src/plugins/discover/public/application/discover_router.test.tsx index 59aede76c6866..dad796bc7c5f6 100644 --- a/src/plugins/discover/public/application/discover_router.test.tsx +++ b/src/plugins/discover/public/application/discover_router.test.tsx @@ -11,10 +11,10 @@ import { Route, RouteProps } from 'react-router-dom'; import { createSearchSessionMock } from '../__mocks__/search_session'; import { discoverServiceMock as mockDiscoverServices } from '../__mocks__/services'; import { discoverRouter } from './discover_router'; -import { DiscoverMainRoute } from './apps/main'; -import { DiscoverMainProps } from './apps/main/discover_main_route'; -import { SingleDocRoute } from './apps/doc'; -import { ContextAppRoute } from './apps/context'; +import { DiscoverMainRoute } from './main'; +import { DiscoverMainProps } from './main/discover_main_route'; +import { SingleDocRoute } from './doc'; +import { ContextAppRoute } from './context'; const pathMap: Record = {}; let mainRouteProps: DiscoverMainProps; diff --git a/src/plugins/discover/public/application/discover_router.tsx b/src/plugins/discover/public/application/discover_router.tsx index 320ce3e5f644a..6f88c28b52bf9 100644 --- a/src/plugins/discover/public/application/discover_router.tsx +++ b/src/plugins/discover/public/application/discover_router.tsx @@ -10,18 +10,19 @@ import { Redirect, Route, Router, Switch } from 'react-router-dom'; import React from 'react'; import { History } from 'history'; import { KibanaContextProvider } from '../../../kibana_react/public'; -import { ContextAppRoute } from './apps/context'; -import { SingleDocRoute } from './apps/doc'; -import { DiscoverMainRoute } from './apps/main'; -import { NotFoundRoute } from './apps/not_found'; +import { ContextAppRoute } from './context'; +import { SingleDocRoute } from './doc'; +import { DiscoverMainRoute } from './main'; +import { NotFoundRoute } from './not_found'; import { DiscoverServices } from '../build_services'; -import { DiscoverMainProps } from './apps/main/discover_main_route'; +import { DiscoverMainProps } from './main/discover_main_route'; export const discoverRouter = (services: DiscoverServices, history: History) => { const mainRouteProps: DiscoverMainProps = { services, history, }; + return ( diff --git a/src/plugins/discover/public/application/apps/doc/components/doc.test.tsx b/src/plugins/discover/public/application/doc/components/doc.test.tsx similarity index 95% rename from src/plugins/discover/public/application/apps/doc/components/doc.test.tsx rename to src/plugins/discover/public/application/doc/components/doc.test.tsx index 68c012ddd92e9..4131a004d299b 100644 --- a/src/plugins/discover/public/application/apps/doc/components/doc.test.tsx +++ b/src/plugins/discover/public/application/doc/components/doc.test.tsx @@ -13,12 +13,12 @@ import { mountWithIntl } from '@kbn/test/jest'; import { ReactWrapper } from 'enzyme'; import { findTestSubject } from '@elastic/eui/lib/test'; import { Doc, DocProps } from './doc'; -import { SEARCH_FIELDS_FROM_SOURCE as mockSearchFieldsFromSource } from '../../../../../common'; -import { indexPatternMock } from '../../../../__mocks__/index_pattern'; +import { SEARCH_FIELDS_FROM_SOURCE as mockSearchFieldsFromSource } from '../../../../common'; +import { indexPatternMock } from '../../../__mocks__/index_pattern'; const mockSearchApi = jest.fn(); -jest.mock('../../../../kibana_services', () => { +jest.mock('../../../kibana_services', () => { // eslint-disable-next-line @typescript-eslint/no-explicit-any let registry: any[] = []; diff --git a/src/plugins/discover/public/application/apps/doc/components/doc.tsx b/src/plugins/discover/public/application/doc/components/doc.tsx similarity index 94% rename from src/plugins/discover/public/application/apps/doc/components/doc.tsx rename to src/plugins/discover/public/application/doc/components/doc.tsx index 7ccf77d2a29d4..3cb416ae8ef46 100644 --- a/src/plugins/discover/public/application/apps/doc/components/doc.tsx +++ b/src/plugins/discover/public/application/doc/components/doc.tsx @@ -10,10 +10,10 @@ import React from 'react'; import { FormattedMessage } from '@kbn/i18n/react'; import { EuiCallOut, EuiLink, EuiLoadingSpinner, EuiPageContent, EuiPage } from '@elastic/eui'; import { IndexPattern } from 'src/plugins/data/public'; -import { getServices } from '../../../../kibana_services'; -import { DocViewer } from '../../../components/doc_viewer/doc_viewer'; +import { getServices } from '../../../kibana_services'; +import { DocViewer } from '../../../services/doc_views/components/doc_viewer'; import { ElasticRequestState } from '../types'; -import { useEsDocSearch } from '../../../services/use_es_doc_search'; +import { useEsDocSearch } from '../../../utils/use_es_doc_search'; export interface DocProps { /** diff --git a/src/plugins/discover/public/application/apps/doc/index.ts b/src/plugins/discover/public/application/doc/index.ts similarity index 100% rename from src/plugins/discover/public/application/apps/doc/index.ts rename to src/plugins/discover/public/application/doc/index.ts diff --git a/src/plugins/discover/public/application/apps/doc/single_doc_route.tsx b/src/plugins/discover/public/application/doc/single_doc_route.tsx similarity index 92% rename from src/plugins/discover/public/application/apps/doc/single_doc_route.tsx rename to src/plugins/discover/public/application/doc/single_doc_route.tsx index aef928d523515..b9887a6f16cdf 100644 --- a/src/plugins/discover/public/application/apps/doc/single_doc_route.tsx +++ b/src/plugins/discover/public/application/doc/single_doc_route.tsx @@ -9,11 +9,11 @@ import React, { useEffect } from 'react'; import { useLocation, useParams } from 'react-router-dom'; import { EuiEmptyPrompt } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; -import { DiscoverServices } from '../../../build_services'; -import { getRootBreadcrumbs } from '../../helpers/breadcrumbs'; +import { DiscoverServices } from '../../build_services'; +import { getRootBreadcrumbs } from '../../utils/breadcrumbs'; import { Doc } from './components/doc'; import { LoadingIndicator } from '../../components/common/loading_indicator'; -import { useIndexPattern } from '../../helpers/use_index_pattern'; +import { useIndexPattern } from '../../utils/use_index_pattern'; export interface SingleDocRouteProps { /** diff --git a/src/plugins/discover/public/application/apps/doc/types.ts b/src/plugins/discover/public/application/doc/types.ts similarity index 100% rename from src/plugins/discover/public/application/apps/doc/types.ts rename to src/plugins/discover/public/application/doc/types.ts diff --git a/src/plugins/discover/public/application/embeddable/constants.ts b/src/plugins/discover/public/application/embeddable/constants.ts deleted file mode 100644 index 57ff91049cd0d..0000000000000 --- a/src/plugins/discover/public/application/embeddable/constants.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 { SEARCH_EMBEDDABLE_TYPE } from '../../../common/index'; diff --git a/src/plugins/discover/public/application/embeddable/helpers/update_search_source.test.ts b/src/plugins/discover/public/application/embeddable/helpers/update_search_source.test.ts deleted file mode 100644 index f09131cb5c926..0000000000000 --- a/src/plugins/discover/public/application/embeddable/helpers/update_search_source.test.ts +++ /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 { createSearchSourceMock } from '../../../../../data/common/search/search_source/mocks'; -import { updateSearchSource } from './update_search_source'; -import { indexPatternMock } from '../../../__mocks__/index_pattern'; -import type { SortOrder } from '../../../saved_searches'; - -describe('updateSearchSource', () => { - const defaults = { - sampleSize: 50, - defaultSort: 'asc', - }; - - it('updates a given search source', async () => { - const searchSource = createSearchSourceMock({}); - updateSearchSource(searchSource, indexPatternMock, [] as SortOrder[], false, defaults); - expect(searchSource.getField('fields')).toBe(undefined); - // does not explicitly request fieldsFromSource when not using fields API - expect(searchSource.getField('fieldsFromSource')).toBe(undefined); - }); - - it('updates a given search source with the usage of the new fields api', async () => { - const searchSource = createSearchSourceMock({}); - updateSearchSource(searchSource, indexPatternMock, [] as SortOrder[], true, defaults); - expect(searchSource.getField('fields')).toEqual([{ field: '*', include_unmapped: 'true' }]); - expect(searchSource.getField('fieldsFromSource')).toBe(undefined); - }); -}); diff --git a/src/plugins/discover/public/application/embeddable/helpers/update_search_source.ts b/src/plugins/discover/public/application/embeddable/helpers/update_search_source.ts deleted file mode 100644 index 1d6c29d65ca85..0000000000000 --- a/src/plugins/discover/public/application/embeddable/helpers/update_search_source.ts +++ /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 { IndexPattern, ISearchSource } from '../../../../../data/common'; -import { getSortForSearchSource } from '../../apps/main/components/doc_table'; -import { SortPairArr } from '../../apps/main/components/doc_table/lib/get_sort'; - -export const updateSearchSource = ( - searchSource: ISearchSource, - indexPattern: IndexPattern | undefined, - sort: (SortPairArr[] & string[][]) | undefined, - useNewFieldsApi: boolean, - defaults: { - sampleSize: number; - defaultSort: string; - } -) => { - const { sampleSize, defaultSort } = defaults; - searchSource.setField('size', sampleSize); - searchSource.setField('sort', getSortForSearchSource(sort, indexPattern, defaultSort)); - if (useNewFieldsApi) { - searchSource.removeField('fieldsFromSource'); - const fields: Record = { field: '*', include_unmapped: 'true' }; - searchSource.setField('fields', [fields]); - } else { - searchSource.removeField('fields'); - } -}; diff --git a/src/plugins/discover/public/application/embeddable/types.ts b/src/plugins/discover/public/application/embeddable/types.ts deleted file mode 100644 index de109e3fa7879..0000000000000 --- a/src/plugins/discover/public/application/embeddable/types.ts +++ /dev/null @@ -1,40 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import { - Embeddable, - EmbeddableInput, - EmbeddableOutput, - IEmbeddable, -} from 'src/plugins/embeddable/public'; -import { Filter, IndexPattern, TimeRange, Query } from '../../../../data/public'; -import { SavedSearch } from '../../saved_searches'; -import { SortOrder } from '../apps/main/components/doc_table/components/table_header/helpers'; - -export interface SearchInput extends EmbeddableInput { - timeRange: TimeRange; - query?: Query; - filters?: Filter[]; - hidePanelTitles?: boolean; - columns?: string[]; - sort?: SortOrder[]; -} - -export interface SearchOutput extends EmbeddableOutput { - editUrl: string; - indexPatterns?: IndexPattern[]; - editable: boolean; -} - -export interface ISearchEmbeddable extends IEmbeddable { - getSavedSearch(): SavedSearch; -} - -export interface SearchEmbeddable extends Embeddable { - type: string; -} diff --git a/src/plugins/discover/public/application/helpers/columns.test.ts b/src/plugins/discover/public/application/helpers/columns.test.ts deleted file mode 100644 index 6b6125193b5f3..0000000000000 --- a/src/plugins/discover/public/application/helpers/columns.test.ts +++ /dev/null @@ -1,51 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import { getDisplayedColumns } from './columns'; -import { indexPatternWithTimefieldMock } from '../../__mocks__/index_pattern_with_timefield'; -import { indexPatternMock } from '../../__mocks__/index_pattern'; - -describe('getDisplayedColumns', () => { - test('returns default columns given a index pattern without timefield', async () => { - const result = getDisplayedColumns([], indexPatternMock); - expect(result).toMatchInlineSnapshot(` - Array [ - "_source", - ] - `); - }); - test('returns default columns given a index pattern with timefield', async () => { - const result = getDisplayedColumns([], indexPatternWithTimefieldMock); - expect(result).toMatchInlineSnapshot(` - Array [ - "_source", - ] - `); - }); - test('returns default columns when just timefield is in state', async () => { - const result = getDisplayedColumns(['timestamp'], indexPatternWithTimefieldMock); - expect(result).toMatchInlineSnapshot(` - Array [ - "_source", - ] - `); - }); - test('returns columns given by argument, no fallback ', async () => { - const result = getDisplayedColumns(['test'], indexPatternWithTimefieldMock); - expect(result).toMatchInlineSnapshot(` - Array [ - "test", - ] - `); - }); - test('returns the same instance of ["_source"] over multiple calls', async () => { - const result = getDisplayedColumns([], indexPatternWithTimefieldMock); - const result2 = getDisplayedColumns([], indexPatternWithTimefieldMock); - expect(result).toBe(result2); - }); -}); diff --git a/src/plugins/discover/public/application/helpers/columns.ts b/src/plugins/discover/public/application/helpers/columns.ts deleted file mode 100644 index 6e77717c5cf05..0000000000000 --- a/src/plugins/discover/public/application/helpers/columns.ts +++ /dev/null @@ -1,28 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import { IndexPattern } from '../../../../data/common'; - -// We store this outside the function as a constant, so we're not creating a new array every time -// the function is returning this. A changing array might cause the data grid to think it got -// new columns, and thus performing worse than using the same array over multiple renders. -const SOURCE_ONLY = ['_source']; - -/** - * Function to provide fallback when - * 1) no columns are given - * 2) Just one column is given, which is the configured timefields - */ -export function getDisplayedColumns(stateColumns: string[] = [], indexPattern: IndexPattern) { - return stateColumns && - stateColumns.length > 0 && - // check if all columns where removed except the configured timeField (this can't be removed) - !(stateColumns.length === 1 && stateColumns[0] === indexPattern.timeFieldName) - ? stateColumns - : SOURCE_ONLY; -} diff --git a/src/plugins/discover/public/application/apps/main/components/chart/discover_chart.test.tsx b/src/plugins/discover/public/application/main/components/chart/discover_chart.test.tsx similarity index 84% rename from src/plugins/discover/public/application/apps/main/components/chart/discover_chart.test.tsx rename to src/plugins/discover/public/application/main/components/chart/discover_chart.test.tsx index f7a383be76b9e..673d831f3fc96 100644 --- a/src/plugins/discover/public/application/apps/main/components/chart/discover_chart.test.tsx +++ b/src/plugins/discover/public/application/main/components/chart/discover_chart.test.tsx @@ -9,21 +9,21 @@ import React from 'react'; import { Subject, BehaviorSubject } from 'rxjs'; import { mountWithIntl } from '@kbn/test/jest'; -import { setHeaderActionMenuMounter } from '../../../../../kibana_services'; -import { esHits } from '../../../../../__mocks__/es_hits'; -import { savedSearchMock } from '../../../../../__mocks__/saved_search'; -import { createSearchSourceMock } from '../../../../../../../data/common/search/search_source/mocks'; +import { setHeaderActionMenuMounter } from '../../../../kibana_services'; +import { esHits } from '../../../../__mocks__/es_hits'; +import { savedSearchMock } from '../../../../__mocks__/saved_search'; +import { createSearchSourceMock } from '../../../../../../data/common/search/search_source/mocks'; import { GetStateReturn } from '../../services/discover_state'; -import { DataCharts$, DataTotalHits$ } from '../../services/use_saved_search'; -import { discoverServiceMock } from '../../../../../__mocks__/services'; -import { FetchStatus } from '../../../../types'; +import { DataCharts$, DataTotalHits$ } from '../../utils/use_saved_search'; +import { discoverServiceMock } from '../../../../__mocks__/services'; +import { FetchStatus } from '../../../types'; import { Chart } from './point_series'; import { DiscoverChart } from './discover_chart'; -import { VIEW_MODE } from '../view_mode_toggle'; +import { VIEW_MODE } from '../../../../components/view_mode_toggle'; setHeaderActionMenuMounter(jest.fn()); -function getProps(timefield?: string) { +function getProps(isTimeBased: boolean = false) { const searchSourceMock = createSearchSourceMock({}); const services = discoverServiceMock; services.data.query.timefilter.timefilter.getAbsoluteTime = () => { @@ -85,6 +85,7 @@ function getProps(timefield?: string) { }) as DataCharts$; return { + isTimeBased, resetSavedSearch: jest.fn(), savedSearch: savedSearchMock, savedSearchDataChart$: charts$, @@ -94,7 +95,6 @@ function getProps(timefield?: string) { services, state: { columns: [] }, stateContainer: {} as GetStateReturn, - timefield, viewMode: VIEW_MODE.DOCUMENT_LEVEL, setDiscoverViewMode: jest.fn(), }; @@ -106,7 +106,7 @@ describe('Discover chart', () => { expect(component.find('[data-test-subj="discoverChartOptionsToggle"]').exists()).toBeFalsy(); }); test('render with filefield', () => { - const component = mountWithIntl(); + const component = mountWithIntl(); expect(component.find('[data-test-subj="discoverChartOptionsToggle"]').exists()).toBeTruthy(); }); }); diff --git a/src/plugins/discover/public/application/apps/main/components/chart/discover_chart.tsx b/src/plugins/discover/public/application/main/components/chart/discover_chart.tsx similarity index 92% rename from src/plugins/discover/public/application/apps/main/components/chart/discover_chart.tsx rename to src/plugins/discover/public/application/main/components/chart/discover_chart.tsx index 1fe149f3eb17d..abda176ab7b76 100644 --- a/src/plugins/discover/public/application/apps/main/components/chart/discover_chart.tsx +++ b/src/plugins/discover/public/application/main/components/chart/discover_chart.tsx @@ -17,14 +17,14 @@ import { } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { HitsCounter } from '../hits_counter'; -import { SavedSearch } from '../../../../../saved_searches'; +import { SavedSearch } from '../../../../services/saved_searches'; import { AppState, GetStateReturn } from '../../services/discover_state'; import { DiscoverHistogram } from './histogram'; -import { DataCharts$, DataTotalHits$ } from '../../services/use_saved_search'; -import { DiscoverServices } from '../../../../../build_services'; +import { DataCharts$, DataTotalHits$ } from '../../utils/use_saved_search'; +import { DiscoverServices } from '../../../../build_services'; import { useChartPanels } from './use_chart_panels'; -import { VIEW_MODE, DocumentViewModeToggle } from '../view_mode_toggle'; -import { SHOW_FIELD_STATISTICS } from '../../../../../../common'; +import { VIEW_MODE, DocumentViewModeToggle } from '../../../../components/view_mode_toggle'; +import { SHOW_FIELD_STATISTICS } from '../../../../../common'; const DiscoverHistogramMemoized = memo(DiscoverHistogram); export const CHART_HIDDEN_KEY = 'discover:chartHidden'; @@ -37,7 +37,7 @@ export function DiscoverChart({ services, state, stateContainer, - timefield, + isTimeBased, viewMode, setDiscoverViewMode, }: { @@ -48,7 +48,7 @@ export function DiscoverChart({ services: DiscoverServices; state: AppState; stateContainer: GetStateReturn; - timefield?: string; + isTimeBased: boolean; viewMode: VIEW_MODE; setDiscoverViewMode: (viewMode: VIEW_MODE) => void; }) { @@ -123,7 +123,7 @@ export function DiscoverChart({ />
    )} - {timefield && ( + {isTimeBased && ( - {timefield && !state.hideChart && ( + {isTimeBased && !state.hideChart && (
    (chartRef.current.element = element)} diff --git a/src/plugins/discover/public/application/apps/main/components/chart/histogram.scss b/src/plugins/discover/public/application/main/components/chart/histogram.scss similarity index 100% rename from src/plugins/discover/public/application/apps/main/components/chart/histogram.scss rename to src/plugins/discover/public/application/main/components/chart/histogram.scss diff --git a/src/plugins/discover/public/application/apps/main/components/chart/histogram.tsx b/src/plugins/discover/public/application/main/components/chart/histogram.tsx similarity index 96% rename from src/plugins/discover/public/application/apps/main/components/chart/histogram.tsx rename to src/plugins/discover/public/application/main/components/chart/histogram.tsx index 315ab7bad40f1..9bdf9dd61e703 100644 --- a/src/plugins/discover/public/application/apps/main/components/chart/histogram.tsx +++ b/src/plugins/discover/public/application/main/components/chart/histogram.tsx @@ -38,12 +38,12 @@ import { Endzones, getAdjustedInterval, renderEndzoneTooltip, -} from '../../../../../../../charts/public'; -import { DataCharts$, DataChartsMessage } from '../../services/use_saved_search'; -import { FetchStatus } from '../../../../types'; -import { DiscoverServices } from '../../../../../build_services'; +} from '../../../../../../charts/public'; +import { DataCharts$, DataChartsMessage } from '../../utils/use_saved_search'; +import { FetchStatus } from '../../../types'; +import { DiscoverServices } from '../../../../build_services'; import { useDataState } from '../../utils/use_data_state'; -import { LEGACY_TIME_AXIS, MULTILAYER_TIME_AXIS_STYLE } from '../../../../../../../charts/common'; +import { LEGACY_TIME_AXIS, MULTILAYER_TIME_AXIS_STYLE } from '../../../../../../charts/common'; export interface DiscoverHistogramProps { savedSearchData$: DataCharts$; diff --git a/src/plugins/discover/public/application/apps/main/components/chart/index.ts b/src/plugins/discover/public/application/main/components/chart/index.ts similarity index 100% rename from src/plugins/discover/public/application/apps/main/components/chart/index.ts rename to src/plugins/discover/public/application/main/components/chart/index.ts diff --git a/src/plugins/discover/public/application/apps/main/components/chart/point_series.test.ts b/src/plugins/discover/public/application/main/components/chart/point_series.test.ts similarity index 100% rename from src/plugins/discover/public/application/apps/main/components/chart/point_series.test.ts rename to src/plugins/discover/public/application/main/components/chart/point_series.test.ts diff --git a/src/plugins/discover/public/application/apps/main/components/chart/point_series.ts b/src/plugins/discover/public/application/main/components/chart/point_series.ts similarity index 96% rename from src/plugins/discover/public/application/apps/main/components/chart/point_series.ts rename to src/plugins/discover/public/application/main/components/chart/point_series.ts index ee057fcb48c6a..b8cdf75d5fd36 100644 --- a/src/plugins/discover/public/application/apps/main/components/chart/point_series.ts +++ b/src/plugins/discover/public/application/main/components/chart/point_series.ts @@ -9,7 +9,7 @@ import { uniq } from 'lodash'; import { Duration, Moment } from 'moment'; import { Unit } from '@elastic/datemath'; -import type { SerializedFieldFormat } from '../../../../../../../field_formats/common'; +import { SerializedFieldFormat } from '../../../../../../field_formats/common'; export interface Column { id: string; diff --git a/src/plugins/discover/public/application/apps/main/components/chart/use_chart_panels.test.ts b/src/plugins/discover/public/application/main/components/chart/use_chart_panels.test.ts similarity index 94% rename from src/plugins/discover/public/application/apps/main/components/chart/use_chart_panels.test.ts rename to src/plugins/discover/public/application/main/components/chart/use_chart_panels.test.ts index a1b9c30380969..20f19ec4719e9 100644 --- a/src/plugins/discover/public/application/apps/main/components/chart/use_chart_panels.test.ts +++ b/src/plugins/discover/public/application/main/components/chart/use_chart_panels.test.ts @@ -11,8 +11,8 @@ import { renderHook } from '@testing-library/react-hooks'; import { useChartPanels } from './use_chart_panels'; import { AppState } from '../../services/discover_state'; import { BehaviorSubject } from 'rxjs'; -import { DataCharts$ } from '../../services/use_saved_search'; -import { FetchStatus } from '../../../../types'; +import { DataCharts$ } from '../../utils/use_saved_search'; +import { FetchStatus } from '../../../types'; import { EuiContextMenuPanelDescriptor } from '@elastic/eui'; describe('test useChartPanels', () => { diff --git a/src/plugins/discover/public/application/apps/main/components/chart/use_chart_panels.ts b/src/plugins/discover/public/application/main/components/chart/use_chart_panels.ts similarity index 95% rename from src/plugins/discover/public/application/apps/main/components/chart/use_chart_panels.ts rename to src/plugins/discover/public/application/main/components/chart/use_chart_panels.ts index 3660173ef761d..cf09506c4f552 100644 --- a/src/plugins/discover/public/application/apps/main/components/chart/use_chart_panels.ts +++ b/src/plugins/discover/public/application/main/components/chart/use_chart_panels.ts @@ -10,9 +10,9 @@ import type { EuiContextMenuPanelItemDescriptor, EuiContextMenuPanelDescriptor, } from '@elastic/eui'; -import { search } from '../../../../../../../data/public'; +import { search } from '../../../../../../data/public'; import { AppState } from '../../services/discover_state'; -import { DataCharts$ } from '../../services/use_saved_search'; +import { DataCharts$ } from '../../utils/use_saved_search'; export function useChartPanels( state: AppState, diff --git a/src/plugins/discover/public/application/apps/main/components/hits_counter/hits_counter.scss b/src/plugins/discover/public/application/main/components/hits_counter/hits_counter.scss similarity index 100% rename from src/plugins/discover/public/application/apps/main/components/hits_counter/hits_counter.scss rename to src/plugins/discover/public/application/main/components/hits_counter/hits_counter.scss diff --git a/src/plugins/discover/public/application/apps/main/components/hits_counter/hits_counter.test.tsx b/src/plugins/discover/public/application/main/components/hits_counter/hits_counter.test.tsx similarity index 95% rename from src/plugins/discover/public/application/apps/main/components/hits_counter/hits_counter.test.tsx rename to src/plugins/discover/public/application/main/components/hits_counter/hits_counter.test.tsx index 02644fbf507dd..d46600fca01f4 100644 --- a/src/plugins/discover/public/application/apps/main/components/hits_counter/hits_counter.test.tsx +++ b/src/plugins/discover/public/application/main/components/hits_counter/hits_counter.test.tsx @@ -12,8 +12,8 @@ import { ReactWrapper } from 'enzyme'; import { HitsCounter, HitsCounterProps } from './hits_counter'; import { findTestSubject } from '@elastic/eui/lib/test'; import { BehaviorSubject } from 'rxjs'; -import { FetchStatus } from '../../../../types'; -import { DataTotalHits$ } from '../../services/use_saved_search'; +import { FetchStatus } from '../../../types'; +import { DataTotalHits$ } from '../../utils/use_saved_search'; describe('hits counter', function () { let props: HitsCounterProps; diff --git a/src/plugins/discover/public/application/apps/main/components/hits_counter/hits_counter.tsx b/src/plugins/discover/public/application/main/components/hits_counter/hits_counter.tsx similarity index 96% rename from src/plugins/discover/public/application/apps/main/components/hits_counter/hits_counter.tsx rename to src/plugins/discover/public/application/main/components/hits_counter/hits_counter.tsx index ab6617cbe79de..47c50f7bd47f5 100644 --- a/src/plugins/discover/public/application/apps/main/components/hits_counter/hits_counter.tsx +++ b/src/plugins/discover/public/application/main/components/hits_counter/hits_counter.tsx @@ -17,8 +17,8 @@ import { } from '@elastic/eui'; import { FormattedMessage, FormattedNumber } from '@kbn/i18n/react'; import { i18n } from '@kbn/i18n'; -import { DataTotalHits$, DataTotalHitsMsg } from '../../services/use_saved_search'; -import { FetchStatus } from '../../../../types'; +import { DataTotalHits$, DataTotalHitsMsg } from '../../utils/use_saved_search'; +import { FetchStatus } from '../../../types'; import { useDataState } from '../../utils/use_data_state'; export interface HitsCounterProps { diff --git a/src/plugins/discover/public/application/apps/main/components/hits_counter/index.ts b/src/plugins/discover/public/application/main/components/hits_counter/index.ts similarity index 100% rename from src/plugins/discover/public/application/apps/main/components/hits_counter/index.ts rename to src/plugins/discover/public/application/main/components/hits_counter/index.ts diff --git a/src/plugins/discover/public/application/apps/main/components/layout/discover_documents.test.tsx b/src/plugins/discover/public/application/main/components/layout/discover_documents.test.tsx similarity index 75% rename from src/plugins/discover/public/application/apps/main/components/layout/discover_documents.test.tsx rename to src/plugins/discover/public/application/main/components/layout/discover_documents.test.tsx index 60540268dcd7f..829d88bbafd08 100644 --- a/src/plugins/discover/public/application/apps/main/components/layout/discover_documents.test.tsx +++ b/src/plugins/discover/public/application/main/components/layout/discover_documents.test.tsx @@ -9,20 +9,20 @@ import React from 'react'; import { BehaviorSubject } from 'rxjs'; import { mountWithIntl } from '@kbn/test/jest'; -import { setHeaderActionMenuMounter } from '../../../../../kibana_services'; -import { esHits } from '../../../../../__mocks__/es_hits'; -import { savedSearchMock } from '../../../../../__mocks__/saved_search'; +import { setHeaderActionMenuMounter } from '../../../../kibana_services'; +import { esHits } from '../../../../__mocks__/es_hits'; +import { savedSearchMock } from '../../../../__mocks__/saved_search'; import { GetStateReturn } from '../../services/discover_state'; -import { DataDocuments$ } from '../../services/use_saved_search'; -import { discoverServiceMock } from '../../../../../__mocks__/services'; -import { FetchStatus } from '../../../../types'; +import { DataDocuments$ } from '../../utils/use_saved_search'; +import { discoverServiceMock } from '../../../../__mocks__/services'; +import { FetchStatus } from '../../../types'; import { DiscoverDocuments } from './discover_documents'; -import { ElasticSearchHit } from '../../../../doc_views/doc_views_types'; -import { indexPatternMock } from '../../../../../__mocks__/index_pattern'; +import { ElasticSearchHit } from '../../../../services/doc_views/doc_views_types'; +import { indexPatternMock } from '../../../../__mocks__/index_pattern'; -jest.mock('../../../../../kibana_services', () => ({ - ...jest.requireActual('../../../../../kibana_services'), - getServices: () => jest.requireActual('../../../../../__mocks__/services').discoverServiceMock, +jest.mock('../../../../kibana_services', () => ({ + ...jest.requireActual('../../../../kibana_services'), + getServices: () => jest.requireActual('../../../../__mocks__/services').discoverServiceMock, })); setHeaderActionMenuMounter(jest.fn()); diff --git a/src/plugins/discover/public/application/apps/main/components/layout/discover_documents.tsx b/src/plugins/discover/public/application/main/components/layout/discover_documents.tsx similarity index 88% rename from src/plugins/discover/public/application/apps/main/components/layout/discover_documents.tsx rename to src/plugins/discover/public/application/main/components/layout/discover_documents.tsx index 64d5e08f25d73..08582b21a90ac 100644 --- a/src/plugins/discover/public/application/apps/main/components/layout/discover_documents.tsx +++ b/src/plugins/discover/public/application/main/components/layout/discover_documents.tsx @@ -14,24 +14,24 @@ import { EuiScreenReaderOnly, } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; -import { DocViewFilterFn, ElasticSearchHit } from '../../../../doc_views/doc_views_types'; +import { DocViewFilterFn, ElasticSearchHit } from '../../../../services/doc_views/doc_views_types'; import { DiscoverGrid } from '../../../../components/discover_grid/discover_grid'; -import { FetchStatus } from '../../../../types'; +import { FetchStatus } from '../../../types'; import { DOC_HIDE_TIME_COLUMN_SETTING, DOC_TABLE_LEGACY, SAMPLE_SIZE_SETTING, SEARCH_FIELDS_FROM_SOURCE, -} from '../../../../../../common'; -import { useColumns } from '../../../../helpers/use_data_grid_columns'; -import { IndexPattern } from '../../../../../../../data/common'; -import { SavedSearch } from '../../../../../saved_searches'; -import { DataDocumentsMsg, DataDocuments$ } from '../../services/use_saved_search'; -import { DiscoverServices } from '../../../../../build_services'; +} from '../../../../../common'; +import { useColumns } from '../../../../utils/use_data_grid_columns'; +import { IndexPattern } from '../../../../../../data/common'; +import { SavedSearch } from '../../../../services/saved_searches'; +import { DataDocumentsMsg, DataDocuments$ } from '../../utils/use_saved_search'; +import { DiscoverServices } from '../../../../build_services'; import { AppState, GetStateReturn } from '../../services/discover_state'; import { useDataState } from '../../utils/use_data_state'; -import { DocTableInfinite } from '../doc_table/doc_table_infinite'; -import { SortPairArr } from '../doc_table/lib/get_sort'; +import { DocTableInfinite } from '../../../../components/doc_table/doc_table_infinite'; +import { SortPairArr } from '../../../../components/doc_table/lib/get_sort'; const DocTableInfiniteMemoized = React.memo(DocTableInfinite); const DataGridMemoized = React.memo(DiscoverGrid); diff --git a/src/plugins/discover/public/application/apps/main/components/layout/discover_layout.scss b/src/plugins/discover/public/application/main/components/layout/discover_layout.scss similarity index 100% rename from src/plugins/discover/public/application/apps/main/components/layout/discover_layout.scss rename to src/plugins/discover/public/application/main/components/layout/discover_layout.scss diff --git a/src/plugins/discover/public/application/apps/main/components/layout/discover_layout.test.tsx b/src/plugins/discover/public/application/main/components/layout/discover_layout.test.tsx similarity index 85% rename from src/plugins/discover/public/application/apps/main/components/layout/discover_layout.test.tsx rename to src/plugins/discover/public/application/main/components/layout/discover_layout.test.tsx index 7e3252dce1ef5..c222e4038f517 100644 --- a/src/plugins/discover/public/application/apps/main/components/layout/discover_layout.test.tsx +++ b/src/plugins/discover/public/application/main/components/layout/discover_layout.test.tsx @@ -9,15 +9,15 @@ import React from 'react'; import { Subject, BehaviorSubject } from 'rxjs'; import { mountWithIntl } from '@kbn/test/jest'; -import { setHeaderActionMenuMounter } from '../../../../../kibana_services'; +import { setHeaderActionMenuMounter } from '../../../../kibana_services'; import { DiscoverLayout, SIDEBAR_CLOSED_KEY } from './discover_layout'; -import { esHits } from '../../../../../__mocks__/es_hits'; -import { indexPatternMock } from '../../../../../__mocks__/index_pattern'; -import { savedSearchMock } from '../../../../../__mocks__/saved_search'; -import { createSearchSourceMock } from '../../../../../../../data/common/search/search_source/mocks'; -import { IndexPattern, IndexPatternAttributes } from '../../../../../../../data/common'; -import { SavedObject } from '../../../../../../../../core/types'; -import { indexPatternWithTimefieldMock } from '../../../../../__mocks__/index_pattern_with_timefield'; +import { esHits } from '../../../../__mocks__/es_hits'; +import { indexPatternMock } from '../../../../__mocks__/index_pattern'; +import { savedSearchMock } from '../../../../__mocks__/saved_search'; +import { createSearchSourceMock } from '../../../../../../data/common/search/search_source/mocks'; +import { IndexPattern, IndexPatternAttributes } from '../../../../../../data/common'; +import { SavedObject } from '../../../../../../../core/types'; +import { indexPatternWithTimefieldMock } from '../../../../__mocks__/index_pattern_with_timefield'; import { GetStateReturn } from '../../services/discover_state'; import { DiscoverLayoutProps } from './types'; import { @@ -25,16 +25,16 @@ import { DataDocuments$, DataMain$, DataTotalHits$, -} from '../../services/use_saved_search'; -import { discoverServiceMock } from '../../../../../__mocks__/services'; -import { FetchStatus } from '../../../../types'; -import { ElasticSearchHit } from '../../../../doc_views/doc_views_types'; -import { RequestAdapter } from '../../../../../../../inspector'; +} from '../../utils/use_saved_search'; +import { discoverServiceMock } from '../../../../__mocks__/services'; +import { FetchStatus } from '../../../types'; +import { ElasticSearchHit } from '../../../../services/doc_views/doc_views_types'; +import { RequestAdapter } from '../../../../../../inspector'; import { Chart } from '../chart/point_series'; import { DiscoverSidebar } from '../sidebar/discover_sidebar'; -jest.mock('../../../../../kibana_services', () => ({ - ...jest.requireActual('../../../../../kibana_services'), +jest.mock('../../../../kibana_services', () => ({ + ...jest.requireActual('../../../../kibana_services'), getServices: () => ({ fieldFormats: { getDefaultInstance: jest.fn(() => ({ convert: (value: unknown) => value })), diff --git a/src/plugins/discover/public/application/apps/main/components/layout/discover_layout.tsx b/src/plugins/discover/public/application/main/components/layout/discover_layout.tsx similarity index 86% rename from src/plugins/discover/public/application/apps/main/components/layout/discover_layout.tsx rename to src/plugins/discover/public/application/main/components/layout/discover_layout.tsx index 76ed7069b294a..d71e99fd2b9a8 100644 --- a/src/plugins/discover/public/application/apps/main/components/layout/discover_layout.tsx +++ b/src/plugins/discover/public/application/main/components/layout/discover_layout.tsx @@ -23,28 +23,33 @@ import { METRIC_TYPE } from '@kbn/analytics'; import classNames from 'classnames'; import { DiscoverNoResults } from '../no_results'; import { LoadingSpinner } from '../loading_spinner/loading_spinner'; -import { esFilters, IndexPatternField } from '../../../../../../../data/public'; +import { esFilters, IndexPatternField } from '../../../../../../data/public'; import { DiscoverSidebarResponsive } from '../sidebar'; import { DiscoverLayoutProps } from './types'; -import { SEARCH_FIELDS_FROM_SOURCE, SHOW_FIELD_STATISTICS } from '../../../../../../common'; -import { popularizeField } from '../../../../helpers/popularize_field'; +import { SEARCH_FIELDS_FROM_SOURCE, SHOW_FIELD_STATISTICS } from '../../../../../common'; +import { popularizeField } from '../../../../utils/popularize_field'; import { DiscoverTopNav } from '../top_nav/discover_topnav'; -import { DocViewFilterFn, ElasticSearchHit } from '../../../../doc_views/doc_views_types'; +import { DocViewFilterFn, ElasticSearchHit } from '../../../../services/doc_views/doc_views_types'; import { DiscoverChart } from '../chart'; import { getResultState } from '../../utils/get_result_state'; -import { InspectorSession } from '../../../../../../../inspector/public'; +import { InspectorSession } from '../../../../../../inspector/public'; import { DiscoverUninitialized } from '../uninitialized/uninitialized'; -import { DataMainMsg } from '../../services/use_saved_search'; -import { useColumns } from '../../../../helpers/use_data_grid_columns'; +import { DataMainMsg } from '../../utils/use_saved_search'; +import { useColumns } from '../../../../utils/use_data_grid_columns'; import { DiscoverDocuments } from './discover_documents'; -import { FetchStatus } from '../../../../types'; +import { FetchStatus } from '../../../types'; import { useDataState } from '../../utils/use_data_state'; import { SavedSearchURLConflictCallout, useSavedSearchAliasMatchRedirect, -} from '../../../../../saved_searches'; -import { DiscoverDataVisualizerGrid } from '../../../../components/data_visualizer_grid'; -import { VIEW_MODE } from '../view_mode_toggle'; +} from '../../../../services/saved_searches'; +import { FieldStatisticsTable } from '../../../components/field_stats_table'; +import { VIEW_MODE } from '../../../../components/view_mode_toggle'; +import { + DOCUMENTS_VIEW_CLICK, + FIELD_STATISTICS_VIEW_CLICK, +} from '../../../components/field_stats_table/constants'; +import { DataViewType } from '../../../../../../data_views/common'; /** * Local storage key for sidebar persistence state @@ -54,7 +59,7 @@ export const SIDEBAR_CLOSED_KEY = 'discover:sidebarClosed'; const SidebarMemoized = React.memo(DiscoverSidebarResponsive); const TopNavMemoized = React.memo(DiscoverTopNav); const DiscoverChartMemoized = React.memo(DiscoverChart); -const DataVisualizerGridMemoized = React.memo(DiscoverDataVisualizerGrid); +const FieldStatisticsTableMemoized = React.memo(FieldStatisticsTable); export function DiscoverLayout({ indexPattern, @@ -95,8 +100,16 @@ export function DiscoverLayout({ const setDiscoverViewMode = useCallback( (mode: VIEW_MODE) => { stateContainer.setAppState({ viewMode: mode }); + + if (trackUiMetric) { + if (mode === VIEW_MODE.AGGREGATED_LEVEL) { + trackUiMetric(METRIC_TYPE.CLICK, FIELD_STATISTICS_VIEW_CLICK); + } else { + trackUiMetric(METRIC_TYPE.CLICK, DOCUMENTS_VIEW_CLICK); + } + } }, - [stateContainer] + [trackUiMetric, stateContainer] ); const fetchCounter = useRef(0); @@ -110,8 +123,12 @@ export function DiscoverLayout({ useSavedSearchAliasMatchRedirect({ savedSearch, spaces, history }); - const timeField = useMemo(() => { - return indexPattern.type !== 'rollup' ? indexPattern.timeFieldName : undefined; + // We treat rollup v1 data views as non time based in Discover, since we query them + // in a non time based way using the regular _search API, since the internal + // representation of those documents does not have the time field that _field_caps + // reports us. + const isTimeBased = useMemo(() => { + return indexPattern.type !== DataViewType.ROLLUP && indexPattern.isTimeBased(); }, [indexPattern]); const initialSidebarClosed = Boolean(storage.get(SIDEBAR_CLOSED_KEY)); @@ -264,7 +281,7 @@ export function DiscoverLayout({ > {resultState === 'none' && ( @@ -315,7 +332,7 @@ export function DiscoverLayout({ stateContainer={stateContainer} /> ) : ( - )} diff --git a/src/plugins/discover/public/application/apps/main/components/layout/index.ts b/src/plugins/discover/public/application/main/components/layout/index.ts similarity index 100% rename from src/plugins/discover/public/application/apps/main/components/layout/index.ts rename to src/plugins/discover/public/application/main/components/layout/index.ts diff --git a/src/plugins/discover/public/application/main/components/layout/types.ts b/src/plugins/discover/public/application/main/components/layout/types.ts new file mode 100644 index 0000000000000..1ed34978416fa --- /dev/null +++ b/src/plugins/discover/public/application/main/components/layout/types.ts @@ -0,0 +1,38 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { + IndexPattern, + IndexPatternAttributes, + Query, + SavedObject, + TimeRange, +} from '../../../../../../data/common'; +import { ISearchSource } from '../../../../../../data/public'; +import { AppState, GetStateReturn } from '../../services/discover_state'; +import { DataRefetch$, SavedSearchData } from '../../utils/use_saved_search'; +import { DiscoverServices } from '../../../../build_services'; +import { SavedSearch } from '../../../../services/saved_searches'; +import { RequestAdapter } from '../../../../../../inspector'; + +export interface DiscoverLayoutProps { + indexPattern: IndexPattern; + indexPatternList: Array>; + inspectorAdapters: { requests: RequestAdapter }; + navigateTo: (url: string) => void; + onChangeIndexPattern: (id: string) => void; + onUpdateQuery: (payload: { dateRange: TimeRange; query?: Query }, isUpdate?: boolean) => void; + resetSavedSearch: () => void; + savedSearch: SavedSearch; + savedSearchData$: SavedSearchData; + savedSearchRefetch$: DataRefetch$; + searchSource: ISearchSource; + services: DiscoverServices; + state: AppState; + stateContainer: GetStateReturn; +} diff --git a/src/plugins/discover/public/application/apps/main/components/loading_spinner/loading_spinner.scss b/src/plugins/discover/public/application/main/components/loading_spinner/loading_spinner.scss similarity index 100% rename from src/plugins/discover/public/application/apps/main/components/loading_spinner/loading_spinner.scss rename to src/plugins/discover/public/application/main/components/loading_spinner/loading_spinner.scss diff --git a/src/plugins/discover/public/application/apps/main/components/loading_spinner/loading_spinner.test.tsx b/src/plugins/discover/public/application/main/components/loading_spinner/loading_spinner.test.tsx similarity index 100% rename from src/plugins/discover/public/application/apps/main/components/loading_spinner/loading_spinner.test.tsx rename to src/plugins/discover/public/application/main/components/loading_spinner/loading_spinner.test.tsx diff --git a/src/plugins/discover/public/application/apps/main/components/loading_spinner/loading_spinner.tsx b/src/plugins/discover/public/application/main/components/loading_spinner/loading_spinner.tsx similarity index 100% rename from src/plugins/discover/public/application/apps/main/components/loading_spinner/loading_spinner.tsx rename to src/plugins/discover/public/application/main/components/loading_spinner/loading_spinner.tsx diff --git a/src/plugins/discover/public/application/apps/main/components/no_results/_no_results.scss b/src/plugins/discover/public/application/main/components/no_results/_no_results.scss similarity index 100% rename from src/plugins/discover/public/application/apps/main/components/no_results/_no_results.scss rename to src/plugins/discover/public/application/main/components/no_results/_no_results.scss diff --git a/src/plugins/discover/public/application/apps/main/components/no_results/assets/no_results_illustration.scss b/src/plugins/discover/public/application/main/components/no_results/assets/no_results_illustration.scss similarity index 100% rename from src/plugins/discover/public/application/apps/main/components/no_results/assets/no_results_illustration.scss rename to src/plugins/discover/public/application/main/components/no_results/assets/no_results_illustration.scss diff --git a/src/plugins/discover/public/application/apps/main/components/no_results/assets/no_results_illustration.tsx b/src/plugins/discover/public/application/main/components/no_results/assets/no_results_illustration.tsx similarity index 100% rename from src/plugins/discover/public/application/apps/main/components/no_results/assets/no_results_illustration.tsx rename to src/plugins/discover/public/application/main/components/no_results/assets/no_results_illustration.tsx diff --git a/src/plugins/discover/public/application/apps/main/components/no_results/index.ts b/src/plugins/discover/public/application/main/components/no_results/index.ts similarity index 100% rename from src/plugins/discover/public/application/apps/main/components/no_results/index.ts rename to src/plugins/discover/public/application/main/components/no_results/index.ts diff --git a/src/plugins/discover/public/application/apps/main/components/no_results/no_results.test.tsx b/src/plugins/discover/public/application/main/components/no_results/no_results.test.tsx similarity index 96% rename from src/plugins/discover/public/application/apps/main/components/no_results/no_results.test.tsx rename to src/plugins/discover/public/application/main/components/no_results/no_results.test.tsx index 3a42a22d1d2eb..ef63e178ecefc 100644 --- a/src/plugins/discover/public/application/apps/main/components/no_results/no_results.test.tsx +++ b/src/plugins/discover/public/application/main/components/no_results/no_results.test.tsx @@ -12,7 +12,7 @@ import { findTestSubject } from '@elastic/eui/lib/test'; import { DiscoverNoResults, DiscoverNoResultsProps } from './no_results'; -jest.mock('../../../../../kibana_services', () => { +jest.mock('../../../../kibana_services', () => { return { getServices: () => ({ docLinks: { @@ -62,7 +62,7 @@ describe('DiscoverNoResults', () => { describe('timeFieldName', () => { test('renders time range feedback', () => { const result = mountAndFindSubjects({ - timeFieldName: 'awesome_time_field', + isTimeBased: true, }); expect(result).toMatchInlineSnapshot(` Object { @@ -94,7 +94,7 @@ describe('DiscoverNoResults', () => { test('renders error message', () => { const error = new Error('Fatal error'); const result = mountAndFindSubjects({ - timeFieldName: 'awesome_time_field', + isTimeBased: true, error, }); expect(result).toMatchInlineSnapshot(` diff --git a/src/plugins/discover/public/application/apps/main/components/no_results/no_results.tsx b/src/plugins/discover/public/application/main/components/no_results/no_results.tsx similarity index 94% rename from src/plugins/discover/public/application/apps/main/components/no_results/no_results.tsx rename to src/plugins/discover/public/application/main/components/no_results/no_results.tsx index 852c0860fd0c1..558760f9c0035 100644 --- a/src/plugins/discover/public/application/apps/main/components/no_results/no_results.tsx +++ b/src/plugins/discover/public/application/main/components/no_results/no_results.tsx @@ -16,13 +16,13 @@ import { EuiSpacer, EuiTitle, } from '@elastic/eui'; -import { DataPublicPluginStart } from '../../../../../../../data/public'; +import { DataPublicPluginStart } from '../../../../../../data/public'; import { AdjustSearch, getTimeFieldMessage } from './no_results_helper'; import './_no_results.scss'; import { NoResultsIllustration } from './assets/no_results_illustration'; export interface DiscoverNoResultsProps { - timeFieldName?: string; + isTimeBased?: boolean; error?: Error; data?: DataPublicPluginStart; hasQuery?: boolean; @@ -31,7 +31,7 @@ export interface DiscoverNoResultsProps { } export function DiscoverNoResults({ - timeFieldName, + isTimeBased, error, data, hasFilters, @@ -54,7 +54,7 @@ export function DiscoverNoResults({ - {!!timeFieldName && getTimeFieldMessage()} + {isTimeBased && getTimeFieldMessage()} {(hasFilters || hasQuery) && ( ({ +jest.mock('../../../../kibana_services', () => ({ getUiActions: jest.fn(() => { return { getTriggerCompatibleActions: jest.fn(() => []), diff --git a/src/plugins/discover/public/application/apps/main/components/sidebar/discover_field.tsx b/src/plugins/discover/public/application/main/components/sidebar/discover_field.tsx similarity index 96% rename from src/plugins/discover/public/application/apps/main/components/sidebar/discover_field.tsx rename to src/plugins/discover/public/application/main/components/sidebar/discover_field.tsx index 89e7b50187630..6864a1c5c2d4a 100644 --- a/src/plugins/discover/public/application/apps/main/components/sidebar/discover_field.tsx +++ b/src/plugins/discover/public/application/main/components/sidebar/discover_field.tsx @@ -24,10 +24,11 @@ import { import { i18n } from '@kbn/i18n'; import { UiCounterMetricType } from '@kbn/analytics'; import classNames from 'classnames'; +import { FieldIcon } from '@kbn/react-field/field_icon'; +import { FieldButton } from '@kbn/react-field/field_button'; import { DiscoverFieldDetails } from './discover_field_details'; -import { FieldIcon, FieldButton } from '../../../../../../../kibana_react/public'; import { FieldDetails } from './types'; -import { IndexPatternField, IndexPattern } from '../../../../../../../data/public'; +import { IndexPatternField, IndexPattern } from '../../../../../../data/public'; import { getFieldTypeName } from './lib/get_field_type_name'; import { DiscoverFieldVisualize } from './discover_field_visualize'; @@ -58,9 +59,11 @@ const FieldInfoIcon: React.FC = memo(() => ( )); -const DiscoverFieldTypeIcon: React.FC<{ field: IndexPatternField }> = memo(({ field }) => ( - -)); +const DiscoverFieldTypeIcon: React.FC<{ field: IndexPatternField }> = memo(({ field }) => { + // If it's a string type, we want to distinguish between keyword and text + const tempType = field.type === 'string' && field.esTypes ? field.esTypes[0] : field.type; + return ; +}); const FieldName: React.FC<{ field: IndexPatternField }> = memo(({ field }) => { const title = diff --git a/src/plugins/discover/public/application/apps/main/components/sidebar/discover_field_bucket.scss b/src/plugins/discover/public/application/main/components/sidebar/discover_field_bucket.scss similarity index 100% rename from src/plugins/discover/public/application/apps/main/components/sidebar/discover_field_bucket.scss rename to src/plugins/discover/public/application/main/components/sidebar/discover_field_bucket.scss diff --git a/src/plugins/discover/public/application/apps/main/components/sidebar/discover_field_bucket.tsx b/src/plugins/discover/public/application/main/components/sidebar/discover_field_bucket.tsx similarity index 98% rename from src/plugins/discover/public/application/apps/main/components/sidebar/discover_field_bucket.tsx rename to src/plugins/discover/public/application/main/components/sidebar/discover_field_bucket.tsx index 7dab8cecf28a9..9b0134bf19406 100644 --- a/src/plugins/discover/public/application/apps/main/components/sidebar/discover_field_bucket.tsx +++ b/src/plugins/discover/public/application/main/components/sidebar/discover_field_bucket.tsx @@ -11,7 +11,7 @@ import { EuiText, EuiButtonIcon, EuiFlexGroup, EuiFlexItem, EuiSpacer } from '@e import { i18n } from '@kbn/i18n'; import { StringFieldProgressBar } from './string_progress_bar'; import { Bucket } from './types'; -import { IndexPatternField } from '../../../../../../../data/public'; +import { IndexPatternField } from '../../../../../../data/public'; import './discover_field_bucket.scss'; interface Props { diff --git a/src/plugins/discover/public/application/apps/main/components/sidebar/discover_field_details.test.tsx b/src/plugins/discover/public/application/main/components/sidebar/discover_field_details.test.tsx similarity index 91% rename from src/plugins/discover/public/application/apps/main/components/sidebar/discover_field_details.test.tsx rename to src/plugins/discover/public/application/main/components/sidebar/discover_field_details.test.tsx index f873cfa2151da..fb3f34b1eff26 100644 --- a/src/plugins/discover/public/application/apps/main/components/sidebar/discover_field_details.test.tsx +++ b/src/plugins/discover/public/application/main/components/sidebar/discover_field_details.test.tsx @@ -11,8 +11,8 @@ import { findTestSubject } from '@elastic/eui/lib/test'; import { mountWithIntl } from '@kbn/test/jest'; import { DiscoverFieldDetails } from './discover_field_details'; -import { IndexPatternField } from '../../../../../../../data/public'; -import { stubIndexPattern } from '../../../../../../../data/common/stubs'; +import { IndexPatternField } from '../../../../../../data/public'; +import { stubIndexPattern } from '../../../../../../data/common/stubs'; describe('discover sidebar field details', function () { const onAddFilter = jest.fn(); diff --git a/src/plugins/discover/public/application/apps/main/components/sidebar/discover_field_details.tsx b/src/plugins/discover/public/application/main/components/sidebar/discover_field_details.tsx similarity index 99% rename from src/plugins/discover/public/application/apps/main/components/sidebar/discover_field_details.tsx rename to src/plugins/discover/public/application/main/components/sidebar/discover_field_details.tsx index e29799b720e21..af2a322d97806 100644 --- a/src/plugins/discover/public/application/apps/main/components/sidebar/discover_field_details.tsx +++ b/src/plugins/discover/public/application/main/components/sidebar/discover_field_details.tsx @@ -11,7 +11,7 @@ import { EuiText, EuiSpacer, EuiLink } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; import { DiscoverFieldBucket } from './discover_field_bucket'; import { Bucket, FieldDetails } from './types'; -import { IndexPatternField, IndexPattern } from '../../../../../../../data/public'; +import { IndexPatternField, IndexPattern } from '../../../../../../data/public'; interface DiscoverFieldDetailsProps { field: IndexPatternField; diff --git a/src/plugins/discover/public/application/apps/main/components/sidebar/discover_field_search.scss b/src/plugins/discover/public/application/main/components/sidebar/discover_field_search.scss similarity index 100% rename from src/plugins/discover/public/application/apps/main/components/sidebar/discover_field_search.scss rename to src/plugins/discover/public/application/main/components/sidebar/discover_field_search.scss diff --git a/src/plugins/discover/public/application/apps/main/components/sidebar/discover_field_search.test.tsx b/src/plugins/discover/public/application/main/components/sidebar/discover_field_search.test.tsx similarity index 100% rename from src/plugins/discover/public/application/apps/main/components/sidebar/discover_field_search.test.tsx rename to src/plugins/discover/public/application/main/components/sidebar/discover_field_search.test.tsx diff --git a/src/plugins/discover/public/application/apps/main/components/sidebar/discover_field_search.tsx b/src/plugins/discover/public/application/main/components/sidebar/discover_field_search.tsx similarity index 100% rename from src/plugins/discover/public/application/apps/main/components/sidebar/discover_field_search.tsx rename to src/plugins/discover/public/application/main/components/sidebar/discover_field_search.tsx diff --git a/src/plugins/discover/public/application/apps/main/components/sidebar/discover_field_visualize.tsx b/src/plugins/discover/public/application/main/components/sidebar/discover_field_visualize.tsx similarity index 100% rename from src/plugins/discover/public/application/apps/main/components/sidebar/discover_field_visualize.tsx rename to src/plugins/discover/public/application/main/components/sidebar/discover_field_visualize.tsx diff --git a/src/plugins/discover/public/application/apps/main/components/sidebar/discover_index_pattern.test.tsx b/src/plugins/discover/public/application/main/components/sidebar/discover_index_pattern.test.tsx similarity index 97% rename from src/plugins/discover/public/application/apps/main/components/sidebar/discover_index_pattern.test.tsx rename to src/plugins/discover/public/application/main/components/sidebar/discover_index_pattern.test.tsx index 50af66511de30..4f1f26f2794a4 100644 --- a/src/plugins/discover/public/application/apps/main/components/sidebar/discover_index_pattern.test.tsx +++ b/src/plugins/discover/public/application/main/components/sidebar/discover_index_pattern.test.tsx @@ -15,7 +15,7 @@ import { SavedObject } from 'kibana/server'; import { DiscoverIndexPattern, DiscoverIndexPatternProps } from './discover_index_pattern'; import { EuiSelectable } from '@elastic/eui'; import { IndexPattern, IndexPatternAttributes } from 'src/plugins/data/public'; -import { indexPatternsMock } from '../../../../../__mocks__/index_patterns'; +import { indexPatternsMock } from '../../../../__mocks__/index_patterns'; const indexPattern = { id: 'the-index-pattern-id-first', diff --git a/src/plugins/discover/public/application/apps/main/components/sidebar/discover_index_pattern.tsx b/src/plugins/discover/public/application/main/components/sidebar/discover_index_pattern.tsx similarity index 100% rename from src/plugins/discover/public/application/apps/main/components/sidebar/discover_index_pattern.tsx rename to src/plugins/discover/public/application/main/components/sidebar/discover_index_pattern.tsx diff --git a/src/plugins/discover/public/application/apps/main/components/sidebar/discover_index_pattern_management.test.tsx b/src/plugins/discover/public/application/main/components/sidebar/discover_index_pattern_management.test.tsx similarity index 92% rename from src/plugins/discover/public/application/apps/main/components/sidebar/discover_index_pattern_management.test.tsx rename to src/plugins/discover/public/application/main/components/sidebar/discover_index_pattern_management.test.tsx index 4e41c457ab8eb..c5b1f4d2612d6 100644 --- a/src/plugins/discover/public/application/apps/main/components/sidebar/discover_index_pattern_management.test.tsx +++ b/src/plugins/discover/public/application/main/components/sidebar/discover_index_pattern_management.test.tsx @@ -7,12 +7,11 @@ */ import React from 'react'; -import { mountWithIntl } from '@kbn/test/jest'; +import { mountWithIntl, findTestSubject } from '@kbn/test/jest'; import { EuiContextMenuPanel, EuiPopover, EuiContextMenuItem } from '@elastic/eui'; -import { findTestSubject } from '@kbn/test/jest'; -import { DiscoverServices } from '../../../../../build_services'; +import { DiscoverServices } from '../../../../build_services'; import { DiscoverIndexPatternManagement } from './discover_index_pattern_management'; -import { stubLogstashIndexPattern } from '../../../../../../../data/common/stubs'; +import { stubLogstashIndexPattern } from '../../../../../../data/common/stubs'; const mockServices = { history: () => ({ diff --git a/src/plugins/discover/public/application/apps/main/components/sidebar/discover_index_pattern_management.tsx b/src/plugins/discover/public/application/main/components/sidebar/discover_index_pattern_management.tsx similarity index 96% rename from src/plugins/discover/public/application/apps/main/components/sidebar/discover_index_pattern_management.tsx rename to src/plugins/discover/public/application/main/components/sidebar/discover_index_pattern_management.tsx index 81e9f821ca6c0..9353073e7fad6 100644 --- a/src/plugins/discover/public/application/apps/main/components/sidebar/discover_index_pattern_management.tsx +++ b/src/plugins/discover/public/application/main/components/sidebar/discover_index_pattern_management.tsx @@ -9,8 +9,8 @@ import React, { useState } from 'react'; import { EuiButtonIcon, EuiContextMenuItem, EuiContextMenuPanel, EuiPopover } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { DiscoverServices } from '../../../../../build_services'; -import { IndexPattern } from '../../../../../../../data/common'; +import { DiscoverServices } from '../../../../build_services'; +import { IndexPattern } from '../../../../../../data/common'; export interface DiscoverIndexPatternManagementProps { /** diff --git a/src/plugins/discover/public/application/apps/main/components/sidebar/discover_sidebar.scss b/src/plugins/discover/public/application/main/components/sidebar/discover_sidebar.scss similarity index 100% rename from src/plugins/discover/public/application/apps/main/components/sidebar/discover_sidebar.scss rename to src/plugins/discover/public/application/main/components/sidebar/discover_sidebar.scss diff --git a/src/plugins/discover/public/application/apps/main/components/sidebar/discover_sidebar.test.tsx b/src/plugins/discover/public/application/main/components/sidebar/discover_sidebar.test.tsx similarity index 86% rename from src/plugins/discover/public/application/apps/main/components/sidebar/discover_sidebar.test.tsx rename to src/plugins/discover/public/application/main/components/sidebar/discover_sidebar.test.tsx index 03616c136df3e..9dd7ef19ffc07 100644 --- a/src/plugins/discover/public/application/apps/main/components/sidebar/discover_sidebar.test.tsx +++ b/src/plugins/discover/public/application/main/components/sidebar/discover_sidebar.test.tsx @@ -6,25 +6,25 @@ * Side Public License, v 1. */ -import { each, cloneDeep } from 'lodash'; +import { cloneDeep, each } from 'lodash'; import { ReactWrapper } from 'enzyme'; import { findTestSubject } from '@elastic/eui/lib/test'; // @ts-expect-error -import realHits from '../../../../../__fixtures__/real_hits.js'; +import realHits from '../../../../__fixtures__/real_hits.js'; import { mountWithIntl } from '@kbn/test/jest'; import React from 'react'; import { DiscoverSidebarProps } from './discover_sidebar'; -import { flattenHit, IndexPatternAttributes } from '../../../../../../../data/common'; -import { SavedObject } from '../../../../../../../../core/types'; +import { flattenHit, IndexPatternAttributes } from '../../../../../../data/common'; +import { SavedObject } from '../../../../../../../core/types'; import { getDefaultFieldFilter } from './lib/field_filter'; import { DiscoverSidebarComponent as DiscoverSidebar } from './discover_sidebar'; -import { ElasticSearchHit } from '../../../../doc_views/doc_views_types'; -import { discoverServiceMock as mockDiscoverServices } from '../../../../../__mocks__/services'; -import { stubLogstashIndexPattern } from '../../../../../../../data/common/stubs'; -import { VIEW_MODE } from '../view_mode_toggle'; +import { ElasticSearchHit } from '../../../../services/doc_views/doc_views_types'; +import { discoverServiceMock as mockDiscoverServices } from '../../../../__mocks__/services'; +import { stubLogstashIndexPattern } from '../../../../../../data/common/stubs'; +import { VIEW_MODE } from '../../../../components/view_mode_toggle'; -jest.mock('../../../../../kibana_services', () => ({ +jest.mock('../../../../kibana_services', () => ({ getServices: () => mockDiscoverServices, })); diff --git a/src/plugins/discover/public/application/apps/main/components/sidebar/discover_sidebar.tsx b/src/plugins/discover/public/application/main/components/sidebar/discover_sidebar.tsx similarity index 98% rename from src/plugins/discover/public/application/apps/main/components/sidebar/discover_sidebar.tsx rename to src/plugins/discover/public/application/main/components/sidebar/discover_sidebar.tsx index d13860eab0d24..fcc4d32151018 100644 --- a/src/plugins/discover/public/application/apps/main/components/sidebar/discover_sidebar.tsx +++ b/src/plugins/discover/public/application/main/components/sidebar/discover_sidebar.tsx @@ -28,19 +28,19 @@ import { FormattedMessage } from '@kbn/i18n/react'; import { DiscoverField } from './discover_field'; import { DiscoverIndexPattern } from './discover_index_pattern'; import { DiscoverFieldSearch } from './discover_field_search'; -import { FIELDS_LIMIT_SETTING } from '../../../../../../common'; +import { FIELDS_LIMIT_SETTING } from '../../../../../common'; import { groupFields } from './lib/group_fields'; import { IndexPatternField, indexPatterns as indexPatternUtils, -} from '../../../../../../../data/public'; +} from '../../../../../../data/public'; import { getDetails } from './lib/get_details'; import { FieldFilterState, getDefaultFieldFilter, setFieldFilterProp } from './lib/field_filter'; import { getIndexPatternFieldList } from './lib/get_index_pattern_field_list'; import { DiscoverSidebarResponsiveProps } from './discover_sidebar_responsive'; import { DiscoverIndexPatternManagement } from './discover_index_pattern_management'; -import { ElasticSearchHit } from '../../../../doc_views/doc_views_types'; -import { VIEW_MODE } from '../view_mode_toggle'; +import { ElasticSearchHit } from '../../../../services/doc_views/doc_views_types'; +import { VIEW_MODE } from '../../../../components/view_mode_toggle'; /** * Default number of available fields displayed and added on scroll diff --git a/src/plugins/discover/public/application/apps/main/components/sidebar/discover_sidebar_responsive.test.tsx b/src/plugins/discover/public/application/main/components/sidebar/discover_sidebar_responsive.test.tsx similarity index 88% rename from src/plugins/discover/public/application/apps/main/components/sidebar/discover_sidebar_responsive.test.tsx rename to src/plugins/discover/public/application/main/components/sidebar/discover_sidebar_responsive.test.tsx index 4e4fed8c65bf7..b412cd69c82af 100644 --- a/src/plugins/discover/public/application/apps/main/components/sidebar/discover_sidebar_responsive.test.tsx +++ b/src/plugins/discover/public/application/main/components/sidebar/discover_sidebar_responsive.test.tsx @@ -11,22 +11,22 @@ import { BehaviorSubject } from 'rxjs'; import { ReactWrapper } from 'enzyme'; import { findTestSubject } from '@elastic/eui/lib/test'; // @ts-expect-error -import realHits from '../../../../../__fixtures__/real_hits.js'; +import realHits from '../../../../__fixtures__/real_hits.js'; import { act } from 'react-dom/test-utils'; import { mountWithIntl } from '@kbn/test/jest'; import React from 'react'; -import { flattenHit, IndexPatternAttributes } from '../../../../../../../data/common'; -import { SavedObject } from '../../../../../../../../core/types'; +import { flattenHit, IndexPatternAttributes } from '../../../../../../data/common'; +import { SavedObject } from '../../../../../../../core/types'; import { DiscoverSidebarResponsive, DiscoverSidebarResponsiveProps, } from './discover_sidebar_responsive'; -import { DiscoverServices } from '../../../../../build_services'; -import { ElasticSearchHit } from '../../../../doc_views/doc_views_types'; -import { FetchStatus } from '../../../../types'; -import { DataDocuments$ } from '../../services/use_saved_search'; -import { stubLogstashIndexPattern } from '../../../../../../../data/common/stubs'; -import { VIEW_MODE } from '../view_mode_toggle'; +import { DiscoverServices } from '../../../../build_services'; +import { ElasticSearchHit } from '../../../../services/doc_views/doc_views_types'; +import { FetchStatus } from '../../../types'; +import { DataDocuments$ } from '../../utils/use_saved_search'; +import { stubLogstashIndexPattern } from '../../../../../../data/common/stubs'; +import { VIEW_MODE } from '../../../../components/view_mode_toggle'; const mockServices = { history: () => ({ @@ -56,7 +56,7 @@ const mockCalcFieldCounts = jest.fn(() => { return mockfieldCounts; }); -jest.mock('../../../../../kibana_services', () => ({ +jest.mock('../../../../kibana_services', () => ({ getUiActions: jest.fn(() => { return { getTriggerCompatibleActions: jest.fn(() => []), diff --git a/src/plugins/discover/public/application/apps/main/components/sidebar/discover_sidebar_responsive.tsx b/src/plugins/discover/public/application/main/components/sidebar/discover_sidebar_responsive.tsx similarity index 96% rename from src/plugins/discover/public/application/apps/main/components/sidebar/discover_sidebar_responsive.tsx rename to src/plugins/discover/public/application/main/components/sidebar/discover_sidebar_responsive.tsx index 368a2b2e92d34..1df3044b81bf8 100644 --- a/src/plugins/discover/public/application/apps/main/components/sidebar/discover_sidebar_responsive.tsx +++ b/src/plugins/discover/public/application/main/components/sidebar/discover_sidebar_responsive.tsx @@ -27,17 +27,17 @@ import { EuiFlexItem, } from '@elastic/eui'; import { DiscoverIndexPattern } from './discover_index_pattern'; -import { IndexPatternAttributes } from '../../../../../../../data/common'; -import { SavedObject } from '../../../../../../../../core/types'; -import { IndexPatternField, IndexPattern } from '../../../../../../../data/public'; +import { IndexPatternAttributes } from '../../../../../../data/common'; +import { SavedObject } from '../../../../../../../core/types'; +import { IndexPatternField, IndexPattern } from '../../../../../../data/public'; import { getDefaultFieldFilter } from './lib/field_filter'; import { DiscoverSidebar } from './discover_sidebar'; -import { DiscoverServices } from '../../../../../build_services'; +import { DiscoverServices } from '../../../../build_services'; import { AppState } from '../../services/discover_state'; import { DiscoverIndexPatternManagement } from './discover_index_pattern_management'; -import { DataDocuments$ } from '../../services/use_saved_search'; +import { DataDocuments$ } from '../../utils/use_saved_search'; import { calcFieldCounts } from '../../utils/calc_field_counts'; -import { VIEW_MODE } from '../view_mode_toggle'; +import { VIEW_MODE } from '../../../../components/view_mode_toggle'; export interface DiscoverSidebarResponsiveProps { /** diff --git a/src/plugins/discover/public/application/apps/main/components/sidebar/index.ts b/src/plugins/discover/public/application/main/components/sidebar/index.ts similarity index 100% rename from src/plugins/discover/public/application/apps/main/components/sidebar/index.ts rename to src/plugins/discover/public/application/main/components/sidebar/index.ts diff --git a/src/plugins/discover/public/application/apps/main/components/sidebar/lib/field_calculator.js b/src/plugins/discover/public/application/main/components/sidebar/lib/field_calculator.js similarity index 98% rename from src/plugins/discover/public/application/apps/main/components/sidebar/lib/field_calculator.js rename to src/plugins/discover/public/application/main/components/sidebar/lib/field_calculator.js index c709f3311105d..4f282f6133ef3 100644 --- a/src/plugins/discover/public/application/apps/main/components/sidebar/lib/field_calculator.js +++ b/src/plugins/discover/public/application/main/components/sidebar/lib/field_calculator.js @@ -8,7 +8,7 @@ import { map, sortBy, without, each, defaults, isObject } from 'lodash'; import { i18n } from '@kbn/i18n'; -import { flattenHit } from '../../../../../../../../data/common'; +import { flattenHit } from '../../../../../../../data/common'; function getFieldValues(hits, field, indexPattern) { const name = field.name; diff --git a/src/plugins/discover/public/application/apps/main/components/sidebar/lib/field_calculator.test.ts b/src/plugins/discover/public/application/main/components/sidebar/lib/field_calculator.test.ts similarity index 96% rename from src/plugins/discover/public/application/apps/main/components/sidebar/lib/field_calculator.test.ts rename to src/plugins/discover/public/application/main/components/sidebar/lib/field_calculator.test.ts index d4bc41f36b2d4..519b298d70072 100644 --- a/src/plugins/discover/public/application/apps/main/components/sidebar/lib/field_calculator.test.ts +++ b/src/plugins/discover/public/application/main/components/sidebar/lib/field_calculator.test.ts @@ -10,14 +10,14 @@ import { keys, each, cloneDeep, clone, uniq, filter, map } from 'lodash'; // @ts-expect-error -import realHits from '../../../../../../__fixtures__/real_hits.js'; +import realHits from '../../../../../__fixtures__/real_hits.js'; -import { IndexPattern } from '../../../../../../../../data/public'; -import { flattenHit } from '../../../../../../../../data/common'; +import { IndexPattern } from '../../../../../../../data/public'; +import { flattenHit } from '../../../../../../../data/common'; // @ts-expect-error import { fieldCalculator } from './field_calculator'; -import { stubLogstashIndexPattern as indexPattern } from '../../../../../../../../data/common/stubs'; +import { stubLogstashIndexPattern as indexPattern } from '../../../../../../../data/common/stubs'; describe('fieldCalculator', function () { it('should have a _countMissing that counts nulls & undefineds in an array', function () { diff --git a/src/plugins/discover/public/application/apps/main/components/sidebar/lib/field_filter.test.ts b/src/plugins/discover/public/application/main/components/sidebar/lib/field_filter.test.ts similarity index 97% rename from src/plugins/discover/public/application/apps/main/components/sidebar/lib/field_filter.test.ts rename to src/plugins/discover/public/application/main/components/sidebar/lib/field_filter.test.ts index 8d7c543157ead..23da318b6b1dd 100644 --- a/src/plugins/discover/public/application/apps/main/components/sidebar/lib/field_filter.test.ts +++ b/src/plugins/discover/public/application/main/components/sidebar/lib/field_filter.test.ts @@ -7,7 +7,7 @@ */ import { getDefaultFieldFilter, setFieldFilterProp, isFieldFiltered } from './field_filter'; -import { IndexPatternField } from '../../../../../../../../data/public'; +import { IndexPatternField } from '../../../../../../../data/public'; describe('field_filter', function () { it('getDefaultFieldFilter should return default filter state', function () { diff --git a/src/plugins/discover/public/application/apps/main/components/sidebar/lib/field_filter.ts b/src/plugins/discover/public/application/main/components/sidebar/lib/field_filter.ts similarity index 96% rename from src/plugins/discover/public/application/apps/main/components/sidebar/lib/field_filter.ts rename to src/plugins/discover/public/application/main/components/sidebar/lib/field_filter.ts index 25a8309d3d963..9d685652be0a6 100644 --- a/src/plugins/discover/public/application/apps/main/components/sidebar/lib/field_filter.ts +++ b/src/plugins/discover/public/application/main/components/sidebar/lib/field_filter.ts @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import { IndexPatternField } from '../../../../../../../../data/public'; +import { IndexPatternField } from '../../../../../../../data/public'; export interface FieldFilterState { missing: boolean; diff --git a/src/plugins/discover/public/application/apps/main/components/sidebar/lib/get_details.ts b/src/plugins/discover/public/application/main/components/sidebar/lib/get_details.ts similarity index 90% rename from src/plugins/discover/public/application/apps/main/components/sidebar/lib/get_details.ts rename to src/plugins/discover/public/application/main/components/sidebar/lib/get_details.ts index cf7d9945a35e6..b5beebf6fb8d4 100644 --- a/src/plugins/discover/public/application/apps/main/components/sidebar/lib/get_details.ts +++ b/src/plugins/discover/public/application/main/components/sidebar/lib/get_details.ts @@ -8,8 +8,8 @@ // @ts-expect-error import { fieldCalculator } from './field_calculator'; -import { IndexPattern, IndexPatternField } from '../../../../../../../../data/public'; -import { ElasticSearchHit } from '../../../../../doc_views/doc_views_types'; +import { IndexPattern, IndexPatternField } from '../../../../../../../data/public'; +import { ElasticSearchHit } from '../../../../../services/doc_views/doc_views_types'; export function getDetails( field: IndexPatternField, diff --git a/src/plugins/discover/public/application/apps/main/components/sidebar/lib/get_field_type_name.ts b/src/plugins/discover/public/application/main/components/sidebar/lib/get_field_type_name.ts similarity index 88% rename from src/plugins/discover/public/application/apps/main/components/sidebar/lib/get_field_type_name.ts rename to src/plugins/discover/public/application/main/components/sidebar/lib/get_field_type_name.ts index e2d4c2f7ddcf2..f68395593bd8b 100644 --- a/src/plugins/discover/public/application/apps/main/components/sidebar/lib/get_field_type_name.ts +++ b/src/plugins/discover/public/application/main/components/sidebar/lib/get_field_type_name.ts @@ -51,6 +51,15 @@ export function getFieldTypeName(type: string) { return i18n.translate('discover.fieldNameIcons.stringFieldAriaLabel', { defaultMessage: 'String field', }); + case 'text': + return i18n.translate('discover.fieldNameIcons.textFieldAriaLabel', { + defaultMessage: 'Text field', + }); + case 'keyword': + return i18n.translate('discover.fieldNameIcons.keywordFieldAriaLabel', { + defaultMessage: 'Keyword field', + }); + case 'nested': return i18n.translate('discover.fieldNameIcons.nestedFieldAriaLabel', { defaultMessage: 'Nested field', diff --git a/src/plugins/discover/public/application/apps/main/components/sidebar/lib/get_index_pattern_field_list.ts b/src/plugins/discover/public/application/main/components/sidebar/lib/get_index_pattern_field_list.ts similarity index 100% rename from src/plugins/discover/public/application/apps/main/components/sidebar/lib/get_index_pattern_field_list.ts rename to src/plugins/discover/public/application/main/components/sidebar/lib/get_index_pattern_field_list.ts diff --git a/src/plugins/discover/public/application/apps/main/components/sidebar/lib/group_fields.test.ts b/src/plugins/discover/public/application/main/components/sidebar/lib/group_fields.test.ts similarity index 98% rename from src/plugins/discover/public/application/apps/main/components/sidebar/lib/group_fields.test.ts rename to src/plugins/discover/public/application/main/components/sidebar/lib/group_fields.test.ts index 4842f652eb730..7e8eabd171d64 100644 --- a/src/plugins/discover/public/application/apps/main/components/sidebar/lib/group_fields.test.ts +++ b/src/plugins/discover/public/application/main/components/sidebar/lib/group_fields.test.ts @@ -8,7 +8,7 @@ import { groupFields } from './group_fields'; import { getDefaultFieldFilter } from './field_filter'; -import { IndexPatternField } from '../../../../../../../../data/common'; +import { IndexPatternField } from '../../../../../../../data/common'; const fields = [ { diff --git a/src/plugins/discover/public/application/apps/main/components/sidebar/lib/group_fields.tsx b/src/plugins/discover/public/application/main/components/sidebar/lib/group_fields.tsx similarity index 97% rename from src/plugins/discover/public/application/apps/main/components/sidebar/lib/group_fields.tsx rename to src/plugins/discover/public/application/main/components/sidebar/lib/group_fields.tsx index 50d742d83c630..e42050dfa216a 100644 --- a/src/plugins/discover/public/application/apps/main/components/sidebar/lib/group_fields.tsx +++ b/src/plugins/discover/public/application/main/components/sidebar/lib/group_fields.tsx @@ -8,7 +8,7 @@ import { IndexPatternField } from 'src/plugins/data/public'; import { FieldFilterState, isFieldFiltered } from './field_filter'; -import { getFieldSubtypeMulti } from '../../../../../../../../data/common'; +import { getFieldSubtypeMulti } from '../../../../../../../data/common'; interface GroupedFields { selected: IndexPatternField[]; diff --git a/src/plugins/discover/public/application/apps/main/components/sidebar/lib/visualize_trigger_utils.test.ts b/src/plugins/discover/public/application/main/components/sidebar/lib/visualize_trigger_utils.test.ts similarity index 98% rename from src/plugins/discover/public/application/apps/main/components/sidebar/lib/visualize_trigger_utils.test.ts rename to src/plugins/discover/public/application/main/components/sidebar/lib/visualize_trigger_utils.test.ts index 0a61bf1ea6029..3c9b1f89e3cbd 100644 --- a/src/plugins/discover/public/application/apps/main/components/sidebar/lib/visualize_trigger_utils.test.ts +++ b/src/plugins/discover/public/application/main/components/sidebar/lib/visualize_trigger_utils.test.ts @@ -26,7 +26,7 @@ const mockGetActions = jest.fn>>, [string, { fieldN () => Promise.resolve([]) ); -jest.mock('../../../../../../kibana_services', () => ({ +jest.mock('../../../../../kibana_services', () => ({ getUiActions: () => ({ getTriggerCompatibleActions: mockGetActions, }), diff --git a/src/plugins/discover/public/application/apps/main/components/sidebar/lib/visualize_trigger_utils.ts b/src/plugins/discover/public/application/main/components/sidebar/lib/visualize_trigger_utils.ts similarity index 95% rename from src/plugins/discover/public/application/apps/main/components/sidebar/lib/visualize_trigger_utils.ts rename to src/plugins/discover/public/application/main/components/sidebar/lib/visualize_trigger_utils.ts index f00b430e5acef..8cee4650a220f 100644 --- a/src/plugins/discover/public/application/apps/main/components/sidebar/lib/visualize_trigger_utils.ts +++ b/src/plugins/discover/public/application/main/components/sidebar/lib/visualize_trigger_utils.ts @@ -11,9 +11,9 @@ import { VISUALIZE_GEO_FIELD_TRIGGER, visualizeFieldTrigger, visualizeGeoFieldTrigger, -} from '../../../../../../../../ui_actions/public'; -import { getUiActions } from '../../../../../../kibana_services'; -import { IndexPatternField, KBN_FIELD_TYPES } from '../../../../../../../../data/public'; +} from '../../../../../../../ui_actions/public'; +import { getUiActions } from '../../../../../kibana_services'; +import { IndexPatternField, KBN_FIELD_TYPES } from '../../../../../../../data/public'; function getTriggerConstant(type: string) { return type === KBN_FIELD_TYPES.GEO_POINT || type === KBN_FIELD_TYPES.GEO_SHAPE diff --git a/src/plugins/discover/public/application/apps/main/components/sidebar/string_progress_bar.tsx b/src/plugins/discover/public/application/main/components/sidebar/string_progress_bar.tsx similarity index 100% rename from src/plugins/discover/public/application/apps/main/components/sidebar/string_progress_bar.tsx rename to src/plugins/discover/public/application/main/components/sidebar/string_progress_bar.tsx diff --git a/src/plugins/discover/public/application/apps/main/components/sidebar/types.ts b/src/plugins/discover/public/application/main/components/sidebar/types.ts similarity index 100% rename from src/plugins/discover/public/application/apps/main/components/sidebar/types.ts rename to src/plugins/discover/public/application/main/components/sidebar/types.ts diff --git a/src/plugins/discover/public/application/apps/main/components/skip_bottom_button/index.ts b/src/plugins/discover/public/application/main/components/skip_bottom_button/index.ts similarity index 100% rename from src/plugins/discover/public/application/apps/main/components/skip_bottom_button/index.ts rename to src/plugins/discover/public/application/main/components/skip_bottom_button/index.ts diff --git a/src/plugins/discover/public/application/apps/main/components/skip_bottom_button/skip_bottom_button.scss b/src/plugins/discover/public/application/main/components/skip_bottom_button/skip_bottom_button.scss similarity index 100% rename from src/plugins/discover/public/application/apps/main/components/skip_bottom_button/skip_bottom_button.scss rename to src/plugins/discover/public/application/main/components/skip_bottom_button/skip_bottom_button.scss diff --git a/src/plugins/discover/public/application/apps/main/components/skip_bottom_button/skip_bottom_button.test.tsx b/src/plugins/discover/public/application/main/components/skip_bottom_button/skip_bottom_button.test.tsx similarity index 100% rename from src/plugins/discover/public/application/apps/main/components/skip_bottom_button/skip_bottom_button.test.tsx rename to src/plugins/discover/public/application/main/components/skip_bottom_button/skip_bottom_button.test.tsx diff --git a/src/plugins/discover/public/application/apps/main/components/skip_bottom_button/skip_bottom_button.tsx b/src/plugins/discover/public/application/main/components/skip_bottom_button/skip_bottom_button.tsx similarity index 100% rename from src/plugins/discover/public/application/apps/main/components/skip_bottom_button/skip_bottom_button.tsx rename to src/plugins/discover/public/application/main/components/skip_bottom_button/skip_bottom_button.tsx diff --git a/src/plugins/discover/public/application/apps/main/components/top_nav/__snapshots__/open_search_panel.test.tsx.snap b/src/plugins/discover/public/application/main/components/top_nav/__snapshots__/open_search_panel.test.tsx.snap similarity index 100% rename from src/plugins/discover/public/application/apps/main/components/top_nav/__snapshots__/open_search_panel.test.tsx.snap rename to src/plugins/discover/public/application/main/components/top_nav/__snapshots__/open_search_panel.test.tsx.snap diff --git a/src/plugins/discover/public/application/apps/main/components/top_nav/discover_topnav.test.tsx b/src/plugins/discover/public/application/main/components/top_nav/discover_topnav.test.tsx similarity index 81% rename from src/plugins/discover/public/application/apps/main/components/top_nav/discover_topnav.test.tsx rename to src/plugins/discover/public/application/main/components/top_nav/discover_topnav.test.tsx index 808346b53304c..2dec7bc991198 100644 --- a/src/plugins/discover/public/application/apps/main/components/top_nav/discover_topnav.test.tsx +++ b/src/plugins/discover/public/application/main/components/top_nav/discover_topnav.test.tsx @@ -8,14 +8,14 @@ import React from 'react'; import { shallowWithIntl } from '@kbn/test/jest'; -import { indexPatternMock } from '../../../../../__mocks__/index_pattern'; -import { savedSearchMock } from '../../../../../__mocks__/saved_search'; +import { indexPatternMock } from '../../../../__mocks__/index_pattern'; +import { savedSearchMock } from '../../../../__mocks__/saved_search'; import { DiscoverTopNav, DiscoverTopNavProps } from './discover_topnav'; -import { TopNavMenuData } from '../../../../../../../navigation/public'; -import { ISearchSource, Query } from '../../../../../../../data/common'; +import { TopNavMenuData } from '../../../../../../navigation/public'; +import { ISearchSource, Query } from '../../../../../../data/common'; import { GetStateReturn } from '../../services/discover_state'; -import { setHeaderActionMenuMounter } from '../../../../../kibana_services'; -import { discoverServiceMock } from '../../../../../__mocks__/services'; +import { setHeaderActionMenuMounter } from '../../../../kibana_services'; +import { discoverServiceMock } from '../../../../__mocks__/services'; setHeaderActionMenuMounter(jest.fn()); diff --git a/src/plugins/discover/public/application/apps/main/components/top_nav/discover_topnav.tsx b/src/plugins/discover/public/application/main/components/top_nav/discover_topnav.tsx similarity index 89% rename from src/plugins/discover/public/application/apps/main/components/top_nav/discover_topnav.tsx rename to src/plugins/discover/public/application/main/components/top_nav/discover_topnav.tsx index 5e3e2dfd96954..2e8261ce165da 100644 --- a/src/plugins/discover/public/application/apps/main/components/top_nav/discover_topnav.tsx +++ b/src/plugins/discover/public/application/main/components/top_nav/discover_topnav.tsx @@ -9,9 +9,10 @@ import React, { useCallback, useMemo } from 'react'; import { useHistory } from 'react-router-dom'; import { DiscoverLayoutProps } from '../layout/types'; import { getTopNavLinks } from './get_top_nav_links'; -import { Query, TimeRange } from '../../../../../../../data/common/query'; -import { getHeaderActionMenuMounter } from '../../../../../kibana_services'; +import { Query, TimeRange } from '../../../../../../data/common/query'; +import { getHeaderActionMenuMounter } from '../../../../kibana_services'; import { GetStateReturn } from '../../services/discover_state'; +import { DataViewType } from '../../../../../../data_views/common'; export type DiscoverTopNavProps = Pick< DiscoverLayoutProps, @@ -39,7 +40,10 @@ export const DiscoverTopNav = ({ resetSavedSearch, }: DiscoverTopNavProps) => { const history = useHistory(); - const showDatePicker = useMemo(() => indexPattern.isTimeBased(), [indexPattern]); + const showDatePicker = useMemo( + () => indexPattern.isTimeBased() && indexPattern.type !== DataViewType.ROLLUP, + [indexPattern] + ); const { TopNavMenu } = services.navigation.ui; const onOpenSavedSearch = useCallback( diff --git a/src/plugins/discover/public/application/apps/main/components/top_nav/get_top_nav_links.test.ts b/src/plugins/discover/public/application/main/components/top_nav/get_top_nav_links.test.ts similarity index 91% rename from src/plugins/discover/public/application/apps/main/components/top_nav/get_top_nav_links.test.ts rename to src/plugins/discover/public/application/main/components/top_nav/get_top_nav_links.test.ts index 20c5b9bae332d..6cf37c5333597 100644 --- a/src/plugins/discover/public/application/apps/main/components/top_nav/get_top_nav_links.test.ts +++ b/src/plugins/discover/public/application/main/components/top_nav/get_top_nav_links.test.ts @@ -8,9 +8,9 @@ import { ISearchSource } from 'src/plugins/data/public'; import { getTopNavLinks } from './get_top_nav_links'; -import { indexPatternMock } from '../../../../../__mocks__/index_pattern'; -import { savedSearchMock } from '../../../../../__mocks__/saved_search'; -import { DiscoverServices } from '../../../../../build_services'; +import { indexPatternMock } from '../../../../__mocks__/index_pattern'; +import { savedSearchMock } from '../../../../__mocks__/saved_search'; +import { DiscoverServices } from '../../../../build_services'; import { GetStateReturn } from '../../services/discover_state'; const services = { diff --git a/src/plugins/discover/public/application/apps/main/components/top_nav/get_top_nav_links.ts b/src/plugins/discover/public/application/main/components/top_nav/get_top_nav_links.ts similarity index 92% rename from src/plugins/discover/public/application/apps/main/components/top_nav/get_top_nav_links.ts rename to src/plugins/discover/public/application/main/components/top_nav/get_top_nav_links.ts index 653e878ad01bb..4b9d48a92e0f5 100644 --- a/src/plugins/discover/public/application/apps/main/components/top_nav/get_top_nav_links.ts +++ b/src/plugins/discover/public/application/main/components/top_nav/get_top_nav_links.ts @@ -9,14 +9,14 @@ import { i18n } from '@kbn/i18n'; import type { IndexPattern, ISearchSource } from 'src/plugins/data/common'; import { showOpenSearchPanel } from './show_open_search_panel'; -import { getSharingData, showPublicUrlSwitch } from '../../utils/get_sharing_data'; -import { unhashUrl } from '../../../../../../../kibana_utils/public'; -import { DiscoverServices } from '../../../../../build_services'; -import { SavedSearch } from '../../../../../saved_searches'; +import { getSharingData, showPublicUrlSwitch } from '../../../../utils/get_sharing_data'; +import { unhashUrl } from '../../../../../../kibana_utils/public'; +import { DiscoverServices } from '../../../../build_services'; +import { SavedSearch } from '../../../../services/saved_searches'; import { onSaveSearch } from './on_save_search'; import { GetStateReturn } from '../../services/discover_state'; import { openOptionsPopover } from './open_options_popover'; -import type { TopNavMenuData } from '../../../../../../../navigation/public'; +import type { TopNavMenuData } from '../../../../../../navigation/public'; /** * Helper function to build the top nav links diff --git a/src/plugins/discover/public/application/main/components/top_nav/on_save_search.test.tsx b/src/plugins/discover/public/application/main/components/top_nav/on_save_search.test.tsx new file mode 100644 index 0000000000000..c0d4a34a4e429 --- /dev/null +++ b/src/plugins/discover/public/application/main/components/top_nav/on_save_search.test.tsx @@ -0,0 +1,36 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 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 { showSaveModal } from '../../../../../../saved_objects/public'; +jest.mock('../../../../../../saved_objects/public'); + +import { onSaveSearch } from './on_save_search'; +import { indexPatternMock } from '../../../../__mocks__/index_pattern'; +import { savedSearchMock } from '../../../../__mocks__/saved_search'; +import { DiscoverServices } from '../../../../build_services'; +import { GetStateReturn } from '../../services/discover_state'; +import { i18nServiceMock } from '../../../../../../../core/public/mocks'; + +test('onSaveSearch', async () => { + const serviceMock = { + core: { + i18n: i18nServiceMock.create(), + }, + } as unknown as DiscoverServices; + const stateMock = {} as unknown as GetStateReturn; + + await onSaveSearch({ + indexPattern: indexPatternMock, + navigateTo: jest.fn(), + savedSearch: savedSearchMock, + services: serviceMock, + state: stateMock, + }); + + expect(showSaveModal).toHaveBeenCalled(); +}); diff --git a/src/plugins/discover/public/application/apps/main/components/top_nav/on_save_search.tsx b/src/plugins/discover/public/application/main/components/top_nav/on_save_search.tsx similarity index 94% rename from src/plugins/discover/public/application/apps/main/components/top_nav/on_save_search.tsx rename to src/plugins/discover/public/application/main/components/top_nav/on_save_search.tsx index 25b04e12c650a..67db4968f9a33 100644 --- a/src/plugins/discover/public/application/apps/main/components/top_nav/on_save_search.tsx +++ b/src/plugins/discover/public/application/main/components/top_nav/on_save_search.tsx @@ -8,12 +8,12 @@ import React from 'react'; import { i18n } from '@kbn/i18n'; -import { SavedObjectSaveModal, showSaveModal } from '../../../../../../../saved_objects/public'; -import { SavedSearch, SaveSavedSearchOptions } from '../../../../../saved_searches'; -import { IndexPattern } from '../../../../../../../data/common'; -import { DiscoverServices } from '../../../../../build_services'; +import { SavedObjectSaveModal, showSaveModal } from '../../../../../../saved_objects/public'; +import { SavedSearch, SaveSavedSearchOptions } from '../../../../services/saved_searches'; +import { IndexPattern } from '../../../../../../data/common'; +import { DiscoverServices } from '../../../../build_services'; import { GetStateReturn } from '../../services/discover_state'; -import { setBreadcrumbsTitle } from '../../../../helpers/breadcrumbs'; +import { setBreadcrumbsTitle } from '../../../../utils/breadcrumbs'; import { persistSavedSearch } from '../../utils/persist_saved_search'; async function saveDataSource({ diff --git a/src/plugins/discover/public/application/main/components/top_nav/open_options_popover.scss b/src/plugins/discover/public/application/main/components/top_nav/open_options_popover.scss new file mode 100644 index 0000000000000..28c68506aedc5 --- /dev/null +++ b/src/plugins/discover/public/application/main/components/top_nav/open_options_popover.scss @@ -0,0 +1,5 @@ +$dscOptionsPopoverWidth: $euiSizeL * 14; + +.dscOptionsPopover { + width: $dscOptionsPopoverWidth; +} diff --git a/src/plugins/discover/public/application/apps/main/components/top_nav/open_options_popover.test.tsx b/src/plugins/discover/public/application/main/components/top_nav/open_options_popover.test.tsx similarity index 91% rename from src/plugins/discover/public/application/apps/main/components/top_nav/open_options_popover.test.tsx rename to src/plugins/discover/public/application/main/components/top_nav/open_options_popover.test.tsx index 28e57aed7b660..8363bfdc57616 100644 --- a/src/plugins/discover/public/application/apps/main/components/top_nav/open_options_popover.test.tsx +++ b/src/plugins/discover/public/application/main/components/top_nav/open_options_popover.test.tsx @@ -9,9 +9,9 @@ import React from 'react'; import { mountWithIntl } from '@kbn/test/jest'; import { findTestSubject } from '@elastic/eui/lib/test'; -import { getServices } from '../../../../../kibana_services'; +import { getServices } from '../../../../kibana_services'; -jest.mock('../../../../../kibana_services', () => { +jest.mock('../../../../kibana_services', () => { const mockUiSettings = new Map(); return { getServices: () => ({ @@ -35,7 +35,7 @@ import { OptionsPopover } from './open_options_popover'; test('should display the correct text if datagrid is selected', () => { const element = document.createElement('div'); const component = mountWithIntl(); - expect(findTestSubject(component, 'docTableMode').text()).toBe('New table'); + expect(findTestSubject(component, 'docTableMode').text()).toBe('Document Explorer'); }); test('should display the correct text if legacy table is selected', () => { @@ -45,5 +45,5 @@ test('should display the correct text if legacy table is selected', () => { uiSettings.set('doc_table:legacy', true); const element = document.createElement('div'); const component = mountWithIntl(); - expect(findTestSubject(component, 'docTableMode').text()).toBe('Classic table'); + expect(findTestSubject(component, 'docTableMode').text()).toBe('Classic'); }); diff --git a/src/plugins/discover/public/application/main/components/top_nav/open_options_popover.tsx b/src/plugins/discover/public/application/main/components/top_nav/open_options_popover.tsx new file mode 100644 index 0000000000000..a68888acb1f48 --- /dev/null +++ b/src/plugins/discover/public/application/main/components/top_nav/open_options_popover.tsx @@ -0,0 +1,146 @@ +/* + * Copyright 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 ReactDOM from 'react-dom'; +import { I18nStart } from 'kibana/public'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { + EuiSpacer, + EuiButton, + EuiText, + EuiWrappingPopover, + EuiCode, + EuiHorizontalRule, + EuiButtonEmpty, + EuiTextAlign, +} from '@elastic/eui'; +import './open_options_popover.scss'; +import { DOC_TABLE_LEGACY } from '../../../../../common'; +import { getServices } from '../../../../kibana_services'; + +const container = document.createElement('div'); +let isOpen = false; + +interface OptionsPopoverProps { + onClose: () => void; + anchorElement: HTMLElement; +} + +export function OptionsPopover(props: OptionsPopoverProps) { + const { + core: { uiSettings }, + addBasePath, + } = getServices(); + const isLegacy = uiSettings.get(DOC_TABLE_LEGACY); + + const mode = isLegacy + ? i18n.translate('discover.openOptionsPopover.classicDiscoverText', { + defaultMessage: 'Classic', + }) + : i18n.translate('discover.openOptionsPopover.documentExplorerText', { + defaultMessage: 'Document Explorer', + }); + + return ( + +
    + +

    + + + + ), + currentViewMode: {mode}, + }} + /> +

    +
    + + + {isLegacy ? ( + + ) : ( + + )} + + {isLegacy && ( + <> + + + {i18n.translate('discover.openOptionsPopover.tryDocumentExplorer', { + defaultMessage: 'Try Document Explorer', + })} + + + )} + + + + {i18n.translate('discover.openOptionsPopover.gotToSettings', { + defaultMessage: 'View Discover settings', + })} + + +
    +
    + ); +} + +function onClose() { + ReactDOM.unmountComponentAtNode(container); + document.body.removeChild(container); + isOpen = false; +} + +export function openOptionsPopover({ + I18nContext, + anchorElement, +}: { + I18nContext: I18nStart['Context']; + anchorElement: HTMLElement; +}) { + if (isOpen) { + onClose(); + return; + } + + isOpen = true; + document.body.appendChild(container); + + const element = ( + + + + ); + ReactDOM.render(element, container); +} diff --git a/src/plugins/discover/public/application/apps/main/components/top_nav/open_search_panel.test.tsx b/src/plugins/discover/public/application/main/components/top_nav/open_search_panel.test.tsx similarity index 96% rename from src/plugins/discover/public/application/apps/main/components/top_nav/open_search_panel.test.tsx rename to src/plugins/discover/public/application/main/components/top_nav/open_search_panel.test.tsx index dc5d3e81744db..80c70d9b1aff5 100644 --- a/src/plugins/discover/public/application/apps/main/components/top_nav/open_search_panel.test.tsx +++ b/src/plugins/discover/public/application/main/components/top_nav/open_search_panel.test.tsx @@ -15,7 +15,7 @@ const mockCapabilities = jest.fn().mockReturnValue({ }, }); -jest.mock('../../../../../kibana_services', () => { +jest.mock('../../../../kibana_services', () => { return { getServices: () => ({ core: { uiSettings: {}, savedObjects: {} }, diff --git a/src/plugins/discover/public/application/apps/main/components/top_nav/open_search_panel.tsx b/src/plugins/discover/public/application/main/components/top_nav/open_search_panel.tsx similarity index 95% rename from src/plugins/discover/public/application/apps/main/components/top_nav/open_search_panel.tsx rename to src/plugins/discover/public/application/main/components/top_nav/open_search_panel.tsx index 1b34e6ffa0b9a..e5a175f0511da 100644 --- a/src/plugins/discover/public/application/apps/main/components/top_nav/open_search_panel.tsx +++ b/src/plugins/discover/public/application/main/components/top_nav/open_search_panel.tsx @@ -20,8 +20,8 @@ import { EuiFlyoutBody, EuiTitle, } from '@elastic/eui'; -import { SavedObjectFinderUi } from '../../../../../../../saved_objects/public'; -import { getServices } from '../../../../../kibana_services'; +import { SavedObjectFinderUi } from '../../../../../../saved_objects/public'; +import { getServices } from '../../../../kibana_services'; const SEARCH_OBJECT_TYPE = 'search'; diff --git a/src/plugins/discover/public/application/apps/main/components/top_nav/show_open_search_panel.tsx b/src/plugins/discover/public/application/main/components/top_nav/show_open_search_panel.tsx similarity index 100% rename from src/plugins/discover/public/application/apps/main/components/top_nav/show_open_search_panel.tsx rename to src/plugins/discover/public/application/main/components/top_nav/show_open_search_panel.tsx diff --git a/src/plugins/discover/public/application/apps/main/components/total_documents/total_documents.tsx b/src/plugins/discover/public/application/main/components/total_documents/total_documents.tsx similarity index 100% rename from src/plugins/discover/public/application/apps/main/components/total_documents/total_documents.tsx rename to src/plugins/discover/public/application/main/components/total_documents/total_documents.tsx diff --git a/src/plugins/discover/public/application/apps/main/components/uninitialized/uninitialized.tsx b/src/plugins/discover/public/application/main/components/uninitialized/uninitialized.tsx similarity index 100% rename from src/plugins/discover/public/application/apps/main/components/uninitialized/uninitialized.tsx rename to src/plugins/discover/public/application/main/components/uninitialized/uninitialized.tsx diff --git a/src/plugins/discover/public/application/main/discover_main_app.test.tsx b/src/plugins/discover/public/application/main/discover_main_app.test.tsx new file mode 100644 index 0000000000000..cb2bad306f43f --- /dev/null +++ b/src/plugins/discover/public/application/main/discover_main_app.test.tsx @@ -0,0 +1,43 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ +import React from 'react'; +import { mountWithIntl } from '@kbn/test/jest'; +import { indexPatternMock } from '../../__mocks__/index_pattern'; +import { DiscoverMainApp } from './discover_main_app'; +import { discoverServiceMock } from '../../__mocks__/services'; +import { savedSearchMock } from '../../__mocks__/saved_search'; +import { createSearchSessionMock } from '../../__mocks__/search_session'; +import { SavedObject } from '../../../../../core/types'; +import { IndexPatternAttributes } from '../../../../data/common'; +import { setHeaderActionMenuMounter } from '../../kibana_services'; +import { findTestSubject } from '@elastic/eui/lib/test'; + +setHeaderActionMenuMounter(jest.fn()); + +describe('DiscoverMainApp', () => { + test('renders', () => { + const { history } = createSearchSessionMock(); + const indexPatternList = [indexPatternMock].map((ip) => { + return { ...ip, ...{ attributes: { title: ip.title } } }; + }) as unknown as Array>; + + const props = { + indexPatternList, + services: discoverServiceMock, + savedSearch: savedSearchMock, + navigateTo: jest.fn(), + history, + }; + + const component = mountWithIntl(); + + expect(findTestSubject(component, 'indexPattern-switch-link').text()).toBe( + indexPatternMock.title + ); + }); +}); diff --git a/src/plugins/discover/public/application/apps/main/discover_main_app.tsx b/src/plugins/discover/public/application/main/discover_main_app.tsx similarity index 90% rename from src/plugins/discover/public/application/apps/main/discover_main_app.tsx rename to src/plugins/discover/public/application/main/discover_main_app.tsx index c7a38032ef405..ea3f852a5290a 100644 --- a/src/plugins/discover/public/application/apps/main/discover_main_app.tsx +++ b/src/plugins/discover/public/application/main/discover_main_app.tsx @@ -8,13 +8,13 @@ import React, { useCallback, useEffect } from 'react'; import { History } from 'history'; import { DiscoverLayout } from './components/layout'; -import { setBreadcrumbsTitle } from '../../helpers/breadcrumbs'; +import { setBreadcrumbsTitle } from '../../utils/breadcrumbs'; import { addHelpMenuToAppChrome } from '../../components/help_menu/help_menu_util'; -import { useDiscoverState } from './services/use_discover_state'; -import { useUrl } from './services/use_url'; -import { IndexPatternAttributes, SavedObject } from '../../../../../data/common'; -import { DiscoverServices } from '../../../build_services'; -import { SavedSearch } from '../../../saved_searches'; +import { useDiscoverState } from './utils/use_discover_state'; +import { useUrl } from './utils/use_url'; +import { IndexPatternAttributes, SavedObject } from '../../../../data/common'; +import { DiscoverServices } from '../../build_services'; +import { SavedSearch } from '../../services/saved_searches'; const DiscoverLayoutMemoized = React.memo(DiscoverLayout); diff --git a/src/plugins/discover/public/application/apps/main/discover_main_route.tsx b/src/plugins/discover/public/application/main/discover_main_route.tsx similarity index 92% rename from src/plugins/discover/public/application/apps/main/discover_main_route.tsx rename to src/plugins/discover/public/application/main/discover_main_route.tsx index b674bfd6568ac..d226e5ef9748b 100644 --- a/src/plugins/discover/public/application/apps/main/discover_main_route.tsx +++ b/src/plugins/discover/public/application/main/discover_main_route.tsx @@ -12,15 +12,19 @@ import { i18n } from '@kbn/i18n'; import { EuiEmptyPrompt } from '@elastic/eui'; import { IndexPatternAttributes, ISearchSource, SavedObject } from 'src/plugins/data/common'; -import { DiscoverServices } from '../../../build_services'; -import { SavedSearch, getSavedSearch, getSavedSearchFullPathUrl } from '../../../saved_searches'; +import { DiscoverServices } from '../../build_services'; +import { + SavedSearch, + getSavedSearch, + getSavedSearchFullPathUrl, +} from '../../services/saved_searches'; import { getState } from './services/discover_state'; import { loadIndexPattern, resolveIndexPattern } from './utils/resolve_index_pattern'; import { DiscoverMainApp } from './discover_main_app'; -import { getRootBreadcrumbs, getSavedSearchBreadcrumbs } from '../../helpers/breadcrumbs'; -import { redirectWhenMissing } from '../../../../../kibana_utils/public'; -import { DataViewSavedObjectConflictError } from '../../../../../data_views/common'; -import { getUrlTracker } from '../../../kibana_services'; +import { getRootBreadcrumbs, getSavedSearchBreadcrumbs } from '../../utils/breadcrumbs'; +import { redirectWhenMissing } from '../../../../kibana_utils/public'; +import { DataViewSavedObjectConflictError } from '../../../../data_views/common'; +import { getUrlTracker } from '../../kibana_services'; import { LoadingIndicator } from '../../components/common/loading_indicator'; const DiscoverMainAppMemoized = memo(DiscoverMainApp); diff --git a/src/plugins/discover/public/application/apps/main/index.ts b/src/plugins/discover/public/application/main/index.ts similarity index 100% rename from src/plugins/discover/public/application/apps/main/index.ts rename to src/plugins/discover/public/application/main/index.ts diff --git a/src/plugins/discover/public/application/apps/main/services/discover_search_session.test.ts b/src/plugins/discover/public/application/main/services/discover_search_session.test.ts similarity index 97% rename from src/plugins/discover/public/application/apps/main/services/discover_search_session.test.ts rename to src/plugins/discover/public/application/main/services/discover_search_session.test.ts index 97c6ada3636d5..0f854438b6749 100644 --- a/src/plugins/discover/public/application/apps/main/services/discover_search_session.test.ts +++ b/src/plugins/discover/public/application/main/services/discover_search_session.test.ts @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import { createSearchSessionMock } from '../../../../__mocks__/search_session'; +import { createSearchSessionMock } from '../../../__mocks__/search_session'; describe('DiscoverSearchSessionManager', () => { const { history, session, searchSessionManager } = createSearchSessionMock(); diff --git a/src/plugins/discover/public/application/apps/main/services/discover_search_session.ts b/src/plugins/discover/public/application/main/services/discover_search_session.ts similarity index 93% rename from src/plugins/discover/public/application/apps/main/services/discover_search_session.ts rename to src/plugins/discover/public/application/main/services/discover_search_session.ts index 81ab578229d19..41e1ad37a353c 100644 --- a/src/plugins/discover/public/application/apps/main/services/discover_search_session.ts +++ b/src/plugins/discover/public/application/main/services/discover_search_session.ts @@ -8,13 +8,13 @@ import { History } from 'history'; import { filter } from 'rxjs/operators'; -import { DataPublicPluginStart } from '../../../../../../data/public'; +import { DataPublicPluginStart } from '../../../../../data/public'; import { createQueryParamObservable, getQueryParams, removeQueryParam, -} from '../../../../../../kibana_utils/public'; -import { SEARCH_SESSION_ID_QUERY_PARAM } from '../../../../url_generator'; +} from '../../../../../kibana_utils/public'; +import { SEARCH_SESSION_ID_QUERY_PARAM } from '../../../url_generator'; export interface DiscoverSearchSessionManagerDeps { history: History; diff --git a/src/plugins/discover/public/application/apps/main/services/discover_state.test.ts b/src/plugins/discover/public/application/main/services/discover_state.test.ts similarity index 97% rename from src/plugins/discover/public/application/apps/main/services/discover_state.test.ts rename to src/plugins/discover/public/application/main/services/discover_state.test.ts index 7f875be0a42c5..5c9900c6a2d78 100644 --- a/src/plugins/discover/public/application/apps/main/services/discover_state.test.ts +++ b/src/plugins/discover/public/application/main/services/discover_state.test.ts @@ -13,9 +13,9 @@ import { createSearchSessionRestorationDataProvider, } from './discover_state'; import { createBrowserHistory, History } from 'history'; -import { dataPluginMock } from '../../../../../../data/public/mocks'; -import type { SavedSearch } from '../../../../saved_searches'; -import { SEARCH_FIELDS_FROM_SOURCE } from '../../../../../common'; +import { dataPluginMock } from '../../../../../data/public/mocks'; +import type { SavedSearch } from '../../../services/saved_searches'; +import { SEARCH_FIELDS_FROM_SOURCE } from '../../../../common'; let history: History; let state: GetStateReturn; diff --git a/src/plugins/discover/public/application/apps/main/services/discover_state.ts b/src/plugins/discover/public/application/main/services/discover_state.ts similarity index 96% rename from src/plugins/discover/public/application/apps/main/services/discover_state.ts rename to src/plugins/discover/public/application/main/services/discover_state.ts index 388d4f19d1c27..0b855a27cc74e 100644 --- a/src/plugins/discover/public/application/apps/main/services/discover_state.ts +++ b/src/plugins/discover/public/application/main/services/discover_state.ts @@ -18,7 +18,7 @@ import { StateContainer, syncState, withNotifyOnErrors, -} from '../../../../../../kibana_utils/public'; +} from '../../../../../kibana_utils/public'; import { connectToQueryState, DataPublicPluginStart, @@ -29,13 +29,13 @@ import { Query, SearchSessionInfoProvider, syncQueryStateWithUrl, -} from '../../../../../../data/public'; -import { migrateLegacyQuery } from '../../../helpers/migrate_legacy_query'; +} from '../../../../../data/public'; +import { migrateLegacyQuery } from '../../../utils/migrate_legacy_query'; import { DiscoverGridSettings } from '../../../components/discover_grid/types'; -import { SavedSearch } from '../../../../saved_searches'; -import { handleSourceColumnState } from '../../../helpers/state_helpers'; -import { DISCOVER_APP_LOCATOR, DiscoverAppLocatorParams } from '../../../../locator'; -import { VIEW_MODE } from '../components/view_mode_toggle'; +import { SavedSearch } from '../../../services/saved_searches'; +import { handleSourceColumnState } from '../../../utils/state_helpers'; +import { DISCOVER_APP_LOCATOR, DiscoverAppLocatorParams } from '../../../locator'; +import { VIEW_MODE } from '../../../components/view_mode_toggle'; export interface AppState { /** @@ -411,5 +411,7 @@ function createUrlGeneratorState({ } : undefined, useHash: false, + viewMode: appState.viewMode, + hideAggregatedPreview: appState.hideAggregatedPreview, }; } diff --git a/src/plugins/discover/public/application/apps/main/utils/calc_field_counts.test.ts b/src/plugins/discover/public/application/main/utils/calc_field_counts.test.ts similarity index 90% rename from src/plugins/discover/public/application/apps/main/utils/calc_field_counts.test.ts rename to src/plugins/discover/public/application/main/utils/calc_field_counts.test.ts index a13fd88df7c25..9d198947e06c7 100644 --- a/src/plugins/discover/public/application/apps/main/utils/calc_field_counts.test.ts +++ b/src/plugins/discover/public/application/main/utils/calc_field_counts.test.ts @@ -7,8 +7,8 @@ */ import { calcFieldCounts } from './calc_field_counts'; -import { indexPatternMock } from '../../../../__mocks__/index_pattern'; -import { ElasticSearchHit } from '../../../doc_views/doc_views_types'; +import { indexPatternMock } from '../../../__mocks__/index_pattern'; +import { ElasticSearchHit } from '../../../services/doc_views/doc_views_types'; describe('calcFieldCounts', () => { test('returns valid field count data', async () => { diff --git a/src/plugins/discover/public/application/apps/main/utils/calc_field_counts.ts b/src/plugins/discover/public/application/main/utils/calc_field_counts.ts similarity index 86% rename from src/plugins/discover/public/application/apps/main/utils/calc_field_counts.ts rename to src/plugins/discover/public/application/main/utils/calc_field_counts.ts index 2198d2f66b6b4..08d1a2639fa0b 100644 --- a/src/plugins/discover/public/application/apps/main/utils/calc_field_counts.ts +++ b/src/plugins/discover/public/application/main/utils/calc_field_counts.ts @@ -6,8 +6,8 @@ * Side Public License, v 1. */ -import { flattenHit, IndexPattern } from '../../../../../../data/common'; -import { ElasticSearchHit } from '../../../doc_views/doc_views_types'; +import { flattenHit, IndexPattern } from '../../../../../data/common'; +import { ElasticSearchHit } from '../../../services/doc_views/doc_views_types'; /** * This function is recording stats of the available fields, for usage in sidebar and sharing diff --git a/src/plugins/discover/public/application/apps/main/utils/fetch_all.test.ts b/src/plugins/discover/public/application/main/utils/fetch_all.test.ts similarity index 85% rename from src/plugins/discover/public/application/apps/main/utils/fetch_all.test.ts rename to src/plugins/discover/public/application/main/utils/fetch_all.test.ts index 88830b2946b5f..9f17054de18d4 100644 --- a/src/plugins/discover/public/application/apps/main/utils/fetch_all.test.ts +++ b/src/plugins/discover/public/application/main/utils/fetch_all.test.ts @@ -5,13 +5,13 @@ * in compliance with, at your election, the Elastic License 2.0 or the Server * Side Public License, v 1. */ -import { FetchStatus } from '../../../types'; +import { FetchStatus } from '../../types'; import { BehaviorSubject } from 'rxjs'; -import { RequestAdapter } from '../../../../../../inspector'; -import { savedSearchMock } from '../../../../__mocks__/saved_search'; -import { ReduxLikeStateContainer } from '../../../../../../kibana_utils/common'; +import { RequestAdapter } from '../../../../../inspector'; +import { savedSearchMock } from '../../../__mocks__/saved_search'; +import { ReduxLikeStateContainer } from '../../../../../kibana_utils/common'; import { AppState } from '../services/discover_state'; -import { discoverServiceMock } from '../../../../__mocks__/services'; +import { discoverServiceMock } from '../../../__mocks__/services'; import { fetchAll } from './fetch_all'; describe('test fetchAll', () => { diff --git a/src/plugins/discover/public/application/main/utils/fetch_all.ts b/src/plugins/discover/public/application/main/utils/fetch_all.ts new file mode 100644 index 0000000000000..471616c9d4261 --- /dev/null +++ b/src/plugins/discover/public/application/main/utils/fetch_all.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 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 { forkJoin, of } from 'rxjs'; +import { + sendCompleteMsg, + sendErrorMsg, + sendLoadingMsg, + sendPartialMsg, + sendResetMsg, +} from './use_saved_search_messages'; +import { updateSearchSource } from './update_search_source'; +import type { SortOrder } from '../../../services/saved_searches'; +import { fetchDocuments } from './fetch_documents'; +import { fetchTotalHits } from './fetch_total_hits'; +import { fetchChart } from './fetch_chart'; +import { ISearchSource } from '../../../../../data/common'; +import { Adapters } from '../../../../../inspector'; +import { AppState } from '../services/discover_state'; +import { FetchStatus } from '../../types'; +import { DataPublicPluginStart } from '../../../../../data/public'; +import { SavedSearchData } from './use_saved_search'; +import { DiscoverServices } from '../../../build_services'; +import { ReduxLikeStateContainer } from '../../../../../kibana_utils/common'; +import { DataViewType } from '../../../../../data_views/common'; + +export function fetchAll( + dataSubjects: SavedSearchData, + searchSource: ISearchSource, + reset = false, + fetchDeps: { + abortController: AbortController; + appStateContainer: ReduxLikeStateContainer; + inspectorAdapters: Adapters; + data: DataPublicPluginStart; + initialFetchStatus: FetchStatus; + searchSessionId: string; + services: DiscoverServices; + useNewFieldsApi: boolean; + } +) { + const { initialFetchStatus, appStateContainer, services, useNewFieldsApi, data } = fetchDeps; + + const indexPattern = searchSource.getField('index')!; + + if (reset) { + sendResetMsg(dataSubjects, initialFetchStatus); + } + + sendLoadingMsg(dataSubjects.main$); + + const { hideChart, sort } = appStateContainer.getState(); + // Update the base searchSource, base for all child fetches + updateSearchSource(searchSource, false, { + indexPattern, + services, + sort: sort as SortOrder[], + useNewFieldsApi, + }); + + const subFetchDeps = { + ...fetchDeps, + onResults: (foundDocuments: boolean) => { + if (!foundDocuments) { + sendCompleteMsg(dataSubjects.main$, foundDocuments); + } else { + sendPartialMsg(dataSubjects.main$); + } + }, + }; + + const isChartVisible = + !hideChart && indexPattern.isTimeBased() && indexPattern.type !== DataViewType.ROLLUP; + + const all = forkJoin({ + documents: fetchDocuments(dataSubjects, searchSource.createCopy(), subFetchDeps), + totalHits: !isChartVisible + ? fetchTotalHits(dataSubjects, searchSource.createCopy(), subFetchDeps) + : of(null), + chart: isChartVisible + ? fetchChart(dataSubjects, searchSource.createCopy(), subFetchDeps) + : of(null), + }); + + all.subscribe( + () => sendCompleteMsg(dataSubjects.main$, true), + (error) => { + if (error instanceof Error && error.name === 'AbortError') return; + data.search.showError(error); + sendErrorMsg(dataSubjects.main$, error); + } + ); + return all; +} diff --git a/src/plugins/discover/public/application/apps/main/utils/fetch_chart.test.ts b/src/plugins/discover/public/application/main/utils/fetch_chart.test.ts similarity index 93% rename from src/plugins/discover/public/application/apps/main/utils/fetch_chart.test.ts rename to src/plugins/discover/public/application/main/utils/fetch_chart.test.ts index 2c9350b457779..5f57484aaa653 100644 --- a/src/plugins/discover/public/application/apps/main/utils/fetch_chart.test.ts +++ b/src/plugins/discover/public/application/main/utils/fetch_chart.test.ts @@ -5,15 +5,15 @@ * in compliance with, at your election, the Elastic License 2.0 or the Server * Side Public License, v 1. */ -import { FetchStatus } from '../../../types'; +import { FetchStatus } from '../../types'; import { BehaviorSubject, of, throwError as throwErrorRx } from 'rxjs'; -import { RequestAdapter } from '../../../../../../inspector'; -import { savedSearchMockWithTimeField } from '../../../../__mocks__/saved_search'; +import { RequestAdapter } from '../../../../../inspector'; +import { savedSearchMockWithTimeField } from '../../../__mocks__/saved_search'; import { fetchChart, updateSearchSource } from './fetch_chart'; -import { ReduxLikeStateContainer } from '../../../../../../kibana_utils/common'; +import { ReduxLikeStateContainer } from '../../../../../kibana_utils/common'; import { AppState } from '../services/discover_state'; -import { discoverServiceMock } from '../../../../__mocks__/services'; -import { calculateBounds, IKibanaSearchResponse } from '../../../../../../data/common'; +import { discoverServiceMock } from '../../../__mocks__/services'; +import { calculateBounds, IKibanaSearchResponse } from '../../../../../data/common'; import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; function getDataSubjects() { diff --git a/src/plugins/discover/public/application/apps/main/utils/fetch_chart.ts b/src/plugins/discover/public/application/main/utils/fetch_chart.ts similarity index 89% rename from src/plugins/discover/public/application/apps/main/utils/fetch_chart.ts rename to src/plugins/discover/public/application/main/utils/fetch_chart.ts index 50f3a1b8bfea7..59377970acb12 100644 --- a/src/plugins/discover/public/application/apps/main/utils/fetch_chart.ts +++ b/src/plugins/discover/public/application/main/utils/fetch_chart.ts @@ -12,16 +12,16 @@ import { isCompleteResponse, search, ISearchSource, -} from '../../../../../../data/public'; -import { Adapters } from '../../../../../../inspector'; +} from '../../../../../data/public'; +import { Adapters } from '../../../../../inspector'; import { getChartAggConfigs, getDimensions } from './index'; -import { tabifyAggResponse } from '../../../../../../data/common'; +import { tabifyAggResponse } from '../../../../../data/common'; import { buildPointSeriesData } from '../components/chart/point_series'; -import { FetchStatus } from '../../../types'; -import { SavedSearchData } from '../services/use_saved_search'; +import { FetchStatus } from '../../types'; +import { SavedSearchData } from './use_saved_search'; import { AppState } from '../services/discover_state'; -import { ReduxLikeStateContainer } from '../../../../../../kibana_utils/common'; -import { sendErrorMsg, sendLoadingMsg } from '../services/use_saved_search_messages'; +import { ReduxLikeStateContainer } from '../../../../../kibana_utils/common'; +import { sendErrorMsg, sendLoadingMsg } from './use_saved_search_messages'; export function fetchChart( data$: SavedSearchData, diff --git a/src/plugins/discover/public/application/apps/main/utils/fetch_documents.test.ts b/src/plugins/discover/public/application/main/utils/fetch_documents.test.ts similarity index 91% rename from src/plugins/discover/public/application/apps/main/utils/fetch_documents.test.ts rename to src/plugins/discover/public/application/main/utils/fetch_documents.test.ts index 6c6c7595b166e..291da255b5068 100644 --- a/src/plugins/discover/public/application/apps/main/utils/fetch_documents.test.ts +++ b/src/plugins/discover/public/application/main/utils/fetch_documents.test.ts @@ -6,11 +6,11 @@ * Side Public License, v 1. */ import { fetchDocuments } from './fetch_documents'; -import { FetchStatus } from '../../../types'; +import { FetchStatus } from '../../types'; import { BehaviorSubject, throwError as throwErrorRx } from 'rxjs'; -import { RequestAdapter } from '../../../../../../inspector'; -import { savedSearchMock } from '../../../../__mocks__/saved_search'; -import { discoverServiceMock } from '../../../../__mocks__/services'; +import { RequestAdapter } from '../../../../../inspector'; +import { savedSearchMock } from '../../../__mocks__/saved_search'; +import { discoverServiceMock } from '../../../__mocks__/services'; function getDataSubjects() { return { diff --git a/src/plugins/discover/public/application/apps/main/utils/fetch_documents.ts b/src/plugins/discover/public/application/main/utils/fetch_documents.ts similarity index 77% rename from src/plugins/discover/public/application/apps/main/utils/fetch_documents.ts rename to src/plugins/discover/public/application/main/utils/fetch_documents.ts index 6c5eff7cff702..b23dd3a0ed932 100644 --- a/src/plugins/discover/public/application/apps/main/utils/fetch_documents.ts +++ b/src/plugins/discover/public/application/main/utils/fetch_documents.ts @@ -7,13 +7,13 @@ */ import { i18n } from '@kbn/i18n'; import { filter } from 'rxjs/operators'; -import { Adapters } from '../../../../../../inspector/common'; -import { isCompleteResponse, ISearchSource } from '../../../../../../data/common'; -import { FetchStatus } from '../../../types'; -import { SavedSearchData } from '../services/use_saved_search'; -import { sendErrorMsg, sendLoadingMsg } from '../services/use_saved_search_messages'; -import { SAMPLE_SIZE_SETTING } from '../../../../../common'; -import { DiscoverServices } from '../../../../build_services'; +import { Adapters } from '../../../../../inspector/common'; +import { isCompleteResponse, ISearchSource } from '../../../../../data/common'; +import { FetchStatus } from '../../types'; +import { SavedSearchData } from './use_saved_search'; +import { sendErrorMsg, sendLoadingMsg } from './use_saved_search_messages'; +import { SAMPLE_SIZE_SETTING } from '../../../../common'; +import { DiscoverServices } from '../../../build_services'; export const fetchDocuments = ( data$: SavedSearchData, @@ -38,6 +38,13 @@ export const fetchDocuments = ( searchSource.setField('trackTotalHits', false); searchSource.setField('highlightAll', true); searchSource.setField('version', true); + if (searchSource.getField('index')?.type === 'rollup') { + // We treat that index pattern as "normal" even if it was a rollup index pattern, + // since the rollup endpoint does not support querying individual documents, but we + // can get them from the regular _search API that will be used if the index pattern + // not a rollup index pattern. + searchSource.setOverwriteDataViewType(undefined); + } sendLoadingMsg(documents$); diff --git a/src/plugins/discover/public/application/apps/main/utils/fetch_total_hits.test.ts b/src/plugins/discover/public/application/main/utils/fetch_total_hits.test.ts similarity index 91% rename from src/plugins/discover/public/application/apps/main/utils/fetch_total_hits.test.ts rename to src/plugins/discover/public/application/main/utils/fetch_total_hits.test.ts index 82a3a2fee6912..c593c9c157422 100644 --- a/src/plugins/discover/public/application/apps/main/utils/fetch_total_hits.test.ts +++ b/src/plugins/discover/public/application/main/utils/fetch_total_hits.test.ts @@ -5,12 +5,12 @@ * in compliance with, at your election, the Elastic License 2.0 or the Server * Side Public License, v 1. */ -import { FetchStatus } from '../../../types'; +import { FetchStatus } from '../../types'; import { BehaviorSubject, throwError as throwErrorRx } from 'rxjs'; -import { RequestAdapter } from '../../../../../../inspector'; -import { savedSearchMock } from '../../../../__mocks__/saved_search'; +import { RequestAdapter } from '../../../../../inspector'; +import { savedSearchMock } from '../../../__mocks__/saved_search'; import { fetchTotalHits } from './fetch_total_hits'; -import { discoverServiceMock } from '../../../../__mocks__/services'; +import { discoverServiceMock } from '../../../__mocks__/services'; function getDataSubjects() { return { diff --git a/src/plugins/discover/public/application/apps/main/utils/fetch_total_hits.ts b/src/plugins/discover/public/application/main/utils/fetch_total_hits.ts similarity index 75% rename from src/plugins/discover/public/application/apps/main/utils/fetch_total_hits.ts rename to src/plugins/discover/public/application/main/utils/fetch_total_hits.ts index cfab0d17fcd54..197e00ce0449f 100644 --- a/src/plugins/discover/public/application/apps/main/utils/fetch_total_hits.ts +++ b/src/plugins/discover/public/application/main/utils/fetch_total_hits.ts @@ -12,11 +12,12 @@ import { DataPublicPluginStart, isCompleteResponse, ISearchSource, -} from '../../../../../../data/public'; -import { Adapters } from '../../../../../../inspector/common'; -import { FetchStatus } from '../../../types'; -import { SavedSearchData } from '../services/use_saved_search'; -import { sendErrorMsg, sendLoadingMsg } from '../services/use_saved_search_messages'; +} from '../../../../../data/public'; +import { DataViewType } from '../../../../../data_views/common'; +import { Adapters } from '../../../../../inspector/common'; +import { FetchStatus } from '../../types'; +import { SavedSearchData } from './use_saved_search'; +import { sendErrorMsg, sendLoadingMsg } from './use_saved_search_messages'; export function fetchTotalHits( data$: SavedSearchData, @@ -36,13 +37,18 @@ export function fetchTotalHits( } ) { const { totalHits$ } = data$; - const indexPattern = searchSource.getField('index'); searchSource.setField('trackTotalHits', true); - searchSource.setField('filter', data.query.timefilter.timefilter.createFilter(indexPattern!)); searchSource.setField('size', 0); searchSource.removeField('sort'); searchSource.removeField('fields'); searchSource.removeField('aggs'); + if (searchSource.getField('index')?.type === DataViewType.ROLLUP) { + // We treat that index pattern as "normal" even if it was a rollup index pattern, + // since the rollup endpoint does not support querying individual documents, but we + // can get them from the regular _search API that will be used if the index pattern + // not a rollup index pattern. + searchSource.setOverwriteDataViewType(undefined); + } sendLoadingMsg(totalHits$); diff --git a/src/plugins/discover/public/application/apps/main/utils/get_chart_agg_config.test.ts b/src/plugins/discover/public/application/main/utils/get_chart_agg_config.test.ts similarity index 87% rename from src/plugins/discover/public/application/apps/main/utils/get_chart_agg_config.test.ts rename to src/plugins/discover/public/application/main/utils/get_chart_agg_config.test.ts index 515565f0062c9..9960b18181ceb 100644 --- a/src/plugins/discover/public/application/apps/main/utils/get_chart_agg_config.test.ts +++ b/src/plugins/discover/public/application/main/utils/get_chart_agg_config.test.ts @@ -5,9 +5,9 @@ * in compliance with, at your election, the Elastic License 2.0 or the Server * Side Public License, v 1. */ -import { indexPatternWithTimefieldMock } from '../../../../__mocks__/index_pattern_with_timefield'; -import { ISearchSource } from '../../../../../../data/public'; -import { dataPluginMock } from '../../../../../../data/public/mocks'; +import { indexPatternWithTimefieldMock } from '../../../__mocks__/index_pattern_with_timefield'; +import { ISearchSource } from '../../../../../data/public'; +import { dataPluginMock } from '../../../../../data/public/mocks'; import { getChartAggConfigs } from './get_chart_agg_configs'; describe('getChartAggConfigs', () => { diff --git a/src/plugins/discover/public/application/apps/main/utils/get_chart_agg_configs.ts b/src/plugins/discover/public/application/main/utils/get_chart_agg_configs.ts similarity index 88% rename from src/plugins/discover/public/application/apps/main/utils/get_chart_agg_configs.ts rename to src/plugins/discover/public/application/main/utils/get_chart_agg_configs.ts index 65f98f72beec0..e33fe48302a04 100644 --- a/src/plugins/discover/public/application/apps/main/utils/get_chart_agg_configs.ts +++ b/src/plugins/discover/public/application/main/utils/get_chart_agg_configs.ts @@ -5,8 +5,8 @@ * in compliance with, at your election, the Elastic License 2.0 or the Server * Side Public License, v 1. */ -import { ISearchSource } from '../../../../../../data/common'; -import { DataPublicPluginStart } from '../../../../../../data/public'; +import { ISearchSource } from '../../../../../data/common'; +import { DataPublicPluginStart } from '../../../../../data/public'; /** * Helper function to apply or remove aggregations to a given search source used for gaining data diff --git a/src/plugins/discover/public/application/apps/main/utils/get_dimensions.test.ts b/src/plugins/discover/public/application/main/utils/get_dimensions.test.ts similarity index 89% rename from src/plugins/discover/public/application/apps/main/utils/get_dimensions.test.ts rename to src/plugins/discover/public/application/main/utils/get_dimensions.test.ts index 35a6e955fe5b2..56822a614ece7 100644 --- a/src/plugins/discover/public/application/apps/main/utils/get_dimensions.test.ts +++ b/src/plugins/discover/public/application/main/utils/get_dimensions.test.ts @@ -5,11 +5,11 @@ * in compliance with, at your election, the Elastic License 2.0 or the Server * Side Public License, v 1. */ -import { dataPluginMock } from '../../../../../../data/public/mocks'; +import { dataPluginMock } from '../../../../../data/public/mocks'; import { getDimensions } from './get_dimensions'; -import { indexPatternWithTimefieldMock } from '../../../../__mocks__/index_pattern_with_timefield'; -import { ISearchSource, calculateBounds } from '../../../../../../data/common'; +import { indexPatternWithTimefieldMock } from '../../../__mocks__/index_pattern_with_timefield'; +import { ISearchSource, calculateBounds } from '../../../../../data/common'; import { getChartAggConfigs } from './get_chart_agg_configs'; test('getDimensions', () => { diff --git a/src/plugins/discover/public/application/apps/main/utils/get_dimensions.ts b/src/plugins/discover/public/application/main/utils/get_dimensions.ts similarity index 92% rename from src/plugins/discover/public/application/apps/main/utils/get_dimensions.ts rename to src/plugins/discover/public/application/main/utils/get_dimensions.ts index 40b378f1c1698..6fb9cd7865c4f 100644 --- a/src/plugins/discover/public/application/apps/main/utils/get_dimensions.ts +++ b/src/plugins/discover/public/application/main/utils/get_dimensions.ts @@ -7,8 +7,8 @@ */ import moment from 'moment'; import dateMath from '@elastic/datemath'; -import { IAggConfigs } from '../../../../../../data/common'; -import { DataPublicPluginStart, search } from '../../../../../../data/public'; +import { IAggConfigs } from '../../../../../data/common'; +import { DataPublicPluginStart, search } from '../../../../../data/public'; import { Dimensions, HistogramParamsBounds } from '../components/chart/point_series'; export function getDimensions( diff --git a/src/plugins/discover/public/application/apps/main/utils/get_fetch_observable.ts b/src/plugins/discover/public/application/main/utils/get_fetch_observable.ts similarity index 93% rename from src/plugins/discover/public/application/apps/main/utils/get_fetch_observable.ts rename to src/plugins/discover/public/application/main/utils/get_fetch_observable.ts index de79a9425f17c..8c0d614f7ac61 100644 --- a/src/plugins/discover/public/application/apps/main/utils/get_fetch_observable.ts +++ b/src/plugins/discover/public/application/main/utils/get_fetch_observable.ts @@ -8,13 +8,13 @@ import { merge } from 'rxjs'; import { debounceTime, filter, skip, tap } from 'rxjs/operators'; -import { FetchStatus } from '../../../types'; +import { FetchStatus } from '../../types'; import type { AutoRefreshDoneFn, DataPublicPluginStart, ISearchSource, -} from '../../../../../../data/public'; -import { DataMain$, DataRefetch$ } from '../services/use_saved_search'; +} from '../../../../../data/public'; +import { DataMain$, DataRefetch$ } from './use_saved_search'; import { DiscoverSearchSessionManager } from '../services/discover_search_session'; /** diff --git a/src/plugins/discover/public/application/apps/main/utils/get_fetch_observeable.test.ts b/src/plugins/discover/public/application/main/utils/get_fetch_observeable.test.ts similarity index 91% rename from src/plugins/discover/public/application/apps/main/utils/get_fetch_observeable.test.ts rename to src/plugins/discover/public/application/main/utils/get_fetch_observeable.test.ts index 39873ff609d64..58fcc20396dee 100644 --- a/src/plugins/discover/public/application/apps/main/utils/get_fetch_observeable.test.ts +++ b/src/plugins/discover/public/application/main/utils/get_fetch_observeable.test.ts @@ -6,12 +6,12 @@ * Side Public License, v 1. */ import { getFetch$ } from './get_fetch_observable'; -import { FetchStatus } from '../../../types'; +import { FetchStatus } from '../../types'; import { BehaviorSubject, Subject } from 'rxjs'; -import { DataPublicPluginStart } from '../../../../../../data/public'; -import { createSearchSessionMock } from '../../../../__mocks__/search_session'; -import { DataRefetch$ } from '../services/use_saved_search'; -import { savedSearchMock, savedSearchMockWithTimeField } from '../../../../__mocks__/saved_search'; +import { DataPublicPluginStart } from '../../../../../data/public'; +import { createSearchSessionMock } from '../../../__mocks__/search_session'; +import { DataRefetch$ } from './use_saved_search'; +import { savedSearchMock, savedSearchMockWithTimeField } from '../../../__mocks__/saved_search'; function createDataMock( queryString$: Subject, diff --git a/src/plugins/discover/public/application/apps/main/utils/get_result_state.test.ts b/src/plugins/discover/public/application/main/utils/get_result_state.test.ts similarity index 97% rename from src/plugins/discover/public/application/apps/main/utils/get_result_state.test.ts rename to src/plugins/discover/public/application/main/utils/get_result_state.test.ts index 7066d22d6aac7..8dd98650f30a5 100644 --- a/src/plugins/discover/public/application/apps/main/utils/get_result_state.test.ts +++ b/src/plugins/discover/public/application/main/utils/get_result_state.test.ts @@ -6,7 +6,7 @@ * Side Public License, v 1. */ import { getResultState, resultStatuses } from './get_result_state'; -import { FetchStatus } from '../../../types'; +import { FetchStatus } from '../../types'; describe('getResultState', () => { test('fetching uninitialized', () => { diff --git a/src/plugins/discover/public/application/apps/main/utils/get_result_state.ts b/src/plugins/discover/public/application/main/utils/get_result_state.ts similarity index 96% rename from src/plugins/discover/public/application/apps/main/utils/get_result_state.ts rename to src/plugins/discover/public/application/main/utils/get_result_state.ts index 424d2feabd830..ceb6de0cc7798 100644 --- a/src/plugins/discover/public/application/apps/main/utils/get_result_state.ts +++ b/src/plugins/discover/public/application/main/utils/get_result_state.ts @@ -5,7 +5,7 @@ * in compliance with, at your election, the Elastic License 2.0 or the Server * Side Public License, v 1. */ -import { FetchStatus } from '../../../types'; +import { FetchStatus } from '../../types'; export const resultStatuses = { UNINITIALIZED: 'uninitialized', diff --git a/src/plugins/discover/public/application/apps/main/utils/get_state_defaults.test.ts b/src/plugins/discover/public/application/main/utils/get_state_defaults.test.ts similarity index 84% rename from src/plugins/discover/public/application/apps/main/utils/get_state_defaults.test.ts rename to src/plugins/discover/public/application/main/utils/get_state_defaults.test.ts index 6cf34fd8cb024..0d38b1997716b 100644 --- a/src/plugins/discover/public/application/apps/main/utils/get_state_defaults.test.ts +++ b/src/plugins/discover/public/application/main/utils/get_state_defaults.test.ts @@ -7,12 +7,12 @@ */ import { getStateDefaults } from './get_state_defaults'; -import { createSearchSourceMock, dataPluginMock } from '../../../../../../data/public/mocks'; -import { uiSettingsMock } from '../../../../__mocks__/ui_settings'; -import { indexPatternWithTimefieldMock } from '../../../../__mocks__/index_pattern_with_timefield'; -import { savedSearchMock } from '../../../../__mocks__/saved_search'; -import { indexPatternMock } from '../../../../__mocks__/index_pattern'; -import { discoverServiceMock } from '../../../../__mocks__/services'; +import { createSearchSourceMock, dataPluginMock } from '../../../../../data/public/mocks'; +import { uiSettingsMock } from '../../../__mocks__/ui_settings'; +import { indexPatternWithTimefieldMock } from '../../../__mocks__/index_pattern_with_timefield'; +import { savedSearchMock } from '../../../__mocks__/saved_search'; +import { indexPatternMock } from '../../../__mocks__/index_pattern'; +import { discoverServiceMock } from '../../../__mocks__/services'; describe('getStateDefaults', () => { const storage = discoverServiceMock.storage; diff --git a/src/plugins/discover/public/application/apps/main/utils/get_state_defaults.ts b/src/plugins/discover/public/application/main/utils/get_state_defaults.ts similarity index 89% rename from src/plugins/discover/public/application/apps/main/utils/get_state_defaults.ts rename to src/plugins/discover/public/application/main/utils/get_state_defaults.ts index 50dab0273d461..4694bec4057b0 100644 --- a/src/plugins/discover/public/application/apps/main/utils/get_state_defaults.ts +++ b/src/plugins/discover/public/application/main/utils/get_state_defaults.ts @@ -12,14 +12,14 @@ import { DEFAULT_COLUMNS_SETTING, SEARCH_FIELDS_FROM_SOURCE, SORT_DEFAULT_ORDER_SETTING, -} from '../../../../../common'; -import { SavedSearch } from '../../../../saved_searches'; -import { DataPublicPluginStart } from '../../../../../../data/public'; +} from '../../../../common'; +import { SavedSearch } from '../../../services/saved_searches'; +import { DataPublicPluginStart } from '../../../../../data/public'; import { AppState } from '../services/discover_state'; -import { getDefaultSort, getSortArray } from '../components/doc_table'; +import { getDefaultSort, getSortArray } from '../../../components/doc_table'; import { CHART_HIDDEN_KEY } from '../components/chart/discover_chart'; -import { Storage } from '../../../../../../kibana_utils/public'; +import { Storage } from '../../../../../kibana_utils/public'; function getDefaultColumns(savedSearch: SavedSearch, config: IUiSettingsClient) { if (savedSearch.columns && savedSearch.columns.length > 0) { diff --git a/src/plugins/discover/public/application/apps/main/utils/get_switch_index_pattern_app_state.test.ts b/src/plugins/discover/public/application/main/utils/get_switch_index_pattern_app_state.test.ts similarity index 98% rename from src/plugins/discover/public/application/apps/main/utils/get_switch_index_pattern_app_state.test.ts rename to src/plugins/discover/public/application/main/utils/get_switch_index_pattern_app_state.test.ts index 83107d6c57ab8..412ad060b5565 100644 --- a/src/plugins/discover/public/application/apps/main/utils/get_switch_index_pattern_app_state.test.ts +++ b/src/plugins/discover/public/application/main/utils/get_switch_index_pattern_app_state.test.ts @@ -7,7 +7,7 @@ */ import { getSwitchIndexPatternAppState } from './get_switch_index_pattern_app_state'; -import { IndexPattern } from '../../../../../../data/common'; +import { IndexPattern } from '../../../../../data/common'; /** * Helper function returning an index pattern diff --git a/src/plugins/discover/public/application/apps/main/utils/get_switch_index_pattern_app_state.ts b/src/plugins/discover/public/application/main/utils/get_switch_index_pattern_app_state.ts similarity index 95% rename from src/plugins/discover/public/application/apps/main/utils/get_switch_index_pattern_app_state.ts rename to src/plugins/discover/public/application/main/utils/get_switch_index_pattern_app_state.ts index ff082587172a0..b6dfe7a63f3a8 100644 --- a/src/plugins/discover/public/application/apps/main/utils/get_switch_index_pattern_app_state.ts +++ b/src/plugins/discover/public/application/main/utils/get_switch_index_pattern_app_state.ts @@ -7,7 +7,7 @@ */ import type { IndexPattern } from 'src/plugins/data/common'; -import { getSortArray, SortPairArr } from '../components/doc_table/lib/get_sort'; +import { getSortArray, SortPairArr } from '../../../components/doc_table/lib/get_sort'; /** * Helper function to remove or adapt the currently selected columns/sort to be valid with the next diff --git a/src/plugins/discover/public/application/apps/main/utils/index.ts b/src/plugins/discover/public/application/main/utils/index.ts similarity index 100% rename from src/plugins/discover/public/application/apps/main/utils/index.ts rename to src/plugins/discover/public/application/main/utils/index.ts diff --git a/src/plugins/discover/public/application/apps/main/utils/nested_fields.ts b/src/plugins/discover/public/application/main/utils/nested_fields.ts similarity index 97% rename from src/plugins/discover/public/application/apps/main/utils/nested_fields.ts rename to src/plugins/discover/public/application/main/utils/nested_fields.ts index beeca801457a1..f06f3dd554b4c 100644 --- a/src/plugins/discover/public/application/apps/main/utils/nested_fields.ts +++ b/src/plugins/discover/public/application/main/utils/nested_fields.ts @@ -8,7 +8,7 @@ import { escapeRegExp } from 'lodash/fp'; import type { IndexPattern } from 'src/plugins/data/public'; -import { getFieldSubtypeNested } from '../../../../../../data/common'; +import { getFieldSubtypeNested } from '../../../../../data/common'; /** * This function checks if the given field in a given index pattern is a nested field's parent. diff --git a/src/plugins/discover/public/application/apps/main/utils/persist_saved_search.ts b/src/plugins/discover/public/application/main/utils/persist_saved_search.ts similarity index 82% rename from src/plugins/discover/public/application/apps/main/utils/persist_saved_search.ts rename to src/plugins/discover/public/application/main/utils/persist_saved_search.ts index fa566fd485942..dcc0f804c27e0 100644 --- a/src/plugins/discover/public/application/apps/main/utils/persist_saved_search.ts +++ b/src/plugins/discover/public/application/main/utils/persist_saved_search.ts @@ -7,13 +7,13 @@ */ import { updateSearchSource } from './update_search_source'; -import { IndexPattern } from '../../../../../../data/public'; -import { SavedSearch } from '../../../../saved_searches'; +import { IndexPattern } from '../../../../../data/public'; +import { SavedSearch } from '../../../services/saved_searches'; import { AppState } from '../services/discover_state'; -import type { SortOrder } from '../../../../saved_searches'; -import { SavedObjectSaveOpts } from '../../../../../../saved_objects/public'; -import { DiscoverServices } from '../../../../build_services'; -import { saveSavedSearch } from '../../../../saved_searches'; +import type { SortOrder } from '../../../services/saved_searches'; +import { SavedObjectSaveOpts } from '../../../../../saved_objects/public'; +import { DiscoverServices } from '../../../build_services'; +import { saveSavedSearch } from '../../../services/saved_searches'; /** * Helper function to update and persist the given savedSearch diff --git a/src/plugins/discover/public/application/apps/main/utils/resolve_index_pattern.test.ts b/src/plugins/discover/public/application/main/utils/resolve_index_pattern.test.ts similarity index 89% rename from src/plugins/discover/public/application/apps/main/utils/resolve_index_pattern.test.ts rename to src/plugins/discover/public/application/main/utils/resolve_index_pattern.test.ts index 56c4f8e6cd1b6..bcf764b57d8cc 100644 --- a/src/plugins/discover/public/application/apps/main/utils/resolve_index_pattern.test.ts +++ b/src/plugins/discover/public/application/main/utils/resolve_index_pattern.test.ts @@ -11,9 +11,9 @@ import { getFallbackIndexPatternId, IndexPatternSavedObject, } from './resolve_index_pattern'; -import { indexPatternsMock } from '../../../../__mocks__/index_patterns'; -import { indexPatternMock } from '../../../../__mocks__/index_pattern'; -import { configMock } from '../../../../__mocks__/config'; +import { indexPatternsMock } from '../../../__mocks__/index_patterns'; +import { indexPatternMock } from '../../../__mocks__/index_pattern'; +import { configMock } from '../../../__mocks__/config'; describe('Resolve index pattern tests', () => { test('returns valid data for an existing index pattern', async () => { diff --git a/src/plugins/discover/public/application/apps/main/utils/resolve_index_pattern.ts b/src/plugins/discover/public/application/main/utils/resolve_index_pattern.ts similarity index 100% rename from src/plugins/discover/public/application/apps/main/utils/resolve_index_pattern.ts rename to src/plugins/discover/public/application/main/utils/resolve_index_pattern.ts diff --git a/src/plugins/discover/public/application/main/utils/update_search_source.test.ts b/src/plugins/discover/public/application/main/utils/update_search_source.test.ts new file mode 100644 index 0000000000000..3d36cf1cce5da --- /dev/null +++ b/src/plugins/discover/public/application/main/utils/update_search_source.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. + */ + +import { updateSearchSource } from './update_search_source'; +import { createSearchSourceMock } from '../../../../../data/common/search/search_source/mocks'; +import { indexPatternMock } from '../../../__mocks__/index_pattern'; +import type { SortOrder } from '../../../services/saved_searches'; +import { discoverServiceMock } from '../../../__mocks__/services'; + +describe('updateSearchSource', () => { + test('updates a given search source', async () => { + const persistentSearchSourceMock = createSearchSourceMock({}); + const volatileSearchSourceMock = createSearchSourceMock({}); + volatileSearchSourceMock.setParent(persistentSearchSourceMock); + updateSearchSource(volatileSearchSourceMock, false, { + indexPattern: indexPatternMock, + services: discoverServiceMock, + sort: [] as SortOrder[], + useNewFieldsApi: false, + }); + expect(persistentSearchSourceMock.getField('index')).toEqual(indexPatternMock); + expect(volatileSearchSourceMock.getField('fields')).toBe(undefined); + }); + + test('updates a given search source with the usage of the new fields api', async () => { + const persistentSearchSourceMock = createSearchSourceMock({}); + const volatileSearchSourceMock = createSearchSourceMock({}); + volatileSearchSourceMock.setParent(persistentSearchSourceMock); + updateSearchSource(volatileSearchSourceMock, false, { + indexPattern: indexPatternMock, + services: discoverServiceMock, + sort: [] as SortOrder[], + useNewFieldsApi: true, + }); + expect(persistentSearchSourceMock.getField('index')).toEqual(indexPatternMock); + expect(volatileSearchSourceMock.getField('fields')).toEqual([ + { field: '*', include_unmapped: 'true' }, + ]); + expect(volatileSearchSourceMock.getField('fieldsFromSource')).toBe(undefined); + }); + + test('updates a given search source when showUnmappedFields option is set to true', async () => { + const persistentSearchSourceMock = createSearchSourceMock({}); + const volatileSearchSourceMock = createSearchSourceMock({}); + volatileSearchSourceMock.setParent(persistentSearchSourceMock); + updateSearchSource(volatileSearchSourceMock, false, { + indexPattern: indexPatternMock, + services: discoverServiceMock, + sort: [] as SortOrder[], + useNewFieldsApi: true, + }); + expect(persistentSearchSourceMock.getField('index')).toEqual(indexPatternMock); + expect(volatileSearchSourceMock.getField('fields')).toEqual([ + { field: '*', include_unmapped: 'true' }, + ]); + expect(volatileSearchSourceMock.getField('fieldsFromSource')).toBe(undefined); + }); + + test('does not explicitly request fieldsFromSource when not using fields API', async () => { + const persistentSearchSourceMock = createSearchSourceMock({}); + const volatileSearchSourceMock = createSearchSourceMock({}); + volatileSearchSourceMock.setParent(persistentSearchSourceMock); + updateSearchSource(volatileSearchSourceMock, false, { + indexPattern: indexPatternMock, + services: discoverServiceMock, + sort: [] as SortOrder[], + useNewFieldsApi: false, + }); + expect(persistentSearchSourceMock.getField('index')).toEqual(indexPatternMock); + expect(volatileSearchSourceMock.getField('fields')).toEqual(undefined); + expect(volatileSearchSourceMock.getField('fieldsFromSource')).toBe(undefined); + }); +}); diff --git a/src/plugins/discover/public/application/main/utils/update_search_source.ts b/src/plugins/discover/public/application/main/utils/update_search_source.ts new file mode 100644 index 0000000000000..1ee15790077cc --- /dev/null +++ b/src/plugins/discover/public/application/main/utils/update_search_source.ts @@ -0,0 +1,66 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { SORT_DEFAULT_ORDER_SETTING } from '../../../../common'; +import { IndexPattern, ISearchSource } from '../../../../../data/common'; +import { DataViewType } from '../../../../../data_views/common'; +import type { SortOrder } from '../../../services/saved_searches'; +import { DiscoverServices } from '../../../build_services'; +import { getSortForSearchSource } from '../../../components/doc_table'; + +/** + * Helper function to update the given searchSource before fetching/sharing/persisting + */ +export function updateSearchSource( + searchSource: ISearchSource, + persist = true, + { + indexPattern, + services, + sort, + useNewFieldsApi, + }: { + indexPattern: IndexPattern; + services: DiscoverServices; + sort: SortOrder[]; + useNewFieldsApi: boolean; + } +) { + const { uiSettings, data } = services; + const parentSearchSource = persist ? searchSource : searchSource.getParent()!; + + parentSearchSource + .setField('index', indexPattern) + .setField('query', data.query.queryString.getQuery() || null) + .setField('filter', data.query.filterManager.getFilters()); + + if (!persist) { + const usedSort = getSortForSearchSource( + sort, + indexPattern, + uiSettings.get(SORT_DEFAULT_ORDER_SETTING) + ); + searchSource.setField('trackTotalHits', true).setField('sort', usedSort); + + if (indexPattern.type !== DataViewType.ROLLUP) { + // Set the date range filter fields from timeFilter using the absolute format. Search sessions requires that it be converted from a relative range + searchSource.setField('filter', data.query.timefilter.timefilter.createFilter(indexPattern)); + } + + if (useNewFieldsApi) { + searchSource.removeField('fieldsFromSource'); + const fields: Record = { field: '*' }; + + fields.include_unmapped = 'true'; + + searchSource.setField('fields', [fields]); + } else { + searchSource.removeField('fields'); + } + } +} diff --git a/src/plugins/discover/public/application/apps/main/utils/use_behavior_subject.ts b/src/plugins/discover/public/application/main/utils/use_behavior_subject.ts similarity index 100% rename from src/plugins/discover/public/application/apps/main/utils/use_behavior_subject.ts rename to src/plugins/discover/public/application/main/utils/use_behavior_subject.ts diff --git a/src/plugins/discover/public/application/apps/main/utils/use_data_state.ts b/src/plugins/discover/public/application/main/utils/use_data_state.ts similarity index 94% rename from src/plugins/discover/public/application/apps/main/utils/use_data_state.ts rename to src/plugins/discover/public/application/main/utils/use_data_state.ts index 2fd571a0dfcb9..7bfa4205081e9 100644 --- a/src/plugins/discover/public/application/apps/main/utils/use_data_state.ts +++ b/src/plugins/discover/public/application/main/utils/use_data_state.ts @@ -7,7 +7,7 @@ */ import { useState, useEffect } from 'react'; import { BehaviorSubject } from 'rxjs'; -import { DataMsg } from '../services/use_saved_search'; +import { DataMsg } from './use_saved_search'; export function useDataState(data$: BehaviorSubject) { const [fetchState, setFetchState] = useState(data$.getValue()); diff --git a/src/plugins/discover/public/application/apps/main/services/use_discover_state.test.ts b/src/plugins/discover/public/application/main/utils/use_discover_state.test.ts similarity index 87% rename from src/plugins/discover/public/application/apps/main/services/use_discover_state.test.ts rename to src/plugins/discover/public/application/main/utils/use_discover_state.test.ts index c719f83980aa0..78f742b9f7c9b 100644 --- a/src/plugins/discover/public/application/apps/main/services/use_discover_state.test.ts +++ b/src/plugins/discover/public/application/main/utils/use_discover_state.test.ts @@ -7,12 +7,12 @@ */ import { renderHook, act } from '@testing-library/react-hooks'; -import { createSearchSessionMock } from '../../../../__mocks__/search_session'; -import { discoverServiceMock } from '../../../../__mocks__/services'; -import { savedSearchMock } from '../../../../__mocks__/saved_search'; +import { createSearchSessionMock } from '../../../__mocks__/search_session'; +import { discoverServiceMock } from '../../../__mocks__/services'; +import { savedSearchMock } from '../../../__mocks__/saved_search'; import { useDiscoverState } from './use_discover_state'; -import { indexPatternMock } from '../../../../__mocks__/index_pattern'; -import { SearchSource } from '../../../../../../data/common'; +import { indexPatternMock } from '../../../__mocks__/index_pattern'; +import { SearchSource } from '../../../../../data/common'; describe('test useDiscoverState', () => { const originalSavedObjectsClient = discoverServiceMock.core.savedObjects.client; diff --git a/src/plugins/discover/public/application/apps/main/services/use_discover_state.ts b/src/plugins/discover/public/application/main/utils/use_discover_state.ts similarity index 93% rename from src/plugins/discover/public/application/apps/main/services/use_discover_state.ts rename to src/plugins/discover/public/application/main/utils/use_discover_state.ts index a1d58fdd6090e..b70bcded4c608 100644 --- a/src/plugins/discover/public/application/apps/main/services/use_discover_state.ts +++ b/src/plugins/discover/public/application/main/utils/use_discover_state.ts @@ -8,22 +8,22 @@ import { useMemo, useEffect, useState, useCallback } from 'react'; import { isEqual } from 'lodash'; import { History } from 'history'; -import { getState } from './discover_state'; -import { getStateDefaults } from '../utils/get_state_defaults'; -import { DiscoverServices } from '../../../../build_services'; -import { SavedSearch, getSavedSearch } from '../../../../saved_searches'; -import { loadIndexPattern } from '../utils/resolve_index_pattern'; +import { getState } from '../services/discover_state'; +import { getStateDefaults } from './get_state_defaults'; +import { DiscoverServices } from '../../../build_services'; +import { SavedSearch, getSavedSearch } from '../../../services/saved_searches'; +import { loadIndexPattern } from './resolve_index_pattern'; import { useSavedSearch as useSavedSearchData } from './use_saved_search'; import { MODIFY_COLUMNS_ON_SWITCH, SEARCH_FIELDS_FROM_SOURCE, SEARCH_ON_PAGE_LOAD_SETTING, SORT_DEFAULT_ORDER_SETTING, -} from '../../../../../common'; +} from '../../../../common'; import { useSearchSession } from './use_search_session'; -import { FetchStatus } from '../../../types'; -import { getSwitchIndexPatternAppState } from '../utils/get_switch_index_pattern_app_state'; -import { SortPairArr } from '../components/doc_table/lib/get_sort'; +import { FetchStatus } from '../../types'; +import { getSwitchIndexPatternAppState } from './get_switch_index_pattern_app_state'; +import { SortPairArr } from '../../../components/doc_table/lib/get_sort'; export function useDiscoverState({ services, diff --git a/src/plugins/discover/public/application/apps/main/services/use_saved_search.test.ts b/src/plugins/discover/public/application/main/utils/use_saved_search.test.ts similarity index 91% rename from src/plugins/discover/public/application/apps/main/services/use_saved_search.test.ts rename to src/plugins/discover/public/application/main/utils/use_saved_search.test.ts index 7f252151920fb..b3ed7ab854190 100644 --- a/src/plugins/discover/public/application/apps/main/services/use_saved_search.test.ts +++ b/src/plugins/discover/public/application/main/utils/use_saved_search.test.ts @@ -7,14 +7,14 @@ */ import { Subject } from 'rxjs'; import { renderHook } from '@testing-library/react-hooks'; -import { createSearchSessionMock } from '../../../../__mocks__/search_session'; -import { discoverServiceMock } from '../../../../__mocks__/services'; -import { savedSearchMock } from '../../../../__mocks__/saved_search'; +import { createSearchSessionMock } from '../../../__mocks__/search_session'; +import { discoverServiceMock } from '../../../__mocks__/services'; +import { savedSearchMock } from '../../../__mocks__/saved_search'; import { useSavedSearch } from './use_saved_search'; -import { getState } from './discover_state'; -import { uiSettingsMock } from '../../../../__mocks__/ui_settings'; +import { getState } from '../services/discover_state'; +import { uiSettingsMock } from '../../../__mocks__/ui_settings'; import { useDiscoverState } from './use_discover_state'; -import { FetchStatus } from '../../../types'; +import { FetchStatus } from '../../types'; describe('test useSavedSearch', () => { test('useSavedSearch return is valid', async () => { diff --git a/src/plugins/discover/public/application/apps/main/services/use_saved_search.ts b/src/plugins/discover/public/application/main/utils/use_saved_search.ts similarity index 88% rename from src/plugins/discover/public/application/apps/main/services/use_saved_search.ts rename to src/plugins/discover/public/application/main/utils/use_saved_search.ts index 6cadfbb89acfb..bfd6f1daa4bc0 100644 --- a/src/plugins/discover/public/application/apps/main/services/use_saved_search.ts +++ b/src/plugins/discover/public/application/main/utils/use_saved_search.ts @@ -7,22 +7,22 @@ */ import { useCallback, useEffect, useMemo, useRef } from 'react'; import { BehaviorSubject, Subject } from 'rxjs'; -import { DiscoverServices } from '../../../../build_services'; -import { DiscoverSearchSessionManager } from './discover_search_session'; -import { ISearchSource } from '../../../../../../data/common'; -import { GetStateReturn } from './discover_state'; -import { ElasticSearchHit } from '../../../doc_views/doc_views_types'; -import { RequestAdapter } from '../../../../../../inspector/public'; -import type { AutoRefreshDoneFn } from '../../../../../../data/public'; -import { validateTimeRange } from '../utils/validate_time_range'; +import { DiscoverServices } from '../../../build_services'; +import { DiscoverSearchSessionManager } from '../services/discover_search_session'; +import { ISearchSource } from '../../../../../data/common'; +import { GetStateReturn } from '../services/discover_state'; +import { ElasticSearchHit } from '../../../services/doc_views/doc_views_types'; +import { RequestAdapter } from '../../../../../inspector/public'; +import type { AutoRefreshDoneFn } from '../../../../../data/public'; +import { validateTimeRange } from './validate_time_range'; import { Chart } from '../components/chart/point_series'; -import { useSingleton } from '../utils/use_singleton'; -import { FetchStatus } from '../../../types'; +import { useSingleton } from './use_singleton'; +import { FetchStatus } from '../../types'; -import { fetchAll } from '../utils/fetch_all'; -import { useBehaviorSubject } from '../utils/use_behavior_subject'; +import { fetchAll } from './fetch_all'; +import { useBehaviorSubject } from './use_behavior_subject'; import { sendResetMsg } from './use_saved_search_messages'; -import { getFetch$ } from '../utils/get_fetch_observable'; +import { getFetch$ } from './get_fetch_observable'; export interface SavedSearchData { main$: DataMain$; diff --git a/src/plugins/discover/public/application/apps/main/services/use_saved_search_messages.test.ts b/src/plugins/discover/public/application/main/utils/use_saved_search_messages.test.ts similarity index 98% rename from src/plugins/discover/public/application/apps/main/services/use_saved_search_messages.test.ts rename to src/plugins/discover/public/application/main/utils/use_saved_search_messages.test.ts index 9810436aebd90..2fa264690329e 100644 --- a/src/plugins/discover/public/application/apps/main/services/use_saved_search_messages.test.ts +++ b/src/plugins/discover/public/application/main/utils/use_saved_search_messages.test.ts @@ -11,7 +11,7 @@ import { sendLoadingMsg, sendPartialMsg, } from './use_saved_search_messages'; -import { FetchStatus } from '../../../types'; +import { FetchStatus } from '../../types'; import { BehaviorSubject } from 'rxjs'; import { DataMainMsg } from './use_saved_search'; diff --git a/src/plugins/discover/public/application/apps/main/services/use_saved_search_messages.ts b/src/plugins/discover/public/application/main/utils/use_saved_search_messages.ts similarity index 98% rename from src/plugins/discover/public/application/apps/main/services/use_saved_search_messages.ts rename to src/plugins/discover/public/application/main/utils/use_saved_search_messages.ts index ff72a69e65fa8..325d63eb6d21a 100644 --- a/src/plugins/discover/public/application/apps/main/services/use_saved_search_messages.ts +++ b/src/plugins/discover/public/application/main/utils/use_saved_search_messages.ts @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import { FetchStatus } from '../../../types'; +import { FetchStatus } from '../../types'; import { DataCharts$, DataDocuments$, diff --git a/src/plugins/discover/public/application/apps/main/services/use_search_session.test.ts b/src/plugins/discover/public/application/main/utils/use_search_session.test.ts similarity index 77% rename from src/plugins/discover/public/application/apps/main/services/use_search_session.test.ts rename to src/plugins/discover/public/application/main/utils/use_search_session.test.ts index 8aa6e0764d8ec..bc9af1001aa77 100644 --- a/src/plugins/discover/public/application/apps/main/services/use_search_session.test.ts +++ b/src/plugins/discover/public/application/main/utils/use_search_session.test.ts @@ -8,11 +8,11 @@ import { useSearchSession } from './use_search_session'; import { renderHook } from '@testing-library/react-hooks'; -import { createSearchSessionMock } from '../../../../__mocks__/search_session'; -import { discoverServiceMock } from '../../../../__mocks__/services'; -import { savedSearchMock } from '../../../../__mocks__/saved_search'; -import { getState } from './discover_state'; -import { uiSettingsMock } from '../../../../__mocks__/ui_settings'; +import { createSearchSessionMock } from '../../../__mocks__/search_session'; +import { discoverServiceMock } from '../../../__mocks__/services'; +import { savedSearchMock } from '../../../__mocks__/saved_search'; +import { getState } from '../services/discover_state'; +import { uiSettingsMock } from '../../../__mocks__/ui_settings'; describe('test useSearchSession', () => { test('getting the next session id', async () => { diff --git a/src/plugins/discover/public/application/apps/main/services/use_search_session.ts b/src/plugins/discover/public/application/main/utils/use_search_session.ts similarity index 82% rename from src/plugins/discover/public/application/apps/main/services/use_search_session.ts rename to src/plugins/discover/public/application/main/utils/use_search_session.ts index e37e8f6f33839..c18261f1ffaac 100644 --- a/src/plugins/discover/public/application/apps/main/services/use_search_session.ts +++ b/src/plugins/discover/public/application/main/utils/use_search_session.ts @@ -7,11 +7,14 @@ */ import { useMemo, useEffect } from 'react'; import { History } from 'history'; -import { DiscoverSearchSessionManager } from './discover_search_session'; -import { createSearchSessionRestorationDataProvider, GetStateReturn } from './discover_state'; -import { noSearchSessionStorageCapabilityMessage } from '../../../../../../data/public'; -import { DiscoverServices } from '../../../../build_services'; -import { SavedSearch } from '../../../../saved_searches'; +import { DiscoverSearchSessionManager } from '../services/discover_search_session'; +import { + createSearchSessionRestorationDataProvider, + GetStateReturn, +} from '../services/discover_state'; +import { noSearchSessionStorageCapabilityMessage } from '../../../../../data/public'; +import { DiscoverServices } from '../../../build_services'; +import { SavedSearch } from '../../../services/saved_searches'; export function useSearchSession({ services, diff --git a/src/plugins/discover/public/application/apps/main/utils/use_singleton.ts b/src/plugins/discover/public/application/main/utils/use_singleton.ts similarity index 100% rename from src/plugins/discover/public/application/apps/main/utils/use_singleton.ts rename to src/plugins/discover/public/application/main/utils/use_singleton.ts diff --git a/src/plugins/discover/public/application/apps/main/services/use_url.test.ts b/src/plugins/discover/public/application/main/utils/use_url.test.ts similarity index 92% rename from src/plugins/discover/public/application/apps/main/services/use_url.test.ts rename to src/plugins/discover/public/application/main/utils/use_url.test.ts index 740ca2bd140d7..9d8191f3581e4 100644 --- a/src/plugins/discover/public/application/apps/main/services/use_url.test.ts +++ b/src/plugins/discover/public/application/main/utils/use_url.test.ts @@ -6,7 +6,7 @@ * Side Public License, v 1. */ import { renderHook } from '@testing-library/react-hooks'; -import { createSearchSessionMock } from '../../../../__mocks__/search_session'; +import { createSearchSessionMock } from '../../../__mocks__/search_session'; import { useUrl } from './use_url'; describe('test useUrl', () => { diff --git a/src/plugins/discover/public/application/apps/main/services/use_url.ts b/src/plugins/discover/public/application/main/utils/use_url.ts similarity index 100% rename from src/plugins/discover/public/application/apps/main/services/use_url.ts rename to src/plugins/discover/public/application/main/utils/use_url.ts diff --git a/src/plugins/discover/public/application/apps/main/utils/validate_time_range.test.ts b/src/plugins/discover/public/application/main/utils/validate_time_range.test.ts similarity index 94% rename from src/plugins/discover/public/application/apps/main/utils/validate_time_range.test.ts rename to src/plugins/discover/public/application/main/utils/validate_time_range.test.ts index 8d9d9adc4e8dc..ff3a66b1edfa3 100644 --- a/src/plugins/discover/public/application/apps/main/utils/validate_time_range.test.ts +++ b/src/plugins/discover/public/application/main/utils/validate_time_range.test.ts @@ -7,7 +7,7 @@ */ import { validateTimeRange } from './validate_time_range'; -import { notificationServiceMock } from '../../../../../../../core/public/mocks'; +import { notificationServiceMock } from '../../../../../../core/public/mocks'; describe('Discover validateTimeRange', () => { test('validates given time ranges correctly', async () => { diff --git a/src/plugins/discover/public/application/apps/main/utils/validate_time_range.ts b/src/plugins/discover/public/application/main/utils/validate_time_range.ts similarity index 100% rename from src/plugins/discover/public/application/apps/main/utils/validate_time_range.ts rename to src/plugins/discover/public/application/main/utils/validate_time_range.ts diff --git a/src/plugins/discover/public/application/apps/not_found/index.ts b/src/plugins/discover/public/application/not_found/index.ts similarity index 100% rename from src/plugins/discover/public/application/apps/not_found/index.ts rename to src/plugins/discover/public/application/not_found/index.ts diff --git a/src/plugins/discover/public/application/apps/not_found/not_found_route.tsx b/src/plugins/discover/public/application/not_found/not_found_route.tsx similarity index 92% rename from src/plugins/discover/public/application/apps/not_found/not_found_route.tsx rename to src/plugins/discover/public/application/not_found/not_found_route.tsx index 6b6ef584d07f1..4848248a7509d 100644 --- a/src/plugins/discover/public/application/apps/not_found/not_found_route.tsx +++ b/src/plugins/discover/public/application/not_found/not_found_route.tsx @@ -10,9 +10,9 @@ import { i18n } from '@kbn/i18n'; import { EuiCallOut } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; import { Redirect } from 'react-router-dom'; -import { toMountPoint } from '../../../../../kibana_react/public'; -import { DiscoverServices } from '../../../build_services'; -import { getUrlTracker } from '../../../kibana_services'; +import { toMountPoint } from '../../../../kibana_react/public'; +import { DiscoverServices } from '../../build_services'; +import { getUrlTracker } from '../../kibana_services'; export interface NotFoundRouteProps { /** diff --git a/src/plugins/discover/public/build_services.ts b/src/plugins/discover/public/build_services.ts index a6b175e34bd13..6003411e647c5 100644 --- a/src/plugins/discover/public/build_services.ts +++ b/src/plugins/discover/public/build_services.ts @@ -32,7 +32,6 @@ import { Storage } from '../../kibana_utils/public'; import { DiscoverStartPlugins } from './plugin'; import { getHistory } from './kibana_services'; -import { KibanaLegacyStart } from '../../kibana_legacy/public'; import { UrlForwardingStart } from '../../url_forwarding/public'; import { NavigationPublicPluginStart } from '../../navigation/public'; import { IndexPatternFieldEditorStart } from '../../index_pattern_field_editor/public'; @@ -58,7 +57,6 @@ export interface DiscoverServices { metadata: { branch: string }; navigation: NavigationPublicPluginStart; share?: SharePluginStart; - kibanaLegacy: KibanaLegacyStart; urlForwarding: UrlForwardingStart; timefilter: TimefilterContract; toastNotifications: ToastsStart; @@ -97,7 +95,6 @@ export function buildServices( }, navigation: plugins.navigation, share: plugins.share, - kibanaLegacy: plugins.kibanaLegacy, urlForwarding: plugins.urlForwarding, timefilter: plugins.data.query.timefilter.timefilter, toastNotifications: core.notifications.toasts, diff --git a/src/plugins/discover/public/application/components/common/__snapshots__/loading_indicator.test.tsx.snap b/src/plugins/discover/public/components/common/__snapshots__/loading_indicator.test.tsx.snap similarity index 100% rename from src/plugins/discover/public/application/components/common/__snapshots__/loading_indicator.test.tsx.snap rename to src/plugins/discover/public/components/common/__snapshots__/loading_indicator.test.tsx.snap diff --git a/src/plugins/discover/public/shared/components.tsx b/src/plugins/discover/public/components/common/deferred_spinner.tsx similarity index 100% rename from src/plugins/discover/public/shared/components.tsx rename to src/plugins/discover/public/components/common/deferred_spinner.tsx diff --git a/src/plugins/discover/public/application/components/common/loading_indicator.test.tsx b/src/plugins/discover/public/components/common/loading_indicator.test.tsx similarity index 100% rename from src/plugins/discover/public/application/components/common/loading_indicator.test.tsx rename to src/plugins/discover/public/components/common/loading_indicator.test.tsx diff --git a/src/plugins/discover/public/application/components/common/loading_indicator.tsx b/src/plugins/discover/public/components/common/loading_indicator.tsx similarity index 100% rename from src/plugins/discover/public/application/components/common/loading_indicator.tsx rename to src/plugins/discover/public/components/common/loading_indicator.tsx diff --git a/src/plugins/discover/public/application/components/discover_grid/constants.ts b/src/plugins/discover/public/components/discover_grid/constants.ts similarity index 100% rename from src/plugins/discover/public/application/components/discover_grid/constants.ts rename to src/plugins/discover/public/components/discover_grid/constants.ts diff --git a/src/plugins/discover/public/application/components/discover_grid/discover_grid.scss b/src/plugins/discover/public/components/discover_grid/discover_grid.scss similarity index 100% rename from src/plugins/discover/public/application/components/discover_grid/discover_grid.scss rename to src/plugins/discover/public/components/discover_grid/discover_grid.scss diff --git a/src/plugins/discover/public/application/components/discover_grid/discover_grid.test.tsx b/src/plugins/discover/public/components/discover_grid/discover_grid.test.tsx similarity index 92% rename from src/plugins/discover/public/application/components/discover_grid/discover_grid.test.tsx rename to src/plugins/discover/public/components/discover_grid/discover_grid.test.tsx index 22284480afc05..4a0e472f17455 100644 --- a/src/plugins/discover/public/application/components/discover_grid/discover_grid.test.tsx +++ b/src/plugins/discover/public/components/discover_grid/discover_grid.test.tsx @@ -10,18 +10,18 @@ import { ReactWrapper } from 'enzyme'; import { EuiCopy } from '@elastic/eui'; import { act } from 'react-dom/test-utils'; import { findTestSubject } from '@elastic/eui/lib/test'; -import { esHits } from '../../../__mocks__/es_hits'; -import { indexPatternMock } from '../../../__mocks__/index_pattern'; +import { esHits } from '../../__mocks__/es_hits'; +import { indexPatternMock } from '../../__mocks__/index_pattern'; import { mountWithIntl } from '@kbn/test/jest'; import { DiscoverGrid, DiscoverGridProps } from './discover_grid'; -import { uiSettingsMock } from '../../../__mocks__/ui_settings'; -import { DiscoverServices } from '../../../build_services'; -import { ElasticSearchHit } from '../../doc_views/doc_views_types'; +import { uiSettingsMock } from '../../__mocks__/ui_settings'; +import { DiscoverServices } from '../../build_services'; +import { ElasticSearchHit } from '../../services/doc_views/doc_views_types'; import { getDocId } from './discover_grid_document_selection'; -jest.mock('../../../kibana_services', () => ({ - ...jest.requireActual('../../../kibana_services'), - getServices: () => jest.requireActual('../../../__mocks__/services').discoverServiceMock, +jest.mock('../../kibana_services', () => ({ + ...jest.requireActual('../../kibana_services'), + getServices: () => jest.requireActual('../../__mocks__/services').discoverServiceMock, })); function getProps() { diff --git a/src/plugins/discover/public/application/components/discover_grid/discover_grid.tsx b/src/plugins/discover/public/components/discover_grid/discover_grid.tsx similarity index 96% rename from src/plugins/discover/public/application/components/discover_grid/discover_grid.tsx rename to src/plugins/discover/public/components/discover_grid/discover_grid.tsx index 6f96f21c9b8c8..9bcc0f90f9259 100644 --- a/src/plugins/discover/public/application/components/discover_grid/discover_grid.tsx +++ b/src/plugins/discover/public/components/discover_grid/discover_grid.tsx @@ -21,8 +21,8 @@ import { EuiLoadingSpinner, EuiIcon, } from '@elastic/eui'; -import { flattenHit, IndexPattern } from '../../../../../data/common'; -import { DocViewFilterFn, ElasticSearchHit } from '../../doc_views/doc_views_types'; +import { flattenHit, IndexPattern } from '../../../../data/common'; +import { DocViewFilterFn, ElasticSearchHit } from '../../services/doc_views/doc_views_types'; import { getSchemaDetectors } from './discover_grid_schema'; import { DiscoverGridFlyout } from './discover_grid_flyout'; import { DiscoverGridContext } from './discover_grid_context'; @@ -34,16 +34,16 @@ import { getVisibleColumns, } from './discover_grid_columns'; import { defaultPageSize, gridStyle, pageSizeArr, toolbarVisibility } from './constants'; -import { DiscoverServices } from '../../../build_services'; -import { getDisplayedColumns } from '../../helpers/columns'; +import { DiscoverServices } from '../../build_services'; +import { getDisplayedColumns } from '../../utils/columns'; import { DOC_HIDE_TIME_COLUMN_SETTING, MAX_DOC_FIELDS_DISPLAYED, SHOW_MULTIFIELDS, -} from '../../../../common'; +} from '../../../common'; import { DiscoverGridDocumentToolbarBtn, getDocId } from './discover_grid_document_selection'; -import { SortPairArr } from '../../apps/main/components/doc_table/lib/get_sort'; -import { getFieldsToShow } from '../../helpers/get_fields_to_show'; +import { SortPairArr } from '../doc_table/lib/get_sort'; +import { getFieldsToShow } from '../../utils/get_fields_to_show'; interface SortObj { id: string; diff --git a/src/plugins/discover/public/application/components/discover_grid/discover_grid_cell_actions.test.tsx b/src/plugins/discover/public/components/discover_grid/discover_grid_cell_actions.test.tsx similarity index 95% rename from src/plugins/discover/public/application/components/discover_grid/discover_grid_cell_actions.test.tsx rename to src/plugins/discover/public/components/discover_grid/discover_grid_cell_actions.test.tsx index de3c55ad7a869..736175c04d3c2 100644 --- a/src/plugins/discover/public/application/components/discover_grid/discover_grid_cell_actions.test.tsx +++ b/src/plugins/discover/public/components/discover_grid/discover_grid_cell_actions.test.tsx @@ -12,8 +12,8 @@ import { findTestSubject } from '@elastic/eui/lib/test'; import { FilterInBtn, FilterOutBtn, buildCellActions } from './discover_grid_cell_actions'; import { DiscoverGridContext } from './discover_grid_context'; -import { indexPatternMock } from '../../../__mocks__/index_pattern'; -import { esHits } from '../../../__mocks__/es_hits'; +import { indexPatternMock } from '../../__mocks__/index_pattern'; +import { esHits } from '../../__mocks__/es_hits'; import { EuiButton } from '@elastic/eui'; import { IndexPatternField } from 'src/plugins/data/common'; diff --git a/src/plugins/discover/public/application/components/discover_grid/discover_grid_cell_actions.tsx b/src/plugins/discover/public/components/discover_grid/discover_grid_cell_actions.tsx similarity index 96% rename from src/plugins/discover/public/application/components/discover_grid/discover_grid_cell_actions.tsx rename to src/plugins/discover/public/components/discover_grid/discover_grid_cell_actions.tsx index a31b551821ddb..41852afe7b32c 100644 --- a/src/plugins/discover/public/application/components/discover_grid/discover_grid_cell_actions.tsx +++ b/src/plugins/discover/public/components/discover_grid/discover_grid_cell_actions.tsx @@ -9,7 +9,7 @@ import React, { useContext } from 'react'; import { EuiDataGridColumnCellActionProps } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { flattenHit, IndexPatternField } from '../../../../../data/common'; +import { flattenHit, IndexPatternField } from '../../../../data/common'; import { DiscoverGridContext } from './discover_grid_context'; export const FilterInBtn = ({ diff --git a/src/plugins/discover/public/application/components/discover_grid/discover_grid_columns.test.tsx b/src/plugins/discover/public/components/discover_grid/discover_grid_columns.test.tsx similarity index 96% rename from src/plugins/discover/public/application/components/discover_grid/discover_grid_columns.test.tsx rename to src/plugins/discover/public/components/discover_grid/discover_grid_columns.test.tsx index e5ea657032403..ea9cd5034551e 100644 --- a/src/plugins/discover/public/application/components/discover_grid/discover_grid_columns.test.tsx +++ b/src/plugins/discover/public/components/discover_grid/discover_grid_columns.test.tsx @@ -6,9 +6,9 @@ * Side Public License, v 1. */ -import { indexPatternMock } from '../../../__mocks__/index_pattern'; +import { indexPatternMock } from '../../__mocks__/index_pattern'; import { getEuiGridColumns } from './discover_grid_columns'; -import { indexPatternWithTimefieldMock } from '../../../__mocks__/index_pattern_with_timefield'; +import { indexPatternWithTimefieldMock } from '../../__mocks__/index_pattern_with_timefield'; describe('Discover grid columns ', function () { it('returns eui grid columns without time column', async () => { diff --git a/src/plugins/discover/public/application/components/discover_grid/discover_grid_columns.tsx b/src/plugins/discover/public/components/discover_grid/discover_grid_columns.tsx similarity index 98% rename from src/plugins/discover/public/application/components/discover_grid/discover_grid_columns.tsx rename to src/plugins/discover/public/components/discover_grid/discover_grid_columns.tsx index 5eb55a8e99cde..872fa3133a024 100644 --- a/src/plugins/discover/public/application/components/discover_grid/discover_grid_columns.tsx +++ b/src/plugins/discover/public/components/discover_grid/discover_grid_columns.tsx @@ -11,7 +11,7 @@ import { i18n } from '@kbn/i18n'; import { EuiDataGridColumn, EuiIconTip, EuiScreenReaderOnly } from '@elastic/eui'; import { ExpandButton } from './discover_grid_expand_button'; import { DiscoverGridSettings } from './types'; -import type { IndexPattern } from '../../../../../data/common'; +import type { IndexPattern } from '../../../../data/common'; import { buildCellActions } from './discover_grid_cell_actions'; import { getSchemaByKbnType } from './discover_grid_schema'; import { SelectButton } from './discover_grid_document_selection'; diff --git a/src/plugins/discover/public/application/components/discover_grid/discover_grid_context.tsx b/src/plugins/discover/public/components/discover_grid/discover_grid_context.tsx similarity index 90% rename from src/plugins/discover/public/application/components/discover_grid/discover_grid_context.tsx rename to src/plugins/discover/public/components/discover_grid/discover_grid_context.tsx index 8d0fbec9d7933..49b72ef126a76 100644 --- a/src/plugins/discover/public/application/components/discover_grid/discover_grid_context.tsx +++ b/src/plugins/discover/public/components/discover_grid/discover_grid_context.tsx @@ -8,7 +8,7 @@ import React from 'react'; import type { IndexPattern } from 'src/plugins/data/common'; -import { DocViewFilterFn, ElasticSearchHit } from '../../doc_views/doc_views_types'; +import { DocViewFilterFn, ElasticSearchHit } from '../../services/doc_views/doc_views_types'; export interface GridContext { expanded: ElasticSearchHit | undefined; diff --git a/src/plugins/discover/public/application/components/discover_grid/discover_grid_document_selection.test.tsx b/src/plugins/discover/public/components/discover_grid/discover_grid_document_selection.test.tsx similarity index 97% rename from src/plugins/discover/public/application/components/discover_grid/discover_grid_document_selection.test.tsx rename to src/plugins/discover/public/components/discover_grid/discover_grid_document_selection.test.tsx index e9b93e21553a2..d57fba241a1e7 100644 --- a/src/plugins/discover/public/application/components/discover_grid/discover_grid_document_selection.test.tsx +++ b/src/plugins/discover/public/components/discover_grid/discover_grid_document_selection.test.tsx @@ -13,8 +13,8 @@ import { getDocId, SelectButton, } from './discover_grid_document_selection'; -import { esHits } from '../../../__mocks__/es_hits'; -import { indexPatternMock } from '../../../__mocks__/index_pattern'; +import { esHits } from '../../__mocks__/es_hits'; +import { indexPatternMock } from '../../__mocks__/index_pattern'; import { DiscoverGridContext } from './discover_grid_context'; const baseContextMock = { diff --git a/src/plugins/discover/public/application/components/discover_grid/discover_grid_document_selection.tsx b/src/plugins/discover/public/components/discover_grid/discover_grid_document_selection.tsx similarity index 96% rename from src/plugins/discover/public/application/components/discover_grid/discover_grid_document_selection.tsx rename to src/plugins/discover/public/components/discover_grid/discover_grid_document_selection.tsx index c87d425d601c5..b1fc8993375da 100644 --- a/src/plugins/discover/public/application/components/discover_grid/discover_grid_document_selection.tsx +++ b/src/plugins/discover/public/components/discover_grid/discover_grid_document_selection.tsx @@ -17,9 +17,11 @@ import { EuiDataGridCellValueElementProps, } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; -import themeDark from '@elastic/eui/dist/eui_theme_dark.json'; -import themeLight from '@elastic/eui/dist/eui_theme_light.json'; -import { ElasticSearchHit } from '../../doc_views/doc_views_types'; +import { + euiLightVars as themeLight, + euiDarkVars as themeDark, +} from '@kbn/ui-shared-deps-src/theme'; +import { ElasticSearchHit } from '../../services/doc_views/doc_views_types'; import { DiscoverGridContext } from './discover_grid_context'; /** diff --git a/src/plugins/discover/public/application/components/discover_grid/discover_grid_expand_button.test.tsx b/src/plugins/discover/public/components/discover_grid/discover_grid_expand_button.test.tsx similarity index 96% rename from src/plugins/discover/public/application/components/discover_grid/discover_grid_expand_button.test.tsx rename to src/plugins/discover/public/components/discover_grid/discover_grid_expand_button.test.tsx index 3f7cb70091cfa..de2117afe7bdb 100644 --- a/src/plugins/discover/public/application/components/discover_grid/discover_grid_expand_button.test.tsx +++ b/src/plugins/discover/public/components/discover_grid/discover_grid_expand_button.test.tsx @@ -11,8 +11,8 @@ import { mountWithIntl } from '@kbn/test/jest'; import { findTestSubject } from '@elastic/eui/lib/test'; import { ExpandButton } from './discover_grid_expand_button'; import { DiscoverGridContext } from './discover_grid_context'; -import { indexPatternMock } from '../../../__mocks__/index_pattern'; -import { esHits } from '../../../__mocks__/es_hits'; +import { indexPatternMock } from '../../__mocks__/index_pattern'; +import { esHits } from '../../__mocks__/es_hits'; const baseContextMock = { expanded: undefined, diff --git a/src/plugins/discover/public/application/components/discover_grid/discover_grid_expand_button.tsx b/src/plugins/discover/public/components/discover_grid/discover_grid_expand_button.tsx similarity index 92% rename from src/plugins/discover/public/application/components/discover_grid/discover_grid_expand_button.tsx rename to src/plugins/discover/public/components/discover_grid/discover_grid_expand_button.tsx index f259d5c5c3658..3453a535f98dd 100644 --- a/src/plugins/discover/public/application/components/discover_grid/discover_grid_expand_button.tsx +++ b/src/plugins/discover/public/components/discover_grid/discover_grid_expand_button.tsx @@ -8,11 +8,13 @@ import React, { useContext, useEffect } from 'react'; import { EuiButtonIcon, EuiDataGridCellValueElementProps, EuiToolTip } from '@elastic/eui'; -import themeDark from '@elastic/eui/dist/eui_theme_dark.json'; -import themeLight from '@elastic/eui/dist/eui_theme_light.json'; +import { + euiLightVars as themeLight, + euiDarkVars as themeDark, +} from '@kbn/ui-shared-deps-src/theme'; import { i18n } from '@kbn/i18n'; import { DiscoverGridContext } from './discover_grid_context'; -import { EsHitRecord } from '../../types'; +import { EsHitRecord } from '../../application/types'; /** * Button to expand a given row */ diff --git a/src/plugins/discover/public/application/components/discover_grid/discover_grid_flyout.test.tsx b/src/plugins/discover/public/components/discover_grid/discover_grid_flyout.test.tsx similarity index 93% rename from src/plugins/discover/public/application/components/discover_grid/discover_grid_flyout.test.tsx rename to src/plugins/discover/public/components/discover_grid/discover_grid_flyout.test.tsx index 83fa447a50ba0..64e97b824a2f9 100644 --- a/src/plugins/discover/public/application/components/discover_grid/discover_grid_flyout.test.tsx +++ b/src/plugins/discover/public/components/discover_grid/discover_grid_flyout.test.tsx @@ -10,13 +10,13 @@ import React from 'react'; import { findTestSubject } from '@elastic/eui/lib/test'; import { mountWithIntl } from '@kbn/test/jest'; import { DiscoverGridFlyout } from './discover_grid_flyout'; -import { esHits } from '../../../__mocks__/es_hits'; -import { createFilterManagerMock } from '../../../../../data/public/query/filter_manager/filter_manager.mock'; -import { indexPatternMock } from '../../../__mocks__/index_pattern'; -import { DiscoverServices } from '../../../build_services'; -import { DocViewsRegistry } from '../../doc_views/doc_views_registry'; -import { setDocViewsRegistry } from '../../../kibana_services'; -import { indexPatternWithTimefieldMock } from '../../../__mocks__/index_pattern_with_timefield'; +import { esHits } from '../../__mocks__/es_hits'; +import { createFilterManagerMock } from '../../../../data/public/query/filter_manager/filter_manager.mock'; +import { indexPatternMock } from '../../__mocks__/index_pattern'; +import { DiscoverServices } from '../../build_services'; +import { DocViewsRegistry } from '../../services/doc_views/doc_views_registry'; +import { setDocViewsRegistry } from '../../kibana_services'; +import { indexPatternWithTimefieldMock } from '../../__mocks__/index_pattern_with_timefield'; describe('Discover flyout', function () { setDocViewsRegistry(new DocViewsRegistry()); diff --git a/src/plugins/discover/public/application/components/discover_grid/discover_grid_flyout.tsx b/src/plugins/discover/public/components/discover_grid/discover_grid_flyout.tsx similarity index 95% rename from src/plugins/discover/public/application/components/discover_grid/discover_grid_flyout.tsx rename to src/plugins/discover/public/components/discover_grid/discover_grid_flyout.tsx index f6e5e25f284ca..d5b2248162b2f 100644 --- a/src/plugins/discover/public/application/components/discover_grid/discover_grid_flyout.tsx +++ b/src/plugins/discover/public/components/discover_grid/discover_grid_flyout.tsx @@ -24,11 +24,11 @@ import { EuiHideFor, keys, } from '@elastic/eui'; -import { DocViewer } from '../doc_viewer/doc_viewer'; -import { DocViewFilterFn, ElasticSearchHit } from '../../doc_views/doc_views_types'; -import { DiscoverServices } from '../../../build_services'; -import { getContextUrl } from '../../helpers/get_context_url'; -import { getSingleDocUrl } from '../../helpers/get_single_doc_url'; +import { DocViewer } from '../../services/doc_views/components/doc_viewer/doc_viewer'; +import { DocViewFilterFn, ElasticSearchHit } from '../../services/doc_views/doc_views_types'; +import { DiscoverServices } from '../../build_services'; +import { getContextUrl } from '../../utils/get_context_url'; +import { getSingleDocUrl } from '../../utils/get_single_doc_url'; interface Props { columns: string[]; diff --git a/src/plugins/discover/public/application/components/discover_grid/discover_grid_schema.ts b/src/plugins/discover/public/components/discover_grid/discover_grid_schema.ts similarity index 94% rename from src/plugins/discover/public/application/components/discover_grid/discover_grid_schema.ts rename to src/plugins/discover/public/components/discover_grid/discover_grid_schema.ts index 0aa6dadd633e0..5cf257fb16f2c 100644 --- a/src/plugins/discover/public/application/components/discover_grid/discover_grid_schema.ts +++ b/src/plugins/discover/public/components/discover_grid/discover_grid_schema.ts @@ -7,7 +7,7 @@ */ import { kibanaJSON } from './constants'; -import { KBN_FIELD_TYPES } from '../../../../../data/common'; +import { KBN_FIELD_TYPES } from '../../../../data/common'; export function getSchemaByKbnType(kbnType: string | undefined) { // Default DataGrid schemas: boolean, numeric, datetime, json, currency, string diff --git a/src/plugins/discover/public/application/components/discover_grid/get_render_cell_value.test.tsx b/src/plugins/discover/public/components/discover_grid/get_render_cell_value.test.tsx similarity index 98% rename from src/plugins/discover/public/application/components/discover_grid/get_render_cell_value.test.tsx rename to src/plugins/discover/public/components/discover_grid/get_render_cell_value.test.tsx index 3fb96ba9e9daa..260cbf42c4d8e 100644 --- a/src/plugins/discover/public/application/components/discover_grid/get_render_cell_value.test.tsx +++ b/src/plugins/discover/public/components/discover_grid/get_render_cell_value.test.tsx @@ -9,18 +9,18 @@ import React from 'react'; import { ReactWrapper, shallow } from 'enzyme'; import { getRenderCellValueFn } from './get_render_cell_value'; -import { indexPatternMock } from '../../../__mocks__/index_pattern'; -import { ElasticSearchHit } from '../../doc_views/doc_views_types'; +import { indexPatternMock } from '../../__mocks__/index_pattern'; +import { ElasticSearchHit } from '../../services/doc_views/doc_views_types'; import { flattenHit } from 'src/plugins/data/common'; -jest.mock('../../../../../kibana_react/public', () => ({ +jest.mock('../../../../kibana_react/public', () => ({ useUiSetting: () => true, withKibana: (comp: ReactWrapper) => { return comp; }, })); -jest.mock('../../../kibana_services', () => ({ +jest.mock('../../kibana_services', () => ({ getServices: () => ({ uiSettings: { get: jest.fn(), diff --git a/src/plugins/discover/public/application/components/discover_grid/get_render_cell_value.tsx b/src/plugins/discover/public/components/discover_grid/get_render_cell_value.tsx similarity index 94% rename from src/plugins/discover/public/application/components/discover_grid/get_render_cell_value.tsx rename to src/plugins/discover/public/components/discover_grid/get_render_cell_value.tsx index 4066c13f6391e..8fd5f73701932 100644 --- a/src/plugins/discover/public/application/components/discover_grid/get_render_cell_value.tsx +++ b/src/plugins/discover/public/components/discover_grid/get_render_cell_value.tsx @@ -7,8 +7,10 @@ */ import React, { Fragment, useContext, useEffect } from 'react'; -import themeLight from '@elastic/eui/dist/eui_theme_light.json'; -import themeDark from '@elastic/eui/dist/eui_theme_dark.json'; +import { + euiLightVars as themeLight, + euiDarkVars as themeDark, +} from '@kbn/ui-shared-deps-src/theme'; import type { IndexPattern } from 'src/plugins/data/common'; import { @@ -17,13 +19,13 @@ import { EuiDescriptionListTitle, EuiDescriptionListDescription, } from '@elastic/eui'; -import { ElasticSearchHit } from '../../doc_views/doc_views_types'; +import { ElasticSearchHit } from '../../services/doc_views/doc_views_types'; import { DiscoverGridContext } from './discover_grid_context'; import { JsonCodeEditor } from '../json_code_editor/json_code_editor'; import { defaultMonacoEditorWidth } from './constants'; -import { EsHitRecord } from '../../types'; -import { formatFieldValue } from '../../helpers/format_value'; -import { formatHit } from '../../helpers/format_hit'; +import { EsHitRecord } from '../../application/types'; +import { formatFieldValue } from '../../utils/format_value'; +import { formatHit } from '../../utils/format_hit'; export const getRenderCellValueFn = ( diff --git a/src/plugins/discover/public/application/components/discover_grid/types.ts b/src/plugins/discover/public/components/discover_grid/types.ts similarity index 100% rename from src/plugins/discover/public/application/components/discover_grid/types.ts rename to src/plugins/discover/public/components/discover_grid/types.ts diff --git a/src/plugins/discover/public/application/apps/main/components/doc_table/_doc_table.scss b/src/plugins/discover/public/components/doc_table/_doc_table.scss similarity index 98% rename from src/plugins/discover/public/application/apps/main/components/doc_table/_doc_table.scss rename to src/plugins/discover/public/components/doc_table/_doc_table.scss index d19a1fd042069..164b61d42df19 100644 --- a/src/plugins/discover/public/application/apps/main/components/doc_table/_doc_table.scss +++ b/src/plugins/discover/public/components/doc_table/_doc_table.scss @@ -103,10 +103,6 @@ text-align: center; } -.truncate-by-height { - overflow: hidden; -} - .table { // Nesting .table { diff --git a/src/plugins/discover/public/components/doc_table/actions/columns.test.ts b/src/plugins/discover/public/components/doc_table/actions/columns.test.ts new file mode 100644 index 0000000000000..5f3c7d203122f --- /dev/null +++ b/src/plugins/discover/public/components/doc_table/actions/columns.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. + */ + +import { getStateColumnActions } from './columns'; +import { configMock } from '../../../__mocks__/config'; +import { indexPatternMock } from '../../../__mocks__/index_pattern'; +import { indexPatternsMock } from '../../../__mocks__/index_patterns'; +import { Capabilities } from '../../../../../../core/types'; +import { AppState } from '../../../application/main/services/discover_state'; + +function getStateColumnAction(state: {}, setAppState: (state: Partial) => void) { + return getStateColumnActions({ + capabilities: { + discover: { + save: false, + }, + } as unknown as Capabilities, + config: configMock, + indexPattern: indexPatternMock, + indexPatterns: indexPatternsMock, + useNewFieldsApi: true, + setAppState, + state, + }); +} + +describe('Test column actions', () => { + test('getStateColumnActions with empty state', () => { + const setAppState = jest.fn(); + const actions = getStateColumnAction({}, setAppState); + + actions.onAddColumn('_score'); + expect(setAppState).toHaveBeenCalledWith({ columns: ['_score'], sort: [['_score', 'desc']] }); + actions.onAddColumn('test'); + expect(setAppState).toHaveBeenCalledWith({ columns: ['test'] }); + }); + test('getStateColumnActions with columns and sort in state', () => { + const setAppState = jest.fn(); + const actions = getStateColumnAction( + { columns: ['first', 'second'], sort: [['first', 'desc']] }, + setAppState + ); + + actions.onAddColumn('_score'); + expect(setAppState).toHaveBeenCalledWith({ + columns: ['first', 'second', '_score'], + sort: [['first', 'desc']], + }); + setAppState.mockClear(); + actions.onAddColumn('third'); + expect(setAppState).toHaveBeenCalledWith({ + columns: ['first', 'second', 'third'], + sort: [['first', 'desc']], + }); + setAppState.mockClear(); + actions.onRemoveColumn('first'); + expect(setAppState).toHaveBeenCalledWith({ + columns: ['second'], + sort: [], + }); + setAppState.mockClear(); + actions.onSetColumns(['first', 'second', 'third'], true); + expect(setAppState).toHaveBeenCalledWith({ + columns: ['first', 'second', 'third'], + }); + setAppState.mockClear(); + + actions.onMoveColumn('second', 0); + expect(setAppState).toHaveBeenCalledWith({ + columns: ['second', 'first'], + }); + }); +}); diff --git a/src/plugins/discover/public/components/doc_table/actions/columns.ts b/src/plugins/discover/public/components/doc_table/actions/columns.ts new file mode 100644 index 0000000000000..cb771cc2c6de3 --- /dev/null +++ b/src/plugins/discover/public/components/doc_table/actions/columns.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 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 { Capabilities, IUiSettingsClient } from 'kibana/public'; +import { SORT_DEFAULT_ORDER_SETTING } from '../../../../common'; +import { + AppState as DiscoverState, + GetStateReturn as DiscoverGetStateReturn, +} from '../../../application/main/services/discover_state'; +import { + AppState as ContextState, + GetStateReturn as ContextGetStateReturn, +} from '../../../application/context/services/context_state'; +import { IndexPattern, IndexPatternsContract } from '../../../../../data/public'; +import { popularizeField } from '../../../utils/popularize_field'; + +/** + * Helper function to provide a fallback to a single _source column if the given array of columns + * is empty, and removes _source if there are more than 1 columns given + * @param columns + * @param useNewFieldsApi should a new fields API be used + */ +function buildColumns(columns: string[], useNewFieldsApi = false) { + if (columns.length > 1 && columns.indexOf('_source') !== -1) { + return columns.filter((col) => col !== '_source'); + } else if (columns.length !== 0) { + return columns; + } + return useNewFieldsApi ? [] : ['_source']; +} + +export function addColumn(columns: string[], columnName: string, useNewFieldsApi?: boolean) { + if (columns.includes(columnName)) { + return columns; + } + return buildColumns([...columns, columnName], useNewFieldsApi); +} + +export function removeColumn(columns: string[], columnName: string, useNewFieldsApi?: boolean) { + if (!columns.includes(columnName)) { + return columns; + } + return buildColumns( + columns.filter((col) => col !== columnName), + useNewFieldsApi + ); +} + +export function moveColumn(columns: string[], columnName: string, newIndex: number) { + if (newIndex < 0 || newIndex >= columns.length || !columns.includes(columnName)) { + return columns; + } + const modifiedColumns = [...columns]; + modifiedColumns.splice(modifiedColumns.indexOf(columnName), 1); // remove at old index + modifiedColumns.splice(newIndex, 0, columnName); // insert before new index + return modifiedColumns; +} + +export function getStateColumnActions({ + capabilities, + config, + indexPattern, + indexPatterns, + useNewFieldsApi, + setAppState, + state, +}: { + capabilities: Capabilities; + config: IUiSettingsClient; + indexPattern: IndexPattern; + indexPatterns: IndexPatternsContract; + useNewFieldsApi: boolean; + setAppState: DiscoverGetStateReturn['setAppState'] | ContextGetStateReturn['setAppState']; + state: DiscoverState | ContextState; +}) { + function onAddColumn(columnName: string) { + popularizeField(indexPattern, columnName, indexPatterns, capabilities); + const columns = addColumn(state.columns || [], columnName, useNewFieldsApi); + const defaultOrder = config.get(SORT_DEFAULT_ORDER_SETTING); + const sort = + columnName === '_score' && !state.sort?.length ? [['_score', defaultOrder]] : state.sort; + setAppState({ columns, sort }); + } + + function onRemoveColumn(columnName: string) { + popularizeField(indexPattern, columnName, indexPatterns, capabilities); + const columns = removeColumn(state.columns || [], columnName, useNewFieldsApi); + // The state's sort property is an array of [sortByColumn,sortDirection] + const sort = + state.sort && state.sort.length + ? state.sort.filter((subArr) => subArr[0] !== columnName) + : []; + setAppState({ columns, sort }); + } + + function onMoveColumn(columnName: string, newIndex: number) { + const columns = moveColumn(state.columns || [], columnName, newIndex); + setAppState({ columns }); + } + + function onSetColumns(columns: string[], hideTimeColumn: boolean) { + // The next line should gone when classic table will be removed + const actualColumns = + !hideTimeColumn && indexPattern.timeFieldName && indexPattern.timeFieldName === columns[0] + ? columns.slice(1) + : columns; + + setAppState({ columns: actualColumns }); + } + return { + onAddColumn, + onRemoveColumn, + onMoveColumn, + onSetColumns, + }; +} diff --git a/src/plugins/discover/public/application/apps/main/components/doc_table/components/_index.scss b/src/plugins/discover/public/components/doc_table/components/_index.scss similarity index 100% rename from src/plugins/discover/public/application/apps/main/components/doc_table/components/_index.scss rename to src/plugins/discover/public/components/doc_table/components/_index.scss diff --git a/src/plugins/discover/public/application/apps/main/components/doc_table/components/_table_header.scss b/src/plugins/discover/public/components/doc_table/components/_table_header.scss similarity index 100% rename from src/plugins/discover/public/application/apps/main/components/doc_table/components/_table_header.scss rename to src/plugins/discover/public/components/doc_table/components/_table_header.scss diff --git a/src/plugins/discover/public/application/apps/main/components/doc_table/components/pager/tool_bar_pagination.tsx b/src/plugins/discover/public/components/doc_table/components/pager/tool_bar_pagination.tsx similarity index 100% rename from src/plugins/discover/public/application/apps/main/components/doc_table/components/pager/tool_bar_pagination.tsx rename to src/plugins/discover/public/components/doc_table/components/pager/tool_bar_pagination.tsx diff --git a/src/plugins/discover/public/application/apps/main/components/doc_table/components/table_header/__snapshots__/table_header.test.tsx.snap b/src/plugins/discover/public/components/doc_table/components/table_header/__snapshots__/table_header.test.tsx.snap similarity index 100% rename from src/plugins/discover/public/application/apps/main/components/doc_table/components/table_header/__snapshots__/table_header.test.tsx.snap rename to src/plugins/discover/public/components/doc_table/components/table_header/__snapshots__/table_header.test.tsx.snap diff --git a/src/plugins/discover/public/application/apps/main/components/doc_table/components/table_header/helpers.tsx b/src/plugins/discover/public/components/doc_table/components/table_header/helpers.tsx similarity index 100% rename from src/plugins/discover/public/application/apps/main/components/doc_table/components/table_header/helpers.tsx rename to src/plugins/discover/public/components/doc_table/components/table_header/helpers.tsx diff --git a/src/plugins/discover/public/application/apps/main/components/doc_table/components/table_header/score_sort_warning.tsx b/src/plugins/discover/public/components/doc_table/components/table_header/score_sort_warning.tsx similarity index 100% rename from src/plugins/discover/public/application/apps/main/components/doc_table/components/table_header/score_sort_warning.tsx rename to src/plugins/discover/public/components/doc_table/components/table_header/score_sort_warning.tsx diff --git a/src/plugins/discover/public/application/apps/main/components/doc_table/components/table_header/table_header.test.tsx b/src/plugins/discover/public/components/doc_table/components/table_header/table_header.test.tsx similarity index 100% rename from src/plugins/discover/public/application/apps/main/components/doc_table/components/table_header/table_header.test.tsx rename to src/plugins/discover/public/components/doc_table/components/table_header/table_header.test.tsx diff --git a/src/plugins/discover/public/application/apps/main/components/doc_table/components/table_header/table_header.tsx b/src/plugins/discover/public/components/doc_table/components/table_header/table_header.tsx similarity index 100% rename from src/plugins/discover/public/application/apps/main/components/doc_table/components/table_header/table_header.tsx rename to src/plugins/discover/public/components/doc_table/components/table_header/table_header.tsx diff --git a/src/plugins/discover/public/application/apps/main/components/doc_table/components/table_header/table_header_column.tsx b/src/plugins/discover/public/components/doc_table/components/table_header/table_header_column.tsx similarity index 100% rename from src/plugins/discover/public/application/apps/main/components/doc_table/components/table_header/table_header_column.tsx rename to src/plugins/discover/public/components/doc_table/components/table_header/table_header_column.tsx diff --git a/src/plugins/discover/public/application/apps/main/components/doc_table/components/table_row.test.tsx b/src/plugins/discover/public/components/doc_table/components/table_row.test.tsx similarity index 85% rename from src/plugins/discover/public/application/apps/main/components/doc_table/components/table_row.test.tsx rename to src/plugins/discover/public/components/doc_table/components/table_row.test.tsx index 887564168ac85..7b0e4d821af65 100644 --- a/src/plugins/discover/public/application/apps/main/components/doc_table/components/table_row.test.tsx +++ b/src/plugins/discover/public/components/doc_table/components/table_row.test.tsx @@ -9,12 +9,12 @@ import React from 'react'; import { mountWithIntl, findTestSubject } from '@kbn/test/jest'; import { TableRow, TableRowProps } from './table_row'; -import { setDocViewsRegistry, setServices } from '../../../../../../kibana_services'; -import { createFilterManagerMock } from '../../../../../../../../data/public/query/filter_manager/filter_manager.mock'; -import { DiscoverServices } from '../../../../../../build_services'; -import { indexPatternWithTimefieldMock } from '../../../../../../__mocks__/index_pattern_with_timefield'; -import { uiSettingsMock } from '../../../../../../__mocks__/ui_settings'; -import { DocViewsRegistry } from '../../../../../doc_views/doc_views_registry'; +import { setDocViewsRegistry, setServices } from '../../../kibana_services'; +import { createFilterManagerMock } from '../../../../../data/public/query/filter_manager/filter_manager.mock'; +import { DiscoverServices } from '../../../build_services'; +import { indexPatternWithTimefieldMock } from '../../../__mocks__/index_pattern_with_timefield'; +import { uiSettingsMock } from '../../../__mocks__/ui_settings'; +import { DocViewsRegistry } from '../../../services/doc_views/doc_views_registry'; jest.mock('../lib/row_formatter', () => { const originalModule = jest.requireActual('../lib/row_formatter'); diff --git a/src/plugins/discover/public/application/apps/main/components/doc_table/components/table_row.tsx b/src/plugins/discover/public/components/doc_table/components/table_row.tsx similarity index 91% rename from src/plugins/discover/public/application/apps/main/components/doc_table/components/table_row.tsx rename to src/plugins/discover/public/components/doc_table/components/table_row.tsx index 0bf4a36555d16..8a980cc4160d8 100644 --- a/src/plugins/discover/public/application/apps/main/components/doc_table/components/table_row.tsx +++ b/src/plugins/discover/public/components/doc_table/components/table_row.tsx @@ -10,14 +10,14 @@ import React, { Fragment, useCallback, useMemo, useState } from 'react'; import classNames from 'classnames'; import { i18n } from '@kbn/i18n'; import { EuiButtonEmpty, EuiIcon } from '@elastic/eui'; -import { formatFieldValue } from '../../../../../helpers/format_value'; -import { flattenHit } from '../../../../../../../../data/common'; -import { DocViewer } from '../../../../../components/doc_viewer/doc_viewer'; -import { FilterManager, IndexPattern } from '../../../../../../../../data/public'; +import { formatFieldValue } from '../../../utils/format_value'; +import { flattenHit } from '../../../../../data/common'; +import { DocViewer } from '../../../services/doc_views/components/doc_viewer/doc_viewer'; +import { FilterManager, IndexPattern } from '../../../../../data/public'; import { TableCell } from './table_row/table_cell'; -import { ElasticSearchHit, DocViewFilterFn } from '../../../../../doc_views/doc_views_types'; -import { getContextUrl } from '../../../../../helpers/get_context_url'; -import { getSingleDocUrl } from '../../../../../helpers/get_single_doc_url'; +import { ElasticSearchHit, DocViewFilterFn } from '../../../services/doc_views/doc_views_types'; +import { getContextUrl } from '../../../utils/get_context_url'; +import { getSingleDocUrl } from '../../../utils/get_single_doc_url'; import { TableRowDetails } from './table_row_details'; import { formatRow, formatTopLevelObject } from '../lib/row_formatter'; @@ -88,7 +88,7 @@ export const TableRow = ({ return ( // formatFieldValue always returns sanitized HTML // eslint-disable-next-line react/no-danger -
    +
    ); }; const inlineFilter = useCallback( diff --git a/src/plugins/discover/public/application/apps/main/components/doc_table/components/table_row/__snapshots__/table_cell.test.tsx.snap b/src/plugins/discover/public/components/doc_table/components/table_row/__snapshots__/table_cell.test.tsx.snap similarity index 100% rename from src/plugins/discover/public/application/apps/main/components/doc_table/components/table_row/__snapshots__/table_cell.test.tsx.snap rename to src/plugins/discover/public/components/doc_table/components/table_row/__snapshots__/table_cell.test.tsx.snap diff --git a/src/plugins/discover/public/application/apps/main/components/doc_table/components/table_row/_cell.scss b/src/plugins/discover/public/components/doc_table/components/table_row/_cell.scss similarity index 100% rename from src/plugins/discover/public/application/apps/main/components/doc_table/components/table_row/_cell.scss rename to src/plugins/discover/public/components/doc_table/components/table_row/_cell.scss diff --git a/src/plugins/discover/public/application/apps/main/components/doc_table/components/table_row/_details.scss b/src/plugins/discover/public/components/doc_table/components/table_row/_details.scss similarity index 100% rename from src/plugins/discover/public/application/apps/main/components/doc_table/components/table_row/_details.scss rename to src/plugins/discover/public/components/doc_table/components/table_row/_details.scss diff --git a/src/plugins/discover/public/application/apps/main/components/doc_table/components/table_row/_index.scss b/src/plugins/discover/public/components/doc_table/components/table_row/_index.scss similarity index 100% rename from src/plugins/discover/public/application/apps/main/components/doc_table/components/table_row/_index.scss rename to src/plugins/discover/public/components/doc_table/components/table_row/_index.scss diff --git a/src/plugins/discover/public/application/apps/main/components/doc_table/components/table_row/_open.scss b/src/plugins/discover/public/components/doc_table/components/table_row/_open.scss similarity index 100% rename from src/plugins/discover/public/application/apps/main/components/doc_table/components/table_row/_open.scss rename to src/plugins/discover/public/components/doc_table/components/table_row/_open.scss diff --git a/src/plugins/discover/public/application/apps/main/components/doc_table/components/table_row/table_cell.test.tsx b/src/plugins/discover/public/components/doc_table/components/table_row/table_cell.test.tsx similarity index 100% rename from src/plugins/discover/public/application/apps/main/components/doc_table/components/table_row/table_cell.test.tsx rename to src/plugins/discover/public/components/doc_table/components/table_row/table_cell.test.tsx diff --git a/src/plugins/discover/public/application/apps/main/components/doc_table/components/table_row/table_cell.tsx b/src/plugins/discover/public/components/doc_table/components/table_row/table_cell.tsx similarity index 100% rename from src/plugins/discover/public/application/apps/main/components/doc_table/components/table_row/table_cell.tsx rename to src/plugins/discover/public/components/doc_table/components/table_row/table_cell.tsx diff --git a/src/plugins/discover/public/application/apps/main/components/doc_table/components/table_row/table_cell_actions.tsx b/src/plugins/discover/public/components/doc_table/components/table_row/table_cell_actions.tsx similarity index 100% rename from src/plugins/discover/public/application/apps/main/components/doc_table/components/table_row/table_cell_actions.tsx rename to src/plugins/discover/public/components/doc_table/components/table_row/table_cell_actions.tsx diff --git a/src/plugins/discover/public/application/apps/main/components/doc_table/components/table_row_details.tsx b/src/plugins/discover/public/components/doc_table/components/table_row_details.tsx similarity index 99% rename from src/plugins/discover/public/application/apps/main/components/doc_table/components/table_row_details.tsx rename to src/plugins/discover/public/components/doc_table/components/table_row_details.tsx index c3ff53fe2d3a8..02d0ee4f2272a 100644 --- a/src/plugins/discover/public/application/apps/main/components/doc_table/components/table_row_details.tsx +++ b/src/plugins/discover/public/components/doc_table/components/table_row_details.tsx @@ -9,7 +9,6 @@ import React from 'react'; import { EuiFlexGroup, EuiFlexItem, EuiIcon, EuiLink, EuiTitle } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; - interface TableRowDetailsProps { open: boolean; colLength: number; diff --git a/src/plugins/discover/public/application/apps/main/components/doc_table/create_doc_table_embeddable.tsx b/src/plugins/discover/public/components/doc_table/create_doc_table_embeddable.tsx similarity index 100% rename from src/plugins/discover/public/application/apps/main/components/doc_table/create_doc_table_embeddable.tsx rename to src/plugins/discover/public/components/doc_table/create_doc_table_embeddable.tsx diff --git a/src/plugins/discover/public/application/apps/main/components/doc_table/doc_table_context.tsx b/src/plugins/discover/public/components/doc_table/doc_table_context.tsx similarity index 92% rename from src/plugins/discover/public/application/apps/main/components/doc_table/doc_table_context.tsx rename to src/plugins/discover/public/components/doc_table/doc_table_context.tsx index 8d29efec73716..976d4e3d5bdf1 100644 --- a/src/plugins/discover/public/application/apps/main/components/doc_table/doc_table_context.tsx +++ b/src/plugins/discover/public/components/doc_table/doc_table_context.tsx @@ -8,7 +8,7 @@ import React, { Fragment } from 'react'; import './index.scss'; -import { SkipBottomButton } from '../skip_bottom_button'; +import { SkipBottomButton } from '../../application/main/components/skip_bottom_button'; import { DocTableProps, DocTableRenderProps, DocTableWrapper } from './doc_table_wrapper'; const DocTableWrapperMemoized = React.memo(DocTableWrapper); diff --git a/src/plugins/discover/public/application/apps/main/components/doc_table/doc_table_embeddable.tsx b/src/plugins/discover/public/components/doc_table/doc_table_embeddable.tsx similarity index 95% rename from src/plugins/discover/public/application/apps/main/components/doc_table/doc_table_embeddable.tsx rename to src/plugins/discover/public/components/doc_table/doc_table_embeddable.tsx index 84edf147dea4c..0743c5ef813e8 100644 --- a/src/plugins/discover/public/application/apps/main/components/doc_table/doc_table_embeddable.tsx +++ b/src/plugins/discover/public/components/doc_table/doc_table_embeddable.tsx @@ -10,12 +10,12 @@ import React, { memo, useCallback, useEffect, useMemo, useRef } from 'react'; import './index.scss'; import { FormattedMessage } from '@kbn/i18n/react'; import { EuiFlexGroup, EuiFlexItem, EuiText } from '@elastic/eui'; -import { SAMPLE_SIZE_SETTING } from '../../../../../../common'; +import { SAMPLE_SIZE_SETTING } from '../../../common'; import { usePager } from './lib/use_pager'; import { ToolBarPagination } from './components/pager/tool_bar_pagination'; import { DocTableProps, DocTableRenderProps, DocTableWrapper } from './doc_table_wrapper'; -import { TotalDocuments } from '../total_documents/total_documents'; -import { getServices } from '../../../../../kibana_services'; +import { TotalDocuments } from '../../application/main/components/total_documents/total_documents'; +import { getServices } from '../../kibana_services'; export interface DocTableEmbeddableProps extends DocTableProps { totalHitCount: number; diff --git a/src/plugins/discover/public/application/apps/main/components/doc_table/doc_table_infinite.tsx b/src/plugins/discover/public/components/doc_table/doc_table_infinite.tsx similarity index 98% rename from src/plugins/discover/public/application/apps/main/components/doc_table/doc_table_infinite.tsx rename to src/plugins/discover/public/components/doc_table/doc_table_infinite.tsx index dddfefa906962..d2e93cdae452e 100644 --- a/src/plugins/discover/public/application/apps/main/components/doc_table/doc_table_infinite.tsx +++ b/src/plugins/discover/public/components/doc_table/doc_table_infinite.tsx @@ -12,7 +12,7 @@ import { FormattedMessage } from '@kbn/i18n/react'; import { debounce } from 'lodash'; import { EuiButtonEmpty } from '@elastic/eui'; import { DocTableProps, DocTableRenderProps, DocTableWrapper } from './doc_table_wrapper'; -import { SkipBottomButton } from '../skip_bottom_button'; +import { SkipBottomButton } from '../../application/main/components/skip_bottom_button'; import { shouldLoadNextDocPatch } from './lib/should_load_next_doc_patch'; const FOOTER_PADDING = { padding: 0 }; diff --git a/src/plugins/discover/public/application/apps/main/components/doc_table/doc_table_wrapper.test.tsx b/src/plugins/discover/public/components/doc_table/doc_table_wrapper.test.tsx similarity index 92% rename from src/plugins/discover/public/application/apps/main/components/doc_table/doc_table_wrapper.test.tsx rename to src/plugins/discover/public/components/doc_table/doc_table_wrapper.test.tsx index df5869bd61e52..a17a5c9b87d73 100644 --- a/src/plugins/discover/public/application/apps/main/components/doc_table/doc_table_wrapper.test.tsx +++ b/src/plugins/discover/public/components/doc_table/doc_table_wrapper.test.tsx @@ -8,11 +8,11 @@ import React from 'react'; import { findTestSubject, mountWithIntl } from '@kbn/test/jest'; -import { setServices } from '../../../../../kibana_services'; -import { indexPatternMock } from '../../../../../__mocks__/index_pattern'; +import { setServices } from '../../kibana_services'; +import { indexPatternMock } from '../../__mocks__/index_pattern'; import { DocTableWrapper, DocTableWrapperProps } from './doc_table_wrapper'; import { DocTableRow } from './components/table_row'; -import { discoverServiceMock } from '../../../../../__mocks__/services'; +import { discoverServiceMock } from '../../__mocks__/services'; const mountComponent = (props: DocTableWrapperProps) => { return mountWithIntl(); diff --git a/src/plugins/discover/public/application/apps/main/components/doc_table/doc_table_wrapper.tsx b/src/plugins/discover/public/components/doc_table/doc_table_wrapper.tsx similarity index 95% rename from src/plugins/discover/public/application/apps/main/components/doc_table/doc_table_wrapper.tsx rename to src/plugins/discover/public/components/doc_table/doc_table_wrapper.tsx index 2fac1c828796d..139b835e2e5c4 100644 --- a/src/plugins/discover/public/application/apps/main/components/doc_table/doc_table_wrapper.tsx +++ b/src/plugins/discover/public/components/doc_table/doc_table_wrapper.tsx @@ -11,18 +11,18 @@ import { EuiIcon, EuiSpacer, EuiText } from '@elastic/eui'; import type { IndexPattern, IndexPatternField } from 'src/plugins/data/common'; import { FormattedMessage } from '@kbn/i18n/react'; import { TableHeader } from './components/table_header/table_header'; -import { FORMATS_UI_SETTINGS } from '../../../../../../../field_formats/common'; +import { FORMATS_UI_SETTINGS } from '../../../../field_formats/common'; import { DOC_HIDE_TIME_COLUMN_SETTING, SAMPLE_SIZE_SETTING, SHOW_MULTIFIELDS, SORT_DEFAULT_ORDER_SETTING, -} from '../../../../../../common'; -import { getServices } from '../../../../../kibana_services'; +} from '../../../common'; +import { getServices } from '../../kibana_services'; import { SortOrder } from './components/table_header/helpers'; import { DocTableRow, TableRow } from './components/table_row'; -import { DocViewFilterFn } from '../../../../doc_views/doc_views_types'; -import { getFieldsToShow } from '../../../../helpers/get_fields_to_show'; +import { DocViewFilterFn } from '../../services/doc_views/doc_views_types'; +import { getFieldsToShow } from '../../utils/get_fields_to_show'; export interface DocTableProps { /** diff --git a/src/plugins/discover/public/application/apps/main/components/doc_table/index.scss b/src/plugins/discover/public/components/doc_table/index.scss similarity index 100% rename from src/plugins/discover/public/application/apps/main/components/doc_table/index.scss rename to src/plugins/discover/public/components/doc_table/index.scss diff --git a/src/plugins/discover/public/application/apps/main/components/doc_table/index.ts b/src/plugins/discover/public/components/doc_table/index.ts similarity index 100% rename from src/plugins/discover/public/application/apps/main/components/doc_table/index.ts rename to src/plugins/discover/public/components/doc_table/index.ts diff --git a/src/plugins/discover/public/application/apps/main/components/doc_table/lib/get_default_sort.test.ts b/src/plugins/discover/public/components/doc_table/lib/get_default_sort.test.ts similarity index 95% rename from src/plugins/discover/public/application/apps/main/components/doc_table/lib/get_default_sort.test.ts rename to src/plugins/discover/public/components/doc_table/lib/get_default_sort.test.ts index 3a62108a16bef..68e7b43eceb9e 100644 --- a/src/plugins/discover/public/application/apps/main/components/doc_table/lib/get_default_sort.test.ts +++ b/src/plugins/discover/public/components/doc_table/lib/get_default_sort.test.ts @@ -10,7 +10,7 @@ import { getDefaultSort } from './get_default_sort'; import { stubIndexPattern, stubIndexPatternWithoutTimeField, -} from '../../../../../../../../data/common/stubs'; +} from '../../../../../data/common/stubs'; describe('getDefaultSort function', function () { test('should be a function', function () { diff --git a/src/plugins/discover/public/application/apps/main/components/doc_table/lib/get_default_sort.ts b/src/plugins/discover/public/components/doc_table/lib/get_default_sort.ts similarity index 100% rename from src/plugins/discover/public/application/apps/main/components/doc_table/lib/get_default_sort.ts rename to src/plugins/discover/public/components/doc_table/lib/get_default_sort.ts diff --git a/src/plugins/discover/public/application/apps/main/components/doc_table/lib/get_sort.test.ts b/src/plugins/discover/public/components/doc_table/lib/get_sort.test.ts similarity index 98% rename from src/plugins/discover/public/application/apps/main/components/doc_table/lib/get_sort.test.ts rename to src/plugins/discover/public/components/doc_table/lib/get_sort.test.ts index 9f7204805dc6f..7deb8075ac286 100644 --- a/src/plugins/discover/public/application/apps/main/components/doc_table/lib/get_sort.test.ts +++ b/src/plugins/discover/public/components/doc_table/lib/get_sort.test.ts @@ -10,7 +10,7 @@ import { getSort, getSortArray } from './get_sort'; import { stubIndexPattern, stubIndexPatternWithoutTimeField, -} from '../../../../../../../../data/common/stubs'; +} from '../../../../../data/common/stubs'; describe('docTable', function () { describe('getSort function', function () { diff --git a/src/plugins/discover/public/application/apps/main/components/doc_table/lib/get_sort.ts b/src/plugins/discover/public/components/doc_table/lib/get_sort.ts similarity index 97% rename from src/plugins/discover/public/application/apps/main/components/doc_table/lib/get_sort.ts rename to src/plugins/discover/public/components/doc_table/lib/get_sort.ts index 1e597f85666fc..bdbb3703ff87d 100644 --- a/src/plugins/discover/public/application/apps/main/components/doc_table/lib/get_sort.ts +++ b/src/plugins/discover/public/components/doc_table/lib/get_sort.ts @@ -7,7 +7,7 @@ */ import { isPlainObject } from 'lodash'; -import { IndexPattern } from '../../../../../../../../data/public'; +import { IndexPattern } from '../../../../../data/public'; export type SortPairObj = Record; export type SortPairArr = [string, string]; diff --git a/src/plugins/discover/public/application/apps/main/components/doc_table/lib/get_sort_for_search_source.test.ts b/src/plugins/discover/public/components/doc_table/lib/get_sort_for_search_source.test.ts similarity index 97% rename from src/plugins/discover/public/application/apps/main/components/doc_table/lib/get_sort_for_search_source.test.ts rename to src/plugins/discover/public/components/doc_table/lib/get_sort_for_search_source.test.ts index 061a458037100..de032c3748fcb 100644 --- a/src/plugins/discover/public/application/apps/main/components/doc_table/lib/get_sort_for_search_source.test.ts +++ b/src/plugins/discover/public/components/doc_table/lib/get_sort_for_search_source.test.ts @@ -11,7 +11,7 @@ import { SortOrder } from '../components/table_header/helpers'; import { stubIndexPattern, stubIndexPatternWithoutTimeField, -} from '../../../../../../../../data/common/stubs'; +} from '../../../../../data/common/stubs'; describe('getSortForSearchSource function', function () { test('should be a function', function () { diff --git a/src/plugins/discover/public/application/apps/main/components/doc_table/lib/get_sort_for_search_source.ts b/src/plugins/discover/public/components/doc_table/lib/get_sort_for_search_source.ts similarity index 100% rename from src/plugins/discover/public/application/apps/main/components/doc_table/lib/get_sort_for_search_source.ts rename to src/plugins/discover/public/components/doc_table/lib/get_sort_for_search_source.ts diff --git a/src/plugins/discover/public/application/apps/main/components/doc_table/lib/row_formatter.scss b/src/plugins/discover/public/components/doc_table/lib/row_formatter.scss similarity index 100% rename from src/plugins/discover/public/application/apps/main/components/doc_table/lib/row_formatter.scss rename to src/plugins/discover/public/components/doc_table/lib/row_formatter.scss diff --git a/src/plugins/discover/public/application/apps/main/components/doc_table/lib/row_formatter.test.ts b/src/plugins/discover/public/components/doc_table/lib/row_formatter.test.ts similarity index 95% rename from src/plugins/discover/public/application/apps/main/components/doc_table/lib/row_formatter.test.ts rename to src/plugins/discover/public/components/doc_table/lib/row_formatter.test.ts index 2e777a18ce906..ada9c1682a35b 100644 --- a/src/plugins/discover/public/application/apps/main/components/doc_table/lib/row_formatter.test.ts +++ b/src/plugins/discover/public/components/doc_table/lib/row_formatter.test.ts @@ -8,11 +8,11 @@ import ReactDOM from 'react-dom/server'; import { formatRow, formatTopLevelObject } from './row_formatter'; -import { IndexPattern } from '../../../../../../../../data/common'; -import { fieldFormatsMock } from '../../../../../../../../field_formats/common/mocks'; -import { setServices } from '../../../../../../kibana_services'; -import { DiscoverServices } from '../../../../../../build_services'; -import { stubbedSavedObjectIndexPattern } from '../../../../../../../../data/common/stubs'; +import { IndexPattern } from '../../../../../data/common'; +import { fieldFormatsMock } from '../../../../../field_formats/common/mocks'; +import { setServices } from '../../../kibana_services'; +import { DiscoverServices } from '../../../build_services'; +import { stubbedSavedObjectIndexPattern } from '../../../../../data/common/stubs'; describe('Row formatter', () => { const hit = { diff --git a/src/plugins/discover/public/application/apps/main/components/doc_table/lib/row_formatter.tsx b/src/plugins/discover/public/components/doc_table/lib/row_formatter.tsx similarity index 91% rename from src/plugins/discover/public/application/apps/main/components/doc_table/lib/row_formatter.tsx rename to src/plugins/discover/public/components/doc_table/lib/row_formatter.tsx index a73bc3f175be1..0ec611a307513 100644 --- a/src/plugins/discover/public/application/apps/main/components/doc_table/lib/row_formatter.tsx +++ b/src/plugins/discover/public/components/doc_table/lib/row_formatter.tsx @@ -9,9 +9,9 @@ import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; import React, { Fragment } from 'react'; import type { IndexPattern } from 'src/plugins/data/common'; -import { MAX_DOC_FIELDS_DISPLAYED } from '../../../../../../../common'; -import { getServices } from '../../../../../../kibana_services'; -import { formatHit } from '../../../../../helpers/format_hit'; +import { MAX_DOC_FIELDS_DISPLAYED } from '../../../../common'; +import { getServices } from '../../../kibana_services'; +import { formatHit } from '../../../utils/format_hit'; import './row_formatter.scss'; @@ -20,7 +20,7 @@ interface Props { } const TemplateComponent = ({ defPairs }: Props) => { return ( -
    +
    {defPairs.map((pair, idx) => (
    {pair[0]}:
    diff --git a/src/plugins/discover/public/application/apps/main/components/doc_table/lib/should_load_next_doc_patch.test.ts b/src/plugins/discover/public/components/doc_table/lib/should_load_next_doc_patch.test.ts similarity index 100% rename from src/plugins/discover/public/application/apps/main/components/doc_table/lib/should_load_next_doc_patch.test.ts rename to src/plugins/discover/public/components/doc_table/lib/should_load_next_doc_patch.test.ts diff --git a/src/plugins/discover/public/application/apps/main/components/doc_table/lib/should_load_next_doc_patch.ts b/src/plugins/discover/public/components/doc_table/lib/should_load_next_doc_patch.ts similarity index 100% rename from src/plugins/discover/public/application/apps/main/components/doc_table/lib/should_load_next_doc_patch.ts rename to src/plugins/discover/public/components/doc_table/lib/should_load_next_doc_patch.ts diff --git a/src/plugins/discover/public/application/apps/main/components/doc_table/lib/use_pager.test.tsx b/src/plugins/discover/public/components/doc_table/lib/use_pager.test.tsx similarity index 100% rename from src/plugins/discover/public/application/apps/main/components/doc_table/lib/use_pager.test.tsx rename to src/plugins/discover/public/components/doc_table/lib/use_pager.test.tsx diff --git a/src/plugins/discover/public/application/apps/main/components/doc_table/lib/use_pager.ts b/src/plugins/discover/public/components/doc_table/lib/use_pager.ts similarity index 100% rename from src/plugins/discover/public/application/apps/main/components/doc_table/lib/use_pager.ts rename to src/plugins/discover/public/components/doc_table/lib/use_pager.ts diff --git a/src/plugins/discover/public/application/components/field_name/__snapshots__/field_name.test.tsx.snap b/src/plugins/discover/public/components/field_name/__snapshots__/field_name.test.tsx.snap similarity index 100% rename from src/plugins/discover/public/application/components/field_name/__snapshots__/field_name.test.tsx.snap rename to src/plugins/discover/public/components/field_name/__snapshots__/field_name.test.tsx.snap diff --git a/src/plugins/discover/public/application/components/field_name/field_name.scss b/src/plugins/discover/public/components/field_name/field_name.scss similarity index 100% rename from src/plugins/discover/public/application/components/field_name/field_name.scss rename to src/plugins/discover/public/components/field_name/field_name.scss diff --git a/src/plugins/discover/public/application/components/field_name/field_name.test.tsx b/src/plugins/discover/public/components/field_name/field_name.test.tsx similarity index 100% rename from src/plugins/discover/public/application/components/field_name/field_name.test.tsx rename to src/plugins/discover/public/components/field_name/field_name.test.tsx diff --git a/src/plugins/discover/public/application/components/field_name/field_name.tsx b/src/plugins/discover/public/components/field_name/field_name.tsx similarity index 92% rename from src/plugins/discover/public/application/components/field_name/field_name.tsx rename to src/plugins/discover/public/components/field_name/field_name.tsx index 0e8ca31f6379a..918cf7166dce4 100644 --- a/src/plugins/discover/public/application/components/field_name/field_name.tsx +++ b/src/plugins/discover/public/components/field_name/field_name.tsx @@ -11,10 +11,10 @@ import './field_name.scss'; import { EuiBadge, EuiFlexGroup, EuiFlexItem, EuiToolTip } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; import { i18n } from '@kbn/i18n'; -import { FieldIcon, FieldIconProps } from '../../../../../kibana_react/public'; +import { FieldIcon, FieldIconProps } from '@kbn/react-field/field_icon'; import { getFieldTypeName } from './field_type_name'; -import { IndexPatternField } from '../../../../../data/public'; -import { getFieldSubtypeMulti } from '../../../../../data/common'; +import { IndexPatternField } from '../../../../data/public'; +import { getFieldSubtypeMulti } from '../../../../data/common'; interface Props { fieldName: string; diff --git a/src/plugins/discover/public/application/components/field_name/field_type_name.ts b/src/plugins/discover/public/components/field_name/field_type_name.ts similarity index 100% rename from src/plugins/discover/public/application/components/field_name/field_type_name.ts rename to src/plugins/discover/public/components/field_name/field_type_name.ts diff --git a/src/plugins/discover/public/application/components/help_menu/help_menu_util.ts b/src/plugins/discover/public/components/help_menu/help_menu_util.ts similarity index 100% rename from src/plugins/discover/public/application/components/help_menu/help_menu_util.ts rename to src/plugins/discover/public/components/help_menu/help_menu_util.ts diff --git a/src/plugins/discover/public/components/index.ts b/src/plugins/discover/public/components/index.ts new file mode 100644 index 0000000000000..a794b8c90a477 --- /dev/null +++ b/src/plugins/discover/public/components/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 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 { DeferredSpinner } from './common/deferred_spinner'; diff --git a/src/plugins/discover/public/application/components/json_code_editor/__snapshots__/json_code_editor.test.tsx.snap b/src/plugins/discover/public/components/json_code_editor/__snapshots__/json_code_editor.test.tsx.snap similarity index 100% rename from src/plugins/discover/public/application/components/json_code_editor/__snapshots__/json_code_editor.test.tsx.snap rename to src/plugins/discover/public/components/json_code_editor/__snapshots__/json_code_editor.test.tsx.snap diff --git a/src/plugins/discover/public/application/components/json_code_editor/json_code_editor.scss b/src/plugins/discover/public/components/json_code_editor/json_code_editor.scss similarity index 100% rename from src/plugins/discover/public/application/components/json_code_editor/json_code_editor.scss rename to src/plugins/discover/public/components/json_code_editor/json_code_editor.scss diff --git a/src/plugins/discover/public/application/components/json_code_editor/json_code_editor.test.tsx b/src/plugins/discover/public/components/json_code_editor/json_code_editor.test.tsx similarity index 100% rename from src/plugins/discover/public/application/components/json_code_editor/json_code_editor.test.tsx rename to src/plugins/discover/public/components/json_code_editor/json_code_editor.test.tsx diff --git a/src/plugins/discover/public/application/components/json_code_editor/json_code_editor.tsx b/src/plugins/discover/public/components/json_code_editor/json_code_editor.tsx similarity index 100% rename from src/plugins/discover/public/application/components/json_code_editor/json_code_editor.tsx rename to src/plugins/discover/public/components/json_code_editor/json_code_editor.tsx diff --git a/src/plugins/discover/public/application/components/json_code_editor/json_code_editor_common.tsx b/src/plugins/discover/public/components/json_code_editor/json_code_editor_common.tsx similarity index 97% rename from src/plugins/discover/public/application/components/json_code_editor/json_code_editor_common.tsx rename to src/plugins/discover/public/components/json_code_editor/json_code_editor_common.tsx index cab4eae41afb8..07d3ec2e759a3 100644 --- a/src/plugins/discover/public/application/components/json_code_editor/json_code_editor_common.tsx +++ b/src/plugins/discover/public/components/json_code_editor/json_code_editor_common.tsx @@ -12,7 +12,7 @@ import React from 'react'; import { i18n } from '@kbn/i18n'; import { monaco, XJsonLang } from '@kbn/monaco'; import { EuiButtonEmpty, EuiCopy, EuiFlexGroup, EuiFlexItem, EuiSpacer } from '@elastic/eui'; -import { CodeEditor } from '../../../../../kibana_react/public'; +import { CodeEditor } from '../../../../kibana_react/public'; const codeEditorAriaLabel = i18n.translate('discover.json.codeEditorAriaLabel', { defaultMessage: 'Read only JSON view of an elasticsearch document', diff --git a/src/plugins/discover/public/application/apps/main/components/view_mode_toggle/_index.scss b/src/plugins/discover/public/components/view_mode_toggle/_index.scss similarity index 100% rename from src/plugins/discover/public/application/apps/main/components/view_mode_toggle/_index.scss rename to src/plugins/discover/public/components/view_mode_toggle/_index.scss diff --git a/src/plugins/discover/public/application/apps/main/components/view_mode_toggle/_view_mode_toggle.scss b/src/plugins/discover/public/components/view_mode_toggle/_view_mode_toggle.scss similarity index 100% rename from src/plugins/discover/public/application/apps/main/components/view_mode_toggle/_view_mode_toggle.scss rename to src/plugins/discover/public/components/view_mode_toggle/_view_mode_toggle.scss diff --git a/src/plugins/discover/public/application/apps/main/components/view_mode_toggle/constants.ts b/src/plugins/discover/public/components/view_mode_toggle/constants.ts similarity index 100% rename from src/plugins/discover/public/application/apps/main/components/view_mode_toggle/constants.ts rename to src/plugins/discover/public/components/view_mode_toggle/constants.ts diff --git a/src/plugins/discover/public/application/apps/main/components/view_mode_toggle/index.ts b/src/plugins/discover/public/components/view_mode_toggle/index.ts similarity index 100% rename from src/plugins/discover/public/application/apps/main/components/view_mode_toggle/index.ts rename to src/plugins/discover/public/components/view_mode_toggle/index.ts diff --git a/src/plugins/discover/public/application/apps/main/components/view_mode_toggle/view_mode_toggle.tsx b/src/plugins/discover/public/components/view_mode_toggle/view_mode_toggle.tsx similarity index 100% rename from src/plugins/discover/public/application/apps/main/components/view_mode_toggle/view_mode_toggle.tsx rename to src/plugins/discover/public/components/view_mode_toggle/view_mode_toggle.tsx diff --git a/src/plugins/discover/public/embeddable/constants.ts b/src/plugins/discover/public/embeddable/constants.ts new file mode 100644 index 0000000000000..6c9de47a0f710 --- /dev/null +++ b/src/plugins/discover/public/embeddable/constants.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 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 { SEARCH_EMBEDDABLE_TYPE } from '../../common'; diff --git a/src/plugins/discover/public/application/embeddable/index.ts b/src/plugins/discover/public/embeddable/index.ts similarity index 100% rename from src/plugins/discover/public/application/embeddable/index.ts rename to src/plugins/discover/public/embeddable/index.ts diff --git a/src/plugins/discover/public/application/embeddable/saved_search_embeddable.tsx b/src/plugins/discover/public/embeddable/saved_search_embeddable.tsx similarity index 91% rename from src/plugins/discover/public/application/embeddable/saved_search_embeddable.tsx rename to src/plugins/discover/public/embeddable/saved_search_embeddable.tsx index 808962dc8319d..c04e6515cfbe1 100644 --- a/src/plugins/discover/public/application/embeddable/saved_search_embeddable.tsx +++ b/src/plugins/discover/public/embeddable/saved_search_embeddable.tsx @@ -11,13 +11,13 @@ import React from 'react'; import ReactDOM from 'react-dom'; import { i18n } from '@kbn/i18n'; import { isEqual } from 'lodash'; -import { Container, Embeddable } from '../../../../embeddable/public'; +import { Container, Embeddable } from '../../../embeddable/public'; import { ISearchEmbeddable, SearchInput, SearchOutput } from './types'; -import { SavedSearch } from '../../saved_searches'; -import { Adapters, RequestAdapter } from '../../../../inspector/common'; +import { SavedSearch } from '../services/saved_searches'; +import { Adapters, RequestAdapter } from '../../../inspector/common'; import { SEARCH_EMBEDDABLE_TYPE } from './constants'; -import { APPLY_FILTER_TRIGGER, esFilters, FilterManager } from '../../../../data/public'; -import { DiscoverServices } from '../../build_services'; +import { APPLY_FILTER_TRIGGER, esFilters, FilterManager } from '../../../data/public'; +import { DiscoverServices } from '../build_services'; import { Filter, IndexPattern, @@ -25,11 +25,11 @@ import { ISearchSource, Query, TimeRange, -} from '../../../../data/common'; -import { ElasticSearchHit } from '../doc_views/doc_views_types'; +} from '../../../data/common'; +import { ElasticSearchHit } from '../services/doc_views/doc_views_types'; import { SavedSearchEmbeddableComponent } from './saved_search_embeddable_component'; -import { UiActionsStart } from '../../../../ui_actions/public'; -import { getServices } from '../../kibana_services'; +import { UiActionsStart } from '../../../ui_actions/public'; +import { getServices } from '../kibana_services'; import { DOC_HIDE_TIME_COLUMN_SETTING, DOC_TABLE_LEGACY, @@ -37,17 +37,17 @@ import { SEARCH_FIELDS_FROM_SOURCE, SHOW_FIELD_STATISTICS, SORT_DEFAULT_ORDER_SETTING, -} from '../../../common'; -import * as columnActions from '../apps/main/components/doc_table/actions/columns'; -import { handleSourceColumnState } from '../helpers/state_helpers'; +} from '../../common'; +import * as columnActions from '../components/doc_table/actions/columns'; +import { handleSourceColumnState } from '../utils/state_helpers'; import { DiscoverGridProps } from '../components/discover_grid/discover_grid'; import { DiscoverGridSettings } from '../components/discover_grid/types'; -import { DocTableProps } from '../apps/main/components/doc_table/doc_table_wrapper'; -import { getDefaultSort } from '../apps/main/components/doc_table'; -import { SortOrder } from '../apps/main/components/doc_table/components/table_header/helpers'; -import { updateSearchSource } from './helpers/update_search_source'; -import { VIEW_MODE } from '../apps/main/components/view_mode_toggle'; -import { FieldStatsTableEmbeddable } from '../components/data_visualizer_grid/field_stats_table_embeddable'; +import { DocTableProps } from '../components/doc_table/doc_table_wrapper'; +import { getDefaultSort } from '../components/doc_table'; +import { SortOrder } from '../components/doc_table/components/table_header/helpers'; +import { VIEW_MODE } from '../components/view_mode_toggle'; +import { updateSearchSource } from './utils/update_search_source'; +import { FieldStatsTableSavedSearchEmbeddable } from '../application/components/field_stats_table'; export type SearchProps = Partial & Partial & { @@ -391,7 +391,7 @@ export class SavedSearchEmbeddable Array.isArray(searchProps.columns) ) { ReactDOM.render( - { + getSavedSearch(): SavedSearch; +} + +export interface SearchEmbeddable extends Embeddable { + type: string; +} diff --git a/src/plugins/discover/public/embeddable/utils/update_search_source.test.ts b/src/plugins/discover/public/embeddable/utils/update_search_source.test.ts new file mode 100644 index 0000000000000..cd2f6cf88cfa3 --- /dev/null +++ b/src/plugins/discover/public/embeddable/utils/update_search_source.test.ts @@ -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 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 { createSearchSourceMock } from '../../../../data/common/search/search_source/mocks'; +import { updateSearchSource } from './update_search_source'; +import { indexPatternMock } from '../../__mocks__/index_pattern'; +import type { SortOrder } from '../../services/saved_searches'; + +describe('updateSearchSource', () => { + const defaults = { + sampleSize: 50, + defaultSort: 'asc', + }; + + it('updates a given search source', async () => { + const searchSource = createSearchSourceMock({}); + updateSearchSource(searchSource, indexPatternMock, [] as SortOrder[], false, defaults); + expect(searchSource.getField('fields')).toBe(undefined); + // does not explicitly request fieldsFromSource when not using fields API + expect(searchSource.getField('fieldsFromSource')).toBe(undefined); + }); + + it('updates a given search source with the usage of the new fields api', async () => { + const searchSource = createSearchSourceMock({}); + updateSearchSource(searchSource, indexPatternMock, [] as SortOrder[], true, defaults); + expect(searchSource.getField('fields')).toEqual([{ field: '*', include_unmapped: 'true' }]); + expect(searchSource.getField('fieldsFromSource')).toBe(undefined); + }); +}); diff --git a/src/plugins/discover/public/embeddable/utils/update_search_source.ts b/src/plugins/discover/public/embeddable/utils/update_search_source.ts new file mode 100644 index 0000000000000..7e24f96d503e8 --- /dev/null +++ b/src/plugins/discover/public/embeddable/utils/update_search_source.ts @@ -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 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, ISearchSource } from '../../../../data/common'; +import { getSortForSearchSource } from '../../components/doc_table'; +import { SortPairArr } from '../../components/doc_table/lib/get_sort'; + +export const updateSearchSource = ( + searchSource: ISearchSource, + indexPattern: IndexPattern | undefined, + sort: (SortPairArr[] & string[][]) | undefined, + useNewFieldsApi: boolean, + defaults: { + sampleSize: number; + defaultSort: string; + } +) => { + const { sampleSize, defaultSort } = defaults; + searchSource.setField('size', sampleSize); + searchSource.setField('sort', getSortForSearchSource(sort, indexPattern, defaultSort)); + if (useNewFieldsApi) { + searchSource.removeField('fieldsFromSource'); + const fields: Record = { field: '*', include_unmapped: 'true' }; + searchSource.setField('fields', [fields]); + } else { + searchSource.removeField('fields'); + } +}; diff --git a/src/plugins/discover/public/application/embeddable/view_saved_search_action.test.ts b/src/plugins/discover/public/embeddable/view_saved_search_action.test.ts similarity index 89% rename from src/plugins/discover/public/application/embeddable/view_saved_search_action.test.ts rename to src/plugins/discover/public/embeddable/view_saved_search_action.test.ts index 990be8927766a..f0dbc0d1d7a57 100644 --- a/src/plugins/discover/public/application/embeddable/view_saved_search_action.test.ts +++ b/src/plugins/discover/public/embeddable/view_saved_search_action.test.ts @@ -10,15 +10,15 @@ import { ContactCardEmbeddable } from 'src/plugins/embeddable/public/lib/test_sa import { ViewSavedSearchAction } from './view_saved_search_action'; import { SavedSearchEmbeddable } from './saved_search_embeddable'; -import { createStartContractMock } from '../../__mocks__/start_contract'; -import { uiSettingsServiceMock } from '../../../../../core/public/mocks'; -import { savedSearchMock } from '../../__mocks__/saved_search'; -import { discoverServiceMock } from '../../__mocks__/services'; +import { createStartContractMock } from '../__mocks__/start_contract'; +import { uiSettingsServiceMock } from '../../../../core/public/mocks'; +import { savedSearchMock } from '../__mocks__/saved_search'; +import { discoverServiceMock } from '../__mocks__/services'; import { IndexPattern } from 'src/plugins/data/common'; import { createFilterManagerMock } from 'src/plugins/data/public/query/filter_manager/filter_manager.mock'; import { ViewMode } from 'src/plugins/embeddable/public'; -import { setServices } from '../../kibana_services'; -import type { DiscoverServices } from '../../build_services'; +import { setServices } from '../kibana_services'; +import type { DiscoverServices } from '../build_services'; const applicationMock = createStartContractMock(); const savedSearch = savedSearchMock; diff --git a/src/plugins/discover/public/application/embeddable/view_saved_search_action.ts b/src/plugins/discover/public/embeddable/view_saved_search_action.ts similarity index 89% rename from src/plugins/discover/public/application/embeddable/view_saved_search_action.ts rename to src/plugins/discover/public/embeddable/view_saved_search_action.ts index e4b97d011ff64..b9bbc21cfef2c 100644 --- a/src/plugins/discover/public/application/embeddable/view_saved_search_action.ts +++ b/src/plugins/discover/public/embeddable/view_saved_search_action.ts @@ -8,11 +8,11 @@ import { ActionExecutionContext } from 'src/plugins/ui_actions/public'; import { ApplicationStart } from 'kibana/public'; import { i18n } from '@kbn/i18n'; -import { IEmbeddable, ViewMode } from '../../../../embeddable/public'; -import { Action } from '../../../../ui_actions/public'; +import { IEmbeddable, ViewMode } from '../../../embeddable/public'; +import { Action } from '../../../ui_actions/public'; import { SavedSearchEmbeddable } from './saved_search_embeddable'; -import { SEARCH_EMBEDDABLE_TYPE } from '../../../common'; -import { getSavedSearchUrl } from '../../saved_searches'; +import { SEARCH_EMBEDDABLE_TYPE } from '../../common'; +import { getSavedSearchUrl } from '../services/saved_searches'; export const ACTION_VIEW_SAVED_SEARCH = 'ACTION_VIEW_SAVED_SEARCH'; diff --git a/src/plugins/discover/public/index.ts b/src/plugins/discover/public/index.ts index cb7b29afe3f9a..e7abbdfc328ab 100644 --- a/src/plugins/discover/public/index.ts +++ b/src/plugins/discover/public/index.ts @@ -9,23 +9,23 @@ import { PluginInitializerContext } from 'kibana/public'; import { DiscoverPlugin } from './plugin'; -export type { SavedSearch } from './saved_searches'; +export type { SavedSearch } from './services/saved_searches'; export { getSavedSearch, getSavedSearchFullPathUrl, getSavedSearchUrl, getSavedSearchUrlConflictMessage, throwErrorOnSavedSearchUrlConflict, -} from './saved_searches'; +} from './services/saved_searches'; export type { DiscoverSetup, DiscoverStart } from './plugin'; export function plugin(initializerContext: PluginInitializerContext) { return new DiscoverPlugin(initializerContext); } -export type { ISearchEmbeddable, SearchInput } from './application/embeddable'; -export { SEARCH_EMBEDDABLE_TYPE } from './application/embeddable'; -export { loadSharingDataHelpers } from './shared'; +export type { ISearchEmbeddable, SearchInput } from './embeddable'; +export { SEARCH_EMBEDDABLE_TYPE } from './embeddable'; +export { loadSharingDataHelpers } from './utils'; export type { DiscoverUrlGeneratorState } from './url_generator'; export { DISCOVER_APP_URL_GENERATOR } from './url_generator'; diff --git a/src/plugins/discover/public/kibana_services.ts b/src/plugins/discover/public/kibana_services.ts index c68d6fbf479a1..12b0a77a7865d 100644 --- a/src/plugins/discover/public/kibana_services.ts +++ b/src/plugins/discover/public/kibana_services.ts @@ -12,7 +12,7 @@ import type { ScopedHistory, AppMountParameters } from 'kibana/public'; import type { UiActionsStart } from 'src/plugins/ui_actions/public'; import { DiscoverServices } from './build_services'; import { createGetterSetter } from '../../kibana_utils/public'; -import { DocViewsRegistry } from './application/doc_views/doc_views_registry'; +import { DocViewsRegistry } from './services/doc_views/doc_views_registry'; let services: DiscoverServices | null = null; let uiActions: UiActionsStart; diff --git a/src/plugins/discover/public/locator.ts b/src/plugins/discover/public/locator.ts index 40b62841f19d1..569d31664eddb 100644 --- a/src/plugins/discover/public/locator.ts +++ b/src/plugins/discover/public/locator.ts @@ -11,6 +11,7 @@ import type { TimeRange, Filter, Query, QueryState, RefreshInterval } from '../. import type { LocatorDefinition, LocatorPublic } from '../../share/public'; import { esFilters } from '../../data/public'; import { setStateToKbnUrl } from '../../kibana_utils/public'; +import type { VIEW_MODE } from './components/view_mode_toggle'; export const DISCOVER_APP_LOCATOR = 'DISCOVER_APP_LOCATOR'; @@ -75,6 +76,14 @@ export interface DiscoverAppLocatorParams extends SerializableRecord { * id of the used saved query */ savedQuery?: string; + /** + * Table view: Documents vs Field Statistics + */ + viewMode?: VIEW_MODE; + /** + * Hide mini distribution/preview charts when in Field Statistics mode + */ + hideAggregatedPreview?: boolean; } export type DiscoverAppLocator = LocatorPublic; @@ -102,6 +111,8 @@ export class DiscoverAppLocatorDefinition implements LocatorDefinition esFilters.isFilterPinned(f)); if (refreshInterval) queryState.refreshInterval = refreshInterval; + if (viewMode) appState.viewMode = viewMode; + if (hideAggregatedPreview) appState.hideAggregatedPreview = hideAggregatedPreview; let path = `#/${savedSearchPath}`; path = setStateToKbnUrl('_g', queryState, { useHash }, path); diff --git a/src/plugins/discover/public/plugin.tsx b/src/plugins/discover/public/plugin.tsx index c91bcf3897e14..ec95a82a5088e 100644 --- a/src/plugins/discover/public/plugin.tsx +++ b/src/plugins/discover/public/plugin.tsx @@ -23,7 +23,6 @@ import { EmbeddableStart, EmbeddableSetup } from 'src/plugins/embeddable/public' import { ChartsPluginStart } from 'src/plugins/charts/public'; import { NavigationPublicPluginStart as NavigationStart } from 'src/plugins/navigation/public'; import { SharePluginStart, SharePluginSetup, UrlGeneratorContract } from 'src/plugins/share/public'; -import { KibanaLegacySetup, KibanaLegacyStart } from 'src/plugins/kibana_legacy/public'; import { UrlForwardingSetup, UrlForwardingStart } from 'src/plugins/url_forwarding/public'; import { HomePublicPluginSetup } from 'src/plugins/home/public'; import { Start as InspectorPublicPluginStart } from 'src/plugins/inspector/public'; @@ -33,8 +32,8 @@ import { SavedObjectsStart } from '../../saved_objects/public'; import { createKbnUrlTracker } from '../../kibana_utils/public'; import { DEFAULT_APP_CATEGORIES } from '../../../core/public'; import { UrlGeneratorState } from '../../share/public'; -import { DocViewInput, DocViewInputFn } from './application/doc_views/doc_views_types'; -import { DocViewsRegistry } from './application/doc_views/doc_views_registry'; +import { DocViewInput, DocViewInputFn } from './services/doc_views/doc_views_types'; +import { DocViewsRegistry } from './services/doc_views/doc_views_registry'; import { setDocViewsRegistry, setUrlTracker, @@ -54,14 +53,16 @@ import { SEARCH_SESSION_ID_QUERY_PARAM, } from './url_generator'; import { DiscoverAppLocatorDefinition, DiscoverAppLocator } from './locator'; -import { SearchEmbeddableFactory } from './application/embeddable'; +import { SearchEmbeddableFactory } from './embeddable'; import { UsageCollectionSetup } from '../../usage_collection/public'; import { replaceUrlHashQuery } from '../../kibana_utils/public/'; import { IndexPatternFieldEditorStart } from '../../../plugins/index_pattern_field_editor/public'; -import { DeferredSpinner } from './shared'; -import { ViewSavedSearchAction } from './application/embeddable/view_saved_search_action'; +import { DeferredSpinner } from './components'; +import { ViewSavedSearchAction } from './embeddable/view_saved_search_action'; import type { SpacesPluginStart } from '../../../../x-pack/plugins/spaces/public'; import { FieldFormatsStart } from '../../field_formats/public'; +import { injectTruncateStyles } from './utils/truncate_styles'; +import { TRUNCATE_MAX_HEIGHT } from '../common'; declare module '../../share/public' { export interface UrlGeneratorStateMapping { @@ -69,11 +70,8 @@ declare module '../../share/public' { } } -const DocViewerTable = React.lazy(() => import('./application/components/table/table')); - -const SourceViewer = React.lazy( - () => import('./application/components/source_viewer/source_viewer') -); +const DocViewerTable = React.lazy(() => import('./services/doc_views/components/doc_viewer_table')); +const SourceViewer = React.lazy(() => import('./services/doc_views/components/doc_viewer_source')); /** * @public @@ -166,7 +164,6 @@ export interface DiscoverSetupPlugins { share?: SharePluginSetup; uiActions: UiActionsSetup; embeddable: EmbeddableSetup; - kibanaLegacy: KibanaLegacySetup; urlForwarding: UrlForwardingSetup; home?: HomePublicPluginSetup; data: DataPublicPluginSetup; @@ -183,7 +180,6 @@ export interface DiscoverStartPlugins { data: DataPublicPluginStart; fieldFormats: FieldFormatsStart; share?: SharePluginStart; - kibanaLegacy: KibanaLegacyStart; urlForwarding: UrlForwardingStart; inspector: InspectorPublicPluginStart; savedObjects: SavedObjectsStart; @@ -348,11 +344,9 @@ export class DiscoverPlugin await depsStart.data.indexPatterns.clearCache(); const { renderApp } = await import('./application'); - // FIXME: Temporarily hide overflow-y in Discover app when Field Stats table is shown // due to EUI bug https://github.com/elastic/eui/pull/5152 params.element.classList.add('dscAppWrapper'); - const unmount = renderApp(params.element); return () => { unlistenParentHistory(); @@ -413,6 +407,8 @@ export class DiscoverPlugin const services = buildServices(core, plugins, this.initializerContext); setServices(services); + injectTruncateStyles(services.uiSettings.get(TRUNCATE_MAX_HEIGHT)); + return { urlGenerator: this.urlGenerator, locator: this.locator, diff --git a/src/plugins/discover/public/saved_searches/types.ts b/src/plugins/discover/public/saved_searches/types.ts deleted file mode 100644 index b3a67ea57e769..0000000000000 --- a/src/plugins/discover/public/saved_searches/types.ts +++ /dev/null @@ -1,52 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import type { ISearchSource } from '../../../data/public'; -import { DiscoverGridSettingsColumn } from '../application/components/discover_grid/types'; -import { VIEW_MODE } from '../application/apps/main/components/view_mode_toggle'; - -/** @internal **/ -export interface SavedSearchAttributes { - title: string; - sort: Array<[string, string]>; - columns: string[]; - description: string; - grid: { - columns?: Record; - }; - hideChart: boolean; - kibanaSavedObjectMeta: { - searchSourceJSON: string; - }; - viewMode?: VIEW_MODE; - hideAggregatedPreview?: boolean; -} - -/** @internal **/ -export type SortOrder = [string, string]; - -/** @public **/ -export interface SavedSearch { - searchSource: ISearchSource; - id?: string; - title?: string; - sort?: SortOrder[]; - columns?: string[]; - description?: string; - grid?: { - columns?: Record; - }; - hideChart?: boolean; - sharingSavedObjectProps?: { - outcome?: 'aliasMatch' | 'exactMatch' | 'conflict'; - aliasTargetId?: string; - errorJSON?: string; - }; - viewMode?: VIEW_MODE; - hideAggregatedPreview?: boolean; -} diff --git a/src/plugins/discover/public/application/components/doc_viewer/__snapshots__/doc_viewer.test.tsx.snap b/src/plugins/discover/public/services/doc_views/components/doc_viewer/__snapshots__/doc_viewer.test.tsx.snap similarity index 100% rename from src/plugins/discover/public/application/components/doc_viewer/__snapshots__/doc_viewer.test.tsx.snap rename to src/plugins/discover/public/services/doc_views/components/doc_viewer/__snapshots__/doc_viewer.test.tsx.snap diff --git a/src/plugins/discover/public/application/components/doc_viewer/__snapshots__/doc_viewer_render_tab.test.tsx.snap b/src/plugins/discover/public/services/doc_views/components/doc_viewer/__snapshots__/doc_viewer_render_tab.test.tsx.snap similarity index 100% rename from src/plugins/discover/public/application/components/doc_viewer/__snapshots__/doc_viewer_render_tab.test.tsx.snap rename to src/plugins/discover/public/services/doc_views/components/doc_viewer/__snapshots__/doc_viewer_render_tab.test.tsx.snap diff --git a/src/plugins/discover/public/application/components/doc_viewer/doc_viewer.scss b/src/plugins/discover/public/services/doc_views/components/doc_viewer/doc_viewer.scss similarity index 100% rename from src/plugins/discover/public/application/components/doc_viewer/doc_viewer.scss rename to src/plugins/discover/public/services/doc_views/components/doc_viewer/doc_viewer.scss diff --git a/src/plugins/discover/public/application/components/doc_viewer/doc_viewer.test.tsx b/src/plugins/discover/public/services/doc_views/components/doc_viewer/doc_viewer.test.tsx similarity index 93% rename from src/plugins/discover/public/application/components/doc_viewer/doc_viewer.test.tsx rename to src/plugins/discover/public/services/doc_views/components/doc_viewer/doc_viewer.test.tsx index de0353a020a67..bedf1047bc4ec 100644 --- a/src/plugins/discover/public/application/components/doc_viewer/doc_viewer.test.tsx +++ b/src/plugins/discover/public/services/doc_views/components/doc_viewer/doc_viewer.test.tsx @@ -10,10 +10,10 @@ import React from 'react'; import { mount, shallow } from 'enzyme'; import { DocViewer } from './doc_viewer'; import { findTestSubject } from '@elastic/eui/lib/test'; -import { getDocViewsRegistry } from '../../../kibana_services'; -import { DocViewRenderProps } from '../../doc_views/doc_views_types'; +import { getDocViewsRegistry } from '../../../../kibana_services'; +import { DocViewRenderProps } from '../../doc_views_types'; -jest.mock('../../../kibana_services', () => { +jest.mock('../../../../kibana_services', () => { // eslint-disable-next-line @typescript-eslint/no-explicit-any let registry: any[] = []; return { diff --git a/src/plugins/discover/public/application/components/doc_viewer/doc_viewer.tsx b/src/plugins/discover/public/services/doc_views/components/doc_viewer/doc_viewer.tsx similarity index 91% rename from src/plugins/discover/public/application/components/doc_viewer/doc_viewer.tsx rename to src/plugins/discover/public/services/doc_views/components/doc_viewer/doc_viewer.tsx index d0476d4c38b48..9b627b1569275 100644 --- a/src/plugins/discover/public/application/components/doc_viewer/doc_viewer.tsx +++ b/src/plugins/discover/public/services/doc_views/components/doc_viewer/doc_viewer.tsx @@ -9,9 +9,9 @@ import './doc_viewer.scss'; import React from 'react'; import { EuiTabbedContent } from '@elastic/eui'; -import { getDocViewsRegistry } from '../../../kibana_services'; +import { getDocViewsRegistry } from '../../../../kibana_services'; import { DocViewerTab } from './doc_viewer_tab'; -import { DocView, DocViewRenderProps } from '../../doc_views/doc_views_types'; +import { DocView, DocViewRenderProps } from '../../doc_views_types'; /** * Rendering tabs with different views of 1 Elasticsearch hit in Discover. diff --git a/src/plugins/discover/public/application/components/doc_viewer/doc_viewer_render_error.tsx b/src/plugins/discover/public/services/doc_views/components/doc_viewer/doc_viewer_render_error.tsx similarity index 100% rename from src/plugins/discover/public/application/components/doc_viewer/doc_viewer_render_error.tsx rename to src/plugins/discover/public/services/doc_views/components/doc_viewer/doc_viewer_render_error.tsx diff --git a/src/plugins/discover/public/application/components/doc_viewer/doc_viewer_render_tab.test.tsx b/src/plugins/discover/public/services/doc_views/components/doc_viewer/doc_viewer_render_tab.test.tsx similarity index 92% rename from src/plugins/discover/public/application/components/doc_viewer/doc_viewer_render_tab.test.tsx rename to src/plugins/discover/public/services/doc_views/components/doc_viewer/doc_viewer_render_tab.test.tsx index de76074357f63..5c61938a9e830 100644 --- a/src/plugins/discover/public/application/components/doc_viewer/doc_viewer_render_tab.test.tsx +++ b/src/plugins/discover/public/services/doc_views/components/doc_viewer/doc_viewer_render_tab.test.tsx @@ -9,7 +9,7 @@ import React from 'react'; import { mount } from 'enzyme'; import { DocViewRenderTab } from './doc_viewer_render_tab'; -import { DocViewRenderProps } from '../../doc_views/doc_views_types'; +import { DocViewRenderProps } from '../../doc_views_types'; test('Mounting and unmounting DocViewerRenderTab', () => { const unmountFn = jest.fn(); diff --git a/src/plugins/discover/public/application/components/doc_viewer/doc_viewer_render_tab.tsx b/src/plugins/discover/public/services/doc_views/components/doc_viewer/doc_viewer_render_tab.tsx similarity index 97% rename from src/plugins/discover/public/application/components/doc_viewer/doc_viewer_render_tab.tsx rename to src/plugins/discover/public/services/doc_views/components/doc_viewer/doc_viewer_render_tab.tsx index a6967cac8cdcc..257f40a9850a1 100644 --- a/src/plugins/discover/public/application/components/doc_viewer/doc_viewer_render_tab.tsx +++ b/src/plugins/discover/public/services/doc_views/components/doc_viewer/doc_viewer_render_tab.tsx @@ -7,7 +7,7 @@ */ import React, { useRef, useEffect } from 'react'; -import { DocViewRenderFn, DocViewRenderProps } from '../../doc_views/doc_views_types'; +import { DocViewRenderFn, DocViewRenderProps } from '../../doc_views_types'; interface Props { render: DocViewRenderFn; diff --git a/src/plugins/discover/public/application/components/doc_viewer/doc_viewer_tab.test.tsx b/src/plugins/discover/public/services/doc_views/components/doc_viewer/doc_viewer_tab.test.tsx similarity index 90% rename from src/plugins/discover/public/application/components/doc_viewer/doc_viewer_tab.test.tsx rename to src/plugins/discover/public/services/doc_views/components/doc_viewer/doc_viewer_tab.test.tsx index 188deba755445..3537699e4fe20 100644 --- a/src/plugins/discover/public/application/components/doc_viewer/doc_viewer_tab.test.tsx +++ b/src/plugins/discover/public/services/doc_views/components/doc_viewer/doc_viewer_tab.test.tsx @@ -9,8 +9,8 @@ import React from 'react'; import { shallow } from 'enzyme'; import { DocViewerTab } from './doc_viewer_tab'; -import { ElasticSearchHit } from '../../doc_views/doc_views_types'; -import { indexPatternMock } from '../../../__mocks__/index_pattern'; +import { ElasticSearchHit } from '../../doc_views_types'; +import { indexPatternMock } from '../../../../__mocks__/index_pattern'; describe('DocViewerTab', () => { test('changing columns triggers an update', () => { diff --git a/src/plugins/discover/public/application/components/doc_viewer/doc_viewer_tab.tsx b/src/plugins/discover/public/services/doc_views/components/doc_viewer/doc_viewer_tab.tsx similarity index 94% rename from src/plugins/discover/public/application/components/doc_viewer/doc_viewer_tab.tsx rename to src/plugins/discover/public/services/doc_views/components/doc_viewer/doc_viewer_tab.tsx index 75ec5a62e9299..f43e445737820 100644 --- a/src/plugins/discover/public/application/components/doc_viewer/doc_viewer_tab.tsx +++ b/src/plugins/discover/public/services/doc_views/components/doc_viewer/doc_viewer_tab.tsx @@ -10,9 +10,9 @@ import React from 'react'; import { isEqual } from 'lodash'; import { DocViewRenderTab } from './doc_viewer_render_tab'; import { DocViewerError } from './doc_viewer_render_error'; -import { DocViewRenderFn, DocViewRenderProps } from '../../doc_views/doc_views_types'; -import { getServices } from '../../../kibana_services'; -import { KibanaContextProvider } from '../../../../../kibana_react/public'; +import { DocViewRenderFn, DocViewRenderProps } from '../../doc_views_types'; +import { getServices } from '../../../../kibana_services'; +import { KibanaContextProvider } from '../../../../../../kibana_react/public'; interface Props { id: number; diff --git a/src/plugins/discover/public/services/doc_views/components/doc_viewer/index.ts b/src/plugins/discover/public/services/doc_views/components/doc_viewer/index.ts new file mode 100644 index 0000000000000..3801c9b2de669 --- /dev/null +++ b/src/plugins/discover/public/services/doc_views/components/doc_viewer/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 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 { DocViewer } from './doc_viewer'; diff --git a/src/plugins/discover/public/services/doc_views/components/doc_viewer_source/__snapshots__/source.test.tsx.snap b/src/plugins/discover/public/services/doc_views/components/doc_viewer_source/__snapshots__/source.test.tsx.snap new file mode 100644 index 0000000000000..352006ba1c83b --- /dev/null +++ b/src/plugins/discover/public/services/doc_views/components/doc_viewer_source/__snapshots__/source.test.tsx.snap @@ -0,0 +1,777 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Source Viewer component renders error state 1`] = ` + + + Could not fetch data at this time. Refresh the tab to try again. + + + Refresh + +
    + } + iconType="alert" + title={ +

    + An Error Occurred +

    + } + > +
    + + + + +
    + + +

    + An Error Occurred +

    +
    + + + +
    + + +
    +
    + Could not fetch data at this time. Refresh the tab to try again. + +
    + + + + + + +
    +
    + + + +
    + + +`; + +exports[`Source Viewer component renders json code editor 1`] = ` + + + + +
    + +
    + +
    + +
    + + + + + + + + + +
    +
    + + +
    + + + } + > + +
    + +
    +
    +
    +
    +
    +
    +
    +
    + + + + +`; + +exports[`Source Viewer component renders loading state 1`] = ` + +
    + + + + +
    + +
    + + Loading JSON + +
    +
    +
    +
    +
    +
    +`; diff --git a/src/plugins/discover/public/services/doc_views/components/doc_viewer_source/index.ts b/src/plugins/discover/public/services/doc_views/components/doc_viewer_source/index.ts new file mode 100644 index 0000000000000..2611bb6f0c4b9 --- /dev/null +++ b/src/plugins/discover/public/services/doc_views/components/doc_viewer_source/index.ts @@ -0,0 +1,12 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 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 { DocViewerSource } from './source'; + +// Required for usage in React.lazy +// eslint-disable-next-line import/no-default-export +export default DocViewerSource; diff --git a/src/plugins/discover/public/application/components/source_viewer/source_viewer.scss b/src/plugins/discover/public/services/doc_views/components/doc_viewer_source/source.scss similarity index 100% rename from src/plugins/discover/public/application/components/source_viewer/source_viewer.scss rename to src/plugins/discover/public/services/doc_views/components/doc_viewer_source/source.scss diff --git a/src/plugins/discover/public/services/doc_views/components/doc_viewer_source/source.test.tsx b/src/plugins/discover/public/services/doc_views/components/doc_viewer_source/source.test.tsx new file mode 100644 index 0000000000000..9b5b27fa979aa --- /dev/null +++ b/src/plugins/discover/public/services/doc_views/components/doc_viewer_source/source.test.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 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 type { IndexPattern } from 'src/plugins/data/common'; +import { mountWithIntl } from '@kbn/test/jest'; +import { DocViewerSource } from './source'; +import * as hooks from '../../../../utils/use_es_doc_search'; +import * as useUiSettingHook from 'src/plugins/kibana_react/public/ui_settings/use_ui_setting'; +import { EuiButton, EuiEmptyPrompt, EuiLoadingSpinner } from '@elastic/eui'; +import { JsonCodeEditorCommon } from '../../../../components/json_code_editor/json_code_editor_common'; + +jest.mock('../../../../kibana_services', () => ({ + getServices: jest.fn(), +})); + +import { getServices } from '../../../../kibana_services'; + +const mockIndexPattern = { + getComputedFields: () => [], +} as never; +const getMock = jest.fn(() => Promise.resolve(mockIndexPattern)); +const mockIndexPatternService = { + get: getMock, +} as unknown as IndexPattern; + +(getServices as jest.Mock).mockImplementation(() => ({ + uiSettings: { + get: (key: string) => { + if (key === 'discover:useNewFieldsApi') { + return true; + } + }, + }, + data: { + indexPatternService: mockIndexPatternService, + }, +})); +describe('Source Viewer component', () => { + test('renders loading state', () => { + jest.spyOn(hooks, 'useEsDocSearch').mockImplementation(() => [0, null, () => {}]); + + const comp = mountWithIntl( + + ); + expect(comp).toMatchSnapshot(); + const loadingIndicator = comp.find(EuiLoadingSpinner); + expect(loadingIndicator).not.toBe(null); + }); + + test('renders error state', () => { + jest.spyOn(hooks, 'useEsDocSearch').mockImplementation(() => [3, null, () => {}]); + + const comp = mountWithIntl( + + ); + expect(comp).toMatchSnapshot(); + const errorPrompt = comp.find(EuiEmptyPrompt); + expect(errorPrompt.length).toBe(1); + const refreshButton = comp.find(EuiButton); + expect(refreshButton.length).toBe(1); + }); + + test('renders json code editor', () => { + const mockHit = { + _index: 'logstash-2014.09.09', + _type: 'doc', + _id: 'id123', + _score: 1, + _source: { + message: 'Lorem ipsum dolor sit amet', + extension: 'html', + not_mapped: 'yes', + bytes: 100, + objectArray: [{ foo: true }], + relatedContent: { + test: 1, + }, + scripted: 123, + _underscore: 123, + }, + } as never; + jest.spyOn(hooks, 'useEsDocSearch').mockImplementation(() => [2, mockHit, () => {}]); + jest.spyOn(useUiSettingHook, 'useUiSetting').mockImplementation(() => { + return false; + }); + const comp = mountWithIntl( + + ); + expect(comp).toMatchSnapshot(); + const jsonCodeEditor = comp.find(JsonCodeEditorCommon); + expect(jsonCodeEditor).not.toBe(null); + }); +}); diff --git a/src/plugins/discover/public/services/doc_views/components/doc_viewer_source/source.tsx b/src/plugins/discover/public/services/doc_views/components/doc_viewer_source/source.tsx new file mode 100644 index 0000000000000..c0475a24489c7 --- /dev/null +++ b/src/plugins/discover/public/services/doc_views/components/doc_viewer_source/source.tsx @@ -0,0 +1,124 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 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 './source.scss'; + +import React, { useEffect, useState } from 'react'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { monaco } from '@kbn/monaco'; +import { EuiButton, EuiEmptyPrompt, EuiLoadingSpinner, EuiSpacer, EuiText } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { JSONCodeEditorCommonMemoized } from '../../../../components/json_code_editor/json_code_editor_common'; +import { getServices } from '../../../../kibana_services'; +import { SEARCH_FIELDS_FROM_SOURCE } from '../../../../../common'; +import { useEsDocSearch } from '../../../../utils/use_es_doc_search'; +import { IndexPattern } from '../../../../../../data_views/common'; +import { ElasticRequestState } from '../../../../application/doc/types'; + +interface SourceViewerProps { + id: string; + index: string; + indexPattern: IndexPattern; + hasLineNumbers: boolean; + width?: number; +} + +export const DocViewerSource = ({ + id, + index, + indexPattern, + width, + hasLineNumbers, +}: SourceViewerProps) => { + const [editor, setEditor] = useState(); + const [jsonValue, setJsonValue] = useState(''); + const useNewFieldsApi = !getServices().uiSettings.get(SEARCH_FIELDS_FROM_SOURCE); + const [reqState, hit, requestData] = useEsDocSearch({ + id, + index, + indexPattern, + requestSource: useNewFieldsApi, + }); + + useEffect(() => { + if (reqState === ElasticRequestState.Found && hit) { + setJsonValue(JSON.stringify(hit, undefined, 2)); + } + }, [reqState, hit]); + + // setting editor height based on lines height and count to stretch and fit its content + useEffect(() => { + if (!editor) { + return; + } + const editorElement = editor.getDomNode(); + + if (!editorElement) { + return; + } + + const lineHeight = editor.getOption(monaco.editor.EditorOption.lineHeight); + const lineCount = editor.getModel()?.getLineCount() || 1; + const height = editor.getTopForLineNumber(lineCount + 1) + lineHeight; + if (!jsonValue || jsonValue === '') { + editorElement.style.height = '0px'; + } else { + editorElement.style.height = `${height}px`; + } + editor.layout(); + }, [editor, jsonValue]); + + const loadingState = ( +
    + + + + +
    + ); + + const errorMessageTitle = ( +

    + {i18n.translate('discover.sourceViewer.errorMessageTitle', { + defaultMessage: 'An Error Occurred', + })} +

    + ); + const errorMessage = ( +
    + {i18n.translate('discover.sourceViewer.errorMessage', { + defaultMessage: 'Could not fetch data at this time. Refresh the tab to try again.', + })} + + + {i18n.translate('discover.sourceViewer.refresh', { + defaultMessage: 'Refresh', + })} + +
    + ); + const errorState = ( + + ); + + if (reqState === ElasticRequestState.Error || reqState === ElasticRequestState.NotFound) { + return errorState; + } + + if (reqState === ElasticRequestState.Loading || jsonValue === '') { + return loadingState; + } + + return ( + setEditor(editorNode)} + /> + ); +}; diff --git a/src/plugins/discover/public/services/doc_views/components/doc_viewer_table/index.ts b/src/plugins/discover/public/services/doc_views/components/doc_viewer_table/index.ts new file mode 100644 index 0000000000000..cfda31e12bb64 --- /dev/null +++ b/src/plugins/discover/public/services/doc_views/components/doc_viewer_table/index.ts @@ -0,0 +1,12 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 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 { DocViewerTable } from './table'; + +// Required for usage in React.lazy +// eslint-disable-next-line import/no-default-export +export default DocViewerTable; diff --git a/src/plugins/discover/public/application/components/table/table.test.tsx b/src/plugins/discover/public/services/doc_views/components/doc_viewer_table/table.test.tsx similarity index 97% rename from src/plugins/discover/public/application/components/table/table.test.tsx rename to src/plugins/discover/public/services/doc_views/components/doc_viewer_table/table.test.tsx index e61333cce1166..5133ca46015a0 100644 --- a/src/plugins/discover/public/application/components/table/table.test.tsx +++ b/src/plugins/discover/public/services/doc_views/components/doc_viewer_table/table.test.tsx @@ -10,14 +10,14 @@ import React from 'react'; import { mountWithIntl } from '@kbn/test/jest'; import { findTestSubject } from '@elastic/eui/lib/test'; import { DocViewerTable, DocViewerTableProps } from './table'; -import { IndexPattern } from '../../../../../data/public'; -import { ElasticSearchHit } from '../../doc_views/doc_views_types'; +import { IndexPattern } from '../../../../../../data/public'; +import { ElasticSearchHit } from '../../doc_views_types'; -jest.mock('../../../kibana_services', () => ({ +jest.mock('../../../../kibana_services', () => ({ getServices: jest.fn(), })); -import { getServices } from '../../../kibana_services'; +import { getServices } from '../../../../kibana_services'; (getServices as jest.Mock).mockImplementation(() => ({ uiSettings: { @@ -236,7 +236,7 @@ describe('DocViewTable at Discover Context', () => { const btn = findTestSubject(component, `collapseBtn`); const html = component.html(); - expect(component.html()).toContain('truncate-by-height'); + expect(component.html()).toContain('dscTruncateByHeight'); expect(btn.length).toBe(1); btn.simulate('click'); diff --git a/src/plugins/discover/public/application/components/table/table.tsx b/src/plugins/discover/public/services/doc_views/components/doc_viewer_table/table.tsx similarity index 85% rename from src/plugins/discover/public/application/components/table/table.tsx rename to src/plugins/discover/public/services/doc_views/components/doc_viewer_table/table.tsx index 78a6d9ddd3237..08cd0306f8759 100644 --- a/src/plugins/discover/public/application/components/table/table.tsx +++ b/src/plugins/discover/public/services/doc_views/components/doc_viewer_table/table.tsx @@ -8,20 +8,16 @@ import React, { useCallback, useMemo } from 'react'; import { EuiInMemoryTable } from '@elastic/eui'; -import { IndexPattern, IndexPatternField } from '../../../../../data/public'; -import { flattenHit } from '../../../../../data/common'; -import { SHOW_MULTIFIELDS } from '../../../../common'; -import { getServices } from '../../../kibana_services'; -import { isNestedFieldParent } from '../../apps/main/utils/nested_fields'; -import { - DocViewFilterFn, - ElasticSearchHit, - DocViewRenderProps, -} from '../../doc_views/doc_views_types'; +import { IndexPattern, IndexPatternField } from '../../../../../../data/public'; +import { flattenHit } from '../../../../../../data/common'; +import { SHOW_MULTIFIELDS } from '../../../../../common'; +import { getServices } from '../../../../kibana_services'; +import { DocViewFilterFn, ElasticSearchHit, DocViewRenderProps } from '../../doc_views_types'; import { ACTIONS_COLUMN, MAIN_COLUMNS } from './table_columns'; -import { getFieldsToShow } from '../../helpers/get_fields_to_show'; -import { getIgnoredReason, IgnoredReason } from '../../helpers/get_ignored_reason'; -import { formatFieldValue } from '../../helpers/format_value'; +import { getFieldsToShow } from '../../../../utils/get_fields_to_show'; +import { getIgnoredReason, IgnoredReason } from '../../../../utils/get_ignored_reason'; +import { formatFieldValue } from '../../../../utils/format_value'; +import { isNestedFieldParent } from '../../../../application/main/utils/nested_fields'; export interface DocViewerTableProps { columns?: string[]; @@ -151,7 +147,3 @@ export const DocViewerTable = ({ /> ); }; - -// Required for usage in React.lazy -// eslint-disable-next-line import/no-default-export -export default DocViewerTable; diff --git a/src/plugins/discover/public/application/components/table/table_cell_actions.tsx b/src/plugins/discover/public/services/doc_views/components/doc_viewer_table/table_cell_actions.tsx similarity index 93% rename from src/plugins/discover/public/application/components/table/table_cell_actions.tsx rename to src/plugins/discover/public/services/doc_views/components/doc_viewer_table/table_cell_actions.tsx index e43a17448de2e..05a7056cf07e6 100644 --- a/src/plugins/discover/public/application/components/table/table_cell_actions.tsx +++ b/src/plugins/discover/public/services/doc_views/components/doc_viewer_table/table_cell_actions.tsx @@ -11,8 +11,8 @@ import { DocViewTableRowBtnFilterRemove } from './table_row_btn_filter_remove'; import { DocViewTableRowBtnFilterExists } from './table_row_btn_filter_exists'; import { DocViewTableRowBtnToggleColumn } from './table_row_btn_toggle_column'; import { DocViewTableRowBtnFilterAdd } from './table_row_btn_filter_add'; -import { IndexPatternField } from '../../../../../data/public'; -import { DocViewFilterFn } from '../../doc_views/doc_views_types'; +import { IndexPatternField } from '../../../../../../data/public'; +import { DocViewFilterFn } from '../../doc_views_types'; interface TableActionsProps { field: string; diff --git a/src/plugins/discover/public/application/components/table/table_cell_value.tsx b/src/plugins/discover/public/services/doc_views/components/doc_viewer_table/table_cell_value.tsx similarity index 97% rename from src/plugins/discover/public/application/components/table/table_cell_value.tsx rename to src/plugins/discover/public/services/doc_views/components/doc_viewer_table/table_cell_value.tsx index e006de1cd7aeb..88d6b30cc633e 100644 --- a/src/plugins/discover/public/application/components/table/table_cell_value.tsx +++ b/src/plugins/discover/public/services/doc_views/components/doc_viewer_table/table_cell_value.tsx @@ -11,7 +11,7 @@ import { EuiFlexGroup, EuiFlexItem, EuiIcon, EuiTextColor, EuiToolTip } from '@e import classNames from 'classnames'; import React, { Fragment, useState } from 'react'; import { i18n } from '@kbn/i18n'; -import { IgnoredReason } from '../../helpers/get_ignored_reason'; +import { IgnoredReason } from '../../../../utils/get_ignored_reason'; import { FieldRecord } from './table'; import { DocViewTableRowBtnCollapse } from './table_row_btn_collapse'; @@ -104,7 +104,7 @@ export const TableFieldValue = ({ const valueClassName = classNames({ // eslint-disable-next-line @typescript-eslint/naming-convention kbnDocViewer__value: true, - 'truncate-by-height': isCollapsible && isCollapsed, + dscTruncateByHeight: isCollapsible && isCollapsed, }); const onToggleCollapse = () => setFieldOpen((fieldOpenPrev) => !fieldOpenPrev); diff --git a/src/plugins/discover/public/application/components/table/table_columns.tsx b/src/plugins/discover/public/services/doc_views/components/doc_viewer_table/table_columns.tsx similarity index 97% rename from src/plugins/discover/public/application/components/table/table_columns.tsx rename to src/plugins/discover/public/services/doc_views/components/doc_viewer_table/table_columns.tsx index 9f502b4491977..0cd6a861b715b 100644 --- a/src/plugins/discover/public/application/components/table/table_columns.tsx +++ b/src/plugins/discover/public/services/doc_views/components/doc_viewer_table/table_columns.tsx @@ -9,7 +9,7 @@ import { EuiBasicTableColumn, EuiText } from '@elastic/eui'; import React from 'react'; import { FormattedMessage } from '@kbn/i18n/react'; -import { FieldName } from '../field_name/field_name'; +import { FieldName } from '../../../../components/field_name/field_name'; import { FieldRecord } from './table'; import { TableActions } from './table_cell_actions'; import { TableFieldValue } from './table_cell_value'; diff --git a/src/plugins/discover/public/application/components/table/table_row_btn_collapse.tsx b/src/plugins/discover/public/services/doc_views/components/doc_viewer_table/table_row_btn_collapse.tsx similarity index 100% rename from src/plugins/discover/public/application/components/table/table_row_btn_collapse.tsx rename to src/plugins/discover/public/services/doc_views/components/doc_viewer_table/table_row_btn_collapse.tsx diff --git a/src/plugins/discover/public/application/components/table/table_row_btn_filter_add.tsx b/src/plugins/discover/public/services/doc_views/components/doc_viewer_table/table_row_btn_filter_add.tsx similarity index 100% rename from src/plugins/discover/public/application/components/table/table_row_btn_filter_add.tsx rename to src/plugins/discover/public/services/doc_views/components/doc_viewer_table/table_row_btn_filter_add.tsx diff --git a/src/plugins/discover/public/application/components/table/table_row_btn_filter_exists.tsx b/src/plugins/discover/public/services/doc_views/components/doc_viewer_table/table_row_btn_filter_exists.tsx similarity index 100% rename from src/plugins/discover/public/application/components/table/table_row_btn_filter_exists.tsx rename to src/plugins/discover/public/services/doc_views/components/doc_viewer_table/table_row_btn_filter_exists.tsx diff --git a/src/plugins/discover/public/application/components/table/table_row_btn_filter_remove.tsx b/src/plugins/discover/public/services/doc_views/components/doc_viewer_table/table_row_btn_filter_remove.tsx similarity index 100% rename from src/plugins/discover/public/application/components/table/table_row_btn_filter_remove.tsx rename to src/plugins/discover/public/services/doc_views/components/doc_viewer_table/table_row_btn_filter_remove.tsx index 4a14c269901d4..9875eb411d0e4 100644 --- a/src/plugins/discover/public/application/components/table/table_row_btn_filter_remove.tsx +++ b/src/plugins/discover/public/services/doc_views/components/doc_viewer_table/table_row_btn_filter_remove.tsx @@ -8,8 +8,8 @@ import React from 'react'; import { FormattedMessage } from '@kbn/i18n/react'; -import { EuiToolTip, EuiButtonIcon } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; +import { EuiToolTip, EuiButtonIcon } from '@elastic/eui'; export interface Props { onClick: () => void; diff --git a/src/plugins/discover/public/application/components/table/table_row_btn_toggle_column.tsx b/src/plugins/discover/public/services/doc_views/components/doc_viewer_table/table_row_btn_toggle_column.tsx similarity index 100% rename from src/plugins/discover/public/application/components/table/table_row_btn_toggle_column.tsx rename to src/plugins/discover/public/services/doc_views/components/doc_viewer_table/table_row_btn_toggle_column.tsx index cd5f73d47b1a1..dfaf6986022cc 100644 --- a/src/plugins/discover/public/application/components/table/table_row_btn_toggle_column.tsx +++ b/src/plugins/discover/public/services/doc_views/components/doc_viewer_table/table_row_btn_toggle_column.tsx @@ -8,8 +8,8 @@ import React from 'react'; import { FormattedMessage } from '@kbn/i18n/react'; -import { EuiToolTip, EuiButtonIcon } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; +import { EuiToolTip, EuiButtonIcon } from '@elastic/eui'; export interface Props { active: boolean; diff --git a/src/plugins/discover/public/application/doc_views/doc_views_registry.ts b/src/plugins/discover/public/services/doc_views/doc_views_registry.ts similarity index 100% rename from src/plugins/discover/public/application/doc_views/doc_views_registry.ts rename to src/plugins/discover/public/services/doc_views/doc_views_registry.ts diff --git a/src/plugins/discover/public/application/doc_views/doc_views_types.ts b/src/plugins/discover/public/services/doc_views/doc_views_types.ts similarity index 100% rename from src/plugins/discover/public/application/doc_views/doc_views_types.ts rename to src/plugins/discover/public/services/doc_views/doc_views_types.ts diff --git a/src/plugins/discover/public/saved_searches/constants.ts b/src/plugins/discover/public/services/saved_searches/constants.ts similarity index 100% rename from src/plugins/discover/public/saved_searches/constants.ts rename to src/plugins/discover/public/services/saved_searches/constants.ts diff --git a/src/plugins/discover/public/saved_searches/get_saved_searches.test.ts b/src/plugins/discover/public/services/saved_searches/get_saved_searches.test.ts similarity index 93% rename from src/plugins/discover/public/saved_searches/get_saved_searches.test.ts rename to src/plugins/discover/public/services/saved_searches/get_saved_searches.test.ts index 560e16b12e5ed..1159320c9a09f 100644 --- a/src/plugins/discover/public/saved_searches/get_saved_searches.test.ts +++ b/src/plugins/discover/public/services/saved_searches/get_saved_searches.test.ts @@ -5,11 +5,11 @@ * in compliance with, at your election, the Elastic License 2.0 or the Server * Side Public License, v 1. */ -import type { SavedObjectsStart } from '../../../../core/public'; -import type { DataPublicPluginStart } from '../../../data/public'; +import type { SavedObjectsStart } from 'kibana/public'; +import type { DataPublicPluginStart } from '../../../../data/public'; -import { savedObjectsServiceMock } from '../../../../core/public/mocks'; -import { dataPluginMock } from '../../../data/public/mocks'; +import { savedObjectsServiceMock } from '../../../../../core/public/mocks'; +import { dataPluginMock } from '../../../../data/public/mocks'; import { getSavedSearch } from './get_saved_searches'; @@ -124,8 +124,8 @@ describe('getSavedSearch', () => { "serialize": [MockFunction], "setField": [MockFunction], "setFields": [MockFunction], + "setOverwriteDataViewType": [MockFunction], "setParent": [MockFunction], - "setPreferredSearchStrategyId": [MockFunction], }, "sharingSavedObjectProps": Object { "aliasTargetId": undefined, diff --git a/src/plugins/discover/public/saved_searches/get_saved_searches.ts b/src/plugins/discover/public/services/saved_searches/get_saved_searches.ts similarity index 88% rename from src/plugins/discover/public/saved_searches/get_saved_searches.ts rename to src/plugins/discover/public/services/saved_searches/get_saved_searches.ts index 32c50f691fe42..b4b9ecc89acf5 100644 --- a/src/plugins/discover/public/saved_searches/get_saved_searches.ts +++ b/src/plugins/discover/public/services/saved_searches/get_saved_searches.ts @@ -6,16 +6,16 @@ * Side Public License, v 1. */ -import type { SavedObjectsStart } from '../../../../core/public'; -import type { DataPublicPluginStart } from '../../../data/public'; +import type { SavedObjectsStart } from 'kibana/public'; +import type { DataPublicPluginStart } from '../../../../data/public'; import type { SavedSearchAttributes, SavedSearch } from './types'; import { SAVED_SEARCH_TYPE } from './constants'; import { fromSavedSearchAttributes } from './saved_searches_utils'; -import { injectSearchSourceReferences, parseSearchSourceJSON } from '../../../data/public'; -import { SavedObjectNotFound } from '../../../kibana_utils/public'; +import { injectSearchSourceReferences, parseSearchSourceJSON } from '../../../../data/public'; +import { SavedObjectNotFound } from '../../../../kibana_utils/public'; -import type { SpacesApi } from '../../../../../x-pack/plugins/spaces/public'; +import type { SpacesApi } from '../../../../../../x-pack/plugins/spaces/public'; interface GetSavedSearchDependencies { search: DataPublicPluginStart['search']; diff --git a/src/plugins/discover/public/saved_searches/index.ts b/src/plugins/discover/public/services/saved_searches/index.ts similarity index 100% rename from src/plugins/discover/public/saved_searches/index.ts rename to src/plugins/discover/public/services/saved_searches/index.ts diff --git a/src/plugins/discover/public/saved_searches/save_saved_searches.test.ts b/src/plugins/discover/public/services/saved_searches/save_saved_searches.test.ts similarity index 93% rename from src/plugins/discover/public/saved_searches/save_saved_searches.test.ts rename to src/plugins/discover/public/services/saved_searches/save_saved_searches.test.ts index eabbfe7f9419f..fd6777155a53b 100644 --- a/src/plugins/discover/public/saved_searches/save_saved_searches.test.ts +++ b/src/plugins/discover/public/services/saved_searches/save_saved_searches.test.ts @@ -6,10 +6,10 @@ * Side Public License, v 1. */ -import type { SavedObjectsStart } from '../../../../core/public'; +import type { SavedObjectsStart } from 'kibana/public'; -import { savedObjectsServiceMock } from '../../../../core/public/mocks'; -import { dataPluginMock } from '../../../data/public/mocks'; +import { savedObjectsServiceMock } from '../../../../../core/public/mocks'; +import { dataPluginMock } from '../../../../data/public/mocks'; import { saveSavedSearch } from './save_saved_searches'; import type { SavedSearch } from './types'; diff --git a/src/plugins/discover/public/saved_searches/save_saved_searches.ts b/src/plugins/discover/public/services/saved_searches/save_saved_searches.ts similarity index 100% rename from src/plugins/discover/public/saved_searches/save_saved_searches.ts rename to src/plugins/discover/public/services/saved_searches/save_saved_searches.ts diff --git a/src/plugins/discover/public/saved_searches/saved_search_alias_match_redirect.test.ts b/src/plugins/discover/public/services/saved_searches/saved_search_alias_match_redirect.test.ts similarity index 95% rename from src/plugins/discover/public/saved_searches/saved_search_alias_match_redirect.test.ts rename to src/plugins/discover/public/services/saved_searches/saved_search_alias_match_redirect.test.ts index 0a871061d2b19..51f1131122553 100644 --- a/src/plugins/discover/public/saved_searches/saved_search_alias_match_redirect.test.ts +++ b/src/plugins/discover/public/services/saved_searches/saved_search_alias_match_redirect.test.ts @@ -12,7 +12,7 @@ import type { History } from 'history'; import { useSavedSearchAliasMatchRedirect } from './saved_search_alias_match_redirect'; import type { SavedSearch } from './types'; -import { spacesPluginMock } from '../../../../../x-pack/plugins/spaces/public/mocks'; +import { spacesPluginMock } from '../../../../../../x-pack/plugins/spaces/public/mocks'; describe('useSavedSearchAliasMatchRedirect', () => { let spaces: ReturnType; diff --git a/src/plugins/discover/public/saved_searches/saved_search_alias_match_redirect.ts b/src/plugins/discover/public/services/saved_searches/saved_search_alias_match_redirect.ts similarity index 94% rename from src/plugins/discover/public/saved_searches/saved_search_alias_match_redirect.ts rename to src/plugins/discover/public/services/saved_searches/saved_search_alias_match_redirect.ts index 3a88c1a2b1989..2d49ebeb8b2de 100644 --- a/src/plugins/discover/public/saved_searches/saved_search_alias_match_redirect.ts +++ b/src/plugins/discover/public/services/saved_searches/saved_search_alias_match_redirect.ts @@ -12,7 +12,7 @@ import { i18n } from '@kbn/i18n'; import { getSavedSearchUrl } from './saved_searches_utils'; import type { SavedSearch } from './types'; -import type { SpacesApi } from '../../../../../x-pack/plugins/spaces/public'; +import type { SpacesApi } from '../../../../../../x-pack/plugins/spaces/public'; interface SavedSearchAliasMatchRedirectProps { savedSearch?: SavedSearch; diff --git a/src/plugins/discover/public/saved_searches/saved_search_url_conflict_callout.test.tsx b/src/plugins/discover/public/services/saved_searches/saved_search_url_conflict_callout.test.tsx similarity index 95% rename from src/plugins/discover/public/saved_searches/saved_search_url_conflict_callout.test.tsx rename to src/plugins/discover/public/services/saved_searches/saved_search_url_conflict_callout.test.tsx index c92c15e771f64..0aac9aea62192 100644 --- a/src/plugins/discover/public/saved_searches/saved_search_url_conflict_callout.test.tsx +++ b/src/plugins/discover/public/services/saved_searches/saved_search_url_conflict_callout.test.tsx @@ -13,7 +13,7 @@ import { mountWithIntl } from '@kbn/test/jest'; import { SavedSearchURLConflictCallout } from './saved_search_url_conflict_callout'; import type { SavedSearch } from './types'; -import { spacesPluginMock } from '../../../../../x-pack/plugins/spaces/public/mocks'; +import { spacesPluginMock } from '../../../../../../x-pack/plugins/spaces/public/mocks'; describe('SavedSearchURLConflictCallout', () => { let spaces: ReturnType; diff --git a/src/plugins/discover/public/saved_searches/saved_search_url_conflict_callout.ts b/src/plugins/discover/public/services/saved_searches/saved_search_url_conflict_callout.ts similarity index 94% rename from src/plugins/discover/public/saved_searches/saved_search_url_conflict_callout.ts rename to src/plugins/discover/public/services/saved_searches/saved_search_url_conflict_callout.ts index fd07126c496cf..4a13b56a5e8d7 100644 --- a/src/plugins/discover/public/saved_searches/saved_search_url_conflict_callout.ts +++ b/src/plugins/discover/public/services/saved_searches/saved_search_url_conflict_callout.ts @@ -11,7 +11,7 @@ import type { History } from 'history'; import { getSavedSearchUrl } from './saved_searches_utils'; import type { SavedSearch } from './types'; -import type { SpacesApi } from '../../../../../x-pack/plugins/spaces/public'; +import type { SpacesApi } from '../../../../../../x-pack/plugins/spaces/public'; interface SavedSearchURLConflictCalloutProps { savedSearch?: SavedSearch; diff --git a/src/plugins/discover/public/saved_searches/saved_searches_utils.test.ts b/src/plugins/discover/public/services/saved_searches/saved_searches_utils.test.ts similarity index 96% rename from src/plugins/discover/public/saved_searches/saved_searches_utils.test.ts rename to src/plugins/discover/public/services/saved_searches/saved_searches_utils.test.ts index 82510340f30f1..f2ad8b92adbc8 100644 --- a/src/plugins/discover/public/saved_searches/saved_searches_utils.test.ts +++ b/src/plugins/discover/public/services/saved_searches/saved_searches_utils.test.ts @@ -14,7 +14,7 @@ import { throwErrorOnSavedSearchUrlConflict, } from './saved_searches_utils'; -import { createSearchSourceMock } from '../../../data/public/mocks'; +import { createSearchSourceMock } from '../../../../data/public/mocks'; import type { SavedSearchAttributes, SavedSearch } from './types'; @@ -68,9 +68,10 @@ describe('saved_searches_utils', () => { "history": Array [], "id": "data_source1", "inheritOptions": Object {}, + "overwriteDataViewType": undefined, "parent": undefined, "requestStartHandlers": Array [], - "searchStrategyId": undefined, + "shouldOverwriteDataViewType": false, }, "sharingSavedObjectProps": Object {}, "sort": Array [], diff --git a/src/plugins/discover/public/saved_searches/saved_searches_utils.ts b/src/plugins/discover/public/services/saved_searches/saved_searches_utils.ts similarity index 100% rename from src/plugins/discover/public/saved_searches/saved_searches_utils.ts rename to src/plugins/discover/public/services/saved_searches/saved_searches_utils.ts diff --git a/src/plugins/discover/public/services/saved_searches/types.ts b/src/plugins/discover/public/services/saved_searches/types.ts new file mode 100644 index 0000000000000..4247f68ba8194 --- /dev/null +++ b/src/plugins/discover/public/services/saved_searches/types.ts @@ -0,0 +1,52 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import type { ISearchSource } from '../../../../data/public'; +import { DiscoverGridSettingsColumn } from '../../components/discover_grid/types'; +import { VIEW_MODE } from '../../components/view_mode_toggle'; + +/** @internal **/ +export interface SavedSearchAttributes { + title: string; + sort: Array<[string, string]>; + columns: string[]; + description: string; + grid: { + columns?: Record; + }; + hideChart: boolean; + kibanaSavedObjectMeta: { + searchSourceJSON: string; + }; + viewMode?: VIEW_MODE; + hideAggregatedPreview?: boolean; +} + +/** @internal **/ +export type SortOrder = [string, string]; + +/** @public **/ +export interface SavedSearch { + searchSource: ISearchSource; + id?: string; + title?: string; + sort?: SortOrder[]; + columns?: string[]; + description?: string; + grid?: { + columns?: Record; + }; + hideChart?: boolean; + sharingSavedObjectProps?: { + outcome?: 'aliasMatch' | 'exactMatch' | 'conflict'; + aliasTargetId?: string; + errorJSON?: string; + }; + viewMode?: VIEW_MODE; + hideAggregatedPreview?: boolean; +} diff --git a/src/plugins/discover/public/shared/index.ts b/src/plugins/discover/public/shared/index.ts deleted file mode 100644 index 7471e1293baa0..0000000000000 --- a/src/plugins/discover/public/shared/index.ts +++ /dev/null @@ -1,16 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -/* - * Allows the getSharingData function to be lazy loadable - */ -export async function loadSharingDataHelpers() { - return await import('../application/apps/main/utils/get_sharing_data'); -} - -export { DeferredSpinner } from './components'; diff --git a/src/plugins/discover/public/url_generator.ts b/src/plugins/discover/public/url_generator.ts index 7cc729fd7f7e5..32e89691574df 100644 --- a/src/plugins/discover/public/url_generator.ts +++ b/src/plugins/discover/public/url_generator.ts @@ -10,6 +10,7 @@ import type { UrlGeneratorsDefinition } from '../../share/public'; import type { TimeRange, Filter, Query, QueryState, RefreshInterval } from '../../data/public'; import { esFilters } from '../../data/public'; import { setStateToKbnUrl } from '../../kibana_utils/public'; +import { VIEW_MODE } from './components/view_mode_toggle'; export const DISCOVER_APP_URL_GENERATOR = 'DISCOVER_APP_URL_GENERATOR'; @@ -75,6 +76,8 @@ export interface DiscoverUrlGeneratorState { * id of the used saved query */ savedQuery?: string; + viewMode?: VIEW_MODE; + hideAggregatedPreview?: boolean; } interface Params { @@ -104,6 +107,8 @@ export class DiscoverUrlGenerator savedQuery, sort, interval, + viewMode, + hideAggregatedPreview, }: DiscoverUrlGeneratorState): Promise => { const savedSearchPath = savedSearchId ? `view/${encodeURIComponent(savedSearchId)}` : ''; const appState: { @@ -114,6 +119,8 @@ export class DiscoverUrlGenerator interval?: string; sort?: string[][]; savedQuery?: string; + viewMode?: VIEW_MODE; + hideAggregatedPreview?: boolean; } = {}; const queryState: QueryState = {}; @@ -130,6 +137,8 @@ export class DiscoverUrlGenerator if (filters && filters.length) queryState.filters = filters?.filter((f) => esFilters.isFilterPinned(f)); if (refreshInterval) queryState.refreshInterval = refreshInterval; + if (viewMode) appState.viewMode = viewMode; + if (hideAggregatedPreview) appState.hideAggregatedPreview = hideAggregatedPreview; let url = `${this.params.appBasePath}#/${savedSearchPath}`; url = setStateToKbnUrl('_g', queryState, { useHash }, url); diff --git a/src/plugins/discover/public/application/helpers/breadcrumbs.ts b/src/plugins/discover/public/utils/breadcrumbs.ts similarity index 96% rename from src/plugins/discover/public/application/helpers/breadcrumbs.ts rename to src/plugins/discover/public/utils/breadcrumbs.ts index fe420328a3171..4a3df34e2da75 100644 --- a/src/plugins/discover/public/application/helpers/breadcrumbs.ts +++ b/src/plugins/discover/public/utils/breadcrumbs.ts @@ -8,7 +8,7 @@ import { ChromeStart } from 'kibana/public'; import { i18n } from '@kbn/i18n'; -import { SavedSearch } from '../../saved_searches'; +import { SavedSearch } from '../services/saved_searches'; export function getRootBreadcrumbs() { return [ diff --git a/src/plugins/discover/public/utils/columns.test.ts b/src/plugins/discover/public/utils/columns.test.ts new file mode 100644 index 0000000000000..dc6677680f0ed --- /dev/null +++ b/src/plugins/discover/public/utils/columns.test.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 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 { getDisplayedColumns } from './columns'; +import { indexPatternWithTimefieldMock } from '../__mocks__/index_pattern_with_timefield'; +import { indexPatternMock } from '../__mocks__/index_pattern'; + +describe('getDisplayedColumns', () => { + test('returns default columns given a index pattern without timefield', async () => { + const result = getDisplayedColumns([], indexPatternMock); + expect(result).toMatchInlineSnapshot(` + Array [ + "_source", + ] + `); + }); + test('returns default columns given a index pattern with timefield', async () => { + const result = getDisplayedColumns([], indexPatternWithTimefieldMock); + expect(result).toMatchInlineSnapshot(` + Array [ + "_source", + ] + `); + }); + test('returns default columns when just timefield is in state', async () => { + const result = getDisplayedColumns(['timestamp'], indexPatternWithTimefieldMock); + expect(result).toMatchInlineSnapshot(` + Array [ + "_source", + ] + `); + }); + test('returns columns given by argument, no fallback ', async () => { + const result = getDisplayedColumns(['test'], indexPatternWithTimefieldMock); + expect(result).toMatchInlineSnapshot(` + Array [ + "test", + ] + `); + }); + test('returns the same instance of ["_source"] over multiple calls', async () => { + const result = getDisplayedColumns([], indexPatternWithTimefieldMock); + const result2 = getDisplayedColumns([], indexPatternWithTimefieldMock); + expect(result).toBe(result2); + }); +}); diff --git a/src/plugins/discover/public/utils/columns.ts b/src/plugins/discover/public/utils/columns.ts new file mode 100644 index 0000000000000..6c32ac509e63b --- /dev/null +++ b/src/plugins/discover/public/utils/columns.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 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 '../../../data/common'; + +// We store this outside the function as a constant, so we're not creating a new array every time +// the function is returning this. A changing array might cause the data grid to think it got +// new columns, and thus performing worse than using the same array over multiple renders. +const SOURCE_ONLY = ['_source']; + +/** + * Function to provide fallback when + * 1) no columns are given + * 2) Just one column is given, which is the configured timefields + */ +export function getDisplayedColumns(stateColumns: string[] = [], indexPattern: IndexPattern) { + return stateColumns && + stateColumns.length > 0 && + // check if all columns where removed except the configured timeField (this can't be removed) + !(stateColumns.length === 1 && stateColumns[0] === indexPattern.timeFieldName) + ? stateColumns + : SOURCE_ONLY; +} diff --git a/src/plugins/discover/public/application/helpers/format_hit.test.ts b/src/plugins/discover/public/utils/format_hit.test.ts similarity index 89% rename from src/plugins/discover/public/application/helpers/format_hit.test.ts rename to src/plugins/discover/public/utils/format_hit.test.ts index ebf5078238ccf..325903ffebdd3 100644 --- a/src/plugins/discover/public/application/helpers/format_hit.test.ts +++ b/src/plugins/discover/public/utils/format_hit.test.ts @@ -7,13 +7,13 @@ */ import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; -import { indexPatternMock as dataViewMock } from '../../__mocks__/index_pattern'; +import { indexPatternMock as dataViewMock } from '../__mocks__/index_pattern'; import { formatHit } from './format_hit'; -import { discoverServiceMock } from '../../__mocks__/services'; -import { MAX_DOC_FIELDS_DISPLAYED } from '../../../common'; +import { discoverServiceMock } from '../__mocks__/services'; +import { MAX_DOC_FIELDS_DISPLAYED } from '../../common'; -jest.mock('../../kibana_services', () => ({ - getServices: () => jest.requireActual('../../__mocks__/services').discoverServiceMock, +jest.mock('../kibana_services', () => ({ + getServices: () => jest.requireActual('../__mocks__/services').discoverServiceMock, })); describe('formatHit', () => { diff --git a/src/plugins/discover/public/application/helpers/format_hit.ts b/src/plugins/discover/public/utils/format_hit.ts similarity index 94% rename from src/plugins/discover/public/application/helpers/format_hit.ts rename to src/plugins/discover/public/utils/format_hit.ts index 1101439515523..b1bbfcd5aa878 100644 --- a/src/plugins/discover/public/application/helpers/format_hit.ts +++ b/src/plugins/discover/public/utils/format_hit.ts @@ -7,9 +7,9 @@ */ import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; -import { DataView, flattenHit } from '../../../../data/common'; -import { MAX_DOC_FIELDS_DISPLAYED } from '../../../common'; -import { getServices } from '../../kibana_services'; +import { DataView, flattenHit } from '../../../data/common'; +import { MAX_DOC_FIELDS_DISPLAYED } from '../../common'; +import { getServices } from '../kibana_services'; import { formatFieldValue } from './format_value'; const formattedHitCache = new WeakMap(); diff --git a/src/plugins/discover/public/application/helpers/format_value.test.ts b/src/plugins/discover/public/utils/format_value.test.ts similarity index 91% rename from src/plugins/discover/public/application/helpers/format_value.test.ts rename to src/plugins/discover/public/utils/format_value.test.ts index 76d95c08e4a19..4684547b7cf3e 100644 --- a/src/plugins/discover/public/application/helpers/format_value.test.ts +++ b/src/plugins/discover/public/utils/format_value.test.ts @@ -6,13 +6,13 @@ * Side Public License, v 1. */ -import type { FieldFormat } from '../../../../field_formats/common'; -import { indexPatternMock } from '../../__mocks__/index_pattern'; +import type { FieldFormat } from '../../../field_formats/common'; +import { indexPatternMock } from '../__mocks__/index_pattern'; import { formatFieldValue } from './format_value'; -import { getServices } from '../../kibana_services'; +import { getServices } from '../kibana_services'; -jest.mock('../../kibana_services', () => { +jest.mock('../kibana_services', () => { const services = { fieldFormats: { getDefaultInstance: jest.fn( diff --git a/src/plugins/discover/public/application/helpers/format_value.ts b/src/plugins/discover/public/utils/format_value.ts similarity index 95% rename from src/plugins/discover/public/application/helpers/format_value.ts rename to src/plugins/discover/public/utils/format_value.ts index 933309d6dcf8e..7a2a67b063191 100644 --- a/src/plugins/discover/public/application/helpers/format_value.ts +++ b/src/plugins/discover/public/utils/format_value.ts @@ -7,8 +7,8 @@ */ import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; -import { DataView, DataViewField, KBN_FIELD_TYPES } from '../../../../data/common'; -import { getServices } from '../../kibana_services'; +import { DataView, DataViewField, KBN_FIELD_TYPES } from '../../../data/common'; +import { getServices } from '../kibana_services'; /** * Formats the value of a specific field using the appropriate field formatter if available diff --git a/src/plugins/discover/public/application/helpers/get_context_url.test.ts b/src/plugins/discover/public/utils/get_context_url.test.ts similarity index 94% rename from src/plugins/discover/public/application/helpers/get_context_url.test.ts rename to src/plugins/discover/public/utils/get_context_url.test.ts index 97d31ca43142d..d6d1db5ca393b 100644 --- a/src/plugins/discover/public/application/helpers/get_context_url.test.ts +++ b/src/plugins/discover/public/utils/get_context_url.test.ts @@ -7,7 +7,7 @@ */ import { getContextUrl } from './get_context_url'; -import { FilterManager } from '../../../../data/public/query/filter_manager'; +import { FilterManager } from '../../../data/public/query/filter_manager'; const filterManager = { getGlobalFilters: () => [], getAppFilters: () => [], diff --git a/src/plugins/discover/public/application/helpers/get_context_url.tsx b/src/plugins/discover/public/utils/get_context_url.tsx similarity index 87% rename from src/plugins/discover/public/application/helpers/get_context_url.tsx rename to src/plugins/discover/public/utils/get_context_url.tsx index 057f8bc2afc52..68c0e935f17e9 100644 --- a/src/plugins/discover/public/application/helpers/get_context_url.tsx +++ b/src/plugins/discover/public/utils/get_context_url.tsx @@ -8,9 +8,9 @@ import { stringify } from 'query-string'; import rison from 'rison-node'; -import { url } from '../../../../kibana_utils/common'; -import { esFilters, FilterManager } from '../../../../data/public'; -import { DiscoverServices } from '../../build_services'; +import { url } from '../../../kibana_utils/common'; +import { esFilters, FilterManager } from '../../../data/public'; +import { DiscoverServices } from '../build_services'; /** * Helper function to generate an URL to a document in Discover's context view diff --git a/src/plugins/discover/public/application/helpers/get_fields_to_show.test.ts b/src/plugins/discover/public/utils/get_fields_to_show.test.ts similarity index 97% rename from src/plugins/discover/public/application/helpers/get_fields_to_show.test.ts rename to src/plugins/discover/public/utils/get_fields_to_show.test.ts index 13c2dbaac6124..5e61a7aec01c0 100644 --- a/src/plugins/discover/public/application/helpers/get_fields_to_show.test.ts +++ b/src/plugins/discover/public/utils/get_fields_to_show.test.ts @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import { IndexPattern, IndexPatternField } from '../../../../data/common'; +import { IndexPattern, IndexPatternField } from '../../../data/common'; import { getFieldsToShow } from './get_fields_to_show'; describe('get fields to show', () => { diff --git a/src/plugins/discover/public/application/helpers/get_fields_to_show.ts b/src/plugins/discover/public/utils/get_fields_to_show.ts similarity index 94% rename from src/plugins/discover/public/application/helpers/get_fields_to_show.ts rename to src/plugins/discover/public/utils/get_fields_to_show.ts index 5e3f0c0b60057..9167165c2b08f 100644 --- a/src/plugins/discover/public/application/helpers/get_fields_to_show.ts +++ b/src/plugins/discover/public/utils/get_fields_to_show.ts @@ -5,7 +5,7 @@ * in compliance with, at your election, the Elastic License 2.0 or the Server * Side Public License, v 1. */ -import { IndexPattern, getFieldSubtypeMulti } from '../../../../data/common'; +import { IndexPattern, getFieldSubtypeMulti } from '../../../data/common'; export const getFieldsToShow = ( fields: string[], diff --git a/src/plugins/discover/public/application/helpers/get_ignored_reason.test.ts b/src/plugins/discover/public/utils/get_ignored_reason.test.ts similarity index 96% rename from src/plugins/discover/public/application/helpers/get_ignored_reason.test.ts rename to src/plugins/discover/public/utils/get_ignored_reason.test.ts index 13632ca5ed901..82af0079702da 100644 --- a/src/plugins/discover/public/application/helpers/get_ignored_reason.test.ts +++ b/src/plugins/discover/public/utils/get_ignored_reason.test.ts @@ -7,7 +7,7 @@ */ import { getIgnoredReason, IgnoredReason } from './get_ignored_reason'; -import { DataViewField, KBN_FIELD_TYPES } from '../../../../data/common'; +import { DataViewField, KBN_FIELD_TYPES } from '../../../data/common'; function field(params: Partial): DataViewField { return { diff --git a/src/plugins/discover/public/application/helpers/get_ignored_reason.ts b/src/plugins/discover/public/utils/get_ignored_reason.ts similarity index 95% rename from src/plugins/discover/public/application/helpers/get_ignored_reason.ts rename to src/plugins/discover/public/utils/get_ignored_reason.ts index bf8df6e000d4c..901765b26b918 100644 --- a/src/plugins/discover/public/application/helpers/get_ignored_reason.ts +++ b/src/plugins/discover/public/utils/get_ignored_reason.ts @@ -7,7 +7,7 @@ */ import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; -import { DataViewField, KBN_FIELD_TYPES } from '../../../../data/common'; +import { DataViewField, KBN_FIELD_TYPES } from '../../../data/common'; export enum IgnoredReason { IGNORE_ABOVE = 'ignore_above', diff --git a/src/plugins/discover/public/application/apps/main/utils/get_sharing_data.test.ts b/src/plugins/discover/public/utils/get_sharing_data.test.ts similarity index 93% rename from src/plugins/discover/public/application/apps/main/utils/get_sharing_data.test.ts rename to src/plugins/discover/public/utils/get_sharing_data.test.ts index 9b518c23a5f89..fda4e731191e7 100644 --- a/src/plugins/discover/public/application/apps/main/utils/get_sharing_data.test.ts +++ b/src/plugins/discover/public/utils/get_sharing_data.test.ts @@ -8,11 +8,11 @@ import { Capabilities, IUiSettingsClient } from 'kibana/public'; import type { IndexPattern } from 'src/plugins/data/public'; -import type { DiscoverServices } from '../../../../build_services'; -import { dataPluginMock } from '../../../../../../data/public/mocks'; -import { createSearchSourceMock } from '../../../../../../data/common/search/search_source/mocks'; -import { DOC_HIDE_TIME_COLUMN_SETTING, SORT_DEFAULT_ORDER_SETTING } from '../../../../../common'; -import { indexPatternMock } from '../../../../__mocks__/index_pattern'; +import type { DiscoverServices } from '../build_services'; +import { dataPluginMock } from '../../../data/public/mocks'; +import { createSearchSourceMock } from '../../../data/common/search/search_source/mocks'; +import { DOC_HIDE_TIME_COLUMN_SETTING, SORT_DEFAULT_ORDER_SETTING } from '../../common'; +import { indexPatternMock } from '../__mocks__/index_pattern'; import { getSharingData, showPublicUrlSwitch } from './get_sharing_data'; describe('getSharingData', () => { diff --git a/src/plugins/discover/public/application/apps/main/utils/get_sharing_data.ts b/src/plugins/discover/public/utils/get_sharing_data.ts similarity index 92% rename from src/plugins/discover/public/application/apps/main/utils/get_sharing_data.ts rename to src/plugins/discover/public/utils/get_sharing_data.ts index 437d4fda666fc..a8e210c980810 100644 --- a/src/plugins/discover/public/application/apps/main/utils/get_sharing_data.ts +++ b/src/plugins/discover/public/utils/get_sharing_data.ts @@ -7,13 +7,13 @@ */ import type { Capabilities } from 'kibana/public'; -import type { IUiSettingsClient } from 'src/core/public'; +import type { IUiSettingsClient } from 'kibana/public'; import type { DataPublicPluginStart } from 'src/plugins/data/public'; import type { ISearchSource, SearchSourceFields } from 'src/plugins/data/common'; -import { DOC_HIDE_TIME_COLUMN_SETTING, SORT_DEFAULT_ORDER_SETTING } from '../../../../../common'; -import type { SavedSearch, SortOrder } from '../../../../saved_searches'; +import { DOC_HIDE_TIME_COLUMN_SETTING, SORT_DEFAULT_ORDER_SETTING } from '../../common'; +import type { SavedSearch, SortOrder } from '../services/saved_searches'; import { getSortForSearchSource } from '../components/doc_table'; -import { AppState } from '../services/discover_state'; +import { AppState } from '../application/main/services/discover_state'; /** * Preparing data to share the current state as link or CSV/Report diff --git a/src/plugins/discover/public/application/helpers/get_single_doc_url.ts b/src/plugins/discover/public/utils/get_single_doc_url.ts similarity index 100% rename from src/plugins/discover/public/application/helpers/get_single_doc_url.ts rename to src/plugins/discover/public/utils/get_single_doc_url.ts diff --git a/src/plugins/discover/public/utils/index.ts b/src/plugins/discover/public/utils/index.ts new file mode 100644 index 0000000000000..9a3e709fc71e4 --- /dev/null +++ b/src/plugins/discover/public/utils/index.ts @@ -0,0 +1,13 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 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. + */ +/* + * Allows the getSharingData function to be lazy loadable + */ +export async function loadSharingDataHelpers() { + return await import('./get_sharing_data'); +} diff --git a/src/plugins/discover/public/application/helpers/migrate_legacy_query.ts b/src/plugins/discover/public/utils/migrate_legacy_query.ts similarity index 100% rename from src/plugins/discover/public/application/helpers/migrate_legacy_query.ts rename to src/plugins/discover/public/utils/migrate_legacy_query.ts diff --git a/src/plugins/discover/public/application/helpers/popularize_field.test.ts b/src/plugins/discover/public/utils/popularize_field.test.ts similarity index 97% rename from src/plugins/discover/public/application/helpers/popularize_field.test.ts rename to src/plugins/discover/public/utils/popularize_field.test.ts index 91673fd17d3be..066b12b9603e4 100644 --- a/src/plugins/discover/public/application/helpers/popularize_field.test.ts +++ b/src/plugins/discover/public/utils/popularize_field.test.ts @@ -7,7 +7,7 @@ */ import { Capabilities } from 'kibana/public'; -import { IndexPattern, IndexPatternsService } from '../../../../data/public'; +import { IndexPattern, IndexPatternsService } from '../../../data/public'; import { popularizeField } from './popularize_field'; const capabilities = { diff --git a/src/plugins/discover/public/application/helpers/popularize_field.ts b/src/plugins/discover/public/utils/popularize_field.ts similarity index 92% rename from src/plugins/discover/public/application/helpers/popularize_field.ts rename to src/plugins/discover/public/utils/popularize_field.ts index 90968dd7c3d58..18e720001edca 100644 --- a/src/plugins/discover/public/application/helpers/popularize_field.ts +++ b/src/plugins/discover/public/utils/popularize_field.ts @@ -7,7 +7,7 @@ */ import type { Capabilities } from 'kibana/public'; -import { IndexPattern, IndexPatternsContract } from '../../../../data/public'; +import { IndexPattern, IndexPatternsContract } from '../../../data/public'; async function popularizeField( indexPattern: IndexPattern, diff --git a/src/plugins/discover/public/application/helpers/state_helpers.ts b/src/plugins/discover/public/utils/state_helpers.ts similarity index 98% rename from src/plugins/discover/public/application/helpers/state_helpers.ts rename to src/plugins/discover/public/utils/state_helpers.ts index bb64f823d61a6..e50cd87ad82ee 100644 --- a/src/plugins/discover/public/application/helpers/state_helpers.ts +++ b/src/plugins/discover/public/utils/state_helpers.ts @@ -8,7 +8,7 @@ import { IUiSettingsClient } from 'kibana/public'; import { isEqual } from 'lodash'; -import { SEARCH_FIELDS_FROM_SOURCE, DEFAULT_COLUMNS_SETTING } from '../../../common'; +import { SEARCH_FIELDS_FROM_SOURCE, DEFAULT_COLUMNS_SETTING } from '../../common'; /** * Makes sure the current state is not referencing the source column when using the fields api diff --git a/src/plugins/discover/public/utils/truncate_styles.ts b/src/plugins/discover/public/utils/truncate_styles.ts new file mode 100644 index 0000000000000..dbe8b770e1793 --- /dev/null +++ b/src/plugins/discover/public/utils/truncate_styles.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 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 createCache from '@emotion/cache'; +import { cache } from '@emotion/css'; +import { serializeStyles } from '@emotion/serialize'; + +/** + * The following emotion cache management was introduced here + * https://ntsim.uk/posts/how-to-update-or-remove-global-styles-in-emotion/ + */ +const TRUNCATE_GRADIENT_HEIGHT = 15; +const globalThemeCache = createCache({ key: 'truncation' }); + +const buildStylesheet = (maxHeight: number) => { + return [ + ` + .dscTruncateByHeight { + overflow: hidden; + max-height: ${maxHeight}px !important; + display: inline-block; + } + .dscTruncateByHeight:before { + top: ${maxHeight - TRUNCATE_GRADIENT_HEIGHT}px; + } + `, + ]; +}; + +const flushThemedGlobals = () => { + globalThemeCache.sheet.flush(); + globalThemeCache.inserted = {}; + globalThemeCache.registered = {}; +}; + +export const injectTruncateStyles = (maxHeight: number) => { + if (maxHeight <= 0) { + flushThemedGlobals(); + return; + } + + const serialized = serializeStyles(buildStylesheet(maxHeight), cache.registered); + if (!globalThemeCache.inserted[serialized.name]) { + globalThemeCache.insert('', serialized, globalThemeCache.sheet, true); + } +}; diff --git a/src/plugins/discover/public/application/helpers/use_data_grid_columns.test.tsx b/src/plugins/discover/public/utils/use_data_grid_columns.test.tsx similarity index 86% rename from src/plugins/discover/public/application/helpers/use_data_grid_columns.test.tsx rename to src/plugins/discover/public/utils/use_data_grid_columns.test.tsx index abb2138e882b1..d09e9d8d1c6ba 100644 --- a/src/plugins/discover/public/application/helpers/use_data_grid_columns.test.tsx +++ b/src/plugins/discover/public/utils/use_data_grid_columns.test.tsx @@ -8,11 +8,11 @@ import { renderHook } from '@testing-library/react-hooks'; import { useColumns } from './use_data_grid_columns'; -import { indexPatternMock } from '../../__mocks__/index_pattern'; -import { configMock } from '../../__mocks__/config'; -import { indexPatternsMock } from '../../__mocks__/index_patterns'; -import { AppState } from '../apps/context/services/context_state'; -import { Capabilities } from '../../../../../core/types'; +import { indexPatternMock } from '../__mocks__/index_pattern'; +import { configMock } from '../__mocks__/config'; +import { indexPatternsMock } from '../__mocks__/index_patterns'; +import { AppState } from '../application/context/services/context_state'; +import { Capabilities } from '../../../../core/types'; describe('useColumns', () => { const defaultProps = { diff --git a/src/plugins/discover/public/application/helpers/use_data_grid_columns.ts b/src/plugins/discover/public/utils/use_data_grid_columns.ts similarity index 90% rename from src/plugins/discover/public/application/helpers/use_data_grid_columns.ts rename to src/plugins/discover/public/utils/use_data_grid_columns.ts index 888d67e2aaff3..09b296ce8138d 100644 --- a/src/plugins/discover/public/application/helpers/use_data_grid_columns.ts +++ b/src/plugins/discover/public/utils/use_data_grid_columns.ts @@ -13,12 +13,12 @@ import { Capabilities, IUiSettingsClient } from 'kibana/public'; import { AppState as DiscoverState, GetStateReturn as DiscoverGetStateReturn, -} from '../apps/main/services/discover_state'; +} from '../application/main/services/discover_state'; import { AppState as ContextState, GetStateReturn as ContextGetStateReturn, -} from '../apps/context/services/context_state'; -import { getStateColumnActions } from '../apps/main/components/doc_table/actions/columns'; +} from '../application/context/services/context_state'; +import { getStateColumnActions } from '../components/doc_table/actions/columns'; interface UseColumnsProps { capabilities: Capabilities; diff --git a/src/plugins/discover/public/application/services/use_es_doc_search.test.tsx b/src/plugins/discover/public/utils/use_es_doc_search.test.tsx similarity index 96% rename from src/plugins/discover/public/application/services/use_es_doc_search.test.tsx rename to src/plugins/discover/public/utils/use_es_doc_search.test.tsx index ca57b470b471a..57538441a2bcc 100644 --- a/src/plugins/discover/public/application/services/use_es_doc_search.test.tsx +++ b/src/plugins/discover/public/utils/use_es_doc_search.test.tsx @@ -10,13 +10,13 @@ import { renderHook } from '@testing-library/react-hooks'; import { buildSearchBody, useEsDocSearch } from './use_es_doc_search'; import { Observable } from 'rxjs'; import { IndexPattern } from 'src/plugins/data/common'; -import { DocProps } from '../apps/doc/components/doc'; -import { ElasticRequestState } from '../apps/doc/types'; -import { SEARCH_FIELDS_FROM_SOURCE as mockSearchFieldsFromSource } from '../../../common'; +import { DocProps } from '../application/doc/components/doc'; +import { ElasticRequestState } from '../application/doc/types'; +import { SEARCH_FIELDS_FROM_SOURCE as mockSearchFieldsFromSource } from '../../common'; const mockSearchResult = new Observable(); -jest.mock('../../kibana_services', () => ({ +jest.mock('../kibana_services', () => ({ getServices: () => ({ data: { search: { diff --git a/src/plugins/discover/public/application/services/use_es_doc_search.ts b/src/plugins/discover/public/utils/use_es_doc_search.ts similarity index 87% rename from src/plugins/discover/public/application/services/use_es_doc_search.ts rename to src/plugins/discover/public/utils/use_es_doc_search.ts index fa7dce9c7e0a4..10c97c53b3fb3 100644 --- a/src/plugins/discover/public/application/services/use_es_doc_search.ts +++ b/src/plugins/discover/public/utils/use_es_doc_search.ts @@ -8,12 +8,12 @@ import { useCallback, useEffect, useMemo, useState } from 'react'; import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; -import { IndexPattern } from '../../../../data/common'; -import { DocProps } from '../apps/doc/components/doc'; -import { ElasticRequestState } from '../apps/doc/types'; -import { ElasticSearchHit } from '../doc_views/doc_views_types'; -import { getServices } from '../../kibana_services'; -import { SEARCH_FIELDS_FROM_SOURCE } from '../../../common'; +import { IndexPattern } from '../../../data/common'; +import { DocProps } from '../application/doc/components/doc'; +import { ElasticRequestState } from '../application/doc/types'; +import { ElasticSearchHit } from '../services/doc_views/doc_views_types'; +import { getServices } from '../kibana_services'; +import { SEARCH_FIELDS_FROM_SOURCE } from '../../common'; type RequestBody = Pick; @@ -66,7 +66,7 @@ export function useEsDocSearch({ index, indexPattern, requestSource, -}: DocProps): [ElasticRequestState, ElasticSearchHit | null | null, () => void] { +}: DocProps): [ElasticRequestState, ElasticSearchHit | null, () => void] { const [status, setStatus] = useState(ElasticRequestState.Loading); const [hit, setHit] = useState(null); const { data, uiSettings } = useMemo(() => getServices(), []); diff --git a/src/plugins/discover/public/application/helpers/use_index_pattern.test.tsx b/src/plugins/discover/public/utils/use_index_pattern.test.tsx similarity index 89% rename from src/plugins/discover/public/application/helpers/use_index_pattern.test.tsx rename to src/plugins/discover/public/utils/use_index_pattern.test.tsx index dfc54d8630742..591478ec371fb 100644 --- a/src/plugins/discover/public/application/helpers/use_index_pattern.test.tsx +++ b/src/plugins/discover/public/utils/use_index_pattern.test.tsx @@ -6,8 +6,8 @@ * Side Public License, v 1. */ import { useIndexPattern } from './use_index_pattern'; -import { indexPatternMock } from '../../__mocks__/index_pattern'; -import { indexPatternsMock } from '../../__mocks__/index_patterns'; +import { indexPatternMock } from '../__mocks__/index_pattern'; +import { indexPatternsMock } from '../__mocks__/index_patterns'; import { renderHook } from '@testing-library/react-hooks'; describe('Use Index Pattern', () => { diff --git a/src/plugins/discover/public/application/helpers/use_index_pattern.tsx b/src/plugins/discover/public/utils/use_index_pattern.tsx similarity index 92% rename from src/plugins/discover/public/application/helpers/use_index_pattern.tsx rename to src/plugins/discover/public/utils/use_index_pattern.tsx index 374f83cbbfe72..3a87d21587fb7 100644 --- a/src/plugins/discover/public/application/helpers/use_index_pattern.tsx +++ b/src/plugins/discover/public/utils/use_index_pattern.tsx @@ -6,7 +6,7 @@ * Side Public License, v 1. */ import { useEffect, useState } from 'react'; -import { IndexPattern, IndexPatternsContract } from '../../../../data/common'; +import { IndexPattern, IndexPatternsContract } from '../../../data/common'; export const useIndexPattern = (indexPatterns: IndexPatternsContract, indexPatternId: string) => { const [indexPattern, setIndexPattern] = useState(undefined); diff --git a/src/plugins/discover/server/ui_settings.ts b/src/plugins/discover/server/ui_settings.ts index 529ba0d1beef1..e9aa51a7384b2 100644 --- a/src/plugins/discover/server/ui_settings.ts +++ b/src/plugins/discover/server/ui_settings.ts @@ -26,6 +26,7 @@ import { SEARCH_FIELDS_FROM_SOURCE, MAX_DOC_FIELDS_DISPLAYED, SHOW_MULTIFIELDS, + TRUNCATE_MAX_HEIGHT, SHOW_FIELD_STATISTICS, } from '../common'; @@ -157,14 +158,14 @@ export const getUiSettings: () => Record = () => ({ schema: schema.arrayOf(schema.string()), }, [DOC_TABLE_LEGACY]: { - name: i18n.translate('discover.advancedSettings.docTableVersionName', { - defaultMessage: 'Use classic table', + name: i18n.translate('discover.advancedSettings.disableDocumentExplorer', { + defaultMessage: 'Document Explorer or classic view', }), value: true, - description: i18n.translate('discover.advancedSettings.docTableVersionDescription', { + description: i18n.translate('discover.advancedSettings.disableDocumentExplorerDescription', { defaultMessage: - 'Discover uses a new table layout that includes better data sorting, drag-and-drop columns, and a full screen view. ' + - 'Turn on this option to use the classic table. Turn off to use the new table. ', + 'To use the new Document Explorer instead of the classic view, turn off this option. ' + + 'The Document Explorer offers better data sorting, resizable columns, and a full screen view.', }), category: ['discover'], schema: schema.boolean(), @@ -241,4 +242,16 @@ export const getUiSettings: () => Record = () => ({ category: ['discover'], schema: schema.boolean(), }, + [TRUNCATE_MAX_HEIGHT]: { + name: i18n.translate('discover.advancedSettings.params.maxCellHeightTitle', { + defaultMessage: 'Maximum table cell height', + }), + value: 115, + category: ['discover'], + description: i18n.translate('discover.advancedSettings.params.maxCellHeightText', { + defaultMessage: + 'The maximum height that a cell in a table should occupy. Set to 0 to disable truncation', + }), + schema: schema.number({ min: 0 }), + }, }); diff --git a/src/plugins/discover/tsconfig.json b/src/plugins/discover/tsconfig.json index eb739e673cacd..86534268c578a 100644 --- a/src/plugins/discover/tsconfig.json +++ b/src/plugins/discover/tsconfig.json @@ -22,7 +22,6 @@ { "path": "../usage_collection/tsconfig.json" }, { "path": "../kibana_utils/tsconfig.json" }, { "path": "../kibana_react/tsconfig.json" }, - { "path": "../kibana_legacy/tsconfig.json" }, { "path": "../index_pattern_field_editor/tsconfig.json"}, { "path": "../field_formats/tsconfig.json" }, { "path": "../data_views/tsconfig.json" }, diff --git a/src/plugins/es_ui_shared/public/request/use_request.ts b/src/plugins/es_ui_shared/public/request/use_request.ts index 33085bdbf4478..1241d6222a38f 100644 --- a/src/plugins/es_ui_shared/public/request/use_request.ts +++ b/src/plugins/es_ui_shared/public/request/use_request.ts @@ -103,8 +103,13 @@ export const useRequest = ( : serializedResponseData; setData(responseData); } - // Setting isLoading to false also acts as a signal for scheduling the next poll request. - setIsLoading(false); + // There can be situations in which a component that consumes this hook gets unmounted when + // the request returns an error. So before changing the isLoading state, check if the component + // is still mounted. + if (isMounted.current === true) { + // Setting isLoading to false also acts as a signal for scheduling the next poll request. + setIsLoading(false); + } }, [requestBody, httpClient, deserializer, clearPollInterval] ); diff --git a/src/plugins/es_ui_shared/static/forms/docs/core/use_async_validation_data.mdx b/src/plugins/es_ui_shared/static/forms/docs/core/use_async_validation_data.mdx deleted file mode 100644 index 8020a54596b46..0000000000000 --- a/src/plugins/es_ui_shared/static/forms/docs/core/use_async_validation_data.mdx +++ /dev/null @@ -1,36 +0,0 @@ ---- -id: formLibCoreUseAsyncValidationData -slug: /form-lib/core/use-async-validation-data -title: useAsyncValidationData() -summary: Provide dynamic data to your validators... asynchronously -tags: ['forms', 'kibana', 'dev'] -date: 2021-08-20 ---- - -**Returns:** `[Observable, (nextValue: T|undefined) => void]` - -This hook creates for you an observable and a handler to update its value. You can then pass the observable directly to . - -See an example on how to use this hook in the section. - -## Options - -### state (optional) - -**Type:** `any` - -If you provide a state when calling the hook, the observable value will keep in sync with the state. - -```js -const MyForm = () => { - ... - const [indices, setIndices] = useState([]); - // Whenever the "indices" state changes, the "indices$" Observable will be updated - const [indices$] = useAsyncValidationData(indices); - - ... - - - -} -``` \ No newline at end of file diff --git a/src/plugins/es_ui_shared/static/forms/docs/core/use_behavior_subject.mdx b/src/plugins/es_ui_shared/static/forms/docs/core/use_behavior_subject.mdx new file mode 100644 index 0000000000000..f7eca9c360ac4 --- /dev/null +++ b/src/plugins/es_ui_shared/static/forms/docs/core/use_behavior_subject.mdx @@ -0,0 +1,26 @@ +--- +id: formLibCoreUseBehaviorSubject +slug: /form-lib/utils/use-behavior-subject +title: useBehaviorSubject() +summary: Util to create a rxjs BehaviorSubject with a handler to change its value +tags: ['forms', 'kibana', 'dev'] +date: 2021-08-20 +--- + +**Returns:** `[Observable, (nextValue: T|undefined) => void]` + +This hook creates for you a rxjs BehaviorSubject and a handler to update its value. + +See an example on how to use this hook in the section. + +## Options + +### initialState + +**Type:** `any` + +The initial value of the BehaviorSubject. + +```js +const [indices$, nextIndices] = useBehaviorSubject([]); +``` \ No newline at end of file diff --git a/src/plugins/es_ui_shared/static/forms/docs/core/use_field.mdx b/src/plugins/es_ui_shared/static/forms/docs/core/use_field.mdx index fd5f3b26cdf0d..dd073e0b38d1f 100644 --- a/src/plugins/es_ui_shared/static/forms/docs/core/use_field.mdx +++ b/src/plugins/es_ui_shared/static/forms/docs/core/use_field.mdx @@ -207,6 +207,14 @@ For example: when we add an item to the ComboBox array, we don't want to block t By default, when any of the validation fails, the following validation are not executed. If you still want to execute the following validation(s), set the `exitOnFail` to `false`. +##### isAsync + +**Type:** `boolean` +**Default:** `false` + +Flag to indicate if the validation is asynchronous. If not specified the lib will first try to run all the validations synchronously and if it detects a Promise it will run the validations a second time asynchronously. This means that HTTP request will be called twice which is not ideal. +**It is thus recommended** to set the `isAsync` flag to `true` for all asynchronous validations. + #### deserializer **Type:** `SerializerFunc` @@ -342,9 +350,9 @@ Use this prop to pass down dynamic data to your field validator. The data is the See an example on how to use this prop in the section. -### validationData$ +### validationDataProvider -Use this prop to pass down an Observable into which you can send, asynchronously, dynamic data required inside your validation. +Use this prop to pass down a Promise to provide dynamic data asynchronously in your validation. See an example on how to use this prop in the section. diff --git a/src/plugins/es_ui_shared/static/forms/docs/core/use_form_data.mdx b/src/plugins/es_ui_shared/static/forms/docs/core/use_form_data.mdx index 17276f41b3dac..0deb449591871 100644 --- a/src/plugins/es_ui_shared/static/forms/docs/core/use_form_data.mdx +++ b/src/plugins/es_ui_shared/static/forms/docs/core/use_form_data.mdx @@ -56,6 +56,31 @@ const [{ type }] = useFormData({ watch: 'type' }); const [{ type, subType }] = useFormData({ watch: ['type', 'subType'] }); ``` +### onChange + +**Type:** `(data: T) => void` + +This handler lets you listen to form fields value change _before_ any validation is executed. + +```js +// With "onChange": listen to changes before any validation is triggered +const onFieldChange = useCallback(({ myField, otherField }) => { + // React to changes before any validation is executed +}, []); + +useFormData({ + watch: ['myField', 'otherField'], + onChange: onFieldChange +}); + +// Without "onChange": the way to go most of the time +const [{ myField, otherField }] = useFormData({ watch['myField', 'otherField'] }); + +useEffect(() => { + // React to changes after validation have been triggered +}, [myField, otherField]); +``` + ## Return As you have noticed, you get back an array from the hook. The first element of the array is form data and the second argument is a handler to get the **serialized** form data if needed. diff --git a/src/plugins/es_ui_shared/static/forms/docs/examples/validation.mdx b/src/plugins/es_ui_shared/static/forms/docs/examples/validation.mdx index 8526a8912ba08..43ec8da11c5cc 100644 --- a/src/plugins/es_ui_shared/static/forms/docs/examples/validation.mdx +++ b/src/plugins/es_ui_shared/static/forms/docs/examples/validation.mdx @@ -334,7 +334,7 @@ const MyForm = () => { Great. Now let's imagine that you want to add a validation to the `indexName` field and mark it as invalid if it does not match at least one index in the cluster. For that you need to provide dynamic data (the list of indices fetched) which is not immediately accesible when the field value changes (and the validation kicks in). We need to ask the validation to **wait** until we have fetched the indices and then have access to the dynamic data. -For that we will use the `validationData$` Observable that you can pass to the field. Whenever a value is sent to the observable (**after** the field value has changed, important!), it will be available in the validator through the `customData.provider()` handler. +For that we will use the `validationDataProvider` prop that you can pass to the field. This data provider will be available in the validator through the `customData.provider()` handler. ```js // form.schema.ts @@ -357,15 +357,28 @@ const schema = { } // myform.tsx +import { firstValueFrom } from '@kbn/std'; + const MyForm = () => { ... const [indices, setIndices] = useState([]); - const [indices$, nextIndices] = useAsyncValidationData(); // Use the provided hook to create the Observable + const [indices$, nextIndices] = useBehaviorSubject(null); // Use the provided util hook to create an observable + + const indicesProvider = useCallback(() => { + // We wait until we have fetched the indices. + // The result will then be sent to the validator (await provider() call); + return await firstValueFrom(indices$.pipe(first((data) => data !== null))); + }, [indices$, nextIndices]); const fetchIndices = useCallback(async () => { + // Reset the subject to not send stale data to the validator + nextIndices(null); + const result = await httpClient.get(`/api/search/${indexName}`); setIndices(result); - nextIndices(result); // Send the indices to your validator "provider()" + + // Send the indices to the BehaviorSubject to resolve the validator "provider()" + nextIndices(result); }, [indexName]); // Whenever the indexName changes we fetch the indices @@ -377,7 +390,7 @@ const MyForm = () => { <>
    /* Pass the Observable to your field */ - + ... diff --git a/src/plugins/es_ui_shared/static/forms/hook_form_lib/components/use_field.test.tsx b/src/plugins/es_ui_shared/static/forms/hook_form_lib/components/use_field.test.tsx index 0950f2dabb1b7..cbf0d9d619636 100644 --- a/src/plugins/es_ui_shared/static/forms/hook_form_lib/components/use_field.test.tsx +++ b/src/plugins/es_ui_shared/static/forms/hook_form_lib/components/use_field.test.tsx @@ -6,13 +6,14 @@ * Side Public License, v 1. */ -import React, { useEffect, FunctionComponent, useState } from 'react'; +import React, { useEffect, FunctionComponent, useState, useCallback } from 'react'; import { act } from 'react-dom/test-utils'; +import { first } from 'rxjs/operators'; import { registerTestBed, TestBed } from '../shared_imports'; import { FormHook, OnUpdateHandler, FieldConfig, FieldHook } from '../types'; import { useForm } from '../hooks/use_form'; -import { useAsyncValidationData } from '../hooks/use_async_validation_data'; +import { useBehaviorSubject } from '../hooks/utils/use_behavior_subject'; import { Form } from './form'; import { UseField } from './use_field'; @@ -420,8 +421,18 @@ describe('', () => { const TestComp = ({ validationData }: DynamicValidationDataProps) => { const { form } = useForm({ schema }); - const [stateValue, setStateValue] = useState('initialValue'); - const [validationData$, next] = useAsyncValidationData(stateValue); + const [validationData$, next] = useBehaviorSubject(undefined); + + const validationDataProvider = useCallback(async () => { + const data = await validationData$ + .pipe(first((value) => value !== undefined)) + .toPromise(); + + // Clear the Observable so we are forced to send a new value to + // resolve the provider + next(undefined); + return data; + }, [validationData$, next]); const setInvalidDynamicData = () => { next('bad'); @@ -431,22 +442,12 @@ describe('', () => { next('good'); }; - // Updating the state should emit a new value in the observable - // which in turn should be available in the validation and allow it to complete. - const setStateValueWithValidValue = () => { - setStateValue('good'); - }; - - const setStateValueWithInValidValue = () => { - setStateValue('bad'); - }; - return (
    <> {/* Dynamic async validation data with an observable. The validation will complete **only after** the observable has emitted a value. */} - path="name" validationData$={validationData$}> + path="name" validationDataProvider={validationDataProvider}> {(field) => { onNameFieldHook(field); return ( @@ -479,15 +480,6 @@ describe('', () => { - - ); @@ -519,7 +511,8 @@ describe('', () => { await act(async () => { jest.advanceTimersByTime(10000); }); - // The field is still validating as no value has been sent to the observable + // The field is still validating as the validationDataProvider has not resolved yet + // (no value has been sent to the observable) expect(nameFieldHook?.isValidating).toBe(true); // We now send a valid value to the observable @@ -545,38 +538,6 @@ describe('', () => { expect(nameFieldHook?.getErrorsMessages()).toBe('Invalid dynamic data'); }); - test('it should access dynamic data coming after the field value changed, **in sync** with a state change', async () => { - const { form, find } = setupDynamicData(); - - await act(async () => { - form.setInputValue('nameField', 'newValue'); - }); - expect(nameFieldHook?.isValidating).toBe(true); - - // We now update the state with a valid value - // this should update the observable - await act(async () => { - find('setValidStateValueBtn').simulate('click'); - }); - - expect(nameFieldHook?.isValidating).toBe(false); - expect(nameFieldHook?.isValid).toBe(true); - - // Let's change the input value to trigger the validation once more - await act(async () => { - form.setInputValue('nameField', 'anotherValue'); - }); - expect(nameFieldHook?.isValidating).toBe(true); - - // And change the state with an invalid value - await act(async () => { - find('setInvalidStateValueBtn').simulate('click'); - }); - - expect(nameFieldHook?.isValidating).toBe(false); - expect(nameFieldHook?.isValid).toBe(false); - }); - test('it should access dynamic data provided through props', async () => { let { form } = setupDynamicData({ validationData: 'good' }); diff --git a/src/plugins/es_ui_shared/static/forms/hook_form_lib/components/use_field.tsx b/src/plugins/es_ui_shared/static/forms/hook_form_lib/components/use_field.tsx index a73eee1bd8bd3..49ee21667752a 100644 --- a/src/plugins/es_ui_shared/static/forms/hook_form_lib/components/use_field.tsx +++ b/src/plugins/es_ui_shared/static/forms/hook_form_lib/components/use_field.tsx @@ -7,7 +7,6 @@ */ import React, { FunctionComponent } from 'react'; -import { Observable } from 'rxjs'; import { FieldHook, FieldConfig, FormData } from '../types'; import { useField } from '../hooks'; @@ -23,8 +22,6 @@ export interface Props { /** * Use this prop to pass down dynamic data **asynchronously** to your validators. * Your validator accesses the dynamic data by resolving the provider() Promise. - * The Promise will resolve **when a new value is sent** to the validationData$ Observable. - * * ```typescript * validator: ({ customData }) => { * // Wait until a value is sent to the "validationData$" Observable @@ -32,7 +29,7 @@ export interface Props { * } * ``` */ - validationData$?: Observable; + validationDataProvider?: () => Promise; /** * Use this prop to pass down dynamic data to your validators. The validation data * is then accessible in your validator inside the `customData.value` property. @@ -63,7 +60,7 @@ function UseFieldComp(props: Props(props: Props(form, path, fieldConfig, onChange, onError, { - customValidationData$, customValidationData, + customValidationDataProvider, }); // Children prevails over anything else provided. diff --git a/src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/index.ts b/src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/index.ts index 6f2dc768508ec..f4911bfaadfa4 100644 --- a/src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/index.ts +++ b/src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/index.ts @@ -11,4 +11,4 @@ export { useField } from './use_field'; export { useForm } from './use_form'; export { useFormData } from './use_form_data'; export { useFormIsModified } from './use_form_is_modified'; -export { useAsyncValidationData } from './use_async_validation_data'; +export { useBehaviorSubject } from './utils'; diff --git a/src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_async_validation_data.ts b/src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_async_validation_data.ts deleted file mode 100644 index 21d5e101536ae..0000000000000 --- a/src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_async_validation_data.ts +++ /dev/null @@ -1,36 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ -import { useCallback, useRef, useMemo, useEffect } from 'react'; -import { Subject, Observable } from 'rxjs'; - -export const useAsyncValidationData = (state?: T) => { - const validationData$ = useRef>(); - - const getValidationData$ = useCallback(() => { - if (validationData$.current === undefined) { - validationData$.current = new Subject(); - } - return validationData$.current; - }, []); - - const hook: [Observable, (value?: T) => void] = useMemo(() => { - const subject = getValidationData$(); - - const observable = subject.asObservable(); - const next = subject.next.bind(subject); - - return [observable, next]; - }, [getValidationData$]); - - // Whenever the state changes we update the observable - useEffect(() => { - getValidationData$().next(state); - }, [state, getValidationData$]); - - return hook; -}; diff --git a/src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_field.ts b/src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_field.ts index c01295f6ee42c..5079a8b69ba80 100644 --- a/src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_field.ts +++ b/src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_field.ts @@ -7,8 +7,6 @@ */ import { useMemo, useState, useEffect, useRef, useCallback } from 'react'; -import { Observable } from 'rxjs'; -import { first } from 'rxjs/operators'; import { FormHook, @@ -33,9 +31,12 @@ export const useField = ( valueChangeListener?: (value: I) => void, errorChangeListener?: (errors: string[] | null) => void, { - customValidationData$, customValidationData = null, - }: { customValidationData$?: Observable; customValidationData?: unknown } = {} + customValidationDataProvider, + }: { + customValidationData?: unknown; + customValidationDataProvider?: () => Promise; + } = {} ) => { const { type = FIELD_TYPES.TEXT, @@ -59,7 +60,7 @@ export const useField = ( __addField, __removeField, __updateFormDataAt, - __validateFields, + validateFields, __getFormData$, } = form; @@ -94,6 +95,14 @@ export const useField = ( errors: null, }); + const hasAsyncValidation = useMemo( + () => + validations === undefined + ? false + : validations.some((validation) => validation.isAsync === true), + [validations] + ); + // ---------------------------------- // -- HELPERS // ---------------------------------- @@ -147,7 +156,7 @@ export const useField = ( __updateFormDataAt(path, value); // Validate field(s) (this will update the form.isValid state) - await __validateFields(fieldsToValidateOnChange ?? [path]); + await validateFields(fieldsToValidateOnChange ?? [path]); if (isMounted.current === false) { return; @@ -156,7 +165,7 @@ export const useField = ( /** * If we have set a delay to display the error message after the field value has changed, * we first check that this is the last "change iteration" (=== the last keystroke from the user) - * and then, we verify how long we've already waited for as form.__validateFields() is asynchronous + * and then, we verify how long we've already waited for as form.validateFields() is asynchronous * and might already have taken more than the specified delay) */ if (changeIteration === changeCounter.current) { @@ -181,7 +190,7 @@ export const useField = ( valueChangeDebounceTime, fieldsToValidateOnChange, __updateFormDataAt, - __validateFields, + validateFields, ]); // Cancel any inflight validation (e.g an HTTP Request) @@ -238,18 +247,13 @@ export const useField = ( return false; }; - let dataProvider: () => Promise = () => Promise.resolve(null); - - if (customValidationData$) { - dataProvider = () => customValidationData$.pipe(first()).toPromise(); - } + const dataProvider: () => Promise = + customValidationDataProvider ?? (() => Promise.resolve(undefined)); const runAsync = async () => { const validationErrors: ValidationError[] = []; for (const validation of validations) { - inflightValidation.current = null; - const { validator, exitOnFail = true, @@ -271,6 +275,8 @@ export const useField = ( const validationResult = await inflightValidation.current; + inflightValidation.current = null; + if (!validationResult) { continue; } @@ -345,17 +351,22 @@ export const useField = ( return validationErrors; }; + if (hasAsyncValidation) { + return runAsync(); + } + // We first try to run the validations synchronously return runSync(); }, [ cancelInflightValidation, validations, + hasAsyncValidation, getFormData, getFields, path, customValidationData, - customValidationData$, + customValidationDataProvider, ] ); @@ -388,7 +399,6 @@ export const useField = ( onlyBlocking = false, } = validationData; - setIsValidated(true); setValidating(true); // By the time our validate function has reached completion, it’s possible @@ -401,6 +411,7 @@ export const useField = ( if (validateIteration === validateCounter.current && isMounted.current) { // This is the most recent invocation setValidating(false); + setIsValidated(true); // Update the errors array setStateErrors((prev) => { const filteredErrors = filterErrors(prev, validationType); diff --git a/src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_form.test.tsx b/src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_form.test.tsx index 92a9876f1cd30..e3e818729340e 100644 --- a/src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_form.test.tsx +++ b/src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_form.test.tsx @@ -572,4 +572,63 @@ describe('useForm() hook', () => { expect(isValid).toBe(false); }); }); + + describe('form.getErrors()', () => { + test('should return the errors in the form', async () => { + const TestComp = () => { + const { form } = useForm(); + formHook = form; + + return ( +
    + + + { + if (value === 'bad') { + return { + message: 'Field2 is invalid', + }; + } + }, + }, + ], + }} + /> + + ); + }; + + const { + form: { setInputValue }, + } = registerTestBed(TestComp)() as TestBed; + + let errors: string[] = formHook!.getErrors(); + expect(errors).toEqual([]); + + await act(async () => { + await formHook!.submit(); + }); + errors = formHook!.getErrors(); + expect(errors).toEqual(['Field1 can not be empty']); + + await setInputValue('field2', 'bad'); + errors = formHook!.getErrors(); + expect(errors).toEqual(['Field1 can not be empty', 'Field2 is invalid']); + }); + }); }); diff --git a/src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_form.ts b/src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_form.ts index 23827c0d1aa3b..f8a773597a823 100644 --- a/src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_form.ts +++ b/src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_form.ts @@ -66,6 +66,7 @@ export function useForm( const [isSubmitted, setIsSubmitted] = useState(false); const [isSubmitting, setSubmitting] = useState(false); const [isValid, setIsValid] = useState(undefined); + const [errorMessages, setErrorMessages] = useState<{ [fieldName: string]: string }>({}); const fieldsRefs = useRef({}); const fieldsRemovedRefs = useRef({}); @@ -73,6 +74,19 @@ export function useForm( const isMounted = useRef(false); const defaultValueDeserialized = useRef(defaultValueMemoized); + /** + * We have both a state and a ref for the error messages so the consumer can, in the same callback, + * validate the form **and** have the errors returned immediately. + * + * ``` + * const myHandler = useCallback(async () => { + * const isFormValid = await validate(); + * const errors = getErrors(); // errors from the validate() call are there + * }, [validate, getErrors]); + * ``` + */ + const errorMessagesRef = useRef<{ [fieldName: string]: string }>({}); + // formData$ is an observable we can subscribe to in order to receive live // update of the raw form data. As an observable it does not trigger any React // render(). @@ -97,6 +111,34 @@ export function useForm( [getFormData$] ); + const updateFieldErrorMessage = useCallback((path: string, errorMessage: string | null) => { + setErrorMessages((prev) => { + const previousMessageValue = prev[path]; + + if ( + errorMessage === previousMessageValue || + (previousMessageValue === undefined && errorMessage === null) + ) { + // Don't update the state, the error message has not changed. + return prev; + } + + if (errorMessage === null) { + // We strip out previous error message + const { [path]: discard, ...next } = prev; + errorMessagesRef.current = next; + return next; + } + + const next = { + ...prev, + [path]: errorMessage, + }; + errorMessagesRef.current = next; + return next; + }); + }, []); + const fieldsToArray = useCallback<() => FieldHook[]>(() => Object.values(fieldsRefs.current), []); const getFieldsForOutput = useCallback( @@ -158,7 +200,7 @@ export function useForm( }); }, [fieldsToArray]); - const validateFields: FormHook['__validateFields'] = useCallback( + const validateFields: FormHook['validateFields'] = useCallback( async (fieldNames, onlyBlocking = false) => { const fieldsToValidate = fieldNames .map((name) => fieldsRefs.current[name]) @@ -224,6 +266,7 @@ export function useForm( delete fieldsRemovedRefs.current[field.path]; updateFormDataAt(field.path, field.value); + updateFieldErrorMessage(field.path, field.getErrorsMessages()); if (!fieldExists && !field.isValidated) { setIsValid(undefined); @@ -235,7 +278,7 @@ export function useForm( setIsSubmitted(false); } }, - [updateFormDataAt] + [updateFormDataAt, updateFieldErrorMessage] ); const removeField: FormHook['__removeField'] = useCallback( @@ -247,7 +290,7 @@ export function useForm( // Keep a track of the fields that have been removed from the form // This will allow us to know if the form has been modified fieldsRemovedRefs.current[name] = fieldsRefs.current[name]; - + updateFieldErrorMessage(name, null); delete fieldsRefs.current[name]; delete currentFormData[name]; }); @@ -267,7 +310,7 @@ export function useForm( return prev; }); }, - [getFormData$, updateFormData$, fieldsToArray] + [getFormData$, updateFormData$, fieldsToArray, updateFieldErrorMessage] ); const getFormDefaultValue: FormHook['__getFormDefaultValue'] = useCallback( @@ -306,15 +349,8 @@ export function useForm( if (isValid === true) { return []; } - - return fieldsToArray().reduce((acc, field) => { - const fieldError = field.getErrorsMessages(); - if (fieldError === null) { - return acc; - } - return [...acc, fieldError]; - }, [] as string[]); - }, [isValid, fieldsToArray]); + return Object.values({ ...errorMessages, ...errorMessagesRef.current }); + }, [isValid, errorMessages]); const validate: FormHook['validate'] = useCallback(async (): Promise => { // Maybe some field are being validated because of their async validation(s). @@ -458,6 +494,7 @@ export function useForm( getFormData, getErrors, reset, + validateFields, __options: formOptions, __getFormData$: getFormData$, __updateFormDataAt: updateFormDataAt, @@ -467,7 +504,6 @@ export function useForm( __addField: addField, __removeField: removeField, __getFieldsRemoved: getFieldsRemoved, - __validateFields: validateFields, }; }, [ isSubmitted, diff --git a/src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_form_data.test.tsx b/src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_form_data.test.tsx index c6f920ef88c69..614d4a5f3fd1d 100644 --- a/src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_form_data.test.tsx +++ b/src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_form_data.test.tsx @@ -15,7 +15,7 @@ import { useForm } from './use_form'; import { useFormData, HookReturn } from './use_form_data'; interface Props { - onChange(data: HookReturn): void; + onHookValueChange(data: HookReturn): void; watch?: string | string[]; } @@ -36,16 +36,16 @@ interface Form3 { } describe('useFormData() hook', () => { - const HookListenerComp = function ({ onChange, watch }: Props) { + const HookListenerComp = function ({ onHookValueChange, watch }: Props) { const hookValue = useFormData({ watch }); const isMounted = useRef(false); useEffect(() => { if (isMounted.current) { - onChange(hookValue); + onHookValueChange(hookValue); } isMounted.current = true; - }, [hookValue, onChange]); + }, [hookValue, onHookValueChange]); return null; }; @@ -77,7 +77,7 @@ describe('useFormData() hook', () => { beforeEach(() => { onChangeSpy = jest.fn(); - testBed = setup({ onChange: onChangeSpy }) as TestBed; + testBed = setup({ onHookValueChange: onChangeSpy }) as TestBed; }); test('should return the form data', () => { @@ -126,7 +126,7 @@ describe('useFormData() hook', () => { beforeEach(() => { onChangeSpy = jest.fn(); - setup({ onChange: onChangeSpy }); + setup({ onHookValueChange: onChangeSpy }); }); test('should expose a handler to build the form data', () => { @@ -171,7 +171,7 @@ describe('useFormData() hook', () => { beforeEach(() => { onChangeSpy = jest.fn(); - testBed = setup({ watch: 'title', onChange: onChangeSpy }) as TestBed; + testBed = setup({ watch: 'title', onHookValueChange: onChangeSpy }) as TestBed; }); test('should not listen to changes on fields we are not interested in', async () => { @@ -199,13 +199,13 @@ describe('useFormData() hook', () => { return onChangeSpy.mock.calls[onChangeSpy.mock.calls.length - 1][0] as HookReturn; }; - const TestComp = ({ onChange }: Props) => { + const TestComp = ({ onHookValueChange }: Props) => { const { form } = useForm(); const hookValue = useFormData({ form }); useEffect(() => { - onChange(hookValue); - }, [hookValue, onChange]); + onHookValueChange(hookValue); + }, [hookValue, onHookValueChange]); return (
    @@ -220,7 +220,7 @@ describe('useFormData() hook', () => { beforeEach(() => { onChangeSpy = jest.fn(); - testBed = setup({ onChange: onChangeSpy }) as TestBed; + testBed = setup({ onHookValueChange: onChangeSpy }) as TestBed; }); test('should allow a form to be provided when the hook is called outside of the FormDataContext', async () => { @@ -239,5 +239,71 @@ describe('useFormData() hook', () => { expect(updatedData).toEqual({ title: 'titleChanged' }); }); }); + + describe('onChange', () => { + let testBed: TestBed; + let onChangeSpy: jest.Mock; + let validationSpy: jest.Mock; + + const TestComp = () => { + const { form } = useForm(); + useFormData({ form, onChange: onChangeSpy }); + + return ( + + { + // This spy should be called **after** the onChangeSpy + validationSpy(); + }, + }, + ], + }} + /> + + ); + }; + + const setup = registerTestBed(TestComp, { + memoryRouter: { wrapComponent: false }, + }); + + beforeEach(() => { + onChangeSpy = jest.fn(); + validationSpy = jest.fn(); + testBed = setup({ watch: 'title' }) as TestBed; + }); + + test('should call onChange handler _before_ running the validations', async () => { + const { + form: { setInputValue }, + } = testBed; + + onChangeSpy.mockReset(); // Reset our counters + validationSpy.mockReset(); + + expect(onChangeSpy).not.toHaveBeenCalled(); + expect(validationSpy).not.toHaveBeenCalled(); + + await act(async () => { + setInputValue('titleField', 'titleChanged'); + }); + + expect(onChangeSpy).toHaveBeenCalled(); + expect(validationSpy).toHaveBeenCalled(); + + const onChangeCallOrder = onChangeSpy.mock.invocationCallOrder[0]; + const validationCallOrder = validationSpy.mock.invocationCallOrder[0]; + + // onChange called before validation + expect(onChangeCallOrder).toBeLessThan(validationCallOrder); + }); + }); }); }); diff --git a/src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_form_data.ts b/src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_form_data.ts index 7ad98bc2483bb..7185421553bbf 100644 --- a/src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_form_data.ts +++ b/src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_form_data.ts @@ -6,23 +6,28 @@ * Side Public License, v 1. */ -import { useState, useEffect, useRef, useCallback, useMemo } from 'react'; +import { useState, useEffect, useRef, useCallback } from 'react'; import { FormData, FormHook } from '../types'; import { unflattenObject } from '../lib'; import { useFormDataContext, Context } from '../form_data_context'; -interface Options { +interface Options { watch?: string | string[]; form?: FormHook; + /** + * Use this handler if you want to listen to field value change + * before the validations are ran. + */ + onChange?: (formData: I) => void; } export type HookReturn = [I, () => T, boolean]; export const useFormData = ( - options: Options = {} + options: Options = {} ): HookReturn => { - const { watch, form } = options; + const { watch, form, onChange } = options; const ctx = useFormDataContext(); const watchToArray: string[] = watch === undefined ? [] : Array.isArray(watch) ? watch : [watch]; // We will use "stringifiedWatch" to compare if the array has changed in the useMemo() below @@ -57,29 +62,38 @@ export const useFormData = ( // eslint-disable-next-line react-hooks/exhaustive-deps }, [getFormData, formData]); - const subscription = useMemo(() => { - return getFormData$().subscribe((raw) => { + useEffect(() => { + const subscription = getFormData$().subscribe((raw) => { if (!isMounted.current && Object.keys(raw).length === 0) { return; } if (watchToArray.length > 0) { + // Only update the state if one of the field we watch has changed. if (watchToArray.some((path) => previousRawData.current[path] !== raw[path])) { previousRawData.current = raw; - // Only update the state if one of the field we watch has changed. - setFormData(unflattenObject(raw)); + const nextState = unflattenObject(raw); + + if (onChange) { + onChange(nextState); + } + + setFormData(nextState); } } else { - setFormData(unflattenObject(raw)); + const nextState = unflattenObject(raw); + if (onChange) { + onChange(nextState); + } + setFormData(nextState); } }); - // To compare we use the stringified version of the "watchToArray" array - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [stringifiedWatch, getFormData$]); - useEffect(() => { return subscription.unsubscribe; - }, [subscription]); + + // To compare we use the stringified version of the "watchToArray" array + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [stringifiedWatch, getFormData$, onChange]); useEffect(() => { isMounted.current = true; diff --git a/src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/utils/index.ts b/src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/utils/index.ts new file mode 100644 index 0000000000000..f7d3bd563ea3b --- /dev/null +++ b/src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/utils/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 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 { useBehaviorSubject } from './use_behavior_subject'; diff --git a/src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/utils/use_behavior_subject.ts b/src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/utils/use_behavior_subject.ts new file mode 100644 index 0000000000000..3bf4a6b225c8b --- /dev/null +++ b/src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/utils/use_behavior_subject.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 { useCallback, useRef, useMemo } from 'react'; +import { BehaviorSubject, Observable } from 'rxjs'; + +export const useBehaviorSubject = (initialState: T) => { + const subjectRef = useRef>(); + + const getSubject$ = useCallback(() => { + if (subjectRef.current === undefined) { + subjectRef.current = new BehaviorSubject(initialState); + } + return subjectRef.current; + }, [initialState]); + + const hook: [Observable, (value: T) => void] = useMemo(() => { + const subject = getSubject$(); + + const observable = subject.asObservable(); + const next = subject.next.bind(subject); + + return [observable, next]; + }, [getSubject$]); + + return hook; +}; diff --git a/src/plugins/es_ui_shared/static/forms/hook_form_lib/index.ts b/src/plugins/es_ui_shared/static/forms/hook_form_lib/index.ts index b5c7f5b4214e0..258b15e96e442 100644 --- a/src/plugins/es_ui_shared/static/forms/hook_form_lib/index.ts +++ b/src/plugins/es_ui_shared/static/forms/hook_form_lib/index.ts @@ -8,7 +8,7 @@ // We don't export the "useField" hook as it is for internal use. // The consumer of the library must use the component to create a field -export { useForm, useFormData, useFormIsModified, useAsyncValidationData } from './hooks'; +export { useForm, useFormData, useFormIsModified, useBehaviorSubject } from './hooks'; export { getFieldValidityAndErrorMessage } from './helpers'; export * from './form_context'; diff --git a/src/plugins/es_ui_shared/static/forms/hook_form_lib/types.ts b/src/plugins/es_ui_shared/static/forms/hook_form_lib/types.ts index cfb211b702ed6..2e1863adaa467 100644 --- a/src/plugins/es_ui_shared/static/forms/hook_form_lib/types.ts +++ b/src/plugins/es_ui_shared/static/forms/hook_form_lib/types.ts @@ -50,15 +50,15 @@ export interface FormHook * all the fields to their initial values. */ reset: (options?: { resetValues?: boolean; defaultValue?: Partial }) => void; - readonly __options: Required; - __getFormData$: () => Subject; - __addField: (field: FieldHook) => void; - __removeField: (fieldNames: string | string[]) => void; - __validateFields: ( + validateFields: ( fieldNames: string[], /** Run only blocking validations */ onlyBlocking?: boolean ) => Promise<{ areFieldsValid: boolean; isFormValid: boolean | undefined }>; + readonly __options: Required; + __getFormData$: () => Subject; + __addField: (field: FieldHook) => void; + __removeField: (fieldNames: string | string[]) => void; __updateFormDataAt: (field: string, value: unknown) => void; __updateDefaultValueAt: (field: string, value: unknown) => void; __readFieldConfigFromSchema: (field: string) => FieldConfig; @@ -206,7 +206,14 @@ export type ValidationFunc< V = unknown > = ( data: ValidationFuncArg -) => ValidationError | void | undefined | Promise | void | undefined>; +) => ValidationError | void | undefined | ValidationCancelablePromise; + +export type ValidationResponsePromise = Promise< + ValidationError | void | undefined +>; + +export type ValidationCancelablePromise = + ValidationResponsePromise & { cancel?(): void }; export interface FieldValidateResponse { isValid: boolean; @@ -239,4 +246,12 @@ export interface ValidationConfig< */ isBlocking?: boolean; exitOnFail?: boolean; + /** + * Flag to indicate if the validation is asynchronous. If not specified the lib will + * first try to run all the validations synchronously and if it detects a Promise it + * will run the validations a second time asynchronously. + * This means that HTTP request will be called twice which is not ideal. It is then + * recommended to set the "isAsync" flag to `true` to all asynchronous validations. + */ + isAsync?: boolean; } diff --git a/src/plugins/expression_error/jest.config.js b/src/plugins/expression_error/jest.config.js deleted file mode 100644 index 27774f4003f9e..0000000000000 --- a/src/plugins/expression_error/jest.config.js +++ /dev/null @@ -1,16 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -module.exports = { - preset: '@kbn/test', - rootDir: '../../..', - roots: ['/src/plugins/expression_error'], - coverageDirectory: '/target/kibana-coverage/jest/src/plugins/expression_error', - coverageReporters: ['text', 'html'], - collectCoverageFrom: ['/src/plugins/expression_error/{common,public}/**/*.{ts,tsx}'], -}; diff --git a/src/plugins/expressions/common/execution/execution.ts b/src/plugins/expressions/common/execution/execution.ts index 54a4800ec7c34..90e05083fd9f1 100644 --- a/src/plugins/expressions/common/execution/execution.ts +++ b/src/plugins/expressions/common/execution/execution.ts @@ -16,7 +16,6 @@ import { from, isObservable, of, - race, throwError, Observable, ReplaySubject, @@ -25,7 +24,7 @@ import { catchError, finalize, map, pluck, shareReplay, switchMap, tap } from 'r import { Executor } from '../executor'; import { createExecutionContainer, ExecutionContainer } from './container'; import { createError } from '../util'; -import { abortSignalToPromise, now } from '../../../kibana_utils/common'; +import { now, AbortError } from '../../../kibana_utils/common'; import { Adapters } from '../../../inspector/common'; import { isExpressionValueError, ExpressionValueError } from '../expression_types/specs/error'; import { @@ -50,13 +49,6 @@ type UnwrapReturnType unknown> = ? UnwrapObservable> : UnwrapPromiseOrReturn>; -// type ArgumentsOf = Function extends ExpressionFunction< -// unknown, -// infer Arguments -// > -// ? Arguments -// : never; - /** * The result returned after an expression function execution. */ @@ -95,6 +87,51 @@ const createAbortErrorValue = () => name: 'AbortError', }); +function markPartial() { + return (source: Observable) => + new Observable>((subscriber) => { + let latest: ExecutionResult | undefined; + + subscriber.add( + source.subscribe({ + next: (result) => { + latest = { result, partial: true }; + subscriber.next(latest); + }, + error: (error) => subscriber.error(error), + complete: () => { + if (latest) { + latest.partial = false; + } + + subscriber.complete(); + }, + }) + ); + + subscriber.add(() => { + latest = undefined; + }); + }); +} + +function takeUntilAborted(signal: AbortSignal) { + return (source: Observable) => + new Observable((subscriber) => { + const throwAbortError = () => { + subscriber.error(new AbortError()); + }; + + subscriber.add(source.subscribe(subscriber)); + subscriber.add(() => signal.removeEventListener('abort', throwAbortError)); + + signal.addEventListener('abort', throwAbortError); + if (signal.aborted) { + throwAbortError(); + } + }); +} + export interface ExecutionParams { executor: Executor; ast?: ExpressionAstExpression; @@ -138,18 +175,6 @@ export class Execution< */ private readonly abortController = getNewAbortController(); - /** - * Promise that rejects if/when abort controller sends "abort" signal. - */ - private readonly abortRejection = abortSignalToPromise(this.abortController.signal); - - /** - * Races a given observable against the "abort" event of `abortController`. - */ - private race(observable: Observable): Observable { - return race(from(this.abortRejection.promise), observable); - } - /** * Whether .start() method has been called. */ @@ -221,32 +246,9 @@ export class Execution< this.result = this.input$.pipe( switchMap((input) => - this.race(this.invokeChain(this.state.get().ast.chain, input)).pipe( - (source) => - new Observable>((subscriber) => { - let latest: ExecutionResult | undefined; - - subscriber.add( - source.subscribe({ - next: (result) => { - latest = { result, partial: true }; - subscriber.next(latest); - }, - error: (error) => subscriber.error(error), - complete: () => { - if (latest) { - latest.partial = false; - } - - subscriber.complete(); - }, - }) - ); - - subscriber.add(() => { - latest = undefined; - }); - }) + this.invokeChain(this.state.get().ast.chain, input).pipe( + takeUntilAborted(this.abortController.signal), + markPartial() ) ), catchError((error) => { @@ -265,7 +267,6 @@ export class Execution< }, error: (error) => this.state.transitions.setError(error), }), - finalize(() => this.abortRejection.cleanup()), shareReplay(1) ); } @@ -356,9 +357,9 @@ export class Execution< // `resolveArgs` returns an object because the arguments themselves might // actually have `then` or `subscribe` methods which would be treated as a `Promise` // or an `Observable` accordingly. - return this.race(this.resolveArgs(fn, currentInput, fnArgs)).pipe( + return this.resolveArgs(fn, currentInput, fnArgs).pipe( tap((args) => this.execution.params.debug && Object.assign(link.debug, { args })), - switchMap((args) => this.race(this.invokeFunction(fn, currentInput, args))), + switchMap((args) => this.invokeFunction(fn, currentInput, args)), switchMap((output) => (getType(output) === 'error' ? throwError(output) : of(output))), tap((output) => this.execution.params.debug && Object.assign(link.debug, { output })), catchError((rawError) => { @@ -390,7 +391,7 @@ export class Execution< ): Observable> { return of(input).pipe( map((currentInput) => this.cast(currentInput, fn.inputTypes)), - switchMap((normalizedInput) => this.race(of(fn.fn(normalizedInput, args, this.context)))), + switchMap((normalizedInput) => of(fn.fn(normalizedInput, args, this.context))), switchMap( (fnResult) => (isObservable(fnResult) diff --git a/src/plugins/expressions/common/expression_functions/specs/font.ts b/src/plugins/expressions/common/expression_functions/specs/font.ts index 3d189a68119d5..628685aa7338c 100644 --- a/src/plugins/expressions/common/expression_functions/specs/font.ts +++ b/src/plugins/expressions/common/expression_functions/specs/font.ts @@ -9,7 +9,15 @@ import { i18n } from '@kbn/i18n'; import { ExpressionFunctionDefinition } from '../types'; import { openSans, FontLabel as FontFamily } from '../../fonts'; -import { CSSStyle, FontStyle, FontWeight, Style, TextAlignment, TextDecoration } from '../../types'; +import { + CSSStyle, + FontSizeUnit, + FontStyle, + FontWeight, + Style, + TextAlignment, + TextDecoration, +} from '../../types'; const dashify = (str: string) => { return str @@ -39,6 +47,7 @@ export interface FontArguments { size?: number; underline?: boolean; weight?: FontWeight; + sizeUnit?: string; } export type ExpressionFunctionFont = ExpressionFunctionDefinition< @@ -101,10 +110,18 @@ export const font: ExpressionFunctionFont = { size: { default: `{ theme "font.size" default=14 }`, help: i18n.translate('expressions.functions.font.args.sizeHelpText', { - defaultMessage: 'The font size in pixels', + defaultMessage: 'The font size', }), types: ['number'], }, + sizeUnit: { + default: `px`, + help: i18n.translate('expressions.functions.font.args.sizeUnitHelpText', { + defaultMessage: 'The font size unit', + }), + types: ['string'], + options: ['px', 'pt'], + }, underline: { default: `{ theme "font.underline" default=false }`, help: i18n.translate('expressions.functions.font.args.underlineHelpText', { @@ -155,13 +172,25 @@ export const font: ExpressionFunctionFont = { // pixel setting const lineHeight = args.lHeight != null ? `${args.lHeight}px` : '1'; + const availableSizeUnits: string[] = [FontSizeUnit.PX, FontSizeUnit.PT]; + if (args.sizeUnit && !availableSizeUnits.includes(args.sizeUnit)) { + throw new Error( + i18n.translate('expressions.functions.font.invalidSizeUnitErrorMessage', { + defaultMessage: "Invalid size unit: '{sizeUnit}'", + values: { + sizeUnit: args.sizeUnit, + }, + }) + ); + } + const spec: CSSStyle = { fontFamily: args.family, fontWeight: args.weight, fontStyle: args.italic ? FontStyle.ITALIC : FontStyle.NORMAL, textDecoration: args.underline ? TextDecoration.UNDERLINE : TextDecoration.NONE, textAlign: args.align, - fontSize: `${args.size}px`, // apply font size as a pixel setting + fontSize: `${args.size}${args.sizeUnit}`, lineHeight, // apply line height as a pixel setting }; diff --git a/src/plugins/expressions/common/fonts.ts b/src/plugins/expressions/common/fonts.ts index f52282aa8f2f6..6f5c64ccea81e 100644 --- a/src/plugins/expressions/common/fonts.ts +++ b/src/plugins/expressions/common/fonts.ts @@ -92,6 +92,12 @@ export const hoeflerText = createFont({ value: "'Hoefler Text', Garamond, Georgia, 'Times New Roman', Times, serif", }); +export const inter = createFont({ + label: 'Inter', + value: + "'Inter', 'Inter UI', -apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'", +}); + export const lucidaGrande = createFont({ label: 'Lucida Grande', value: "'Lucida Grande', 'Lucida Sans Unicode', Lucida, Verdana, Helvetica, Arial, sans-serif", @@ -132,6 +138,7 @@ export const fonts = [ gillSans, helveticaNeue, hoeflerText, + inter, lucidaGrande, myriad, openSans, diff --git a/src/plugins/expressions/common/types/style.ts b/src/plugins/expressions/common/types/style.ts index 8c37dfa34b02a..861dcf2580d25 100644 --- a/src/plugins/expressions/common/types/style.ts +++ b/src/plugins/expressions/common/types/style.ts @@ -84,6 +84,11 @@ export enum TextDecoration { UNDERLINE = 'underline', } +export enum FontSizeUnit { + PX = 'px', + PT = 'pt', +} + /** * Represents the various style properties that can be applied to an element. */ diff --git a/src/plugins/expressions/public/react_expression_renderer.tsx b/src/plugins/expressions/public/react_expression_renderer.tsx index 77b4402b22c06..b42ea3f3fd149 100644 --- a/src/plugins/expressions/public/react_expression_renderer.tsx +++ b/src/plugins/expressions/public/react_expression_renderer.tsx @@ -12,7 +12,7 @@ import { Observable, Subscription } from 'rxjs'; import { filter } from 'rxjs/operators'; import useShallowCompareEffect from 'react-use/lib/useShallowCompareEffect'; import { EuiLoadingChart, EuiProgress } from '@elastic/eui'; -import theme from '@elastic/eui/dist/eui_theme_light.json'; +import { euiLightVars as theme } from '@kbn/ui-shared-deps-src/theme'; import { IExpressionLoaderParams, ExpressionRenderError, ExpressionRendererEvent } from './types'; import { ExpressionAstExpression, IInterpreterRenderHandlers } from '../common'; import { ExpressionLoader } from './loader'; diff --git a/src/plugins/field_formats/common/index.ts b/src/plugins/field_formats/common/index.ts index 5863bf79adcba..092fc49af3d28 100644 --- a/src/plugins/field_formats/common/index.ts +++ b/src/plugins/field_formats/common/index.ts @@ -25,7 +25,6 @@ export { NumberFormat, PercentFormat, RelativeDateFormat, - SourceFormat, StaticLookupFormat, UrlFormat, StringFormat, diff --git a/src/plugins/home/public/application/components/tutorial/__snapshots__/introduction.test.js.snap b/src/plugins/home/public/application/components/tutorial/__snapshots__/introduction.test.js.snap index 39bdda213acba..247f43df76d3b 100644 --- a/src/plugins/home/public/application/components/tutorial/__snapshots__/introduction.test.js.snap +++ b/src/plugins/home/public/application/components/tutorial/__snapshots__/introduction.test.js.snap @@ -1,118 +1,198 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`props exportedFieldsUrl 1`] = ` - - +
    + + + +
    + + -
    - - - + + +
    + + + +
    + + } + pageTitle={ + + Great tutorial - - } - pageTitle={ - - Great tutorial - - } -/> + } + /> + `; exports[`props iconType 1`] = ` - - +
    + + - - } - iconType="logoElastic" - pageTitle={ - - Great tutorial - - } -/> + +
    + + + + + } + iconType="logoElastic" + pageTitle={ + + Great tutorial + + } + /> + `; exports[`props isBeta 1`] = ` - - +
    + + - - } - pageTitle={ - - Great tutorial + +
    + + -   - - - } -/> + } + pageTitle={ + + Great tutorial + +   + + + + } + /> + `; exports[`props previewUrl 1`] = ` - - +
    + + - - } - pageTitle={ - - Great tutorial - - } - rightSideItems={ - Array [ - , - ] - } -/> + +
    + + + + + } + pageTitle={ + + Great tutorial + + } + rightSideItems={ + Array [ + , + ] + } + /> + `; exports[`render 1`] = ` - - +
    + + - - } - pageTitle={ - - Great tutorial - - } -/> + +
    + + + + + } + pageTitle={ + + Great tutorial + + } + /> + `; diff --git a/src/plugins/home/public/application/components/tutorial/__snapshots__/tutorial.test.js.snap b/src/plugins/home/public/application/components/tutorial/__snapshots__/tutorial.test.js.snap index 91dcdabd75dee..9dfa395d04b71 100644 --- a/src/plugins/home/public/application/components/tutorial/__snapshots__/tutorial.test.js.snap +++ b/src/plugins/home/public/application/components/tutorial/__snapshots__/tutorial.test.js.snap @@ -6,6 +6,11 @@ exports[`isCloudEnabled is false should not render instruction toggle when ON_PR >
    - {title} - {betaBadge && ( - <> -   - {betaBadge} - - )} - - } - description={ - <> - - {exportedFields} - {notices} - - } - rightSideItems={rightSideItems} - /> + <> +
    + + + +
    + + + {title} + {betaBadge && ( + <> +   + {betaBadge} + + )} + + } + description={ + <> + + {exportedFields} + {notices} + + } + rightSideItems={rightSideItems} + /> + ); } diff --git a/src/plugins/home/public/application/components/tutorial/introduction.test.js b/src/plugins/home/public/application/components/tutorial/introduction.test.js index 949f84d0181ed..49293fa13b015 100644 --- a/src/plugins/home/public/application/components/tutorial/introduction.test.js +++ b/src/plugins/home/public/application/components/tutorial/introduction.test.js @@ -10,12 +10,16 @@ import React from 'react'; import { shallowWithIntl } from '@kbn/test/jest'; import { Introduction } from './introduction'; +import { httpServiceMock } from '../../../../../../core/public/mocks'; + +const basePathMock = httpServiceMock.createBasePath(); test('render', () => { const component = shallowWithIntl( ); expect(component).toMatchSnapshot(); // eslint-disable-line @@ -27,6 +31,7 @@ describe('props', () => { ); @@ -38,6 +43,7 @@ describe('props', () => { ); @@ -49,6 +55,7 @@ describe('props', () => { ); @@ -60,6 +67,7 @@ describe('props', () => { ); diff --git a/src/plugins/home/public/application/components/tutorial/tutorial.js b/src/plugins/home/public/application/components/tutorial/tutorial.js index 4af5e362baca9..fa77d46a5394f 100644 --- a/src/plugins/home/public/application/components/tutorial/tutorial.js +++ b/src/plugins/home/public/application/components/tutorial/tutorial.js @@ -421,6 +421,7 @@ class TutorialUi extends React.Component { iconType={icon} isBeta={this.state.tutorial.isBeta} notices={this.renderModuleNotices()} + basePath={getServices().http.basePath} /> {this.renderInstructionSetsToggle()} diff --git a/src/plugins/home/public/application/components/tutorial/tutorial.test.js b/src/plugins/home/public/application/components/tutorial/tutorial.test.js index c68f5ec69e161..73499c0dcb75f 100644 --- a/src/plugins/home/public/application/components/tutorial/tutorial.test.js +++ b/src/plugins/home/public/application/components/tutorial/tutorial.test.js @@ -15,6 +15,7 @@ jest.mock('../../kibana_services', () => ({ getServices: () => ({ http: { post: jest.fn().mockImplementation(async () => ({ count: 1 })), + basePath: { prepend: (path) => `/foo/${path}` }, }, getBasePath: jest.fn(() => 'path'), chrome: { diff --git a/src/plugins/index_pattern_editor/public/components/form_fields/type_field.tsx b/src/plugins/index_pattern_editor/public/components/form_fields/type_field.tsx index e8a48c5679879..0f4a040d1317b 100644 --- a/src/plugins/index_pattern_editor/public/components/form_fields/type_field.tsx +++ b/src/plugins/index_pattern_editor/public/components/form_fields/type_field.tsx @@ -8,8 +8,7 @@ import React from 'react'; import { i18n } from '@kbn/i18n'; -// @ts-ignore -import { euiColorAccent } from '@elastic/eui/dist/eui_theme_light.json'; +import { euiLightVars } from '@kbn/ui-shared-deps-src/theme'; import { FormattedMessage } from '@kbn/i18n/react'; import { @@ -54,7 +53,7 @@ const rollupSelectItem = ( defaultMessage="Rollup data view" />   - + diff --git a/src/plugins/index_pattern_field_editor/__jest__/client_integration/field_editor.helpers.ts b/src/plugins/index_pattern_field_editor/__jest__/client_integration/field_editor.helpers.ts index 0d58b2ce89358..1fd280a937a03 100644 --- a/src/plugins/index_pattern_field_editor/__jest__/client_integration/field_editor.helpers.ts +++ b/src/plugins/index_pattern_field_editor/__jest__/client_integration/field_editor.helpers.ts @@ -12,12 +12,10 @@ import { Context } from '../../public/components/field_editor_context'; import { FieldEditor, Props } from '../../public/components/field_editor/field_editor'; import { WithFieldEditorDependencies, getCommonActions } from './helpers'; +export { waitForUpdates, waitForDocumentsAndPreviewUpdate } from './helpers'; + export const defaultProps: Props = { onChange: jest.fn(), - syntaxError: { - error: null, - clear: () => {}, - }, }; export type FieldEditorTestBed = TestBed & { actions: ReturnType }; diff --git a/src/plugins/index_pattern_field_editor/__jest__/client_integration/field_editor.test.tsx b/src/plugins/index_pattern_field_editor/__jest__/client_integration/field_editor.test.tsx index 4a4c42f69fc8e..55b9876ac54ad 100644 --- a/src/plugins/index_pattern_field_editor/__jest__/client_integration/field_editor.test.tsx +++ b/src/plugins/index_pattern_field_editor/__jest__/client_integration/field_editor.test.tsx @@ -5,20 +5,18 @@ * in compliance with, at your election, the Elastic License 2.0 or the Server * Side Public License, v 1. */ -import React, { useState, useMemo } from 'react'; import { act } from 'react-dom/test-utils'; -import { registerTestBed, TestBed } from '@kbn/test/jest'; // This import needs to come first as it contains the jest.mocks -import { setupEnvironment, getCommonActions, WithFieldEditorDependencies } from './helpers'; -import { - FieldEditor, - FieldEditorFormState, - Props, -} from '../../public/components/field_editor/field_editor'; +import { setupEnvironment, mockDocuments } from './helpers'; +import { FieldEditorFormState, Props } from '../../public/components/field_editor/field_editor'; import type { Field } from '../../public/types'; -import type { RuntimeFieldPainlessError } from '../../public/lib'; -import { setup, FieldEditorTestBed, defaultProps } from './field_editor.helpers'; +import { setSearchResponse } from './field_editor_flyout_preview.helpers'; +import { + setup, + FieldEditorTestBed, + waitForDocumentsAndPreviewUpdate, +} from './field_editor.helpers'; describe('', () => { const { server, httpRequestsMockHelpers } = setupEnvironment(); @@ -42,18 +40,14 @@ describe('', () => { let promise: ReturnType; await act(async () => { - // We can't await for the promise here as the validation for the - // "script" field has a setTimeout which is mocked by jest. If we await - // we don't have the chance to call jest.advanceTimersByTime and thus the - // test times out. + // We can't await for the promise here ("await state.submit()") as the validation for the + // "script" field has different setTimeout mocked by jest. + // If we await here (await state.submit()) we don't have the chance to call jest.advanceTimersByTime() + // below and the test times out. promise = state.submit(); }); - await act(async () => { - // The painless syntax validation has a timeout set to 600ms - // we give it a bit more time just to be on the safe side - jest.advanceTimersByTime(1000); - }); + await waitForDocumentsAndPreviewUpdate(); await act(async () => { promise.then((response) => { @@ -61,7 +55,13 @@ describe('', () => { }); }); - return formState!; + if (formState === undefined) { + throw new Error( + `The form state is not defined, this probably means that the promise did not resolve due to an unresolved validation.` + ); + } + + return formState; }; beforeAll(() => { @@ -75,6 +75,7 @@ describe('', () => { beforeEach(async () => { onChange = jest.fn(); + setSearchResponse(mockDocuments); httpRequestsMockHelpers.setFieldPreviewResponse({ values: ['mockedScriptValue'] }); }); @@ -88,7 +89,7 @@ describe('', () => { try { expect(isOn).toBe(false); - } catch (e) { + } catch (e: any) { e.message = `"${row}" row toggle expected to be 'off' but was 'on'. \n${e.message}`; throw e; } @@ -179,74 +180,5 @@ describe('', () => { expect(getLastStateUpdate().isValid).toBe(true); expect(form.getErrorsMessages()).toEqual([]); }); - - test('should clear the painless syntax error whenever the field type changes', async () => { - const field: Field = { - name: 'myRuntimeField', - type: 'keyword', - script: { source: 'emit(6)' }, - }; - - const dummyError = { - reason: 'Awwww! Painless syntax error', - message: '', - position: { offset: 0, start: 0, end: 0 }, - scriptStack: [''], - }; - - const ComponentToProvidePainlessSyntaxErrors = () => { - const [error, setError] = useState(null); - const clearError = useMemo(() => () => setError(null), []); - const syntaxError = useMemo(() => ({ error, clear: clearError }), [error, clearError]); - - return ( - <> - - - {/* Button to forward dummy syntax error */} - - - ); - }; - - let testBedToCapturePainlessErrors: TestBed; - - await act(async () => { - testBedToCapturePainlessErrors = await registerTestBed( - WithFieldEditorDependencies(ComponentToProvidePainlessSyntaxErrors), - { - memoryRouter: { - wrapComponent: false, - }, - } - )(); - }); - - testBed = { - ...testBedToCapturePainlessErrors!, - actions: getCommonActions(testBedToCapturePainlessErrors!), - }; - - const { - form, - component, - find, - actions: { fields }, - } = testBed; - - // We set some dummy painless error - act(() => { - find('setPainlessErrorButton').simulate('click'); - }); - component.update(); - - expect(form.getErrorsMessages()).toEqual(['Awwww! Painless syntax error']); - - // We change the type and expect the form error to not be there anymore - await fields.updateType('keyword'); - expect(form.getErrorsMessages()).toEqual([]); - }); }); }); diff --git a/src/plugins/index_pattern_field_editor/__jest__/client_integration/field_editor_flyout_content.helpers.ts b/src/plugins/index_pattern_field_editor/__jest__/client_integration/field_editor_flyout_content.helpers.ts index 5b916c1cd9960..0e87756819bf2 100644 --- a/src/plugins/index_pattern_field_editor/__jest__/client_integration/field_editor_flyout_content.helpers.ts +++ b/src/plugins/index_pattern_field_editor/__jest__/client_integration/field_editor_flyout_content.helpers.ts @@ -15,10 +15,11 @@ import { } from '../../public/components/field_editor_flyout_content'; import { WithFieldEditorDependencies, getCommonActions } from './helpers'; +export { waitForUpdates, waitForDocumentsAndPreviewUpdate } from './helpers'; + const defaultProps: Props = { onSave: () => {}, onCancel: () => {}, - runtimeFieldValidator: () => Promise.resolve(null), isSavingField: false, }; diff --git a/src/plugins/index_pattern_field_editor/__jest__/client_integration/field_editor_flyout_content.test.ts b/src/plugins/index_pattern_field_editor/__jest__/client_integration/field_editor_flyout_content.test.ts index 9b00ff762fe8f..1730593dbda20 100644 --- a/src/plugins/index_pattern_field_editor/__jest__/client_integration/field_editor_flyout_content.test.ts +++ b/src/plugins/index_pattern_field_editor/__jest__/client_integration/field_editor_flyout_content.test.ts @@ -7,15 +7,17 @@ */ import { act } from 'react-dom/test-utils'; -import type { Props } from '../../public/components/field_editor_flyout_content'; +// This import needs to come first as it contains the jest.mocks import { setupEnvironment } from './helpers'; +import type { Props } from '../../public/components/field_editor_flyout_content'; +import { setSearchResponse } from './field_editor_flyout_preview.helpers'; import { setup } from './field_editor_flyout_content.helpers'; +import { mockDocuments, createPreviewError } from './helpers/mocks'; describe('', () => { const { server, httpRequestsMockHelpers } = setupEnvironment(); beforeAll(() => { - httpRequestsMockHelpers.setFieldPreviewResponse({ values: ['foo'] }); jest.useFakeTimers(); }); @@ -24,6 +26,11 @@ describe('', () => { server.restore(); }); + beforeEach(async () => { + setSearchResponse(mockDocuments); + httpRequestsMockHelpers.setFieldPreviewResponse({ values: ['mockedScriptValue'] }); + }); + test('should have the correct title', async () => { const { exists, find } = await setup(); expect(exists('flyoutTitle')).toBe(true); @@ -55,17 +62,13 @@ describe('', () => { }; const onSave: jest.Mock = jest.fn(); - const { find } = await setup({ onSave, field }); + const { find, actions } = await setup({ onSave, field }); await act(async () => { find('fieldSaveButton').simulate('click'); }); - await act(async () => { - // The painless syntax validation has a timeout set to 600ms - // we give it a bit more time just to be on the safe side - jest.advanceTimersByTime(1000); - }); + await actions.waitForUpdates(); // Run the validations expect(onSave).toHaveBeenCalled(); const fieldReturned = onSave.mock.calls[onSave.mock.calls.length - 1][0]; @@ -85,7 +88,11 @@ describe('', () => { test('should validate the fields and prevent saving invalid form', async () => { const onSave: jest.Mock = jest.fn(); - const { find, exists, form, component } = await setup({ onSave }); + const { + find, + form, + actions: { waitForUpdates }, + } = await setup({ onSave }); expect(find('fieldSaveButton').props().disabled).toBe(false); @@ -93,17 +100,11 @@ describe('', () => { find('fieldSaveButton').simulate('click'); }); - await act(async () => { - jest.advanceTimersByTime(1000); - }); - - component.update(); + await waitForUpdates(); expect(onSave).toHaveBeenCalledTimes(0); expect(find('fieldSaveButton').props().disabled).toBe(true); expect(form.getErrorsMessages()).toEqual(['A name is required.']); - expect(exists('formError')).toBe(true); - expect(find('formError').text()).toBe('Fix errors in form before continuing.'); }); test('should forward values from the form', async () => { @@ -111,17 +112,14 @@ describe('', () => { const { find, - actions: { toggleFormRow, fields }, + actions: { toggleFormRow, fields, waitForUpdates }, } = await setup({ onSave }); await fields.updateName('someName'); await toggleFormRow('value'); await fields.updateScript('echo("hello")'); - await act(async () => { - // Let's make sure that validation has finished running - jest.advanceTimersByTime(1000); - }); + await waitForUpdates(); await act(async () => { find('fieldSaveButton').simulate('click'); @@ -138,7 +136,8 @@ describe('', () => { }); // Change the type and make sure it is forwarded - await fields.updateType('other_type', 'Other type'); + await fields.updateType('date'); + await waitForUpdates(); await act(async () => { find('fieldSaveButton').simulate('click'); @@ -148,7 +147,44 @@ describe('', () => { expect(fieldReturned).toEqual({ name: 'someName', - type: 'other_type', + type: 'date', + script: { source: 'echo("hello")' }, + }); + }); + + test('should not block validation if no documents could be fetched from server', async () => { + // If no documents can be fetched from the cluster (either because there are none or because + // the request failed), we still need to be able to resolve the painless script validation. + // In this test we will make sure that the validation for the script does not block saving the + // field even when no documentes where returned from the search query. + // successfully even though the script is invalid. + const error = createPreviewError({ reason: 'Houston we got a problem' }); + httpRequestsMockHelpers.setFieldPreviewResponse({ values: [], error, status: 400 }); + setSearchResponse([]); + + const onSave: jest.Mock = jest.fn(); + + const { + find, + actions: { toggleFormRow, fields, waitForUpdates }, + } = await setup({ onSave }); + + await fields.updateName('someName'); + await toggleFormRow('value'); + await fields.updateScript('echo("hello")'); + + await waitForUpdates(); // Wait for validation... it should not block and wait for preview response + + await act(async () => { + find('fieldSaveButton').simulate('click'); + }); + + expect(onSave).toBeCalled(); + const fieldReturned = onSave.mock.calls[onSave.mock.calls.length - 1][0]; + + expect(fieldReturned).toEqual({ + name: 'someName', + type: 'keyword', script: { source: 'echo("hello")' }, }); }); diff --git a/src/plugins/index_pattern_field_editor/__jest__/client_integration/field_editor_flyout_preview.helpers.ts b/src/plugins/index_pattern_field_editor/__jest__/client_integration/field_editor_flyout_preview.helpers.ts index 068ebce638aa1..305cf84d59622 100644 --- a/src/plugins/index_pattern_field_editor/__jest__/client_integration/field_editor_flyout_preview.helpers.ts +++ b/src/plugins/index_pattern_field_editor/__jest__/client_integration/field_editor_flyout_preview.helpers.ts @@ -21,12 +21,12 @@ import { spyIndexPatternGetAllFields, spySearchQuery, spySearchQueryResponse, + TestDoc, } from './helpers'; const defaultProps: Props = { onSave: () => {}, onCancel: () => {}, - runtimeFieldValidator: () => Promise.resolve(null), isSavingField: false, }; @@ -38,12 +38,6 @@ export const setIndexPatternFields = (fields: Array<{ name: string; displayName: spyIndexPatternGetAllFields.mockReturnValue(fields); }; -export interface TestDoc { - title: string; - subTitle: string; - description: string; -} - export const getSearchCallMeta = () => { const totalCalls = spySearchQuery.mock.calls.length; const lastCall = spySearchQuery.mock.calls[totalCalls - 1] ?? null; diff --git a/src/plugins/index_pattern_field_editor/__jest__/client_integration/field_editor_flyout_preview.test.ts b/src/plugins/index_pattern_field_editor/__jest__/client_integration/field_editor_flyout_preview.test.ts index 67309aab44a76..2403ae8c12e51 100644 --- a/src/plugins/index_pattern_field_editor/__jest__/client_integration/field_editor_flyout_preview.test.ts +++ b/src/plugins/index_pattern_field_editor/__jest__/client_integration/field_editor_flyout_preview.test.ts @@ -7,22 +7,21 @@ */ import { act } from 'react-dom/test-utils'; -import { setupEnvironment, fieldFormatsOptions, indexPatternNameForTest } from './helpers'; +import { + setupEnvironment, + fieldFormatsOptions, + indexPatternNameForTest, + EsDoc, + setSearchResponseLatency, +} from './helpers'; import { setup, setIndexPatternFields, getSearchCallMeta, setSearchResponse, FieldEditorFlyoutContentTestBed, - TestDoc, } from './field_editor_flyout_preview.helpers'; -import { createPreviewError } from './helpers/mocks'; - -interface EsDoc { - _id: string; - _index: string; - _source: TestDoc; -} +import { mockDocuments, createPreviewError } from './helpers/mocks'; describe('Field editor Preview panel', () => { const { server, httpRequestsMockHelpers } = setupEnvironment(); @@ -38,36 +37,6 @@ describe('Field editor Preview panel', () => { let testBed: FieldEditorFlyoutContentTestBed; - const mockDocuments: EsDoc[] = [ - { - _id: '001', - _index: 'testIndex', - _source: { - title: 'First doc - title', - subTitle: 'First doc - subTitle', - description: 'First doc - description', - }, - }, - { - _id: '002', - _index: 'testIndex', - _source: { - title: 'Second doc - title', - subTitle: 'Second doc - subTitle', - description: 'Second doc - description', - }, - }, - { - _id: '003', - _index: 'testIndex', - _source: { - title: 'Third doc - title', - subTitle: 'Third doc - subTitle', - description: 'Third doc - description', - }, - }, - ]; - const [doc1, doc2, doc3] = mockDocuments; const indexPatternFields: Array<{ name: string; displayName: string }> = [ @@ -86,43 +55,31 @@ describe('Field editor Preview panel', () => { ]; beforeEach(async () => { + server.respondImmediately = true; + server.autoRespond = true; + httpRequestsMockHelpers.setFieldPreviewResponse({ values: ['mockedScriptValue'] }); setIndexPatternFields(indexPatternFields); setSearchResponse(mockDocuments); + setSearchResponseLatency(0); testBed = await setup(); }); - test('should display the preview panel when either "set value" or "set format" is activated', async () => { - const { - exists, - actions: { toggleFormRow }, - } = testBed; - - expect(exists('previewPanel')).toBe(false); + test('should display the preview panel along with the editor', async () => { + const { exists } = testBed; - await toggleFormRow('value'); expect(exists('previewPanel')).toBe(true); - - await toggleFormRow('value', 'off'); - expect(exists('previewPanel')).toBe(false); - - await toggleFormRow('format'); - expect(exists('previewPanel')).toBe(true); - - await toggleFormRow('format', 'off'); - expect(exists('previewPanel')).toBe(false); }); test('should correctly set the title and subtitle of the panel', async () => { const { find, - actions: { toggleFormRow, fields, waitForUpdates }, + actions: { toggleFormRow, fields }, } = testBed; await toggleFormRow('value'); await fields.updateName('myRuntimeField'); - await waitForUpdates(); expect(find('previewPanel.title').text()).toBe('Preview'); expect(find('previewPanel.subTitle').text()).toBe(`From: ${indexPatternNameForTest}`); @@ -130,12 +87,11 @@ describe('Field editor Preview panel', () => { test('should list the list of fields of the index pattern', async () => { const { - actions: { toggleFormRow, fields, getRenderedIndexPatternFields, waitForUpdates }, + actions: { toggleFormRow, fields, getRenderedIndexPatternFields }, } = testBed; await toggleFormRow('value'); await fields.updateName('myRuntimeField'); - await waitForUpdates(); expect(getRenderedIndexPatternFields()).toEqual([ { @@ -158,18 +114,11 @@ describe('Field editor Preview panel', () => { exists, find, component, - actions: { - toggleFormRow, - fields, - setFilterFieldsValue, - getRenderedIndexPatternFields, - waitForUpdates, - }, + actions: { toggleFormRow, fields, setFilterFieldsValue, getRenderedIndexPatternFields }, } = testBed; await toggleFormRow('value'); await fields.updateName('myRuntimeField'); - await waitForUpdates(); // Should find a single field await setFilterFieldsValue('descr'); @@ -218,26 +167,21 @@ describe('Field editor Preview panel', () => { fields, getWrapperRenderedIndexPatternFields, getRenderedIndexPatternFields, - waitForUpdates, }, } = testBed; await toggleFormRow('value'); await fields.updateName('myRuntimeField'); - await waitForUpdates(); const fieldsRendered = getWrapperRenderedIndexPatternFields(); - if (fieldsRendered === null) { - throw new Error('No index pattern field rendered.'); - } - - expect(fieldsRendered.length).toBe(Object.keys(doc1._source).length); + expect(fieldsRendered).not.toBe(null); + expect(fieldsRendered!.length).toBe(Object.keys(doc1._source).length); // make sure that the last one if the "description" field - expect(fieldsRendered.at(2).text()).toBe('descriptionFirst doc - description'); + expect(fieldsRendered!.at(2).text()).toBe('descriptionFirst doc - description'); // Click the third field in the list ("description") - const descriptionField = fieldsRendered.at(2); + const descriptionField = fieldsRendered!.at(2); find('pinFieldButton', descriptionField).simulate('click'); component.update(); @@ -252,7 +196,7 @@ describe('Field editor Preview panel', () => { test('should display an empty prompt if no name and no script are defined', async () => { const { exists, - actions: { toggleFormRow, fields, waitForUpdates }, + actions: { toggleFormRow, fields }, } = testBed; await toggleFormRow('value'); @@ -260,20 +204,16 @@ describe('Field editor Preview panel', () => { expect(exists('previewPanel.emptyPrompt')).toBe(true); await fields.updateName('someName'); - await waitForUpdates(); expect(exists('previewPanel.emptyPrompt')).toBe(false); await fields.updateName(' '); - await waitForUpdates(); expect(exists('previewPanel.emptyPrompt')).toBe(true); // The name is empty and the empty prompt is displayed, let's now add a script... await fields.updateScript('echo("hello")'); - await waitForUpdates(); expect(exists('previewPanel.emptyPrompt')).toBe(false); await fields.updateScript(' '); - await waitForUpdates(); expect(exists('previewPanel.emptyPrompt')).toBe(true); }); @@ -286,9 +226,8 @@ describe('Field editor Preview panel', () => { }, }; - // We open the editor with a field to edit. The preview panel should be open - // and the empty prompt should not be there as we have a script and we'll load - // the preview. + // We open the editor with a field to edit the empty prompt should not be there + // as we have a script and we'll load the preview. await act(async () => { testBed = await setup({ field }); }); @@ -296,7 +235,6 @@ describe('Field editor Preview panel', () => { const { exists, component } = testBed; component.update(); - expect(exists('previewPanel')).toBe(true); expect(exists('previewPanel.emptyPrompt')).toBe(false); }); @@ -310,9 +248,6 @@ describe('Field editor Preview panel', () => { }, }; - // We open the editor with a field to edit. The preview panel should be open - // and the empty prompt should not be there as we have a script and we'll load - // the preview. await act(async () => { testBed = await setup({ field }); }); @@ -320,7 +255,6 @@ describe('Field editor Preview panel', () => { const { exists, component } = testBed; component.update(); - expect(exists('previewPanel')).toBe(true); expect(exists('previewPanel.emptyPrompt')).toBe(false); }); }); @@ -328,14 +262,15 @@ describe('Field editor Preview panel', () => { describe('key & value', () => { test('should set an empty value when no script is provided', async () => { const { - actions: { toggleFormRow, fields, getRenderedFieldsPreview, waitForUpdates }, + actions: { toggleFormRow, fields, getRenderedFieldsPreview }, } = testBed; await toggleFormRow('value'); await fields.updateName('myRuntimeField'); - await waitForUpdates(); - expect(getRenderedFieldsPreview()).toEqual([{ key: 'myRuntimeField', value: '-' }]); + expect(getRenderedFieldsPreview()).toEqual([ + { key: 'myRuntimeField', value: 'Value not set' }, + ]); }); test('should set the value returned by the painless _execute API', async () => { @@ -346,7 +281,7 @@ describe('Field editor Preview panel', () => { actions: { toggleFormRow, fields, - waitForDocumentsAndPreviewUpdate, + waitForUpdates, getLatestPreviewHttpRequest, getRenderedFieldsPreview, }, @@ -355,7 +290,7 @@ describe('Field editor Preview panel', () => { await toggleFormRow('value'); await fields.updateName('myRuntimeField'); await fields.updateScript('echo("hello")'); - await waitForDocumentsAndPreviewUpdate(); + await waitForUpdates(); // Run validations const request = getLatestPreviewHttpRequest(server); // Make sure the payload sent is correct @@ -379,46 +314,6 @@ describe('Field editor Preview panel', () => { ]); }); - test('should display an updating indicator while fetching the preview', async () => { - httpRequestsMockHelpers.setFieldPreviewResponse({ values: ['ok'] }); - - const { - exists, - actions: { toggleFormRow, fields, waitForUpdates, waitForDocumentsAndPreviewUpdate }, - } = testBed; - - await toggleFormRow('value'); - await waitForUpdates(); // wait for docs to be fetched - expect(exists('isUpdatingIndicator')).toBe(false); - - await fields.updateScript('echo("hello")'); - expect(exists('isUpdatingIndicator')).toBe(true); - - await waitForDocumentsAndPreviewUpdate(); - expect(exists('isUpdatingIndicator')).toBe(false); - }); - - test('should not display the updating indicator when neither the type nor the script has changed', async () => { - httpRequestsMockHelpers.setFieldPreviewResponse({ values: ['ok'] }); - - const { - exists, - actions: { toggleFormRow, fields, waitForUpdates, waitForDocumentsAndPreviewUpdate }, - } = testBed; - - await toggleFormRow('value'); - await waitForUpdates(); // wait for docs to be fetched - await fields.updateName('myRuntimeField'); - await fields.updateScript('echo("hello")'); - expect(exists('isUpdatingIndicator')).toBe(true); - await waitForDocumentsAndPreviewUpdate(); - expect(exists('isUpdatingIndicator')).toBe(false); - - await fields.updateName('nameChanged'); - // We haven't changed the type nor the script so there should not be any updating indicator - expect(exists('isUpdatingIndicator')).toBe(false); - }); - describe('read from _source', () => { test('should display the _source value when no script is provided and the name matched one of the fields in _source', async () => { const { @@ -445,12 +340,12 @@ describe('Field editor Preview panel', () => { const { actions: { toggleFormRow, fields, waitForUpdates, getRenderedFieldsPreview }, } = testBed; + await waitForUpdates(); // fetch documents await toggleFormRow('value'); - await waitForUpdates(); // fetch documents await fields.updateName('description'); // Field name is a field in _source await fields.updateScript('echo("hello")'); - await waitForUpdates(); // fetch preview + await waitForUpdates(); // Run validations // We render the value from the _execute API expect(getRenderedFieldsPreview()).toEqual([ @@ -468,6 +363,71 @@ describe('Field editor Preview panel', () => { }); }); + describe('updating indicator', () => { + beforeEach(async () => { + // Add some latency to be able to test the "updatingIndicator" state + setSearchResponseLatency(2000); + testBed = await setup(); + }); + + test('should display an updating indicator while fetching the docs and the preview', async () => { + // We want to test if the loading indicator is in the DOM, for that we don't want the server to + // respond immediately. We'll manualy send the response. + server.respondImmediately = false; + server.autoRespond = false; + + httpRequestsMockHelpers.setFieldPreviewResponse({ values: ['ok'] }); + + const { + exists, + actions: { toggleFormRow, fields, waitForUpdates }, + } = testBed; + await fields.updateName('myRuntimeField'); // Give a name to remove the empty prompt + expect(exists('isUpdatingIndicator')).toBe(true); // indicator while fetching the docs + + await waitForUpdates(); // wait for docs to be fetched + expect(exists('isUpdatingIndicator')).toBe(false); + + await toggleFormRow('value'); + expect(exists('isUpdatingIndicator')).toBe(false); + + await fields.updateScript('echo("hello")'); + expect(exists('isUpdatingIndicator')).toBe(true); // indicator while getting preview + + server.respond(); + await waitForUpdates(); + expect(exists('isUpdatingIndicator')).toBe(false); + }); + + test('should not display the updating indicator when neither the type nor the script has changed', async () => { + httpRequestsMockHelpers.setFieldPreviewResponse({ values: ['ok'] }); + // We want to test if the loading indicator is in the DOM, for that we need to manually + // send the response from the server + server.respondImmediately = false; + server.autoRespond = false; + + const { + exists, + actions: { toggleFormRow, fields, waitForUpdates, waitForDocumentsAndPreviewUpdate }, + } = testBed; + await waitForUpdates(); // wait for docs to be fetched + + await toggleFormRow('value'); + await fields.updateName('myRuntimeField'); + await fields.updateScript('echo("hello")'); + expect(exists('isUpdatingIndicator')).toBe(true); + + server.respond(); + await waitForDocumentsAndPreviewUpdate(); + + expect(exists('isUpdatingIndicator')).toBe(false); + + await fields.updateName('nameChanged'); + // We haven't changed the type nor the script so there should not be any updating indicator + expect(exists('isUpdatingIndicator')).toBe(false); + }); + }); + describe('format', () => { test('should apply the format to the value', async () => { /** @@ -513,32 +473,25 @@ describe('Field editor Preview panel', () => { const { exists, - find, - actions: { - toggleFormRow, - fields, - waitForUpdates, - waitForDocumentsAndPreviewUpdate, - getRenderedFieldsPreview, - }, + actions: { toggleFormRow, fields, waitForUpdates, getRenderedFieldsPreview }, } = testBed; + expect(exists('scriptErrorBadge')).toBe(false); + await fields.updateName('myRuntimeField'); await toggleFormRow('value'); await fields.updateScript('bad()'); - await waitForDocumentsAndPreviewUpdate(); + await waitForUpdates(); // Run validations - expect(exists('fieldPreviewItem')).toBe(false); - expect(exists('indexPatternFieldList')).toBe(false); - expect(exists('previewError')).toBe(true); - expect(find('previewError.reason').text()).toBe(error.caused_by.reason); + expect(exists('scriptErrorBadge')).toBe(true); + expect(fields.getScriptError()).toBe(error.caused_by.reason); httpRequestsMockHelpers.setFieldPreviewResponse({ values: ['ok'] }); await fields.updateScript('echo("ok")'); await waitForUpdates(); - expect(exists('fieldPreviewItem')).toBe(true); - expect(find('indexPatternFieldList.listItem').length).toBeGreaterThan(0); + expect(exists('scriptErrorBadge')).toBe(false); + expect(fields.getScriptError()).toBe(null); expect(getRenderedFieldsPreview()).toEqual([{ key: 'myRuntimeField', value: 'ok' }]); }); @@ -547,12 +500,12 @@ describe('Field editor Preview panel', () => { exists, find, form, - actions: { toggleFormRow, fields, waitForUpdates, waitForDocumentsAndPreviewUpdate }, + component, + actions: { toggleFormRow, fields }, } = testBed; await fields.updateName('myRuntimeField'); await toggleFormRow('value'); - await waitForDocumentsAndPreviewUpdate(); // We will return no document from the search setSearchResponse([]); @@ -560,12 +513,34 @@ describe('Field editor Preview panel', () => { await act(async () => { form.setInputValue('documentIdField', 'wrongID'); }); - await waitForUpdates(); + component.update(); - expect(exists('previewError')).toBe(true); - expect(find('previewError').text()).toContain('Document ID not found'); + expect(exists('fetchDocError')).toBe(true); + expect(find('fetchDocError').text()).toContain('Document ID not found'); expect(exists('isUpdatingIndicator')).toBe(false); }); + + test('should clear the error when disabling "Set value"', async () => { + const error = createPreviewError({ reason: 'Houston we got a problem' }); + httpRequestsMockHelpers.setFieldPreviewResponse({ values: [], error, status: 400 }); + + const { + exists, + actions: { toggleFormRow, fields, waitForUpdates }, + } = testBed; + + await toggleFormRow('value'); + await fields.updateScript('bad()'); + await waitForUpdates(); // Run validations + + expect(exists('scriptErrorBadge')).toBe(true); + expect(fields.getScriptError()).toBe(error.caused_by.reason); + + await toggleFormRow('value', 'off'); + + expect(exists('scriptErrorBadge')).toBe(false); + expect(fields.getScriptError()).toBe(null); + }); }); describe('Cluster document load and navigation', () => { @@ -581,19 +556,10 @@ describe('Field editor Preview panel', () => { test('should update the field list when the document changes', async () => { const { - actions: { - toggleFormRow, - fields, - getRenderedIndexPatternFields, - goToNextDocument, - goToPreviousDocument, - waitForUpdates, - }, + actions: { fields, getRenderedIndexPatternFields, goToNextDocument, goToPreviousDocument }, } = testBed; - await toggleFormRow('value'); - await fields.updateName('myRuntimeField'); - await waitForUpdates(); + await fields.updateName('myRuntimeField'); // Give a name to remove empty prompt expect(getRenderedIndexPatternFields()[0]).toEqual({ key: 'title', @@ -636,26 +602,17 @@ describe('Field editor Preview panel', () => { test('should update the field preview value when the document changes', async () => { httpRequestsMockHelpers.setFieldPreviewResponse({ values: ['valueDoc1'] }); const { - actions: { - toggleFormRow, - fields, - waitForUpdates, - waitForDocumentsAndPreviewUpdate, - getRenderedFieldsPreview, - goToNextDocument, - }, + actions: { toggleFormRow, fields, getRenderedFieldsPreview, goToNextDocument }, } = testBed; await toggleFormRow('value'); await fields.updateName('myRuntimeField'); await fields.updateScript('echo("hello world")'); - await waitForDocumentsAndPreviewUpdate(); expect(getRenderedFieldsPreview()).toEqual([{ key: 'myRuntimeField', value: 'valueDoc1' }]); httpRequestsMockHelpers.setFieldPreviewResponse({ values: ['valueDoc2'] }); await goToNextDocument(); - await waitForUpdates(); expect(getRenderedFieldsPreview()).toEqual([{ key: 'myRuntimeField', value: 'valueDoc2' }]); }); @@ -665,20 +622,12 @@ describe('Field editor Preview panel', () => { component, form, exists, - actions: { - toggleFormRow, - fields, - getRenderedIndexPatternFields, - getRenderedFieldsPreview, - waitForUpdates, - waitForDocumentsAndPreviewUpdate, - }, + actions: { toggleFormRow, fields, getRenderedIndexPatternFields, getRenderedFieldsPreview }, } = testBed; await toggleFormRow('value'); await fields.updateName('myRuntimeField'); await fields.updateScript('echo("hello world")'); - await waitForDocumentsAndPreviewUpdate(); // First make sure that we have the original cluster data is loaded // and the preview value rendered. @@ -697,10 +646,6 @@ describe('Field editor Preview panel', () => { form.setInputValue('documentIdField', '123456'); }); component.update(); - // We immediately remove the index pattern fields - expect(getRenderedIndexPatternFields()).toEqual([]); - - await waitForDocumentsAndPreviewUpdate(); expect(getRenderedIndexPatternFields()).toEqual([ { @@ -717,8 +662,6 @@ describe('Field editor Preview panel', () => { }, ]); - await waitForUpdates(); // Then wait for the preview HTTP request - // The preview should have updated expect(getRenderedFieldsPreview()).toEqual([ { key: 'myRuntimeField', value: 'loadedDocPreview' }, @@ -735,18 +678,10 @@ describe('Field editor Preview panel', () => { form, component, find, - actions: { - toggleFormRow, - fields, - getRenderedFieldsPreview, - getRenderedIndexPatternFields, - waitForUpdates, - waitForDocumentsAndPreviewUpdate, - }, + actions: { toggleFormRow, fields, getRenderedFieldsPreview, waitForUpdates }, } = testBed; await toggleFormRow('value'); - await waitForUpdates(); // fetch documents await fields.updateName('myRuntimeField'); await fields.updateScript('echo("hello world")'); await waitForUpdates(); // fetch preview @@ -758,7 +693,7 @@ describe('Field editor Preview panel', () => { await act(async () => { form.setInputValue('documentIdField', '123456'); }); - await waitForDocumentsAndPreviewUpdate(); + component.update(); // Load back the cluster data httpRequestsMockHelpers.setFieldPreviewResponse({ values: ['clusterDataDocPreview'] }); @@ -768,10 +703,6 @@ describe('Field editor Preview panel', () => { find('loadDocsFromClusterButton').simulate('click'); }); component.update(); - // We immediately remove the index pattern fields - expect(getRenderedIndexPatternFields()).toEqual([]); - - await waitForDocumentsAndPreviewUpdate(); // The preview should be updated with the cluster data preview expect(getRenderedFieldsPreview()).toEqual([ @@ -779,22 +710,16 @@ describe('Field editor Preview panel', () => { ]); }); - test('should not lose the state of single document vs cluster data after displaying the empty prompt', async () => { + test('should not lose the state of single document vs cluster data after toggling on/off the empty prompt', async () => { const { form, component, exists, - actions: { - toggleFormRow, - fields, - getRenderedIndexPatternFields, - waitForDocumentsAndPreviewUpdate, - }, + actions: { toggleFormRow, fields, getRenderedIndexPatternFields }, } = testBed; await toggleFormRow('value'); await fields.updateName('myRuntimeField'); - await waitForDocumentsAndPreviewUpdate(); // Initial state where we have the cluster data loaded and the doc navigation expect(exists('documentsNav')).toBe(true); @@ -806,7 +731,6 @@ describe('Field editor Preview panel', () => { form.setInputValue('documentIdField', '123456'); }); component.update(); - await waitForDocumentsAndPreviewUpdate(); expect(exists('documentsNav')).toBe(false); expect(exists('loadDocsFromClusterButton')).toBe(true); @@ -833,24 +757,20 @@ describe('Field editor Preview panel', () => { form, component, find, - actions: { toggleFormRow, fields, waitForUpdates }, + actions: { fields }, } = testBed; const expectedParamsToFetchClusterData = { - params: { index: 'testIndexPattern', body: { size: 50 } }, + params: { index: indexPatternNameForTest, body: { size: 50 } }, }; // Initial state let searchMeta = getSearchCallMeta(); - const initialCount = searchMeta.totalCalls; - // Open the preview panel. This will trigger document fetchint - await fields.updateName('myRuntimeField'); - await toggleFormRow('value'); - await waitForUpdates(); + await fields.updateName('myRuntimeField'); // hide the empty prompt searchMeta = getSearchCallMeta(); - expect(searchMeta.totalCalls).toBe(initialCount + 1); + const initialCount = searchMeta.totalCalls; expect(searchMeta.lastCallParams).toEqual(expectedParamsToFetchClusterData); // Load single doc @@ -860,10 +780,9 @@ describe('Field editor Preview panel', () => { form.setInputValue('documentIdField', nextId); }); component.update(); - await waitForUpdates(); searchMeta = getSearchCallMeta(); - expect(searchMeta.totalCalls).toBe(initialCount + 2); + expect(searchMeta.totalCalls).toBe(initialCount + 1); expect(searchMeta.lastCallParams).toEqual({ params: { body: { @@ -874,7 +793,7 @@ describe('Field editor Preview panel', () => { }, size: 1, }, - index: 'testIndexPattern', + index: indexPatternNameForTest, }, }); @@ -884,8 +803,30 @@ describe('Field editor Preview panel', () => { find('loadDocsFromClusterButton').simulate('click'); }); searchMeta = getSearchCallMeta(); - expect(searchMeta.totalCalls).toBe(initialCount + 3); + expect(searchMeta.totalCalls).toBe(initialCount + 2); expect(searchMeta.lastCallParams).toEqual(expectedParamsToFetchClusterData); }); }); + + describe('When no documents could be fetched from cluster', () => { + beforeEach(() => { + setSearchResponse([]); + }); + + test('should not display the updating indicator and have a callout to indicate that preview is not available', async () => { + setSearchResponseLatency(2000); + testBed = await setup(); + + const { + exists, + actions: { fields, waitForUpdates }, + } = testBed; + await fields.updateName('myRuntimeField'); // Give a name to remove the empty prompt + expect(exists('isUpdatingIndicator')).toBe(true); // indicator while fetching the docs + + await waitForUpdates(); // wait for docs to be fetched + expect(exists('isUpdatingIndicator')).toBe(false); + expect(exists('previewNotAvailableCallout')).toBe(true); + }); + }); }); diff --git a/src/plugins/index_pattern_field_editor/__jest__/client_integration/helpers/common_actions.ts b/src/plugins/index_pattern_field_editor/__jest__/client_integration/helpers/common_actions.ts index ca061968dae20..9f8b52af5878e 100644 --- a/src/plugins/index_pattern_field_editor/__jest__/client_integration/helpers/common_actions.ts +++ b/src/plugins/index_pattern_field_editor/__jest__/client_integration/helpers/common_actions.ts @@ -8,6 +8,36 @@ import { act } from 'react-dom/test-utils'; import { TestBed } from '@kbn/test/jest'; +/** + * We often need to wait for both the documents & the preview to be fetched. + * We can't increase the `jest.advanceTimersByTime()` time + * as those are 2 different operations that occur in sequence. + */ +export const waitForDocumentsAndPreviewUpdate = async (testBed?: TestBed) => { + // Wait for documents to be fetched + await act(async () => { + jest.advanceTimersByTime(5000); + }); + + // Wait for the syntax validation debounced + await act(async () => { + jest.advanceTimersByTime(1000); + }); + + testBed?.component.update(); +}; + +/** + * Handler to bypass the debounce time in our tests + */ +export const waitForUpdates = async (testBed?: TestBed) => { + await act(async () => { + jest.advanceTimersByTime(5000); + }); + + testBed?.component.update(); +}; + export const getCommonActions = (testBed: TestBed) => { const toggleFormRow = async ( row: 'customLabel' | 'value' | 'format', @@ -66,46 +96,28 @@ export const getCommonActions = (testBed: TestBed) => { testBed.component.update(); }; - /** - * Allows us to bypass the debounce time of 500ms before updating the preview. We also simulate - * a 2000ms latency when searching ES documents (see setup_environment.tsx). - */ - const waitForUpdates = async () => { - await act(async () => { - jest.runAllTimers(); - }); + const getScriptError = () => { + const scriptError = testBed.component.find('#runtimeFieldScript-error-0'); - testBed.component.update(); - }; - - /** - * When often need to both wait for the documents to be fetched and - * the preview to be fetched. We can't increase the `jest.advanceTimersByTime` time - * as those are 2 different operations that occur in sequence. - */ - const waitForDocumentsAndPreviewUpdate = async () => { - // Wait for documents to be fetched - await act(async () => { - jest.runAllTimers(); - }); - - // Wait for preview to update - await act(async () => { - jest.runAllTimers(); - }); + if (scriptError.length === 0) { + return null; + } else if (scriptError.length > 1) { + return scriptError.at(0).text(); + } - testBed.component.update(); + return scriptError.text(); }; return { toggleFormRow, - waitForUpdates, - waitForDocumentsAndPreviewUpdate, + waitForUpdates: waitForUpdates.bind(null, testBed), + waitForDocumentsAndPreviewUpdate: waitForDocumentsAndPreviewUpdate.bind(null, testBed), fields: { updateName, updateType, updateScript, updateFormat, + getScriptError, }, }; }; diff --git a/src/plugins/index_pattern_field_editor/__jest__/client_integration/helpers/index.ts b/src/plugins/index_pattern_field_editor/__jest__/client_integration/helpers/index.ts index e8ff7eb7538f2..2fc870bd42d66 100644 --- a/src/plugins/index_pattern_field_editor/__jest__/client_integration/helpers/index.ts +++ b/src/plugins/index_pattern_field_editor/__jest__/client_integration/helpers/index.ts @@ -17,6 +17,14 @@ export { spyIndexPatternGetAllFields, fieldFormatsOptions, indexPatternNameForTest, + setSearchResponseLatency, } from './setup_environment'; -export { getCommonActions } from './common_actions'; +export { + getCommonActions, + waitForUpdates, + waitForDocumentsAndPreviewUpdate, +} from './common_actions'; + +export type { EsDoc, TestDoc } from './mocks'; +export { mockDocuments } from './mocks'; diff --git a/src/plugins/index_pattern_field_editor/__jest__/client_integration/helpers/jest.mocks.tsx b/src/plugins/index_pattern_field_editor/__jest__/client_integration/helpers/jest.mocks.tsx index d33a0d2a87fb5..7161776c21fb1 100644 --- a/src/plugins/index_pattern_field_editor/__jest__/client_integration/helpers/jest.mocks.tsx +++ b/src/plugins/index_pattern_field_editor/__jest__/client_integration/helpers/jest.mocks.tsx @@ -5,7 +5,11 @@ * in compliance with, at your election, the Elastic License 2.0 or the Server * Side Public License, v 1. */ -import React from 'react'; +import React, { useEffect } from 'react'; +import { of } from 'rxjs'; + +const mockUseEffect = useEffect; +const mockOf = of; const EDITOR_ID = 'testEditor'; @@ -39,6 +43,7 @@ jest.mock('@elastic/eui', () => { jest.mock('@kbn/monaco', () => { const original = jest.requireActual('@kbn/monaco'); + const originalMonaco = original.monaco; return { ...original, @@ -48,10 +53,28 @@ jest.mock('@kbn/monaco', () => { getSyntaxErrors: () => ({ [EDITOR_ID]: [], }), + validation$() { + return mockOf({ isValid: true, isValidating: false, errors: [] }); + }, + }, + monaco: { + ...originalMonaco, + editor: { + ...originalMonaco.editor, + setModelMarkers() {}, + }, }, }; }); +jest.mock('react-use/lib/useDebounce', () => { + return (cb: () => void, ms: number, deps: any[]) => { + mockUseEffect(() => { + cb(); + }, deps); + }; +}); + jest.mock('../../../../kibana_react/public', () => { const original = jest.requireActual('../../../../kibana_react/public'); @@ -60,15 +83,19 @@ jest.mock('../../../../kibana_react/public', () => { * with the uiSettings passed down. Let's use a simple in our tests. */ const CodeEditorMock = (props: any) => { - // Forward our deterministic ID to the consumer - // We need below for the PainlessLang.getSyntaxErrors mock - props.editorDidMount({ - getModel() { - return { - id: EDITOR_ID, - }; - }, - }); + const { editorDidMount } = props; + + mockUseEffect(() => { + // Forward our deterministic ID to the consumer + // We need below for the PainlessLang.getSyntaxErrors mock + editorDidMount({ + getModel() { + return { + id: EDITOR_ID, + }; + }, + }); + }, [editorDidMount]); return ( Promise.resolve({})); export const spyIndexPatternGetAllFields = jest.fn().mockImplementation(() => []); -spySearchQuery.mockImplementation((params) => { +let searchResponseDelay = 0; + +// Add latency to the search request +export const setSearchResponseLatency = (ms: number) => { + searchResponseDelay = ms; +}; + +spySearchQuery.mockImplementation(() => { return { toPromise: () => { + if (searchResponseDelay === 0) { + // no delay, it is synchronous + return spySearchQueryResponse(); + } + return new Promise((resolve) => { setTimeout(() => { resolve(undefined); - }, 2000); // simulate 2s latency for the HTTP request - }).then(() => spySearchQueryResponse()); + }, searchResponseDelay); + }).then(() => { + return spySearchQueryResponse(); + }); }, }; }); diff --git a/src/plugins/index_pattern_field_editor/public/components/field_editor/field_editor.tsx b/src/plugins/index_pattern_field_editor/public/components/field_editor/field_editor.tsx index 11183d575e955..ddc3aa72c7610 100644 --- a/src/plugins/index_pattern_field_editor/public/components/field_editor/field_editor.tsx +++ b/src/plugins/index_pattern_field_editor/public/components/field_editor/field_editor.tsx @@ -42,7 +42,6 @@ import { ScriptField, FormatField, PopularityField, - ScriptSyntaxError, } from './form_fields'; import { FormRow } from './form_row'; import { AdvancedParametersSection } from './advanced_parameters_section'; @@ -50,6 +49,7 @@ import { AdvancedParametersSection } from './advanced_parameters_section'; export interface FieldEditorFormState { isValid: boolean | undefined; isSubmitted: boolean; + isSubmitting: boolean; submit: FormHook['submit']; } @@ -70,7 +70,6 @@ export interface Props { onChange?: (state: FieldEditorFormState) => void; /** Handler to receive update on the form "isModified" state */ onFormModifiedChange?: (isModified: boolean) => void; - syntaxError: ScriptSyntaxError; } const geti18nTexts = (): { @@ -150,12 +149,11 @@ const formSerializer = (field: FieldFormInternal): Field => { }; }; -const FieldEditorComponent = ({ field, onChange, onFormModifiedChange, syntaxError }: Props) => { +const FieldEditorComponent = ({ field, onChange, onFormModifiedChange }: Props) => { const { links, namesNotAllowed, existingConcreteFields, fieldTypeToProcess } = useFieldEditorContext(); const { params: { update: updatePreviewParams }, - panel: { setIsVisible: setIsPanelVisible }, } = useFieldPreviewContext(); const { form } = useForm({ defaultValue: field, @@ -163,8 +161,7 @@ const FieldEditorComponent = ({ field, onChange, onFormModifiedChange, syntaxErr deserializer: formDeserializer, serializer: formSerializer, }); - const { submit, isValid: isFormValid, isSubmitted, getFields } = form; - const { clear: clearSyntaxError } = syntaxError; + const { submit, isValid: isFormValid, isSubmitted, getFields, isSubmitting } = form; const nameFieldConfig = getNameFieldConfig(namesNotAllowed, field); const i18nTexts = geti18nTexts(); @@ -191,19 +188,12 @@ const FieldEditorComponent = ({ field, onChange, onFormModifiedChange, syntaxErr const typeHasChanged = (Boolean(field?.type) && typeField?.isModified) ?? false; const isValueVisible = get(formData, '__meta__.isValueVisible'); - const isFormatVisible = get(formData, '__meta__.isFormatVisible'); useEffect(() => { if (onChange) { - onChange({ isValid: isFormValid, isSubmitted, submit }); + onChange({ isValid: isFormValid, isSubmitted, isSubmitting, submit }); } - }, [onChange, isFormValid, isSubmitted, submit]); - - useEffect(() => { - // Whenever the field "type" changes we clear any possible painless syntax - // error as it is possibly stale. - clearSyntaxError(); - }, [updatedType, clearSyntaxError]); + }, [onChange, isFormValid, isSubmitted, isSubmitting, submit]); useEffect(() => { updatePreviewParams({ @@ -217,14 +207,6 @@ const FieldEditorComponent = ({ field, onChange, onFormModifiedChange, syntaxErr }); }, [updatedName, updatedType, updatedScript, isValueVisible, updatedFormat, updatePreviewParams]); - useEffect(() => { - if (isValueVisible || isFormatVisible) { - setIsPanelVisible(true); - } else { - setIsPanelVisible(false); - } - }, [isValueVisible, isFormatVisible, setIsPanelVisible]); - useEffect(() => { if (onFormModifiedChange) { onFormModifiedChange(isFormModified); @@ -236,6 +218,8 @@ const FieldEditorComponent = ({ field, onChange, onFormModifiedChange, syntaxErr form={form} className="indexPatternFieldEditor__form" data-test-subj="indexPatternFieldEditorForm" + isInvalid={isSubmitted && isFormValid === false} + error={form.getErrors()} > {/* Name */} @@ -296,11 +280,7 @@ const FieldEditorComponent = ({ field, onChange, onFormModifiedChange, syntaxErr data-test-subj="valueRow" withDividerRule > - + )} diff --git a/src/plugins/index_pattern_field_editor/public/components/field_editor/form_fields/index.ts b/src/plugins/index_pattern_field_editor/public/components/field_editor/form_fields/index.ts index 693709729ed92..cfa09db3cdc83 100644 --- a/src/plugins/index_pattern_field_editor/public/components/field_editor/form_fields/index.ts +++ b/src/plugins/index_pattern_field_editor/public/components/field_editor/form_fields/index.ts @@ -12,7 +12,6 @@ export { CustomLabelField } from './custom_label_field'; export { PopularityField } from './popularity_field'; -export type { ScriptSyntaxError } from './script_field'; export { ScriptField } from './script_field'; export { FormatField } from './format_field'; diff --git a/src/plugins/index_pattern_field_editor/public/components/field_editor/form_fields/script_field.tsx b/src/plugins/index_pattern_field_editor/public/components/field_editor/form_fields/script_field.tsx index d73e8046e5db7..b1dcddd459c8a 100644 --- a/src/plugins/index_pattern_field_editor/public/components/field_editor/form_fields/script_field.tsx +++ b/src/plugins/index_pattern_field_editor/public/components/field_editor/form_fields/script_field.tsx @@ -6,32 +6,32 @@ * Side Public License, v 1. */ -import React, { useState, useEffect, useMemo, useRef } from 'react'; +import React, { useState, useEffect, useMemo, useRef, useCallback } from 'react'; +import { first } from 'rxjs/operators'; +import type { Subscription } from 'rxjs'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; -import { EuiFormRow, EuiLink, EuiCode, EuiCodeBlock, EuiSpacer, EuiTitle } from '@elastic/eui'; -import { PainlessLang, PainlessContext } from '@kbn/monaco'; +import { EuiFormRow, EuiLink, EuiCode } from '@elastic/eui'; +import { PainlessLang, PainlessContext, monaco } from '@kbn/monaco'; +import { firstValueFrom } from '@kbn/std'; import { UseField, useFormData, + useBehaviorSubject, RuntimeType, - FieldConfig, CodeEditor, + useFormContext, } from '../../../shared_imports'; -import { RuntimeFieldPainlessError } from '../../../lib'; +import type { RuntimeFieldPainlessError } from '../../../types'; +import { painlessErrorToMonacoMarker } from '../../../lib'; +import { useFieldPreviewContext, Context } from '../../preview'; import { schema } from '../form_schema'; import type { FieldFormInternal } from '../field_editor'; interface Props { links: { runtimePainless: string }; existingConcreteFields?: Array<{ name: string; type: string }>; - syntaxError: ScriptSyntaxError; -} - -export interface ScriptSyntaxError { - error: RuntimeFieldPainlessError | null; - clear: () => void; } const mapReturnTypeToPainlessContext = (runtimeType: RuntimeType): PainlessContext => { @@ -53,87 +53,166 @@ const mapReturnTypeToPainlessContext = (runtimeType: RuntimeType): PainlessConte } }; -export const ScriptField = React.memo(({ existingConcreteFields, links, syntaxError }: Props) => { - const editorValidationTimeout = useRef>(); +const ScriptFieldComponent = ({ existingConcreteFields, links }: Props) => { + const monacoEditor = useRef(null); + const editorValidationSubscription = useRef(); + const fieldCurrentValue = useRef(''); + + const { + error, + isLoadingPreview, + isPreviewAvailable, + currentDocument: { isLoading: isFetchingDoc, value: currentDocument }, + validation: { setScriptEditorValidation }, + } = useFieldPreviewContext(); + const [validationData$, nextValidationData$] = useBehaviorSubject< + | { + isFetchingDoc: boolean; + isLoadingPreview: boolean; + error: Context['error']; + } + | undefined + >(undefined); const [painlessContext, setPainlessContext] = useState( - mapReturnTypeToPainlessContext(schema.type.defaultValue[0].value!) + mapReturnTypeToPainlessContext(schema.type.defaultValue![0].value!) + ); + + const currentDocId = currentDocument?._id; + + const suggestionProvider = useMemo( + () => PainlessLang.getSuggestionProvider(painlessContext, existingConcreteFields), + [painlessContext, existingConcreteFields] ); - const [editorId, setEditorId] = useState(); + const { validateFields } = useFormContext(); - const suggestionProvider = PainlessLang.getSuggestionProvider( - painlessContext, - existingConcreteFields + // Listen to formData changes **before** validations are executed + const onFormDataChange = useCallback( + ({ type }: FieldFormInternal) => { + if (type !== undefined) { + setPainlessContext(mapReturnTypeToPainlessContext(type[0]!.value!)); + } + + if (isPreviewAvailable) { + // To avoid a race condition where the validation would run before + // the context state are updated, we clear the old value of the observable. + // This way the validationDataProvider() will await until new values come in before resolving + nextValidationData$(undefined); + } + }, + [nextValidationData$, isPreviewAvailable] ); - const [{ type, script: { source } = { source: '' } }] = useFormData({ + useFormData({ watch: ['type', 'script.source'], + onChange: onFormDataChange, }); - const { clear: clearSyntaxError } = syntaxError; - - const sourceFieldConfig: FieldConfig = useMemo(() => { - return { - ...schema.script.source, - validations: [ - ...schema.script.source.validations, - { - validator: () => { - if (editorValidationTimeout.current) { - clearTimeout(editorValidationTimeout.current); - } - - return new Promise((resolve) => { - // monaco waits 500ms before validating, so we also add a delay - // before checking if there are any syntax errors - editorValidationTimeout.current = setTimeout(() => { - const painlessSyntaxErrors = PainlessLang.getSyntaxErrors(); - // It is possible for there to be more than one editor in a view, - // so we need to get the syntax errors based on the editor (aka model) ID - const editorHasSyntaxErrors = - editorId && - painlessSyntaxErrors[editorId] && - painlessSyntaxErrors[editorId].length > 0; - - if (editorHasSyntaxErrors) { - return resolve({ - message: i18n.translate( - 'indexPatternFieldEditor.editor.form.scriptEditorValidationMessage', - { - defaultMessage: 'Invalid Painless syntax.', - } - ), - }); - } - - resolve(undefined); - }, 600); - }); - }, - }, - ], - }; - }, [editorId]); + const validationDataProvider = useCallback(async () => { + const validationData = await firstValueFrom( + validationData$.pipe( + first((data) => { + // We first wait to get field preview data + if (data === undefined) { + return false; + } + + // We are not interested in preview data meanwhile it + // is still making HTTP request + if (data.isFetchingDoc || data.isLoadingPreview) { + return false; + } + + return true; + }) + ) + ); + + return validationData!.error; + }, [validationData$]); + + const onEditorDidMount = useCallback( + (editor: monaco.editor.IStandaloneCodeEditor) => { + monacoEditor.current = editor; + + if (editorValidationSubscription.current) { + editorValidationSubscription.current.unsubscribe(); + } + + editorValidationSubscription.current = PainlessLang.validation$().subscribe( + ({ isValid, isValidating, errors }) => { + setScriptEditorValidation({ + isValid, + isValidating, + message: errors[0]?.message ?? null, + }); + } + ); + }, + [setScriptEditorValidation] + ); + + const updateMonacoMarkers = useCallback((markers: monaco.editor.IMarkerData[]) => { + const model = monacoEditor.current?.getModel(); + if (model) { + monaco.editor.setModelMarkers(model, PainlessLang.ID, markers); + } + }, []); + + const displayPainlessScriptErrorInMonaco = useCallback( + (painlessError: RuntimeFieldPainlessError) => { + const model = monacoEditor.current?.getModel(); + + if (painlessError.position !== null && Boolean(model)) { + const { offset } = painlessError.position; + // Get the monaco Position (lineNumber and colNumber) from the ES Painless error position + const errorStartPosition = model!.getPositionAt(offset); + const markerData = painlessErrorToMonacoMarker(painlessError, errorStartPosition); + const errorMarkers = markerData ? [markerData] : []; + updateMonacoMarkers(errorMarkers); + } + }, + [updateMonacoMarkers] + ); + + // Whenever we navigate to a different doc we validate the script + // field as it could be invalid against the new document. + useEffect(() => { + if (fieldCurrentValue.current.trim() !== '' && currentDocId !== undefined) { + validateFields(['script.source']); + } + }, [currentDocId, validateFields]); useEffect(() => { - setPainlessContext(mapReturnTypeToPainlessContext(type[0]!.value!)); - }, [type]); + nextValidationData$({ isFetchingDoc, isLoadingPreview, error }); + }, [nextValidationData$, isFetchingDoc, isLoadingPreview, error]); useEffect(() => { - // Whenever the source changes we clear potential syntax errors - clearSyntaxError(); - }, [source, clearSyntaxError]); + if (error?.code === 'PAINLESS_SCRIPT_ERROR') { + displayPainlessScriptErrorInMonaco(error!.error as RuntimeFieldPainlessError); + } else if (error === null) { + updateMonacoMarkers([]); + } + }, [error, displayPainlessScriptErrorInMonaco, updateMonacoMarkers]); + + useEffect(() => { + return () => { + if (editorValidationSubscription.current) { + editorValidationSubscription.current.unsubscribe(); + } + }; + }, []); return ( - path="script.source" config={sourceFieldConfig}> + path="script.source" validationDataProvider={validationDataProvider}> {({ value, setValue, label, isValid, getErrorsMessages }) => { - let errorMessage: string | null = ''; - if (syntaxError.error !== null) { - errorMessage = syntaxError.error.reason ?? syntaxError.error.message; - } else { - errorMessage = getErrorsMessages(); + let errorMessage = getErrorsMessages(); + + if (error) { + errorMessage = error.error.reason!; } + fieldCurrentValue.current = value; return ( <> @@ -141,7 +220,7 @@ export const ScriptField = React.memo(({ existingConcreteFields, links, syntaxEr label={label} id="runtimeFieldScript" error={errorMessage} - isInvalid={syntaxError.error !== null || !isValid} + isInvalid={!isValid} helpText={ setEditorId(editor.getModel()?.id)} + editorDidMount={onEditorDidMount} options={{ fontSize: 12, minimap: { @@ -199,33 +278,11 @@ export const ScriptField = React.memo(({ existingConcreteFields, links, syntaxEr )} /> - - {/* Help the user debug the error by showing where it failed in the script */} - {syntaxError.error !== null && ( - <> - - -

    - {i18n.translate( - 'indexPatternFieldEditor.editor.form.scriptEditor.debugErrorMessage', - { - defaultMessage: 'Syntax error detail', - } - )} -

    -
    - - - {syntaxError.error.scriptStack.join('\n')} - - - )} ); }} ); -}); +}; + +export const ScriptField = React.memo(ScriptFieldComponent); diff --git a/src/plugins/index_pattern_field_editor/public/components/field_editor/form_schema.ts b/src/plugins/index_pattern_field_editor/public/components/field_editor/form_schema.ts index 979a1fdb1adc1..7a15dce3af019 100644 --- a/src/plugins/index_pattern_field_editor/public/components/field_editor/form_schema.ts +++ b/src/plugins/index_pattern_field_editor/public/components/field_editor/form_schema.ts @@ -7,11 +7,77 @@ */ import { i18n } from '@kbn/i18n'; -import { fieldValidators } from '../../shared_imports'; +import { EuiComboBoxOptionOption } from '@elastic/eui'; +import type { Subscription } from 'rxjs'; +import { first } from 'rxjs/operators'; +import { PainlessLang } from '@kbn/monaco'; +import { + fieldValidators, + FieldConfig, + RuntimeType, + ValidationFunc, + ValidationCancelablePromise, +} from '../../shared_imports'; +import type { Context } from '../preview'; import { RUNTIME_FIELD_OPTIONS } from './constants'; const { containsCharsField, emptyField, numberGreaterThanField } = fieldValidators; +const i18nTexts = { + invalidScriptErrorMessage: i18n.translate( + 'indexPatternFieldEditor.editor.form.scriptEditorPainlessValidationMessage', + { + defaultMessage: 'Invalid Painless script.', + } + ), +}; + +// Validate the painless **syntax** (no need to make an HTTP request) +const painlessSyntaxValidator = () => { + let isValidatingSub: Subscription; + + return (() => { + const promise: ValidationCancelablePromise<'ERR_PAINLESS_SYNTAX'> = new Promise((resolve) => { + isValidatingSub = PainlessLang.validation$() + .pipe( + first(({ isValidating }) => { + return isValidating === false; + }) + ) + .subscribe(({ errors }) => { + const editorHasSyntaxErrors = errors.length > 0; + + if (editorHasSyntaxErrors) { + return resolve({ + message: i18nTexts.invalidScriptErrorMessage, + code: 'ERR_PAINLESS_SYNTAX', + }); + } + + resolve(undefined); + }); + }); + + promise.cancel = () => { + if (isValidatingSub) { + isValidatingSub.unsubscribe(); + } + }; + + return promise; + }) as ValidationFunc; +}; + +// Validate the painless **script** +const painlessScriptValidator: ValidationFunc = async ({ customData: { provider } }) => { + const previewError = (await provider()) as Context['error']; + + if (previewError && previewError.code === 'PAINLESS_SCRIPT_ERROR') { + return { + message: i18nTexts.invalidScriptErrorMessage, + }; + } +}; export const schema = { name: { @@ -47,7 +113,8 @@ export const schema = { defaultMessage: 'Type', }), defaultValue: [RUNTIME_FIELD_OPTIONS[0]], - }, + fieldsToValidateOnChange: ['script.source'], + } as FieldConfig>>, script: { source: { label: i18n.translate('indexPatternFieldEditor.editor.form.defineFieldLabel', { @@ -64,6 +131,14 @@ export const schema = { ) ), }, + { + validator: painlessSyntaxValidator(), + isAsync: true, + }, + { + validator: painlessScriptValidator, + isAsync: true, + }, ], }, }, diff --git a/src/plugins/index_pattern_field_editor/public/components/field_editor_flyout_content.tsx b/src/plugins/index_pattern_field_editor/public/components/field_editor_flyout_content.tsx index f13b30f13327c..d1dbb50ebf2e4 100644 --- a/src/plugins/index_pattern_field_editor/public/components/field_editor_flyout_content.tsx +++ b/src/plugins/index_pattern_field_editor/public/components/field_editor_flyout_content.tsx @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import React, { useState, useCallback, useMemo, useEffect } from 'react'; +import React, { useState, useCallback, useEffect, useRef } from 'react'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import { @@ -15,13 +15,10 @@ import { EuiFlexItem, EuiButtonEmpty, EuiButton, - EuiCallOut, - EuiSpacer, EuiText, } from '@elastic/eui'; -import type { Field, EsRuntimeField } from '../types'; -import { RuntimeFieldPainlessError } from '../lib'; +import type { Field } from '../types'; import { euiFlyoutClassname } from '../constants'; import { FlyoutPanels } from './flyout_panels'; import { useFieldEditorContext } from './field_editor_context'; @@ -36,9 +33,6 @@ const i18nTexts = { saveButtonLabel: i18n.translate('indexPatternFieldEditor.editor.flyoutSaveButtonLabel', { defaultMessage: 'Save', }), - formErrorsCalloutTitle: i18n.translate('indexPatternFieldEditor.editor.validationErrorTitle', { - defaultMessage: 'Fix errors in form before continuing.', - }), }; const defaultModalVisibility = { @@ -55,8 +49,6 @@ export interface Props { * Handler for the "cancel" footer button */ onCancel: () => void; - /** Handler to validate the script */ - runtimeFieldValidator: (field: EsRuntimeField) => Promise; /** Optional field to process */ field?: Field; isSavingField: boolean; @@ -70,10 +62,10 @@ const FieldEditorFlyoutContentComponent = ({ field, onSave, onCancel, - runtimeFieldValidator, isSavingField, onMounted, }: Props) => { + const isMounted = useRef(false); const isEditingExistingField = !!field; const { indexPattern } = useFieldEditorContext(); const { @@ -82,32 +74,18 @@ const FieldEditorFlyoutContentComponent = ({ const [formState, setFormState] = useState({ isSubmitted: false, + isSubmitting: false, isValid: field ? true : undefined, submit: field ? async () => ({ isValid: true, data: field }) : async () => ({ isValid: false, data: {} as Field }), }); - const [painlessSyntaxError, setPainlessSyntaxError] = useState( - null - ); - - const [isValidating, setIsValidating] = useState(false); const [modalVisibility, setModalVisibility] = useState(defaultModalVisibility); const [isFormModified, setIsFormModified] = useState(false); - const { submit, isValid: isFormValid, isSubmitted } = formState; - const hasErrors = isFormValid === false || painlessSyntaxError !== null; - - const clearSyntaxError = useCallback(() => setPainlessSyntaxError(null), []); - - const syntaxError = useMemo( - () => ({ - error: painlessSyntaxError, - clear: clearSyntaxError, - }), - [painlessSyntaxError, clearSyntaxError] - ); + const { submit, isValid: isFormValid, isSubmitting } = formState; + const hasErrors = isFormValid === false; const canCloseValidator = useCallback(() => { if (isFormModified) { @@ -121,25 +99,15 @@ const FieldEditorFlyoutContentComponent = ({ const onClickSave = useCallback(async () => { const { isValid, data } = await submit(); - const nameChange = field?.name !== data.name; - const typeChange = field?.type !== data.type; - - if (isValid) { - if (data.script) { - setIsValidating(true); - - const error = await runtimeFieldValidator({ - type: data.type, - script: data.script, - }); - setIsValidating(false); - setPainlessSyntaxError(error); + if (!isMounted.current) { + // User has closed the flyout meanwhile submitting the form + return; + } - if (error) { - return; - } - } + if (isValid) { + const nameChange = field?.name !== data.name; + const typeChange = field?.type !== data.type; if (isEditingExistingField && (nameChange || typeChange)) { setModalVisibility({ @@ -150,7 +118,7 @@ const FieldEditorFlyoutContentComponent = ({ onSave(data); } } - }, [onSave, submit, runtimeFieldValidator, field, isEditingExistingField]); + }, [onSave, submit, field, isEditingExistingField]); const onClickCancel = useCallback(() => { const canClose = canCloseValidator(); @@ -206,6 +174,14 @@ const FieldEditorFlyoutContentComponent = ({ } }, [onMounted, canCloseValidator]); + useEffect(() => { + isMounted.current = true; + + return () => { + isMounted.current = false; + }; + }, []); + return ( <> <> - {isSubmitted && hasErrors && ( - <> - - - - )} {i18nTexts.saveButtonLabel} diff --git a/src/plugins/index_pattern_field_editor/public/components/field_editor_flyout_content_container.tsx b/src/plugins/index_pattern_field_editor/public/components/field_editor_flyout_content_container.tsx index ea981662c1ff7..1738c55ba1f55 100644 --- a/src/plugins/index_pattern_field_editor/public/components/field_editor_flyout_content_container.tsx +++ b/src/plugins/index_pattern_field_editor/public/components/field_editor_flyout_content_container.tsx @@ -20,7 +20,7 @@ import { } from '../shared_imports'; import type { Field, PluginStart, InternalFieldType } from '../types'; import { pluginName } from '../constants'; -import { deserializeField, getRuntimeFieldValidator, getLinks, ApiService } from '../lib'; +import { deserializeField, getLinks, ApiService } from '../lib'; import { FieldEditorFlyoutContent, Props as FieldEditorFlyoutContentProps, @@ -103,11 +103,6 @@ export const FieldEditorFlyoutContentContainer = ({ return existing; }, [fields, field]); - const validateRuntimeField = useMemo( - () => getRuntimeFieldValidator(indexPattern.title, search), - [search, indexPattern] - ); - const services = useMemo( () => ({ api: apiService, @@ -207,7 +202,6 @@ export const FieldEditorFlyoutContentContainer = ({ onCancel={onCancel} onMounted={onMounted} field={fieldToEdit} - runtimeFieldValidator={validateRuntimeField} isSavingField={isSaving} /> diff --git a/src/plugins/index_pattern_field_editor/public/components/preview/documents_nav_preview.tsx b/src/plugins/index_pattern_field_editor/public/components/preview/documents_nav_preview.tsx index fa4097725cde1..04f5e2e542f40 100644 --- a/src/plugins/index_pattern_field_editor/public/components/preview/documents_nav_preview.tsx +++ b/src/plugins/index_pattern_field_editor/public/components/preview/documents_nav_preview.tsx @@ -21,22 +21,11 @@ import { useFieldPreviewContext } from './field_preview_context'; export const DocumentsNavPreview = () => { const { currentDocument: { id: documentId, isCustomId }, - documents: { loadSingle, loadFromCluster }, + documents: { loadSingle, loadFromCluster, fetchDocError }, navigation: { prev, next }, - error, } = useFieldPreviewContext(); - const errorMessage = - error !== null && error.code === 'DOC_NOT_FOUND' - ? i18n.translate( - 'indexPatternFieldEditor.fieldPreview.documentIdField.documentNotFoundError', - { - defaultMessage: 'Document not found', - } - ) - : null; - - const isInvalid = error !== null && error.code === 'DOC_NOT_FOUND'; + const isInvalid = fetchDocError?.code === 'DOC_NOT_FOUND'; // We don't display the nav button when the user has entered a custom // document ID as at that point there is no more reference to what's "next" @@ -58,13 +47,12 @@ export const DocumentsNavPreview = () => { label={i18n.translate('indexPatternFieldEditor.fieldPreview.documentIdField.label', { defaultMessage: 'Document ID', })} - error={errorMessage} isInvalid={isInvalid} fullWidth > void; - highlighted?: boolean; + hasScriptError?: boolean; + /** Indicates whether the field list item comes from the Painless script */ + isFromScript?: boolean; } export const PreviewListItem: React.FC = ({ field: { key, value, formattedValue, isPinned = false }, - highlighted, toggleIsPinned, + hasScriptError, + isFromScript = false, }) => { + const { isLoadingPreview } = useFieldPreviewContext(); + const [isPreviewImageModalVisible, setIsPreviewImageModalVisible] = useState(false); /* eslint-disable @typescript-eslint/naming-convention */ const classes = classnames('indexPatternFieldEditor__previewFieldList__item', { - 'indexPatternFieldEditor__previewFieldList__item--highlighted': highlighted, + 'indexPatternFieldEditor__previewFieldList__item--highlighted': isFromScript, 'indexPatternFieldEditor__previewFieldList__item--pinned': isPinned, }); /* eslint-enable @typescript-eslint/naming-convention */ const doesContainImage = formattedValue?.includes(' { + if (isFromScript && !Boolean(key)) { + return ( + + + {i18n.translate('indexPatternFieldEditor.fieldPreview.fieldNameNotSetLabel', { + defaultMessage: 'Field name not set', + })} + + + ); + } + + return key; + }; + + const withTooltip = (content: JSX.Element) => ( + + {content} + + ); + const renderValue = () => { + if (isFromScript && isLoadingPreview) { + return ( + + + + ); + } + + if (hasScriptError) { + return ( +
    + + {i18n.translate('indexPatternFieldEditor.fieldPreview.scriptErrorBadgeLabel', { + defaultMessage: 'Script error', + })} + +
    + ); + } + + if (isFromScript && value === undefined) { + return ( + + + {i18n.translate('indexPatternFieldEditor.fieldPreview.valueNotSetLabel', { + defaultMessage: 'Value not set', + })} + + + ); + } + if (doesContainImage) { return ( = ({ } if (formattedValue !== undefined) { - return ( + return withTooltip( = ({ ); } - return ( + return withTooltip( {JSON.stringify(value)} @@ -76,19 +145,14 @@ export const PreviewListItem: React.FC = ({ className="indexPatternFieldEditor__previewFieldList__item__key__wrapper" data-test-subj="key" > - {key} + {renderName()}
    - - {renderValue()} - + {renderValue()} { }, fields, error, + documents: { fetchDocError }, reset, + isPreviewAvailable, } = useFieldPreviewContext(); // To show the preview we at least need a name to be defined, the script or the format @@ -38,12 +40,15 @@ export const FieldPreview = () => { name === null && script === null && format === null ? true : // If we have some result from the _execute API call don't show the empty prompt - error !== null || fields.length > 0 + Boolean(error) || fields.length > 0 ? false : name === null && format === null ? true : false; + const doRenderListOfFields = fetchDocError === null; + const showWarningPreviewNotAvailable = isPreviewAvailable === false && fetchDocError === null; + const onFieldListResize = useCallback(({ height }: { height: number }) => { setFieldListHeight(height); }, []); @@ -58,7 +63,7 @@ export const FieldPreview = () => { return (
    • - +
    ); @@ -70,9 +75,6 @@ export const FieldPreview = () => { return reset; }, [reset]); - const doShowFieldList = - error === null || (error.code !== 'DOC_NOT_FOUND' && error.code !== 'ERR_FETCHING_DOC'); - return (
    { - - - - setSearchValue(e.target.value)} - placeholder={i18n.translate( - 'indexPatternFieldEditor.fieldPreview.filterFieldsPlaceholder', - { - defaultMessage: 'Filter fields', - } - )} - fullWidth - data-test-subj="filterFieldsInput" - /> - - - - - - {doShowFieldList && ( - <> - {/* The current field(s) the user is creating */} - {renderFieldsToPreview()} - - {/* List of other fields in the document */} - - {(resizeRef) => ( -
    - setSearchValue('')} - searchValue={searchValue} - // We add a key to force rerender the virtual list whenever the window height changes - key={fieldListHeight} - /> -
    + {showWarningPreviewNotAvailable ? ( + +

    + {i18n.translate( + 'indexPatternFieldEditor.fieldPreview.notAvailableWarningCallout.description', + { + defaultMessage: + 'Runtime field preview is disabled because no documents could be fetched from the cluster.', + } )} - +

    +
    + ) : ( + <> + + + + {doRenderListOfFields && ( + <> + setSearchValue(e.target.value)} + placeholder={i18n.translate( + 'indexPatternFieldEditor.fieldPreview.filterFieldsPlaceholder', + { + defaultMessage: 'Filter fields', + } + )} + fullWidth + data-test-subj="filterFieldsInput" + /> + + + )} + + + + + {doRenderListOfFields && ( + <> + {/* The current field(s) the user is creating */} + {renderFieldsToPreview()} + + {/* List of other fields in the document */} + + {(resizeRef) => ( +
    + setSearchValue('')} + searchValue={searchValue} + // We add a key to force rerender the virtual list whenever the window height changes + key={fieldListHeight} + /> +
    + )} +
    + + )} )} diff --git a/src/plugins/index_pattern_field_editor/public/components/preview/field_preview_context.tsx b/src/plugins/index_pattern_field_editor/public/components/preview/field_preview_context.tsx index 21ab055c9b05e..74f77f91e2f13 100644 --- a/src/plugins/index_pattern_field_editor/public/components/preview/field_preview_context.tsx +++ b/src/plugins/index_pattern_field_editor/public/components/preview/field_preview_context.tsx @@ -20,81 +20,18 @@ import useDebounce from 'react-use/lib/useDebounce'; import { i18n } from '@kbn/i18n'; import { get } from 'lodash'; -import type { FieldPreviewContext, FieldFormatConfig } from '../../types'; import { parseEsError } from '../../lib/runtime_field_validation'; -import { RuntimeType, RuntimeField } from '../../shared_imports'; import { useFieldEditorContext } from '../field_editor_context'; - -type From = 'cluster' | 'custom'; -interface EsDocument { - _id: string; - [key: string]: any; -} - -interface PreviewError { - code: 'DOC_NOT_FOUND' | 'PAINLESS_SCRIPT_ERROR' | 'ERR_FETCHING_DOC'; - error: Record; -} - -interface ClusterData { - documents: EsDocument[]; - currentIdx: number; -} - -// The parameters required to preview the field -interface Params { - name: string | null; - index: string | null; - type: RuntimeType | null; - script: Required['script'] | null; - format: FieldFormatConfig | null; - document: EsDocument | null; -} - -export interface FieldPreview { - key: string; - value: unknown; - formattedValue?: string; -} - -interface Context { - fields: FieldPreview[]; - error: PreviewError | null; - params: { - value: Params; - update: (updated: Partial) => void; - }; - isLoadingPreview: boolean; - currentDocument: { - value?: EsDocument; - id: string; - isLoading: boolean; - isCustomId: boolean; - }; - documents: { - loadSingle: (id: string) => void; - loadFromCluster: () => Promise; - }; - panel: { - isVisible: boolean; - setIsVisible: (isVisible: boolean) => void; - }; - from: { - value: From; - set: (value: From) => void; - }; - navigation: { - isFirstDoc: boolean; - isLastDoc: boolean; - next: () => void; - prev: () => void; - }; - reset: () => void; - pinnedFields: { - value: { [key: string]: boolean }; - set: React.Dispatch>; - }; -} +import type { + PainlessExecuteContext, + Context, + Params, + ClusterData, + From, + EsDocument, + ScriptErrorCodes, + FetchDocError, +} from './types'; const fieldPreviewContext = createContext(undefined); @@ -112,7 +49,10 @@ export const defaultValueFormatter = (value: unknown) => export const FieldPreviewProvider: FunctionComponent = ({ children }) => { const previewCount = useRef(0); - const [lastExecutePainlessRequestParams, setLastExecutePainlessReqParams] = useState<{ + + // We keep in cache the latest params sent to the _execute API so we don't make unecessary requests + // when changing parameters that don't affect the preview result (e.g. changing the "name" field). + const lastExecutePainlessRequestParams = useRef<{ type: Params['type']; script: string | undefined; documentId: string | undefined; @@ -138,6 +78,8 @@ export const FieldPreviewProvider: FunctionComponent = ({ children }) => { fields: Context['fields']; error: Context['error']; }>({ fields: [], error: null }); + /** Possible error while fetching sample documents */ + const [fetchDocError, setFetchDocError] = useState(null); /** The parameters required for the Painless _execute API */ const [params, setParams] = useState(defaultParams); /** The sample documents fetched from the cluster */ @@ -146,7 +88,7 @@ export const FieldPreviewProvider: FunctionComponent = ({ children }) => { currentIdx: 0, }); /** Flag to show/hide the preview panel */ - const [isPanelVisible, setIsPanelVisible] = useState(false); + const [isPanelVisible, setIsPanelVisible] = useState(true); /** Flag to indicate if we are loading document from cluster */ const [isFetchingDocument, setIsFetchingDocument] = useState(false); /** Flag to indicate if we are calling the _execute API */ @@ -157,44 +99,66 @@ export const FieldPreviewProvider: FunctionComponent = ({ children }) => { const [from, setFrom] = useState('cluster'); /** Map of fields pinned to the top of the list */ const [pinnedFields, setPinnedFields] = useState<{ [key: string]: boolean }>({}); + /** Keep track if the script painless syntax is being validated and if it is valid */ + const [scriptEditorValidation, setScriptEditorValidation] = useState<{ + isValidating: boolean; + isValid: boolean; + message: string | null; + }>({ isValidating: false, isValid: true, message: null }); const { documents, currentIdx } = clusterData; - const currentDocument: EsDocument | undefined = useMemo( - () => documents[currentIdx], - [documents, currentIdx] - ); - - const currentDocIndex = currentDocument?._index; - const currentDocId: string = currentDocument?._id ?? ''; + const currentDocument: EsDocument | undefined = documents[currentIdx]; + const currentDocIndex: string | undefined = currentDocument?._index; + const currentDocId: string | undefined = currentDocument?._id; const totalDocs = documents.length; + const isCustomDocId = customDocIdToLoad !== null; + let isPreviewAvailable = true; + + // If no documents could be fetched from the cluster (and we are not trying to load + // a custom doc ID) then we disable preview as the script field validation expect the result + // of the preview to before resolving. If there are no documents we can't have a preview + // (the _execute API expects one) and thus the validation should not expect any value. + if (!isFetchingDocument && !isCustomDocId && documents.length === 0) { + isPreviewAvailable = false; + } + const { name, document, script, format, type } = params; const updateParams: Context['params']['update'] = useCallback((updated) => { setParams((prev) => ({ ...prev, ...updated })); }, []); - const needToUpdatePreview = useMemo(() => { - const isCurrentDocIdDefined = currentDocId !== ''; - - if (!isCurrentDocIdDefined) { + const allParamsDefined = useMemo(() => { + if (!currentDocIndex || !script?.source || !type) { return false; } - - const allParamsDefined = (['type', 'script', 'index', 'document'] as Array).every( - (key) => Boolean(params[key]) + return true; + }, [currentDocIndex, script?.source, type]); + + const hasSomeParamsChanged = useMemo(() => { + return ( + lastExecutePainlessRequestParams.current.type !== type || + lastExecutePainlessRequestParams.current.script !== script?.source || + lastExecutePainlessRequestParams.current.documentId !== currentDocId ); + }, [type, script, currentDocId]); - if (!allParamsDefined) { - return false; - } - - const hasSomeParamsChanged = - lastExecutePainlessRequestParams.type !== type || - lastExecutePainlessRequestParams.script !== script?.source || - lastExecutePainlessRequestParams.documentId !== currentDocId; + const setPreviewError = useCallback((error: Context['error']) => { + setPreviewResponse((prev) => ({ + ...prev, + error, + })); + }, []); - return hasSomeParamsChanged; - }, [type, script?.source, currentDocId, params, lastExecutePainlessRequestParams]); + const clearPreviewError = useCallback((errorCode: ScriptErrorCodes) => { + setPreviewResponse((prev) => { + const error = prev.error === null || prev.error?.code === errorCode ? null : prev.error; + return { + ...prev, + error, + }; + }); + }, []); const valueFormatter = useCallback( (value: unknown) => { @@ -217,14 +181,11 @@ export const FieldPreviewProvider: FunctionComponent = ({ children }) => { throw new Error('The "limit" option must be a number'); } + lastExecutePainlessRequestParams.current.documentId = undefined; setIsFetchingDocument(true); - setClusterData({ - documents: [], - currentIdx: 0, - }); setPreviewResponse({ fields: [], error: null }); - const [response, error] = await search + const [response, searchError] = await search .search({ params: { index: indexPattern.title, @@ -240,12 +201,29 @@ export const FieldPreviewProvider: FunctionComponent = ({ children }) => { setIsFetchingDocument(false); setCustomDocIdToLoad(null); - setClusterData({ - documents: response ? response.rawResponse.hits.hits : [], - currentIdx: 0, - }); + const error: FetchDocError | null = Boolean(searchError) + ? { + code: 'ERR_FETCHING_DOC', + error: { + message: searchError.toString(), + reason: i18n.translate( + 'indexPatternFieldEditor.fieldPreview.error.errorLoadingSampleDocumentsDescription', + { + defaultMessage: 'Error loading sample documents.', + } + ), + }, + } + : null; - setPreviewResponse((prev) => ({ ...prev, error })); + setFetchDocError(error); + + if (error === null) { + setClusterData({ + documents: response ? response.rawResponse.hits.hits : [], + currentIdx: 0, + }); + } }, [indexPattern, search] ); @@ -256,6 +234,7 @@ export const FieldPreviewProvider: FunctionComponent = ({ children }) => { return; } + lastExecutePainlessRequestParams.current.documentId = undefined; setIsFetchingDocument(true); const [response, searchError] = await search @@ -280,11 +259,17 @@ export const FieldPreviewProvider: FunctionComponent = ({ children }) => { const isDocumentFound = response?.rawResponse.hits.total > 0; const loadedDocuments: EsDocument[] = isDocumentFound ? response.rawResponse.hits.hits : []; - const error: Context['error'] = Boolean(searchError) + const error: FetchDocError | null = Boolean(searchError) ? { code: 'ERR_FETCHING_DOC', error: { message: searchError.toString(), + reason: i18n.translate( + 'indexPatternFieldEditor.fieldPreview.error.errorLoadingDocumentDescription', + { + defaultMessage: 'Error loading document.', + } + ), }, } : isDocumentFound === false @@ -301,14 +286,14 @@ export const FieldPreviewProvider: FunctionComponent = ({ children }) => { } : null; - setPreviewResponse((prev) => ({ ...prev, error })); + setFetchDocError(error); - setClusterData({ - documents: loadedDocuments, - currentIdx: 0, - }); - - if (error !== null) { + if (error === null) { + setClusterData({ + documents: loadedDocuments, + currentIdx: 0, + }); + } else { // Make sure we disable the "Updating..." indicator as we have an error // and we won't fetch the preview setIsLoadingPreview(false); @@ -318,23 +303,28 @@ export const FieldPreviewProvider: FunctionComponent = ({ children }) => { ); const updatePreview = useCallback(async () => { - setLastExecutePainlessReqParams({ - type: params.type, - script: params.script?.source, - documentId: currentDocId, - }); + if (scriptEditorValidation.isValidating) { + return; + } - if (!needToUpdatePreview) { + if (!allParamsDefined || !hasSomeParamsChanged || scriptEditorValidation.isValid === false) { + setIsLoadingPreview(false); return; } + lastExecutePainlessRequestParams.current = { + type, + script: script?.source, + documentId: currentDocId, + }; + const currentApiCall = ++previewCount.current; const response = await getFieldPreview({ - index: currentDocIndex, - document: params.document!, - context: `${params.type!}_field` as FieldPreviewContext, - script: params.script!, + index: currentDocIndex!, + document: document!, + context: `${type!}_field` as PainlessExecuteContext, + script: script!, documentId: currentDocId, }); @@ -344,8 +334,6 @@ export const FieldPreviewProvider: FunctionComponent = ({ children }) => { return; } - setIsLoadingPreview(false); - const { error: serverError } = response; if (serverError) { @@ -355,39 +343,43 @@ export const FieldPreviewProvider: FunctionComponent = ({ children }) => { }); notifications.toasts.addError(serverError, { title }); + setIsLoadingPreview(false); return; } - const { values, error } = response.data ?? { values: [], error: {} }; - - if (error) { - const fallBackError = { - message: i18n.translate('indexPatternFieldEditor.fieldPreview.defaultErrorTitle', { - defaultMessage: 'Unable to run the provided script', - }), - }; - - setPreviewResponse({ - fields: [], - error: { code: 'PAINLESS_SCRIPT_ERROR', error: parseEsError(error, true) ?? fallBackError }, - }); - } else { - const [value] = values; - const formattedValue = valueFormatter(value); - - setPreviewResponse({ - fields: [{ key: params.name!, value, formattedValue }], - error: null, - }); + if (response.data) { + const { values, error } = response.data; + + if (error) { + setPreviewResponse({ + fields: [{ key: name ?? '', value: '', formattedValue: defaultValueFormatter('') }], + error: { code: 'PAINLESS_SCRIPT_ERROR', error: parseEsError(error) }, + }); + } else { + const [value] = values; + const formattedValue = valueFormatter(value); + + setPreviewResponse({ + fields: [{ key: name!, value, formattedValue }], + error: null, + }); + } } + + setIsLoadingPreview(false); }, [ - needToUpdatePreview, - params, + name, + type, + script, + document, currentDocIndex, currentDocId, getFieldPreview, notifications.toasts, valueFormatter, + allParamsDefined, + scriptEditorValidation, + hasSomeParamsChanged, ]); const goToNextDoc = useCallback(() => { @@ -416,11 +408,6 @@ export const FieldPreviewProvider: FunctionComponent = ({ children }) => { currentIdx: 0, }); setPreviewResponse({ fields: [], error: null }); - setLastExecutePainlessReqParams({ - type: null, - script: undefined, - documentId: undefined, - }); setFrom('cluster'); setIsLoadingPreview(false); setIsFetchingDocument(false); @@ -430,6 +417,7 @@ export const FieldPreviewProvider: FunctionComponent = ({ children }) => { () => ({ fields: previewResponse.fields, error: previewResponse.error, + isPreviewAvailable, isLoadingPreview, params: { value: params, @@ -437,13 +425,14 @@ export const FieldPreviewProvider: FunctionComponent = ({ children }) => { }, currentDocument: { value: currentDocument, - id: customDocIdToLoad !== null ? customDocIdToLoad : currentDocId, + id: isCustomDocId ? customDocIdToLoad! : currentDocId, isLoading: isFetchingDocument, - isCustomId: customDocIdToLoad !== null, + isCustomId: isCustomDocId, }, documents: { loadSingle: setCustomDocIdToLoad, loadFromCluster: fetchSampleDocuments, + fetchDocError, }, navigation: { isFirstDoc: currentIdx === 0, @@ -464,14 +453,20 @@ export const FieldPreviewProvider: FunctionComponent = ({ children }) => { value: pinnedFields, set: setPinnedFields, }, + validation: { + setScriptEditorValidation, + }, }), [ previewResponse, + fetchDocError, params, + isPreviewAvailable, isLoadingPreview, updateParams, currentDocument, currentDocId, + isCustomDocId, fetchSampleDocuments, isFetchingDocument, customDocIdToLoad, @@ -488,38 +483,23 @@ export const FieldPreviewProvider: FunctionComponent = ({ children }) => { /** * In order to immediately display the "Updating..." state indicator and not have to wait - * the 500ms of the debounce, we set the isLoadingPreview state in this effect + * the 500ms of the debounce, we set the isLoadingPreview state in this effect whenever + * one of the _execute API param changes */ useEffect(() => { - if (needToUpdatePreview) { + if (allParamsDefined && hasSomeParamsChanged) { setIsLoadingPreview(true); } - }, [needToUpdatePreview, customDocIdToLoad]); + }, [allParamsDefined, hasSomeParamsChanged, script?.source, type, currentDocId]); /** - * Whenever we enter manually a document ID to load we'll clear the - * documents and the preview value. + * In order to immediately display the "Updating..." state indicator and not have to wait + * the 500ms of the debounce, we set the isFetchingDocument state in this effect whenever + * "customDocIdToLoad" changes */ useEffect(() => { - if (customDocIdToLoad !== null) { + if (customDocIdToLoad !== null && Boolean(customDocIdToLoad.trim())) { setIsFetchingDocument(true); - - setClusterData({ - documents: [], - currentIdx: 0, - }); - - setPreviewResponse((prev) => { - const { - fields: { 0: field }, - } = prev; - return { - ...prev, - fields: [ - { ...field, value: undefined, formattedValue: defaultValueFormatter(undefined) }, - ], - }; - }); } }, [customDocIdToLoad]); @@ -566,14 +546,60 @@ export const FieldPreviewProvider: FunctionComponent = ({ children }) => { }); }, [name, script, document, valueFormatter]); - useDebounce( - // Whenever updatePreview() changes (meaning whenever any of the params changes) - // we call it to update the preview response with the field(s) value or possible error. - updatePreview, - 500, - [updatePreview] - ); + useEffect(() => { + if (script?.source === undefined) { + // Whenever the source is not defined ("Set value" is toggled off or the + // script is empty) we clear the error and update the params cache. + lastExecutePainlessRequestParams.current.script = undefined; + setPreviewError(null); + } + }, [script?.source, setPreviewError]); + + // Handle the validation state coming from the Painless DiagnosticAdapter + // (see @kbn-monaco/src/painless/diagnostics_adapter.ts) + useEffect(() => { + if (scriptEditorValidation.isValidating) { + return; + } + if (scriptEditorValidation.isValid === false) { + // Make sure to remove the "Updating..." spinner + setIsLoadingPreview(false); + + // Set preview response error so it is displayed in the flyout footer + const error = + script?.source === undefined + ? null + : { + code: 'PAINLESS_SYNTAX_ERROR' as const, + error: { + reason: + scriptEditorValidation.message ?? + i18n.translate('indexPatternFieldEditor.fieldPreview.error.painlessSyntax', { + defaultMessage: 'Invalid Painless syntax', + }), + }, + }; + setPreviewError(error); + + // Make sure to update the lastExecutePainlessRequestParams cache so when the user updates + // the script and fixes the syntax the "updatePreview()" will run + lastExecutePainlessRequestParams.current.script = script?.source; + } else { + // Clear possible previous syntax error + clearPreviewError('PAINLESS_SYNTAX_ERROR'); + } + }, [scriptEditorValidation, script?.source, setPreviewError, clearPreviewError]); + + /** + * Whenever updatePreview() changes (meaning whenever any of the params changes) + * we call it to update the preview response with the field(s) value or possible error. + */ + useDebounce(updatePreview, 500, [updatePreview]); + + /** + * Whenever the doc ID to load changes we load the document (after a 500ms debounce) + */ useDebounce( () => { if (customDocIdToLoad === null) { diff --git a/src/plugins/index_pattern_field_editor/public/components/preview/field_preview_error.tsx b/src/plugins/index_pattern_field_editor/public/components/preview/field_preview_error.tsx index 7994e649e1ebb..6ca38d4d186fb 100644 --- a/src/plugins/index_pattern_field_editor/public/components/preview/field_preview_error.tsx +++ b/src/plugins/index_pattern_field_editor/public/components/preview/field_preview_error.tsx @@ -12,27 +12,25 @@ import { i18n } from '@kbn/i18n'; import { useFieldPreviewContext } from './field_preview_context'; export const FieldPreviewError = () => { - const { error } = useFieldPreviewContext(); + const { + documents: { fetchDocError }, + } = useFieldPreviewContext(); - if (error === null) { + if (fetchDocError === null) { return null; } return ( - {error.code === 'PAINLESS_SCRIPT_ERROR' ? ( -

    {error.error.reason}

    - ) : ( -

    {error.error.message}

    - )} +

    {fetchDocError.error.message ?? fetchDocError.error.reason}

    ); }; diff --git a/src/plugins/index_pattern_field_editor/public/components/preview/field_preview_header.tsx b/src/plugins/index_pattern_field_editor/public/components/preview/field_preview_header.tsx index 2d3d5c20ba7b3..28b75a43b7d11 100644 --- a/src/plugins/index_pattern_field_editor/public/components/preview/field_preview_header.tsx +++ b/src/plugins/index_pattern_field_editor/public/components/preview/field_preview_header.tsx @@ -7,18 +7,12 @@ */ import React from 'react'; -import { - EuiTitle, - EuiText, - EuiTextColor, - EuiFlexGroup, - EuiFlexItem, - EuiLoadingSpinner, -} from '@elastic/eui'; +import { EuiTitle, EuiText, EuiTextColor, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { useFieldEditorContext } from '../field_editor_context'; import { useFieldPreviewContext } from './field_preview_context'; +import { IsUpdatingIndicator } from './is_updating_indicator'; const i18nTexts = { title: i18n.translate('indexPatternFieldEditor.fieldPreview.title', { @@ -27,21 +21,15 @@ const i18nTexts = { customData: i18n.translate('indexPatternFieldEditor.fieldPreview.subTitle.customData', { defaultMessage: 'Custom data', }), - updatingLabel: i18n.translate('indexPatternFieldEditor.fieldPreview.updatingPreviewLabel', { - defaultMessage: 'Updating...', - }), }; export const FieldPreviewHeader = () => { const { indexPattern } = useFieldEditorContext(); const { from, - isLoadingPreview, - currentDocument: { isLoading }, + currentDocument: { isLoading: isFetchingDocument }, } = useFieldPreviewContext(); - const isUpdating = isLoadingPreview || isLoading; - return (
    @@ -50,15 +38,9 @@ export const FieldPreviewHeader = () => {

    {i18nTexts.title}

    - - {isUpdating && ( - - - - - - {i18nTexts.updatingLabel} - + {isFetchingDocument && ( + + )}
    diff --git a/src/plugins/index_pattern_field_editor/public/components/preview/index.ts b/src/plugins/index_pattern_field_editor/public/components/preview/index.ts index 5d3b4bb41fc5f..2f93616ef72eb 100644 --- a/src/plugins/index_pattern_field_editor/public/components/preview/index.ts +++ b/src/plugins/index_pattern_field_editor/public/components/preview/index.ts @@ -9,3 +9,5 @@ export { useFieldPreviewContext, FieldPreviewProvider } from './field_preview_context'; export { FieldPreview } from './field_preview'; + +export type { PainlessExecuteContext, FieldPreviewResponse, Context } from './types'; diff --git a/src/plugins/index_pattern_field_editor/public/components/preview/is_updating_indicator.tsx b/src/plugins/index_pattern_field_editor/public/components/preview/is_updating_indicator.tsx new file mode 100644 index 0000000000000..0c030d498c617 --- /dev/null +++ b/src/plugins/index_pattern_field_editor/public/components/preview/is_updating_indicator.tsx @@ -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 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 { EuiFlexGroup, EuiFlexItem, EuiLoadingSpinner } from '@elastic/eui'; + +export const IsUpdatingIndicator = () => { + return ( +
    + + + + + + {i18n.translate('indexPatternFieldEditor.fieldPreview.updatingPreviewLabel', { + defaultMessage: 'Updating...', + })} + + +
    + ); +}; diff --git a/src/plugins/index_pattern_field_editor/public/components/preview/types.ts b/src/plugins/index_pattern_field_editor/public/components/preview/types.ts new file mode 100644 index 0000000000000..d7c0a5867efd6 --- /dev/null +++ b/src/plugins/index_pattern_field_editor/public/components/preview/types.ts @@ -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 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 type { RuntimeType, RuntimeField } from '../../shared_imports'; +import type { FieldFormatConfig, RuntimeFieldPainlessError } from '../../types'; + +export type From = 'cluster' | 'custom'; + +export interface EsDocument { + _id: string; + _index: string; + _source: { + [key: string]: unknown; + }; + [key: string]: unknown; +} + +export type ScriptErrorCodes = 'PAINLESS_SCRIPT_ERROR' | 'PAINLESS_SYNTAX_ERROR'; +export type FetchDocErrorCodes = 'DOC_NOT_FOUND' | 'ERR_FETCHING_DOC'; + +interface PreviewError { + code: ScriptErrorCodes; + error: + | RuntimeFieldPainlessError + | { + reason?: string; + [key: string]: unknown; + }; +} + +export interface FetchDocError { + code: FetchDocErrorCodes; + error: { + message?: string; + reason?: string; + [key: string]: unknown; + }; +} + +export interface ClusterData { + documents: EsDocument[]; + currentIdx: number; +} + +// The parameters required to preview the field +export interface Params { + name: string | null; + index: string | null; + type: RuntimeType | null; + script: Required['script'] | null; + format: FieldFormatConfig | null; + document: { [key: string]: unknown } | null; +} + +export interface FieldPreview { + key: string; + value: unknown; + formattedValue?: string; +} + +export interface Context { + fields: FieldPreview[]; + error: PreviewError | null; + params: { + value: Params; + update: (updated: Partial) => void; + }; + isPreviewAvailable: boolean; + isLoadingPreview: boolean; + currentDocument: { + value?: EsDocument; + id?: string; + isLoading: boolean; + isCustomId: boolean; + }; + documents: { + loadSingle: (id: string) => void; + loadFromCluster: () => Promise; + fetchDocError: FetchDocError | null; + }; + panel: { + isVisible: boolean; + setIsVisible: (isVisible: boolean) => void; + }; + from: { + value: From; + set: (value: From) => void; + }; + navigation: { + isFirstDoc: boolean; + isLastDoc: boolean; + next: () => void; + prev: () => void; + }; + reset: () => void; + pinnedFields: { + value: { [key: string]: boolean }; + set: React.Dispatch>; + }; + validation: { + setScriptEditorValidation: React.Dispatch< + React.SetStateAction<{ isValid: boolean; isValidating: boolean; message: string | null }> + >; + }; +} + +export type PainlessExecuteContext = + | 'boolean_field' + | 'date_field' + | 'double_field' + | 'geo_point_field' + | 'ip_field' + | 'keyword_field' + | 'long_field'; + +export interface FieldPreviewResponse { + values: unknown[]; + error?: ScriptError; +} + +export interface ScriptError { + caused_by: { + reason: string; + [key: string]: unknown; + }; + position?: { + offset: number; + start: number; + end: number; + }; + script_stack?: string[]; + [key: string]: unknown; +} diff --git a/src/plugins/index_pattern_field_editor/public/lib/api.ts b/src/plugins/index_pattern_field_editor/public/lib/api.ts index 9641619640a52..594cd07ecb70e 100644 --- a/src/plugins/index_pattern_field_editor/public/lib/api.ts +++ b/src/plugins/index_pattern_field_editor/public/lib/api.ts @@ -8,7 +8,7 @@ import { HttpSetup } from 'src/core/public'; import { API_BASE_PATH } from '../../common/constants'; import { sendRequest } from '../shared_imports'; -import { FieldPreviewContext, FieldPreviewResponse } from '../types'; +import { PainlessExecuteContext, FieldPreviewResponse } from '../components/preview'; export const initApi = (httpClient: HttpSetup) => { const getFieldPreview = ({ @@ -19,7 +19,7 @@ export const initApi = (httpClient: HttpSetup) => { documentId, }: { index: string; - context: FieldPreviewContext; + context: PainlessExecuteContext; script: { source: string } | null; document: Record; documentId: string; diff --git a/src/plugins/index_pattern_field_editor/public/lib/index.ts b/src/plugins/index_pattern_field_editor/public/lib/index.ts index d9aaab77ff66a..c7627a63da9ff 100644 --- a/src/plugins/index_pattern_field_editor/public/lib/index.ts +++ b/src/plugins/index_pattern_field_editor/public/lib/index.ts @@ -6,12 +6,12 @@ * Side Public License, v 1. */ -export { deserializeField } from './serialization'; +export { deserializeField, painlessErrorToMonacoMarker } from './serialization'; export { getLinks } from './documentation'; -export type { RuntimeFieldPainlessError } from './runtime_field_validation'; -export { getRuntimeFieldValidator, parseEsError } from './runtime_field_validation'; +export { parseEsError } from './runtime_field_validation'; export type { ApiService } from './api'; + export { initApi } from './api'; diff --git a/src/plugins/index_pattern_field_editor/public/lib/runtime_field_validation.test.ts b/src/plugins/index_pattern_field_editor/public/lib/runtime_field_validation.test.ts deleted file mode 100644 index b25d47b3d0d15..0000000000000 --- a/src/plugins/index_pattern_field_editor/public/lib/runtime_field_validation.test.ts +++ /dev/null @@ -1,165 +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 { dataPluginMock } from '../../../data/public/mocks'; -import { getRuntimeFieldValidator } from './runtime_field_validation'; - -const dataStart = dataPluginMock.createStartContract(); -const { search } = dataStart; - -const runtimeField = { - type: 'keyword', - script: { - source: 'emit("hello")', - }, -}; - -const spy = jest.fn(); - -search.search = () => - ({ - toPromise: spy, - } as any); - -const validator = getRuntimeFieldValidator('myIndex', search); - -describe('Runtime field validation', () => { - const expectedError = { - message: 'Error compiling the painless script', - position: { offset: 4, start: 0, end: 18 }, - reason: 'cannot resolve symbol [emit]', - scriptStack: ["emit.some('value')", ' ^---- HERE'], - }; - - [ - { - title: 'should return null when there are no errors', - response: {}, - status: 200, - expected: null, - }, - { - title: 'should return the error in the first failed shard', - response: { - attributes: { - type: 'status_exception', - reason: 'error while executing search', - caused_by: { - failed_shards: [ - { - shard: 0, - index: 'kibana_sample_data_logs', - node: 'gVwk20UWSdO6VyuNOc_6UA', - reason: { - type: 'script_exception', - script_stack: ["emit.some('value')", ' ^---- HERE'], - position: { offset: 4, start: 0, end: 18 }, - caused_by: { - type: 'illegal_argument_exception', - reason: 'cannot resolve symbol [emit]', - }, - }, - }, - ], - }, - }, - }, - status: 400, - expected: expectedError, - }, - { - title: 'should return the error in the third failed shard', - response: { - attributes: { - type: 'status_exception', - reason: 'error while executing search', - caused_by: { - failed_shards: [ - { - shard: 0, - index: 'kibana_sample_data_logs', - node: 'gVwk20UWSdO6VyuNOc_6UA', - reason: { - type: 'foo', - }, - }, - { - shard: 1, - index: 'kibana_sample_data_logs', - node: 'gVwk20UWSdO6VyuNOc_6UA', - reason: { - type: 'bar', - }, - }, - { - shard: 2, - index: 'kibana_sample_data_logs', - node: 'gVwk20UWSdO6VyuNOc_6UA', - reason: { - type: 'script_exception', - script_stack: ["emit.some('value')", ' ^---- HERE'], - position: { offset: 4, start: 0, end: 18 }, - caused_by: { - type: 'illegal_argument_exception', - reason: 'cannot resolve symbol [emit]', - }, - }, - }, - ], - }, - }, - }, - status: 400, - expected: expectedError, - }, - { - title: 'should have default values if an error prop is not found', - response: { - attributes: { - type: 'status_exception', - reason: 'error while executing search', - caused_by: { - failed_shards: [ - { - shard: 0, - index: 'kibana_sample_data_logs', - node: 'gVwk20UWSdO6VyuNOc_6UA', - reason: { - // script_stack, position and caused_by are missing - type: 'script_exception', - caused_by: { - type: 'illegal_argument_exception', - }, - }, - }, - ], - }, - }, - }, - status: 400, - expected: { - message: 'Error compiling the painless script', - position: null, - reason: null, - scriptStack: [], - }, - }, - ].map(({ title, response, status, expected }) => { - test(title, async () => { - if (status !== 200) { - spy.mockRejectedValueOnce(response); - } else { - spy.mockResolvedValueOnce(response); - } - - const result = await validator(runtimeField); - - expect(result).toEqual(expected); - }); - }); -}); diff --git a/src/plugins/index_pattern_field_editor/public/lib/runtime_field_validation.ts b/src/plugins/index_pattern_field_editor/public/lib/runtime_field_validation.ts index 5f80b7823b6a0..770fb548f1251 100644 --- a/src/plugins/index_pattern_field_editor/public/lib/runtime_field_validation.ts +++ b/src/plugins/index_pattern_field_editor/public/lib/runtime_field_validation.ts @@ -6,72 +6,28 @@ * Side Public License, v 1. */ import { i18n } from '@kbn/i18n'; +import { ScriptError } from '../components/preview/types'; +import { RuntimeFieldPainlessError, PainlessErrorCode } from '../types'; -import { DataPublicPluginStart } from '../shared_imports'; -import type { EsRuntimeField } from '../types'; - -export interface RuntimeFieldPainlessError { - message: string; - reason: string; - position: { - offset: number; - start: number; - end: number; - } | null; - scriptStack: string[]; -} - -type Error = Record; - -/** - * We are only interested in "script_exception" error type - */ -const getScriptExceptionErrorOnShard = (error: Error): Error | null => { - if (error.type === 'script_exception') { - return error; - } - - if (!error.caused_by) { - return null; +export const getErrorCodeFromErrorReason = (reason: string = ''): PainlessErrorCode => { + if (reason.startsWith('Cannot cast from')) { + return 'CAST_ERROR'; } - - // Recursively try to get a script exception error - return getScriptExceptionErrorOnShard(error.caused_by); + return 'UNKNOWN'; }; -/** - * We get the first script exception error on any failing shard. - * The UI can only display one error at the time so there is no need - * to look any further. - */ -const getScriptExceptionError = (error: Error): Error | null => { - if (error === undefined || !Array.isArray(error.failed_shards)) { - return null; - } +export const parseEsError = (scriptError: ScriptError): RuntimeFieldPainlessError => { + let reason = scriptError.caused_by?.reason; + const errorCode = getErrorCodeFromErrorReason(reason); - let scriptExceptionError = null; - for (const err of error.failed_shards) { - scriptExceptionError = getScriptExceptionErrorOnShard(err.reason); - - if (scriptExceptionError !== null) { - break; - } - } - return scriptExceptionError; -}; - -export const parseEsError = ( - error?: Error, - isScriptError = false -): RuntimeFieldPainlessError | null => { - if (error === undefined) { - return null; - } - - const scriptError = isScriptError ? error : getScriptExceptionError(error.caused_by); - - if (scriptError === null) { - return null; + if (errorCode === 'CAST_ERROR') { + // Help the user as he might have forgot to change the runtime type + reason = `${reason} ${i18n.translate( + 'indexPatternFieldEditor.editor.form.scriptEditor.castErrorMessage', + { + defaultMessage: 'Verify that you have correctly set the runtime field type.', + } + )}`; } return { @@ -83,36 +39,7 @@ export const parseEsError = ( ), position: scriptError.position ?? null, scriptStack: scriptError.script_stack ?? [], - reason: scriptError.caused_by?.reason ?? null, + reason: reason ?? null, + code: errorCode, }; }; - -/** - * Handler to validate the painless script for syntax and semantic errors. - * This is a temporary solution. In a future work we will have a dedicate - * ES API to debug the script. - */ -export const getRuntimeFieldValidator = - (index: string, searchService: DataPublicPluginStart['search']) => - async (runtimeField: EsRuntimeField) => { - return await searchService - .search({ - params: { - index, - body: { - runtime_mappings: { - temp: runtimeField, - }, - size: 0, - query: { - match_none: {}, - }, - }, - }, - }) - .toPromise() - .then(() => null) - .catch((e) => { - return parseEsError(e.attributes); - }); - }; diff --git a/src/plugins/index_pattern_field_editor/public/lib/serialization.ts b/src/plugins/index_pattern_field_editor/public/lib/serialization.ts index 8a0a47e07c9c9..0f042cdac114f 100644 --- a/src/plugins/index_pattern_field_editor/public/lib/serialization.ts +++ b/src/plugins/index_pattern_field_editor/public/lib/serialization.ts @@ -5,9 +5,9 @@ * in compliance with, at your election, the Elastic License 2.0 or the Server * Side Public License, v 1. */ - +import { monaco } from '@kbn/monaco'; import { IndexPatternField, IndexPattern } from '../shared_imports'; -import type { Field } from '../types'; +import type { Field, RuntimeFieldPainlessError } from '../types'; export const deserializeField = ( indexPattern: IndexPattern, @@ -26,3 +26,20 @@ export const deserializeField = ( format: indexPattern.getFormatterForFieldNoDefault(field.name)?.toJSON(), }; }; + +export const painlessErrorToMonacoMarker = ( + { reason }: RuntimeFieldPainlessError, + startPosition: monaco.Position +): monaco.editor.IMarkerData | undefined => { + return { + startLineNumber: startPosition.lineNumber, + startColumn: startPosition.column, + endLineNumber: startPosition.lineNumber, + // Ideally we'd want the endColumn to be the end of the error but + // ES does not return that info. There is an issue to track the enhancement: + // https://github.com/elastic/elasticsearch/issues/78072 + endColumn: startPosition.column + 1, + message: reason, + severity: monaco.MarkerSeverity.Error, + }; +}; diff --git a/src/plugins/index_pattern_field_editor/public/open_editor.tsx b/src/plugins/index_pattern_field_editor/public/open_editor.tsx index 19b5d1fde8315..0109b8d95db52 100644 --- a/src/plugins/index_pattern_field_editor/public/open_editor.tsx +++ b/src/plugins/index_pattern_field_editor/public/open_editor.tsx @@ -150,6 +150,9 @@ export const getFieldEditorOpener = flyout.close(); } }, + maskProps: { + className: 'indexPatternFieldEditorMaskOverlay', + }, } ); diff --git a/src/plugins/index_pattern_field_editor/public/shared_imports.ts b/src/plugins/index_pattern_field_editor/public/shared_imports.ts index e2154800908cb..5b377bdd1d2b5 100644 --- a/src/plugins/index_pattern_field_editor/public/shared_imports.ts +++ b/src/plugins/index_pattern_field_editor/public/shared_imports.ts @@ -23,7 +23,9 @@ export type { FormHook, ValidationFunc, FieldConfig, + ValidationCancelablePromise, } from '../../es_ui_shared/static/forms/hook_form_lib'; + export { useForm, useFormData, @@ -31,6 +33,7 @@ export { useFormIsModified, Form, UseField, + useBehaviorSubject, } from '../../es_ui_shared/static/forms/hook_form_lib'; export { fieldValidators } from '../../es_ui_shared/static/forms/helpers'; diff --git a/src/plugins/index_pattern_field_editor/public/types.ts b/src/plugins/index_pattern_field_editor/public/types.ts index f7efc9d82fc48..9d62a5568584c 100644 --- a/src/plugins/index_pattern_field_editor/public/types.ts +++ b/src/plugins/index_pattern_field_editor/public/types.ts @@ -66,16 +66,24 @@ export interface EsRuntimeField { export type CloseEditor = () => void; -export type FieldPreviewContext = - | 'boolean_field' - | 'date_field' - | 'double_field' - | 'geo_point_field' - | 'ip_field' - | 'keyword_field' - | 'long_field'; +export type PainlessErrorCode = 'CAST_ERROR' | 'UNKNOWN'; -export interface FieldPreviewResponse { - values: unknown[]; - error?: Record; +export interface RuntimeFieldPainlessError { + message: string; + reason: string; + position: { + offset: number; + start: number; + end: number; + } | null; + scriptStack: string[]; + code: PainlessErrorCode; +} + +export interface MonacoEditorErrorMarker { + startLineNumber: number; + startColumn: number; + endLineNumber: number; + endColumn: number; + message: string; } diff --git a/src/plugins/index_pattern_field_editor/server/routes/field_preview.ts b/src/plugins/index_pattern_field_editor/server/routes/field_preview.ts index 9ffa5c88df8e8..e95c12469ffb9 100644 --- a/src/plugins/index_pattern_field_editor/server/routes/field_preview.ts +++ b/src/plugins/index_pattern_field_editor/server/routes/field_preview.ts @@ -58,6 +58,13 @@ export const registerFieldPreviewRoute = ({ router }: RouteDependencies): void = }; try { + // Ideally we want to use the Painless _execute API to get the runtime field preview. + // There is a current ES limitation that requires a user to have too many privileges + // to execute the script. (issue: https://github.com/elastic/elasticsearch/issues/48856) + // Until we find a way to execute a script without advanced privileges we are going to + // use the Search API to get the field value (and possible errors). + // Note: here is the PR were we changed from using Painless _execute to _search and should be + // reverted when the ES issue is fixed: https://github.com/elastic/kibana/pull/115070 const response = await client.asCurrentUser.search({ index: req.body.index, body, diff --git a/src/plugins/index_pattern_management/public/components/index_pattern_table/index_pattern_table.tsx b/src/plugins/index_pattern_management/public/components/index_pattern_table/index_pattern_table.tsx index 0ad71d9a23cc2..89230ae03a923 100644 --- a/src/plugins/index_pattern_management/public/components/index_pattern_table/index_pattern_table.tsx +++ b/src/plugins/index_pattern_management/public/components/index_pattern_table/index_pattern_table.tsx @@ -50,6 +50,13 @@ const title = i18n.translate('indexPatternManagement.dataViewTable.title', { defaultMessage: 'Data views', }); +const securityDataView = i18n.translate( + 'indexPatternManagement.indexPatternTable.badge.securityDataViewTitle', + { + defaultMessage: 'Security Data View', + } +); + interface Props extends RouteComponentProps { canSave: boolean; showCreateDialog?: boolean; @@ -116,6 +123,10 @@ export const IndexPatternTable = ({   + {index.id && index.id === 'security-solution' && ( + {securityDataView} + )} + {index.tags && index.tags.map(({ key: tagKey, name: tagName }) => ( {tagName} diff --git a/src/plugins/kibana_legacy/public/plugin.ts b/src/plugins/kibana_legacy/public/plugin.ts index ac78e8cac4f07..a154770bbfffd 100644 --- a/src/plugins/kibana_legacy/public/plugin.ts +++ b/src/plugins/kibana_legacy/public/plugin.ts @@ -6,16 +6,14 @@ * Side Public License, v 1. */ -import { CoreStart, CoreSetup } from 'kibana/public'; -import { injectHeaderStyle } from './utils/inject_header_style'; +import type { CoreSetup } from 'kibana/public'; export class KibanaLegacyPlugin { public setup(core: CoreSetup<{}, KibanaLegacyStart>) { return {}; } - public start({ uiSettings }: CoreStart) { - injectHeaderStyle(uiSettings); + public start() { return { /** * Loads the font-awesome icon font. Should be removed once the last consumer has migrated to EUI diff --git a/src/plugins/kibana_legacy/public/utils/inject_header_style.ts b/src/plugins/kibana_legacy/public/utils/inject_header_style.ts deleted file mode 100644 index 967aa2232838e..0000000000000 --- a/src/plugins/kibana_legacy/public/utils/inject_header_style.ts +++ /dev/null @@ -1,32 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import { IUiSettingsClient } from 'kibana/public'; - -export function buildCSS(maxHeight = 0, truncateGradientHeight = 15) { - return ` -.truncate-by-height { - max-height: ${maxHeight > 0 ? `${maxHeight}px !important` : 'none'}; - display: inline-block; -} -.truncate-by-height:before { - top: ${maxHeight > 0 ? maxHeight - truncateGradientHeight : truncateGradientHeight * -1}px; -} -`; -} - -export function injectHeaderStyle(uiSettings: IUiSettingsClient) { - const style = document.createElement('style'); - style.setAttribute('id', 'style-compile'); - document.getElementsByTagName('head')[0].appendChild(style); - - uiSettings.get$('truncate:maxHeight').subscribe((value: number) => { - // eslint-disable-next-line no-unsanitized/property - style.innerHTML = buildCSS(value); - }); -} diff --git a/src/plugins/kibana_overview/public/components/overview/overview.tsx b/src/plugins/kibana_overview/public/components/overview/overview.tsx index 6a0279bd12465..5108150f7ff8d 100644 --- a/src/plugins/kibana_overview/public/components/overview/overview.tsx +++ b/src/plugins/kibana_overview/public/components/overview/overview.tsx @@ -84,6 +84,9 @@ export const Overview: FC = ({ newsFetchResult, solutions, features }) => solution: i18n.translate('kibanaOverview.noDataConfig.solutionName', { defaultMessage: `Analytics`, }), + pageTitle: i18n.translate('kibanaOverview.noDataConfig.pageTitle', { + defaultMessage: `Welcome to Analytics!`, + }), logo: 'logoKibana', actions: { elasticAgent: { diff --git a/src/plugins/kibana_react/public/code_editor/code_editor_field.tsx b/src/plugins/kibana_react/public/code_editor/code_editor_field.tsx index 0e6ab21159f15..85263b7006c16 100644 --- a/src/plugins/kibana_react/public/code_editor/code_editor_field.tsx +++ b/src/plugins/kibana_react/public/code_editor/code_editor_field.tsx @@ -7,8 +7,10 @@ */ import React from 'react'; -import darkTheme from '@elastic/eui/dist/eui_theme_dark.json'; -import lightTheme from '@elastic/eui/dist/eui_theme_light.json'; +import { + euiLightVars as lightTheme, + euiDarkVars as darkTheme, +} from '@kbn/ui-shared-deps-src/theme'; import { EuiFormControlLayout } from '@elastic/eui'; import { CodeEditor, Props } from './code_editor'; diff --git a/src/plugins/kibana_react/public/code_editor/editor_theme.ts b/src/plugins/kibana_react/public/code_editor/editor_theme.ts index 0f362a28ea622..6c2727b123de8 100644 --- a/src/plugins/kibana_react/public/code_editor/editor_theme.ts +++ b/src/plugins/kibana_react/public/code_editor/editor_theme.ts @@ -8,8 +8,10 @@ import { monaco } from '@kbn/monaco'; -import darkTheme from '@elastic/eui/dist/eui_theme_dark.json'; -import lightTheme from '@elastic/eui/dist/eui_theme_light.json'; +import { + euiLightVars as lightTheme, + euiDarkVars as darkTheme, +} from '@kbn/ui-shared-deps-src/theme'; // NOTE: For talk around where this theme information will ultimately live, // please see this discuss issue: https://github.com/elastic/kibana/issues/43814 diff --git a/src/plugins/kibana_react/public/code_editor/languages/constants.ts b/src/plugins/kibana_react/public/code_editor/languages/constants.ts index af80e4ccc56e2..510cc91cf5e76 100644 --- a/src/plugins/kibana_react/public/code_editor/languages/constants.ts +++ b/src/plugins/kibana_react/public/code_editor/languages/constants.ts @@ -10,3 +10,4 @@ export { LANG as CssLang } from './css/constants'; export { LANG as MarkdownLang } from './markdown/constants'; export { LANG as YamlLang } from './yaml/constants'; export { LANG as HandlebarsLang } from './handlebars/constants'; +export { LANG as HJsonLang } from './hjson/constants'; diff --git a/src/plugins/kibana_react/public/code_editor/languages/hjson/constants.ts b/src/plugins/kibana_react/public/code_editor/languages/hjson/constants.ts new file mode 100644 index 0000000000000..61e851e0b6f58 --- /dev/null +++ b/src/plugins/kibana_react/public/code_editor/languages/hjson/constants.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 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 LANG = 'hjson'; diff --git a/src/plugins/kibana_react/public/code_editor/languages/hjson/index.ts b/src/plugins/kibana_react/public/code_editor/languages/hjson/index.ts new file mode 100644 index 0000000000000..ff3c08267da9b --- /dev/null +++ b/src/plugins/kibana_react/public/code_editor/languages/hjson/index.ts @@ -0,0 +1,13 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 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 { LangModuleType } from '@kbn/monaco'; +import { languageConfiguration, lexerRules } from './language'; +import { LANG } from './constants'; + +export const Lang: LangModuleType = { ID: LANG, languageConfiguration, lexerRules }; diff --git a/src/plugins/kibana_react/public/code_editor/languages/hjson/language.ts b/src/plugins/kibana_react/public/code_editor/languages/hjson/language.ts new file mode 100644 index 0000000000000..d93cdfe4c4a22 --- /dev/null +++ b/src/plugins/kibana_react/public/code_editor/languages/hjson/language.ts @@ -0,0 +1,90 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { monaco } from '@kbn/monaco'; + +export const languageConfiguration: monaco.languages.LanguageConfiguration = { + brackets: [ + ['{', '}'], + ['[', ']'], + ], + autoClosingPairs: [ + { open: '{', close: '}' }, + { open: '[', close: ']' }, + { open: '"', close: '"', notIn: ['string'] }, + ], + comments: { + lineComment: '//', + blockComment: ['/*', '*/'], + }, +}; + +export const lexerRules: monaco.languages.IMonarchLanguage = { + defaultToken: '', + tokenPostfix: '', + escapes: /\\(?:[abfnrtv\\"']|x[0-9A-Fa-f]{1,4}|u[0-9A-Fa-f]{4}|U[0-9A-Fa-f]{8})/, + digits: /-?(?:0|[1-9]\d*)(?:(?:\.\d+)?(?:[eE][+-]?\d+)?)?/, + symbols: /[,:]+/, + tokenizer: { + root: [ + [/(@digits)n?/, 'number'], + [/(@symbols)n?/, 'delimiter'], + + { include: '@keyword' }, + { include: '@url' }, + { include: '@whitespace' }, + { include: '@brackets' }, + { include: '@keyName' }, + { include: '@string' }, + ], + + keyword: [[/(?:true|false|null)\b/, 'keyword']], + + url: [ + [ + /(https?:\/\/(?:www\.|(?!www))[a-zA-Z0-9][a-zA-Z0-9-]+[a-zA-Z0-9]\.[^\s]{2,}|www\.[a-zA-Z0-9][a-zA-Z0-9-]+[a-zA-Z0-9]\.[^\s]{2,}|https?:\/\/(?:www\.|(?!www))[a-zA-Z0-9]+\.[^\s]{2,}|www\.[a-zA-Z0-9]+\.[^\s]{2,})/, + 'string', + ], + ], + + keyName: [[/(?:[^,\{\[\}\]\s]+|"(?:[^"\\]|\\.)*")\s*(?=:)/, 'variable']], + + brackets: [[/{/, '@push'], [/}/, '@pop'], [/[[(]/], [/[\])]/]], + + whitespace: [ + [/[ \t\r\n]+/, ''], + [/\/\*/, 'comment', '@comment'], + [/\/\/.*$/, 'comment'], + ], + + comment: [ + [/[^\/*]+/, 'comment'], + [/\*\//, 'comment', '@pop'], + [/[\/*]/, 'comment'], + ], + + string: [ + [/(?:[^,\{\[\}\]\s]+|"(?:[^"\\]|\\.)*")\s*/, 'string'], + [/"""/, 'string', '@stringLiteral'], + [/"/, 'string', '@stringDouble'], + ], + + stringDouble: [ + [/[^\\"]+/, 'string'], + [/@escapes/, 'string.escape'], + [/\\./, 'string.escape.invalid'], + [/"/, 'string', '@pop'], + ], + + stringLiteral: [ + [/"""/, 'string', '@pop'], + [/\\""""/, 'string', '@pop'], + [/./, 'string'], + ], + }, +} as monaco.languages.IMonarchLanguage; diff --git a/src/plugins/kibana_react/public/code_editor/languages/index.ts b/src/plugins/kibana_react/public/code_editor/languages/index.ts index b797ea44d1f91..f862997fdc2e3 100644 --- a/src/plugins/kibana_react/public/code_editor/languages/index.ts +++ b/src/plugins/kibana_react/public/code_editor/languages/index.ts @@ -10,5 +10,6 @@ import { Lang as CssLang } from './css'; import { Lang as HandlebarsLang } from './handlebars'; import { Lang as MarkdownLang } from './markdown'; import { Lang as YamlLang } from './yaml'; +import { Lang as HJson } from './hjson'; -export { CssLang, HandlebarsLang, MarkdownLang, YamlLang }; +export { CssLang, HandlebarsLang, MarkdownLang, YamlLang, HJson }; diff --git a/src/plugins/kibana_react/public/code_editor/register_languages.ts b/src/plugins/kibana_react/public/code_editor/register_languages.ts index a32318a7e4b20..62eccdabb5d98 100644 --- a/src/plugins/kibana_react/public/code_editor/register_languages.ts +++ b/src/plugins/kibana_react/public/code_editor/register_languages.ts @@ -6,9 +6,10 @@ * Side Public License, v 1. */ import { registerLanguage } from '@kbn/monaco'; -import { CssLang, HandlebarsLang, MarkdownLang, YamlLang } from './languages'; +import { CssLang, HandlebarsLang, MarkdownLang, YamlLang, HJson } from './languages'; registerLanguage(CssLang); registerLanguage(HandlebarsLang); registerLanguage(MarkdownLang); registerLanguage(YamlLang); +registerLanguage(HJson); diff --git a/src/plugins/kibana_react/public/field_button/index.ts b/src/plugins/kibana_react/public/field_button/index.ts deleted file mode 100644 index 298160652db49..0000000000000 --- a/src/plugins/kibana_react/public/field_button/index.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 './field_button'; diff --git a/src/plugins/kibana_react/public/field_icon/index.ts b/src/plugins/kibana_react/public/field_icon/index.ts deleted file mode 100644 index 1ef8f3d75ed06..0000000000000 --- a/src/plugins/kibana_react/public/field_icon/index.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 './field_icon'; diff --git a/src/plugins/kibana_react/public/index.ts b/src/plugins/kibana_react/public/index.ts index 03e2bb5f9c272..46f8599b996a2 100644 --- a/src/plugins/kibana_react/public/index.ts +++ b/src/plugins/kibana_react/public/index.ts @@ -16,8 +16,6 @@ export * from './context'; export * from './overview_page'; export * from './overlays'; export * from './ui_settings'; -export * from './field_icon'; -export * from './field_button'; export * from './table_list_view'; export * from './toolbar_button'; export * from './split_panel'; diff --git a/src/plugins/kibana_usage_collection/server/collectors/application_usage/schema.ts b/src/plugins/kibana_usage_collection/server/collectors/application_usage/schema.ts index 7c112083875d1..adfe8da335a14 100644 --- a/src/plugins/kibana_usage_collection/server/collectors/application_usage/schema.ts +++ b/src/plugins/kibana_usage_collection/server/collectors/application_usage/schema.ts @@ -150,6 +150,7 @@ export const applicationUsageSchema = { 'observability-overview': commonSchema, osquery: commonSchema, security_account: commonSchema, + reportingRedirect: commonSchema, security_access_agreement: commonSchema, security_capture_url: commonSchema, // It's a forward app so we'll likely never report it security_logged_out: commonSchema, diff --git a/src/plugins/kibana_usage_collection/server/collectors/event_loop_delays/rollups/integration_tests/daily_rollups.test.ts b/src/plugins/kibana_usage_collection/server/collectors/event_loop_delays/rollups/integration_tests/daily_rollups.test.ts index dafd4414db192..d4f069560443b 100644 --- a/src/plugins/kibana_usage_collection/server/collectors/event_loop_delays/rollups/integration_tests/daily_rollups.test.ts +++ b/src/plugins/kibana_usage_collection/server/collectors/event_loop_delays/rollups/integration_tests/daily_rollups.test.ts @@ -56,7 +56,8 @@ const outdatedRawEventLoopDelaysDaily = [ createRawObject(moment().subtract(7, 'days')), ]; -describe('daily rollups integration test', () => { +// FLAKY https://github.com/elastic/kibana/issues/111821 +describe.skip('daily rollups integration test', () => { let esServer: TestElasticsearchUtils; let root: TestKibanaUtils['root']; let internalRepository: ISavedObjectsRepository; diff --git a/src/plugins/kibana_usage_collection/server/collectors/management/schema.ts b/src/plugins/kibana_usage_collection/server/collectors/management/schema.ts index d8d0215fd751f..356aaf60b423c 100644 --- a/src/plugins/kibana_usage_collection/server/collectors/management/schema.ts +++ b/src/plugins/kibana_usage_collection/server/collectors/management/schema.ts @@ -420,6 +420,10 @@ export const stackManagementSchema: MakeSchemaFrom = { type: 'integer', _meta: { description: 'Non-default value of setting.' }, }, + 'observability:enableComparisonByDefault': { + type: 'boolean', + _meta: { description: 'Non-default value of setting.' }, + }, 'banners:placement': { type: 'keyword', _meta: { description: 'Non-default value of setting.' }, @@ -440,6 +444,10 @@ export const stackManagementSchema: MakeSchemaFrom = { type: 'boolean', _meta: { description: 'Non-default value of setting.' }, }, + 'labs:canvas:byValueEmbeddable': { + type: 'boolean', + _meta: { description: 'Non-default value of setting.' }, + }, 'labs:canvas:useDataService': { 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 9dcd2038edb9d..69287d37dfa28 100644 --- a/src/plugins/kibana_usage_collection/server/collectors/management/types.ts +++ b/src/plugins/kibana_usage_collection/server/collectors/management/types.ts @@ -37,6 +37,7 @@ export interface UsageStats { 'securitySolution:rulesTableRefresh': string; 'observability:enableInspectEsQueries': boolean; 'observability:maxSuggestions': number; + 'observability:enableComparisonByDefault': boolean; 'visualize:enableLabs': boolean; 'visualization:heatmap:maxBuckets': number; 'visualization:colorMapping': string; @@ -121,6 +122,7 @@ export interface UsageStats { 'banners:textColor': string; 'banners:backgroundColor': string; 'labs:canvas:enable_ui': boolean; + 'labs:canvas:byValueEmbeddable': boolean; 'labs:canvas:useDataService': boolean; 'labs:presentation:timeToPresent': boolean; 'labs:dashboard:enable_ui': boolean; diff --git a/src/plugins/presentation_util/common/labs.ts b/src/plugins/presentation_util/common/labs.ts index cb976e73b5edf..8eefbd6981280 100644 --- a/src/plugins/presentation_util/common/labs.ts +++ b/src/plugins/presentation_util/common/labs.ts @@ -11,7 +11,9 @@ import { i18n } from '@kbn/i18n'; export const LABS_PROJECT_PREFIX = 'labs:'; export const DEFER_BELOW_FOLD = `${LABS_PROJECT_PREFIX}dashboard:deferBelowFold` as const; export const DASHBOARD_CONTROLS = `${LABS_PROJECT_PREFIX}dashboard:dashboardControls` as const; -export const projectIDs = [DEFER_BELOW_FOLD, DASHBOARD_CONTROLS] as const; +export const BY_VALUE_EMBEDDABLE = `${LABS_PROJECT_PREFIX}canvas:byValueEmbeddable` as const; + +export const projectIDs = [DEFER_BELOW_FOLD, DASHBOARD_CONTROLS, BY_VALUE_EMBEDDABLE] as const; export const environmentNames = ['kibana', 'browser', 'session'] as const; export const solutionNames = ['canvas', 'dashboard', 'presentation'] as const; @@ -48,6 +50,19 @@ export const projects: { [ID in ProjectID]: ProjectConfig & { id: ID } } = { }), solutions: ['dashboard'], }, + [BY_VALUE_EMBEDDABLE]: { + id: BY_VALUE_EMBEDDABLE, + isActive: true, + isDisplayed: true, + environments: ['kibana', 'browser', 'session'], + name: i18n.translate('presentationUtil.labs.enableByValueEmbeddableName', { + defaultMessage: 'By-Value Embeddables', + }), + description: i18n.translate('presentationUtil.labs.enableByValueEmbeddableDescription', { + defaultMessage: 'Enables support for by-value embeddables in Canvas', + }), + solutions: ['canvas'], + }, }; export type ProjectID = typeof projectIDs[number]; diff --git a/src/plugins/presentation_util/public/components/field_picker/field_picker.tsx b/src/plugins/presentation_util/public/components/field_picker/field_picker.tsx index c9be9993c3ec1..b0203e3df1d2b 100644 --- a/src/plugins/presentation_util/public/components/field_picker/field_picker.tsx +++ b/src/plugins/presentation_util/public/components/field_picker/field_picker.tsx @@ -11,10 +11,11 @@ import { sortBy, uniq } from 'lodash'; import React, { useState } from 'react'; import { FormattedMessage } from '@kbn/i18n/react'; import { EuiFlexGroup, EuiFlexItem, EuiPanel, EuiText } from '@elastic/eui'; +import { FieldIcon } from '@kbn/react-field/field_icon'; +import { FieldButton } from '@kbn/react-field/field_button'; import { FieldSearch } from './field_search'; import { DataView, DataViewField } from '../../../../data_views/common'; -import { FieldIcon, FieldButton } from '../../../../kibana_react/public'; import './field_picker.scss'; diff --git a/src/plugins/presentation_util/public/components/field_picker/field_search.tsx b/src/plugins/presentation_util/public/components/field_picker/field_search.tsx index f3988167c1317..c0c73ebafa049 100644 --- a/src/plugins/presentation_util/public/components/field_picker/field_search.tsx +++ b/src/plugins/presentation_util/public/components/field_picker/field_search.tsx @@ -21,8 +21,8 @@ import { EuiSpacer, EuiPopoverTitle, } from '@elastic/eui'; +import { FieldIcon } from '@kbn/react-field/field_icon'; import { FormattedMessage } from '@kbn/i18n/react'; -import { FieldIcon } from '../../../../kibana_react/public'; export interface Props { onSearchChange: (value: string) => void; diff --git a/src/plugins/presentation_util/public/components/solution_toolbar/items/quick_group.scss b/src/plugins/presentation_util/public/components/solution_toolbar/items/quick_group.scss index 434f06b69a684..9f70ae353405b 100644 --- a/src/plugins/presentation_util/public/components/solution_toolbar/items/quick_group.scss +++ b/src/plugins/presentation_util/public/components/solution_toolbar/items/quick_group.scss @@ -1,9 +1,25 @@ .quickButtonGroup { - .quickButtonGroup__button { - background-color: $euiColorEmptyShade; - // sass-lint:disable-block no-important - border-width: $euiBorderWidthThin !important; - border-style: solid !important; - border-color: $euiBorderColor !important; + .euiButtonGroup__buttons { + border-radius: $euiBorderRadius; + + .quickButtonGroup__button { + background-color: $euiColorEmptyShade; + // sass-lint:disable-block no-important + border-width: $euiBorderWidthThin !important; + border-style: solid !important; + border-color: $euiBorderColor !important; + } + + .quickButtonGroup__button:first-of-type { + // sass-lint:disable-block no-important + border-top-left-radius: $euiBorderRadius !important; + border-bottom-left-radius: $euiBorderRadius !important; + } + + .quickButtonGroup__button:last-of-type { + // sass-lint:disable-block no-important + border-top-right-radius: $euiBorderRadius !important; + border-bottom-right-radius: $euiBorderRadius !important; + } } } diff --git a/src/plugins/saved_objects/public/saved_object/saved_object.test.ts b/src/plugins/saved_objects/public/saved_object/saved_object.test.ts index bd8d69d6b693e..01fc75df459bc 100644 --- a/src/plugins/saved_objects/public/saved_object/saved_object.test.ts +++ b/src/plugins/saved_objects/public/saved_object/saved_object.test.ts @@ -6,7 +6,6 @@ * Side Public License, v 1. */ -import Bluebird from 'bluebird'; import { createSavedObjectClass } from './saved_object'; import { SavedObject, @@ -55,16 +54,16 @@ describe('Saved Object', () => { */ function stubESResponse(mockDocResponse: SimpleSavedObject) { // Stub out search for duplicate title: - savedObjectsClientStub.get = jest.fn().mockReturnValue(Bluebird.resolve(mockDocResponse)); - savedObjectsClientStub.update = jest.fn().mockReturnValue(Bluebird.resolve(mockDocResponse)); + savedObjectsClientStub.get = jest.fn().mockReturnValue(Promise.resolve(mockDocResponse)); + savedObjectsClientStub.update = jest.fn().mockReturnValue(Promise.resolve(mockDocResponse)); savedObjectsClientStub.find = jest .fn() - .mockReturnValue(Bluebird.resolve({ savedObjects: [], total: 0 })); + .mockReturnValue(Promise.resolve({ savedObjects: [], total: 0 })); savedObjectsClientStub.bulkGet = jest .fn() - .mockReturnValue(Bluebird.resolve({ savedObjects: [mockDocResponse] })); + .mockReturnValue(Promise.resolve({ savedObjects: [mockDocResponse] })); } function stubSavedObjectsClientCreate( @@ -73,7 +72,7 @@ describe('Saved Object', () => { ) { savedObjectsClientStub.create = jest .fn() - .mockReturnValue(resolve ? Bluebird.resolve(resp) : Bluebird.reject(resp)); + .mockReturnValue(resolve ? Promise.resolve(resp) : Promise.reject(resp)); } /** @@ -262,7 +261,7 @@ describe('Saved Object', () => { return createInitializedSavedObject({ type: 'dashboard', id: myId }).then((savedObject) => { savedObjectsClientStub.create = jest.fn().mockImplementation(() => { expect(savedObject.id).toBe(myId); - return Bluebird.resolve({ id: myId }); + return Promise.resolve({ id: myId }); }); savedObject.copyOnSave = false; @@ -296,7 +295,7 @@ describe('Saved Object', () => { return createInitializedSavedObject({ type: 'dashboard', id }).then((savedObject) => { savedObjectsClientStub.create = jest.fn().mockImplementation(() => { expect(savedObject.isSaving).toBe(true); - return Bluebird.resolve({ + return Promise.resolve({ type: 'dashboard', id, _version: 'foo', @@ -315,7 +314,7 @@ describe('Saved Object', () => { return createInitializedSavedObject({ type: 'dashboard' }).then((savedObject) => { savedObjectsClientStub.create = jest.fn().mockImplementation(() => { expect(savedObject.isSaving).toBe(true); - return Bluebird.reject(''); + return Promise.reject(''); }); expect(savedObject.isSaving).toBe(false); @@ -745,7 +744,7 @@ describe('Saved Object', () => { }, }); savedObject.searchSource!.setField('index', indexPattern); - return Bluebird.resolve(indexPattern); + return Promise.resolve(indexPattern); }); expect(!!savedObject.searchSource!.getField('index')).toBe(false); diff --git a/src/plugins/telemetry/public/services/telemetry_sender.test.ts b/src/plugins/telemetry/public/services/telemetry_sender.test.ts index 10da46fe2761d..d4678ce0ea23a 100644 --- a/src/plugins/telemetry/public/services/telemetry_sender.test.ts +++ b/src/plugins/telemetry/public/services/telemetry_sender.test.ts @@ -127,67 +127,111 @@ describe('TelemetrySender', () => { expect(telemetryService.getIsOptedIn).toBeCalledTimes(0); expect(shouldSendReport).toBe(false); }); + }); + describe('sendIfDue', () => { + let originalFetch: typeof window['fetch']; + let mockFetch: jest.Mock; - describe('sendIfDue', () => { - let originalFetch: typeof window['fetch']; - let mockFetch: jest.Mock; + beforeAll(() => { + originalFetch = window.fetch; + }); - beforeAll(() => { - originalFetch = window.fetch; - }); + beforeEach(() => (window.fetch = mockFetch = jest.fn())); + afterAll(() => (window.fetch = originalFetch)); - beforeEach(() => (window.fetch = mockFetch = jest.fn())); - afterAll(() => (window.fetch = originalFetch)); + it('does not send if shouldSendReport returns false', async () => { + const telemetryService = mockTelemetryService(); + const telemetrySender = new TelemetrySender(telemetryService); + telemetrySender['shouldSendReport'] = jest.fn().mockReturnValue(false); + telemetrySender['retryCount'] = 0; + await telemetrySender['sendIfDue'](); - it('does not send if already sending', async () => { - const telemetryService = mockTelemetryService(); - const telemetrySender = new TelemetrySender(telemetryService); - telemetrySender['shouldSendReport'] = jest.fn(); - telemetrySender['isSending'] = true; - await telemetrySender['sendIfDue'](); + expect(telemetrySender['shouldSendReport']).toBeCalledTimes(1); + expect(mockFetch).toBeCalledTimes(0); + }); - expect(telemetrySender['shouldSendReport']).toBeCalledTimes(0); - expect(mockFetch).toBeCalledTimes(0); - }); + it('does not send if we are in screenshot mode', async () => { + const telemetryService = mockTelemetryService({ isScreenshotMode: true }); + const telemetrySender = new TelemetrySender(telemetryService); + await telemetrySender['sendIfDue'](); - it('does not send if shouldSendReport returns false', async () => { - const telemetryService = mockTelemetryService(); - const telemetrySender = new TelemetrySender(telemetryService); - telemetrySender['shouldSendReport'] = jest.fn().mockReturnValue(false); - telemetrySender['isSending'] = false; - await telemetrySender['sendIfDue'](); + expect(mockFetch).toBeCalledTimes(0); + }); - expect(telemetrySender['shouldSendReport']).toBeCalledTimes(1); - expect(mockFetch).toBeCalledTimes(0); - }); + it('updates last lastReported and calls saveToBrowser', async () => { + const lastReported = Date.now() - (REPORT_INTERVAL_MS + 1000); + + const telemetryService = mockTelemetryService(); + const telemetrySender = new TelemetrySender(telemetryService); + telemetrySender['shouldSendReport'] = jest.fn().mockReturnValue(true); + telemetrySender['sendUsageData'] = jest.fn().mockReturnValue(true); + telemetrySender['saveToBrowser'] = jest.fn(); + telemetrySender['lastReported'] = `${lastReported}`; - it('does not send if we are in screenshot mode', async () => { - const telemetryService = mockTelemetryService({ isScreenshotMode: true }); - const telemetrySender = new TelemetrySender(telemetryService); - telemetrySender['isSending'] = false; - await telemetrySender['sendIfDue'](); + await telemetrySender['sendIfDue'](); - expect(mockFetch).toBeCalledTimes(0); - }); + expect(telemetrySender['lastReported']).not.toBe(lastReported); + expect(telemetrySender['saveToBrowser']).toBeCalledTimes(1); + expect(telemetrySender['retryCount']).toEqual(0); + expect(telemetrySender['sendUsageData']).toHaveBeenCalledTimes(1); + }); + + it('resets the retry counter when report is due', async () => { + const telemetryService = mockTelemetryService(); + const telemetrySender = new TelemetrySender(telemetryService); + telemetrySender['shouldSendReport'] = jest.fn().mockReturnValue(true); + telemetrySender['sendUsageData'] = jest.fn(); + telemetrySender['saveToBrowser'] = jest.fn(); + telemetrySender['retryCount'] = 9; + + await telemetrySender['sendIfDue'](); + expect(telemetrySender['retryCount']).toEqual(0); + expect(telemetrySender['sendUsageData']).toHaveBeenCalledTimes(1); + }); + }); + + describe('sendUsageData', () => { + let originalFetch: typeof window['fetch']; + let mockFetch: jest.Mock; + let consoleWarnMock: jest.SpyInstance; + + beforeAll(() => { + originalFetch = window.fetch; + }); - it('sends report if due', async () => { - const mockClusterUuid = 'mk_uuid'; - const mockTelemetryUrl = 'telemetry_cluster_url'; - const mockTelemetryPayload = [ - { clusterUuid: mockClusterUuid, stats: 'hashed_cluster_usage_data1' }, - ]; - - const telemetryService = mockTelemetryService(); - const telemetrySender = new TelemetrySender(telemetryService); - telemetryService.getTelemetryUrl = jest.fn().mockReturnValue(mockTelemetryUrl); - telemetryService.fetchTelemetry = jest.fn().mockReturnValue(mockTelemetryPayload); - telemetrySender['shouldSendReport'] = jest.fn().mockReturnValue(true); - telemetrySender['isSending'] = false; - await telemetrySender['sendIfDue'](); - - expect(telemetryService.fetchTelemetry).toBeCalledTimes(1); - expect(mockFetch).toBeCalledTimes(1); - expect(mockFetch.mock.calls[0]).toMatchInlineSnapshot(` + beforeEach(() => { + window.fetch = mockFetch = jest.fn(); + jest.useFakeTimers(); + consoleWarnMock = jest.spyOn(global.console, 'warn').mockImplementation(() => {}); + }); + + afterEach(() => { + jest.resetAllMocks(); + }); + + afterAll(() => { + window.fetch = originalFetch; + jest.useRealTimers(); + }); + + it('sends the report', async () => { + const mockClusterUuid = 'mk_uuid'; + const mockTelemetryUrl = 'telemetry_cluster_url'; + const mockTelemetryPayload = [ + { clusterUuid: mockClusterUuid, stats: 'hashed_cluster_usage_data1' }, + ]; + + const telemetryService = mockTelemetryService(); + const telemetrySender = new TelemetrySender(telemetryService); + telemetryService.getTelemetryUrl = jest.fn().mockReturnValue(mockTelemetryUrl); + telemetryService.fetchTelemetry = jest.fn().mockReturnValue(mockTelemetryPayload); + telemetrySender['shouldSendReport'] = jest.fn().mockReturnValue(true); + + await telemetrySender['sendUsageData'](); + + expect(telemetryService.fetchTelemetry).toBeCalledTimes(1); + expect(mockFetch).toBeCalledTimes(1); + expect(mockFetch.mock.calls[0]).toMatchInlineSnapshot(` Array [ "telemetry_cluster_url", Object { @@ -202,73 +246,113 @@ describe('TelemetrySender', () => { }, ] `); - }); + }); - it('sends report separately for every cluster', async () => { - const mockTelemetryUrl = 'telemetry_cluster_url'; - const mockTelemetryPayload = ['hashed_cluster_usage_data1', 'hashed_cluster_usage_data2']; + it('sends report separately for every cluster', async () => { + const mockTelemetryUrl = 'telemetry_cluster_url'; + const mockTelemetryPayload = ['hashed_cluster_usage_data1', 'hashed_cluster_usage_data2']; - const telemetryService = mockTelemetryService(); - const telemetrySender = new TelemetrySender(telemetryService); - telemetryService.getTelemetryUrl = jest.fn().mockReturnValue(mockTelemetryUrl); - telemetryService.fetchTelemetry = jest.fn().mockReturnValue(mockTelemetryPayload); - telemetrySender['shouldSendReport'] = jest.fn().mockReturnValue(true); - telemetrySender['isSending'] = false; - await telemetrySender['sendIfDue'](); + const telemetryService = mockTelemetryService(); + const telemetrySender = new TelemetrySender(telemetryService); + telemetryService.getTelemetryUrl = jest.fn().mockReturnValue(mockTelemetryUrl); + telemetryService.fetchTelemetry = jest.fn().mockReturnValue(mockTelemetryPayload); + telemetrySender['shouldSendReport'] = jest.fn().mockReturnValue(true); + await telemetrySender['sendIfDue'](); - expect(telemetryService.fetchTelemetry).toBeCalledTimes(1); - expect(mockFetch).toBeCalledTimes(2); - }); + expect(telemetryService.fetchTelemetry).toBeCalledTimes(1); + expect(mockFetch).toBeCalledTimes(2); + }); - it('updates last lastReported and calls saveToBrowser', async () => { - const mockTelemetryUrl = 'telemetry_cluster_url'; - const mockTelemetryPayload = ['hashed_cluster_usage_data1']; + it('does not increase the retry counter on successful send', async () => { + const mockTelemetryUrl = 'telemetry_cluster_url'; + const mockTelemetryPayload = ['hashed_cluster_usage_data1']; - const telemetryService = mockTelemetryService(); - const telemetrySender = new TelemetrySender(telemetryService); - telemetryService.getTelemetryUrl = jest.fn().mockReturnValue(mockTelemetryUrl); - telemetryService.fetchTelemetry = jest.fn().mockReturnValue(mockTelemetryPayload); - telemetrySender['shouldSendReport'] = jest.fn().mockReturnValue(true); - telemetrySender['saveToBrowser'] = jest.fn(); + const telemetryService = mockTelemetryService(); + const telemetrySender = new TelemetrySender(telemetryService); + telemetryService.getTelemetryUrl = jest.fn().mockReturnValue(mockTelemetryUrl); + telemetryService.fetchTelemetry = jest.fn().mockReturnValue(mockTelemetryPayload); + telemetrySender['shouldSendReport'] = jest.fn().mockReturnValue(true); + telemetrySender['saveToBrowser'] = jest.fn(); - await telemetrySender['sendIfDue'](); + await telemetrySender['sendUsageData'](); + + expect(mockFetch).toBeCalledTimes(1); + expect(telemetrySender['retryCount']).toBe(0); + }); - expect(mockFetch).toBeCalledTimes(1); - expect(telemetrySender['lastReported']).toBeDefined(); - expect(telemetrySender['saveToBrowser']).toBeCalledTimes(1); - expect(telemetrySender['isSending']).toBe(false); + it('catches fetchTelemetry errors and retries again', async () => { + const telemetryService = mockTelemetryService(); + const telemetrySender = new TelemetrySender(telemetryService); + telemetryService.getTelemetryUrl = jest.fn(); + telemetryService.fetchTelemetry = jest.fn().mockImplementation(() => { + throw Error('Error fetching usage'); }); + await telemetrySender['sendUsageData'](); + expect(telemetryService.fetchTelemetry).toBeCalledTimes(1); + expect(telemetrySender['retryCount']).toBe(1); + expect(setTimeout).toBeCalledTimes(1); + expect(setTimeout).toBeCalledWith(telemetrySender['sendUsageData'], 120000); + expect(consoleWarnMock).not.toBeCalled(); // console.warn is only triggered when the retryCount exceeds the allowed number + }); - it('catches fetchTelemetry errors and sets isSending to false', async () => { - const telemetryService = mockTelemetryService(); - const telemetrySender = new TelemetrySender(telemetryService); - telemetryService.getTelemetryUrl = jest.fn(); - telemetryService.fetchTelemetry = jest.fn().mockImplementation(() => { - throw Error('Error fetching usage'); - }); - await telemetrySender['sendIfDue'](); - expect(telemetryService.fetchTelemetry).toBeCalledTimes(1); - expect(telemetrySender['lastReported']).toBeUndefined(); - expect(telemetrySender['isSending']).toBe(false); + it('catches fetch errors and sets a new timeout if fetch fails more than once', async () => { + const mockTelemetryPayload = ['hashed_cluster_usage_data1', 'hashed_cluster_usage_data2']; + const telemetryService = mockTelemetryService(); + const telemetrySender = new TelemetrySender(telemetryService); + telemetryService.getTelemetryUrl = jest.fn(); + telemetryService.fetchTelemetry = jest.fn().mockReturnValue(mockTelemetryPayload); + mockFetch.mockImplementation(() => { + throw Error('Error sending usage'); }); + telemetrySender['retryCount'] = 3; + await telemetrySender['sendUsageData'](); + + expect(telemetryService.fetchTelemetry).toBeCalledTimes(1); + expect(mockFetch).toBeCalledTimes(2); + expect(telemetrySender['retryCount']).toBe(4); + expect(setTimeout).toBeCalledWith(telemetrySender['sendUsageData'], 960000); + + await telemetrySender['sendUsageData'](); + expect(telemetrySender['retryCount']).toBe(5); + expect(setTimeout).toBeCalledWith(telemetrySender['sendUsageData'], 1920000); + expect(consoleWarnMock).not.toBeCalled(); // console.warn is only triggered when the retryCount exceeds the allowed number + }); - it('catches fetch errors and sets isSending to false', async () => { - const mockTelemetryPayload = ['hashed_cluster_usage_data1', 'hashed_cluster_usage_data2']; - const telemetryService = mockTelemetryService(); - const telemetrySender = new TelemetrySender(telemetryService); - telemetryService.getTelemetryUrl = jest.fn(); - telemetryService.fetchTelemetry = jest.fn().mockReturnValue(mockTelemetryPayload); - mockFetch.mockImplementation(() => { - throw Error('Error sending usage'); - }); - await telemetrySender['sendIfDue'](); - expect(telemetryService.fetchTelemetry).toBeCalledTimes(1); - expect(mockFetch).toBeCalledTimes(2); - expect(telemetrySender['lastReported']).toBeUndefined(); - expect(telemetrySender['isSending']).toBe(false); + it('stops trying to resend the data after 20 retries', async () => { + const telemetryService = mockTelemetryService(); + const telemetrySender = new TelemetrySender(telemetryService); + telemetryService.getTelemetryUrl = jest.fn(); + telemetryService.fetchTelemetry = jest.fn().mockImplementation(() => { + throw Error('Error fetching usage'); }); + telemetrySender['retryCount'] = 21; + await telemetrySender['sendUsageData'](); + expect(setTimeout).not.toBeCalled(); + expect(consoleWarnMock.mock.calls[0][0]).toBe( + 'TelemetrySender.sendUsageData exceeds number of retry attempts with Error fetching usage' + ); + }); + }); + + describe('getRetryDelay', () => { + beforeEach(() => jest.useFakeTimers()); + afterAll(() => jest.useRealTimers()); + + it('sets a minimum retry delay of 60 seconds', () => { + expect(TelemetrySender.getRetryDelay(0)).toBe(60000); + }); + + it('changes the retry delay depending on the retry count', () => { + expect(TelemetrySender.getRetryDelay(3)).toBe(480000); + expect(TelemetrySender.getRetryDelay(5)).toBe(1920000); + }); + + it('sets a maximum retry delay of 64 min', () => { + expect(TelemetrySender.getRetryDelay(8)).toBe(3840000); + expect(TelemetrySender.getRetryDelay(10)).toBe(3840000); }); }); + describe('startChecking', () => { beforeEach(() => jest.useFakeTimers()); afterAll(() => jest.useRealTimers()); diff --git a/src/plugins/telemetry/public/services/telemetry_sender.ts b/src/plugins/telemetry/public/services/telemetry_sender.ts index 87287a420e725..fb87b0b23ad56 100644 --- a/src/plugins/telemetry/public/services/telemetry_sender.ts +++ b/src/plugins/telemetry/public/services/telemetry_sender.ts @@ -17,10 +17,14 @@ import type { EncryptedTelemetryPayload } from '../../common/types'; export class TelemetrySender { private readonly telemetryService: TelemetryService; - private isSending: boolean = false; private lastReported?: string; private readonly storage: Storage; - private intervalId?: number; + private intervalId: number = 0; // setInterval returns a positive integer, 0 means no interval is set + private retryCount: number = 0; + + static getRetryDelay(retryCount: number) { + return 60 * (1000 * Math.min(Math.pow(2, retryCount), 64)); // 120s, 240s, 480s, 960s, 1920s, 3840s, 3840s, 3840s + } constructor(telemetryService: TelemetryService) { this.telemetryService = telemetryService; @@ -54,12 +58,17 @@ export class TelemetrySender { }; private sendIfDue = async (): Promise => { - if (this.isSending || !this.shouldSendReport()) { + if (!this.shouldSendReport()) { return; } + // optimistically update the report date and reset the retry counter for a new time report interval window + this.lastReported = `${Date.now()}`; + this.saveToBrowser(); + this.retryCount = 0; + await this.sendUsageData(); + }; - // mark that we are working so future requests are ignored until we're done - this.isSending = true; + private sendUsageData = async (): Promise => { try { const telemetryUrl = this.telemetryService.getTelemetryUrl(); const telemetryPayload: EncryptedTelemetryPayload = @@ -80,17 +89,23 @@ export class TelemetrySender { }) ) ); - this.lastReported = `${Date.now()}`; - this.saveToBrowser(); } catch (err) { - // ignore err - } finally { - this.isSending = false; + // ignore err and try again but after a longer wait period. + this.retryCount = this.retryCount + 1; + if (this.retryCount < 20) { + // exponentially backoff the time between subsequent retries to up to 19 attempts, after which we give up until the next report is due + window.setTimeout(this.sendUsageData, TelemetrySender.getRetryDelay(this.retryCount)); + } else { + /* eslint no-console: ["error", { allow: ["warn"] }] */ + console.warn( + `TelemetrySender.sendUsageData exceeds number of retry attempts with ${err.message}` + ); + } } }; public startChecking = () => { - if (typeof this.intervalId === 'undefined') { + if (this.intervalId === 0) { this.intervalId = window.setInterval(this.sendIfDue, 60000); } }; diff --git a/src/plugins/telemetry/schema/oss_plugins.json b/src/plugins/telemetry/schema/oss_plugins.json index 251fed955788e..60c5bbd4346ec 100644 --- a/src/plugins/telemetry/schema/oss_plugins.json +++ b/src/plugins/telemetry/schema/oss_plugins.json @@ -4232,6 +4232,137 @@ } } }, + "reportingRedirect": { + "properties": { + "appId": { + "type": "keyword", + "_meta": { + "description": "The application being tracked" + } + }, + "viewId": { + "type": "keyword", + "_meta": { + "description": "Always `main`" + } + }, + "clicks_total": { + "type": "long", + "_meta": { + "description": "General number of clicks in the application since we started counting them" + } + }, + "clicks_7_days": { + "type": "long", + "_meta": { + "description": "General number of clicks in the application over the last 7 days" + } + }, + "clicks_30_days": { + "type": "long", + "_meta": { + "description": "General number of clicks in the application over the last 30 days" + } + }, + "clicks_90_days": { + "type": "long", + "_meta": { + "description": "General number of clicks in the application over the last 90 days" + } + }, + "minutes_on_screen_total": { + "type": "float", + "_meta": { + "description": "Minutes the application is active and on-screen since we started counting them." + } + }, + "minutes_on_screen_7_days": { + "type": "float", + "_meta": { + "description": "Minutes the application is active and on-screen over the last 7 days" + } + }, + "minutes_on_screen_30_days": { + "type": "float", + "_meta": { + "description": "Minutes the application is active and on-screen over the last 30 days" + } + }, + "minutes_on_screen_90_days": { + "type": "float", + "_meta": { + "description": "Minutes the application is active and on-screen over the last 90 days" + } + }, + "views": { + "type": "array", + "items": { + "properties": { + "appId": { + "type": "keyword", + "_meta": { + "description": "The application being tracked" + } + }, + "viewId": { + "type": "keyword", + "_meta": { + "description": "The application view being tracked" + } + }, + "clicks_total": { + "type": "long", + "_meta": { + "description": "General number of clicks in the application sub view since we started counting them" + } + }, + "clicks_7_days": { + "type": "long", + "_meta": { + "description": "General number of clicks in the active application sub view over the last 7 days" + } + }, + "clicks_30_days": { + "type": "long", + "_meta": { + "description": "General number of clicks in the active application sub view over the last 30 days" + } + }, + "clicks_90_days": { + "type": "long", + "_meta": { + "description": "General number of clicks in the active application sub view over the last 90 days" + } + }, + "minutes_on_screen_total": { + "type": "float", + "_meta": { + "description": "Minutes the application sub view is active and on-screen since we started counting them." + } + }, + "minutes_on_screen_7_days": { + "type": "float", + "_meta": { + "description": "Minutes the application is active and on-screen active application sub view over the last 7 days" + } + }, + "minutes_on_screen_30_days": { + "type": "float", + "_meta": { + "description": "Minutes the application is active and on-screen active application sub view over the last 30 days" + } + }, + "minutes_on_screen_90_days": { + "type": "float", + "_meta": { + "description": "Minutes the application is active and on-screen active application sub view over the last 90 days" + } + } + } + } + } + } + }, "security_access_agreement": { "properties": { "appId": { @@ -7647,6 +7778,12 @@ "description": "Non-default value of setting." } }, + "observability:enableComparisonByDefault": { + "type": "boolean", + "_meta": { + "description": "Non-default value of setting." + } + }, "banners:placement": { "type": "keyword", "_meta": { @@ -7677,6 +7814,12 @@ "description": "Non-default value of setting." } }, + "labs:canvas:byValueEmbeddable": { + "type": "boolean", + "_meta": { + "description": "Non-default value of setting." + } + }, "labs:canvas:useDataService": { "type": "boolean", "_meta": { @@ -9158,4 +9301,4 @@ } } } -} +} \ No newline at end of file diff --git a/src/plugins/telemetry/server/fetcher.ts b/src/plugins/telemetry/server/fetcher.ts index 97180f351986e..77770227a887d 100644 --- a/src/plugins/telemetry/server/fetcher.ts +++ b/src/plugins/telemetry/server/fetcher.ts @@ -16,7 +16,6 @@ import { SavedObjectsClientContract, SavedObjectsClient, CoreStart, - ICustomClusterClient, } from '../../../core/server'; import { getTelemetryChannelEndpoint, @@ -53,7 +52,6 @@ export class FetcherTask { private isSending = false; private internalRepository?: SavedObjectsClientContract; private telemetryCollectionManager?: TelemetryCollectionManagerPluginStart; - private elasticsearchClient?: ICustomClusterClient; constructor(initializerContext: PluginInitializerContext) { this.config$ = initializerContext.config.create(); @@ -67,7 +65,6 @@ export class FetcherTask { ) { this.internalRepository = new SavedObjectsClient(savedObjects.createInternalRepository()); this.telemetryCollectionManager = telemetryCollectionManager; - this.elasticsearchClient = elasticsearch.createClient('telemetry-fetcher'); this.intervalId = timer(this.initialCheckDelayMs, this.checkIntervalMs).subscribe(() => this.sendIfDue() @@ -78,9 +75,6 @@ export class FetcherTask { if (this.intervalId) { this.intervalId.unsubscribe(); } - if (this.elasticsearchClient) { - this.elasticsearchClient.close(); - } } private async areAllCollectorsReady() { diff --git a/src/plugins/ui_actions/README.asciidoc b/src/plugins/ui_actions/README.asciidoc index 8fd3d0378ce6e..b9e25edde77e6 100644 --- a/src/plugins/ui_actions/README.asciidoc +++ b/src/plugins/ui_actions/README.asciidoc @@ -71,13 +71,3 @@ action to execute. https://github.com/elastic/kibana/blob/main/examples/ui_action_examples/README.md[ui_action examples] -=== API Docs - -==== Server API -https://github.com/elastic/kibana/blob/main/docs/development/plugins/ui_actions/server/kibana-plugin-plugins-ui_actions-server.uiactionssetup.md[Browser Setup contract] -https://github.com/elastic/kibana/blob/main/docs/development/plugins/ui_actions/server/kibana-plugin-plugins-ui_actions-server.uiactionsstart.md[Browser Start contract] - -==== Browser API -https://github.com/elastic/kibana/blob/main/docs/development/plugins/ui_actions/public/kibana-plugin-plugins-ui_actions-public.uiactionssetup.md[Browser Setup contract] -https://github.com/elastic/kibana/blob/main/docs/development/plugins/ui_actions/public/kibana-plugin-plugins-ui_actions-public.uiactionsstart.md[Browser Start contract] - diff --git a/src/plugins/vis_type_markdown/server/index.ts b/src/plugins/vis_type_markdown/server/index.ts index d9665a0902317..cc1c995a185f9 100644 --- a/src/plugins/vis_type_markdown/server/index.ts +++ b/src/plugins/vis_type_markdown/server/index.ts @@ -13,7 +13,7 @@ import { configSchema, ConfigSchema } from '../config'; export const config: PluginConfigDescriptor = { schema: configSchema, deprecations: ({ renameFromRoot }) => [ - renameFromRoot('markdown_vis.enabled', 'vis_type_markdown.enabled'), + renameFromRoot('markdown_vis.enabled', 'vis_type_markdown.enabled', { level: 'warning' }), ], }; diff --git a/src/plugins/vis_types/metric/public/__snapshots__/to_ast.test.ts.snap b/src/plugins/vis_types/metric/public/__snapshots__/to_ast.test.ts.snap index 46e86c4c25de1..233e38874e6da 100644 --- a/src/plugins/vis_types/metric/public/__snapshots__/to_ast.test.ts.snap +++ b/src/plugins/vis_types/metric/public/__snapshots__/to_ast.test.ts.snap @@ -34,6 +34,31 @@ Object { }, Object { "arguments": Object { + "font": Array [ + Object { + "chain": Array [ + Object { + "arguments": Object { + "align": Array [ + "center", + ], + "family": Array [ + "'Inter', 'Inter UI', -apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'", + ], + "sizeUnit": Array [ + "pt", + ], + "weight": Array [ + "bold", + ], + }, + "function": "font", + "type": "function", + }, + ], + "type": "expression", + }, + ], "percentageMode": Array [ true, ], @@ -83,6 +108,31 @@ Object { }, Object { "arguments": Object { + "font": Array [ + Object { + "chain": Array [ + Object { + "arguments": Object { + "align": Array [ + "center", + ], + "family": Array [ + "'Inter', 'Inter UI', -apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'", + ], + "sizeUnit": Array [ + "pt", + ], + "weight": Array [ + "bold", + ], + }, + "function": "font", + "type": "function", + }, + ], + "type": "expression", + }, + ], "showLabels": Array [ false, ], diff --git a/src/plugins/vis_types/metric/public/to_ast.ts b/src/plugins/vis_types/metric/public/to_ast.ts index 1e23a10dd7608..852aa70269994 100644 --- a/src/plugins/vis_types/metric/public/to_ast.ts +++ b/src/plugins/vis_types/metric/public/to_ast.ts @@ -9,11 +9,14 @@ import { get } from 'lodash'; import { getVisSchemas, SchemaConfig, VisToExpressionAst } from '../../../visualizations/public'; import { buildExpression, buildExpressionFunction } from '../../../expressions/public'; +import { inter } from '../../../expressions/common'; + import { EsaggsExpressionFunctionDefinition, IndexPatternLoadExpressionFunctionDefinition, } from '../../../data/public'; import { VisParams } from './types'; +import { getStopsWithColorsFromRanges } from './utils'; const prepareDimension = (params: SchemaConfig) => { const visdimension = buildExpressionFunction('visdimension', { accessor: params.accessor }); @@ -43,7 +46,6 @@ export const toExpressionAst: VisToExpressionAst = (vis, params) => { const { percentageMode, percentageFormatPattern, - useRanges, colorSchema, metricColorMode, colorsRange, @@ -64,26 +66,32 @@ export const toExpressionAst: VisToExpressionAst = (vis, params) => { const metricVis = buildExpressionFunction('metricVis', { percentageMode, - colorSchema, colorMode: metricColorMode, - useRanges, - invertColors, showLabels: labels?.show ?? false, }); - if (style) { - metricVis.addArgument('bgFill', style.bgFill); - metricVis.addArgument('font', buildExpression(`font size=${style.fontSize}`)); - metricVis.addArgument('subText', style.subText); - } + // Pt unit is provided to support the previous view of the metricVis at vis_types editor. + // Inter font is defined here to override the default `openSans` font, which comes from the expession. + metricVis.addArgument( + 'font', + buildExpression( + `font family="${inter.value}" + weight="bold" + align="center" + sizeUnit="pt" + ${style ? `size=${style.fontSize}` : ''}` + ) + ); - if (colorsRange) { - colorsRange.forEach((range: any) => { - metricVis.addArgument( - 'colorRange', - buildExpression(`range from=${range.from} to=${range.to}`) - ); + if (colorsRange && colorsRange.length) { + const stopsWithColors = getStopsWithColorsFromRanges(colorsRange, colorSchema, invertColors); + const palette = buildExpressionFunction('palette', { + ...stopsWithColors, + range: 'number', + continuity: 'none', }); + + metricVis.addArgument('palette', buildExpression([palette])); } if (schemas.group) { diff --git a/src/plugins/vis_types/metric/public/utils/index.ts b/src/plugins/vis_types/metric/public/utils/index.ts new file mode 100644 index 0000000000000..fb23c97d835fe --- /dev/null +++ b/src/plugins/vis_types/metric/public/utils/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 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 { getStopsWithColorsFromRanges } from './palette'; diff --git a/src/plugins/vis_types/metric/public/utils/palette.ts b/src/plugins/vis_types/metric/public/utils/palette.ts new file mode 100644 index 0000000000000..ff3a4b10a0118 --- /dev/null +++ b/src/plugins/vis_types/metric/public/utils/palette.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 { ColorSchemas, getHeatmapColors } from '../../../../charts/common'; +import { Range } from '../../../../expressions'; + +export interface PaletteConfig { + color: Array; + stop: number[]; +} + +const TRANSPARENT = 'rgb(0, 0, 0, 0)'; + +const getColor = ( + index: number, + elementsCount: number, + colorSchema: ColorSchemas, + invertColors: boolean = false +) => { + const divider = Math.max(elementsCount - 1, 1); + const value = invertColors ? 1 - index / divider : index / divider; + return getHeatmapColors(value, colorSchema); +}; + +export const getStopsWithColorsFromRanges = ( + ranges: Range[], + colorSchema: ColorSchemas, + invertColors: boolean = false +) => { + return ranges.reduce( + (acc, range, index, rangesArr) => { + if ((index && range.from !== rangesArr[index - 1].to) || index === 0) { + acc.color.push(TRANSPARENT); + acc.stop.push(range.from); + } + + acc.color.push(getColor(index, rangesArr.length, colorSchema, invertColors)); + acc.stop.push(range.to); + + return acc; + }, + { color: [], stop: [] } + ); +}; diff --git a/src/plugins/vis_types/pie/public/pie_component.tsx b/src/plugins/vis_types/pie/public/pie_component.tsx index 9211274a8abc8..053d06bb84e29 100644 --- a/src/plugins/vis_types/pie/public/pie_component.tsx +++ b/src/plugins/vis_types/pie/public/pie_component.tsx @@ -234,9 +234,21 @@ const PieComponent = (props: PieComponentProps) => { syncColors, ] ); + + const rescaleFactor = useMemo(() => { + const overallSum = visData.rows.reduce((sum, row) => sum + row[metricColumn.id], 0); + const slices = visData.rows.map((row) => row[metricColumn.id] / overallSum); + const smallSlices = slices.filter((value) => value < 0.02).length; + if (smallSlices) { + // shrink up to 20% to give some room for the linked values + return 1 / (1 + Math.min(smallSlices * 0.05, 0.2)); + } + return 1; + }, [visData.rows, metricColumn]); + const config = useMemo( - () => getConfig(visParams, chartTheme, dimensions), - [chartTheme, visParams, dimensions] + () => getConfig(visParams, chartTheme, dimensions, rescaleFactor), + [chartTheme, visParams, dimensions, rescaleFactor] ); const tooltip: TooltipProps = { type: visParams.addTooltip ? TooltipType.Follow : TooltipType.None, diff --git a/src/plugins/vis_types/pie/public/utils/get_config.test.ts b/src/plugins/vis_types/pie/public/utils/get_config.test.ts new file mode 100644 index 0000000000000..82907002a19d5 --- /dev/null +++ b/src/plugins/vis_types/pie/public/utils/get_config.test.ts @@ -0,0 +1,66 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { getConfig } from './get_config'; +import { createMockPieParams } from '../mocks'; + +const visParams = createMockPieParams(); + +describe('getConfig', () => { + it('should cap the outerSizeRatio to 1', () => { + expect(getConfig(visParams, {}, { width: 400, height: 400 }).outerSizeRatio).toBe(1); + }); + + it('should not have outerSizeRatio for split chart', () => { + expect( + getConfig( + { + ...visParams, + dimensions: { + ...visParams.dimensions, + splitColumn: [ + { + accessor: 1, + format: { + id: 'number', + }, + }, + ], + }, + }, + {}, + { width: 400, height: 400 } + ).outerSizeRatio + ).toBeUndefined(); + + expect( + getConfig( + { + ...visParams, + dimensions: { + ...visParams.dimensions, + splitRow: [ + { + accessor: 1, + format: { + id: 'number', + }, + }, + ], + }, + }, + {}, + { width: 400, height: 400 } + ).outerSizeRatio + ).toBeUndefined(); + }); + + it('should not set outerSizeRatio if dimensions are not defined', () => { + expect(getConfig(visParams, {}).outerSizeRatio).toBeUndefined(); + }); +}); diff --git a/src/plugins/vis_types/pie/public/utils/get_config.ts b/src/plugins/vis_types/pie/public/utils/get_config.ts index 40f8f84b127f9..9f67155145820 100644 --- a/src/plugins/vis_types/pie/public/utils/get_config.ts +++ b/src/plugins/vis_types/pie/public/utils/get_config.ts @@ -13,7 +13,8 @@ const MAX_SIZE = 1000; export const getConfig = ( visParams: PieVisParams, chartTheme: RecursivePartial, - dimensions?: PieContainerDimensions + dimensions?: PieContainerDimensions, + rescaleFactor: number = 1 ): RecursivePartial => { // On small multiples we want the labels to only appear inside const isSplitChart = Boolean(visParams.dimensions.splitColumn || visParams.dimensions.splitRow); @@ -32,7 +33,9 @@ export const getConfig = ( const usingOuterSizeRatio = dimensions && !isSplitChart ? { - outerSizeRatio: MAX_SIZE / Math.min(dimensions?.width, dimensions?.height), + outerSizeRatio: + // Cap the ratio to 1 and then rescale + rescaleFactor * Math.min(MAX_SIZE / Math.min(dimensions?.width, dimensions?.height), 1), } : null; const config: RecursivePartial = { diff --git a/src/plugins/vis_types/pie/public/utils/get_legend_actions.tsx b/src/plugins/vis_types/pie/public/utils/get_legend_actions.tsx index cd1d1d71aaa76..2a27063a0e7d5 100644 --- a/src/plugins/vis_types/pie/public/utils/get_legend_actions.tsx +++ b/src/plugins/vis_types/pie/public/utils/get_legend_actions.tsx @@ -10,7 +10,7 @@ import React, { useState, useEffect, useMemo } from 'react'; import { i18n } from '@kbn/i18n'; import { EuiContextMenuPanelDescriptor, EuiIcon, EuiPopover, EuiContextMenu } from '@elastic/eui'; -import { LegendAction, SeriesIdentifier } from '@elastic/charts'; +import { LegendAction, SeriesIdentifier, useLegendAction } from '@elastic/charts'; import { DataPublicPluginStart } from '../../../../data/public'; import { PieVisParams } from '../types'; import { ClickTriggerEvent } from '../../../../charts/public'; @@ -30,6 +30,7 @@ export const getLegendActions = ( const [popoverOpen, setPopoverOpen] = useState(false); const [isfilterable, setIsfilterable] = useState(true); const filterData = useMemo(() => getFilterEventData(pieSeries), [pieSeries]); + const [ref, onClose] = useLegendAction(); useEffect(() => { (async () => setIsfilterable(await canFilter(filterData, actions)))(); @@ -82,6 +83,7 @@ export const getLegendActions = ( const Button = (
    setPopoverOpen(false)} + closePopover={() => { + setPopoverOpen(false); + onClose(); + }} panelPaddingSize="none" anchorPosition="upLeft" title={i18n.translate('visTypePie.legend.filterOptionsLegend', { diff --git a/src/plugins/vis_types/timelion/server/handlers/chain_runner.js b/src/plugins/vis_types/timelion/server/handlers/chain_runner.js index 3710d015f3f69..fac1a68acc907 100644 --- a/src/plugins/vis_types/timelion/server/handlers/chain_runner.js +++ b/src/plugins/vis_types/timelion/server/handlers/chain_runner.js @@ -7,7 +7,6 @@ */ import _ from 'lodash'; -import Bluebird from 'bluebird'; import { i18n } from '@kbn/i18n'; import moment from 'moment'; @@ -42,7 +41,7 @@ export default function chainRunner(tlConfig) { function resolveArgument(item) { if (Array.isArray(item)) { - return Bluebird.all(_.map(item, resolveArgument)); + return Promise.all(_.map(item, resolveArgument)); } if (_.isObject(item)) { @@ -51,7 +50,7 @@ export default function chainRunner(tlConfig) { const itemFunctionDef = tlConfig.getFunction(item.function); if (itemFunctionDef.cacheKey && queryCache[itemFunctionDef.cacheKey(item)]) { stats.queryCount++; - return Bluebird.resolve(_.cloneDeep(queryCache[itemFunctionDef.cacheKey(item)])); + return Promise.resolve(_.cloneDeep(queryCache[itemFunctionDef.cacheKey(item)])); } return invoke(item.function, item.arguments); } @@ -94,7 +93,7 @@ export default function chainRunner(tlConfig) { args = _.map(args, resolveArgument); - return Bluebird.all(args).then(function (args) { + return Promise.all(args).then(function (args) { args.byName = indexArguments(functionDef, args); return functionDef.fn(args, tlConfig); }); @@ -128,7 +127,7 @@ export default function chainRunner(tlConfig) { return args; }); }); - return Bluebird.all(seriesList).then(function (args) { + return Promise.all(seriesList).then(function (args) { const list = _.chain(args).map('list').flatten().value(); const seriesList = _.merge.apply(this, _.flatten([{}, args])); seriesList.list = list; @@ -158,22 +157,22 @@ export default function chainRunner(tlConfig) { }) .value(); - return Bluebird.settle(promises).then(function (resolvedDatasources) { + return Promise.allSettled(promises).then(function (resolvedDatasources) { stats.queryTime = new Date().getTime(); _.each(queries, function (query, i) { const functionDef = tlConfig.getFunction(query.function); const resolvedDatasource = resolvedDatasources[i]; - if (resolvedDatasource.isRejected()) { - if (resolvedDatasource.reason().isBoom) { - throw resolvedDatasource.reason(); + if (resolvedDatasource.status === 'rejected') { + if (resolvedDatasource.reason.isBoom) { + throw resolvedDatasource.reason; } else { - throwWithCell(query.cell, resolvedDatasource.reason()); + throwWithCell(query.cell, resolvedDatasource.reason); } } - queryCache[functionDef.cacheKey(query)] = resolvedDatasource.value(); + queryCache[functionDef.cacheKey(query)] = resolvedDatasource.value; }); stats.cacheCount = _.keys(queryCache).length; diff --git a/src/plugins/vis_types/timelion/server/lib/alter.js b/src/plugins/vis_types/timelion/server/lib/alter.js index 2e234f3405c21..47a57f213cdbb 100644 --- a/src/plugins/vis_types/timelion/server/lib/alter.js +++ b/src/plugins/vis_types/timelion/server/lib/alter.js @@ -6,7 +6,6 @@ * Side Public License, v 1. */ -import Bluebird from 'bluebird'; import _ from 'lodash'; /* @param {Array} args @@ -18,7 +17,7 @@ import _ from 'lodash'; export default function alter(args, fn) { // In theory none of the args should ever be promises. This is probably a waste. - return Bluebird.all(args) + return Promise.all(args) .then(function (args) { const seriesList = args.shift(); diff --git a/src/plugins/vis_types/timelion/server/routes/run.ts b/src/plugins/vis_types/timelion/server/routes/run.ts index b8c0ce4ea6599..d615302253a1d 100644 --- a/src/plugins/vis_types/timelion/server/routes/run.ts +++ b/src/plugins/vis_types/timelion/server/routes/run.ts @@ -8,7 +8,6 @@ import { IRouter, Logger, CoreSetup } from 'kibana/server'; import { schema } from '@kbn/config-schema'; -import Bluebird from 'bluebird'; import _ from 'lodash'; // @ts-ignore import chainRunnerFn from '../handlers/chain_runner.js'; @@ -96,7 +95,7 @@ export function runRoute( }); try { const chainRunner = chainRunnerFn(tlConfig); - const sheet = await Bluebird.all(chainRunner.processRequest(request.body)); + const sheet = await Promise.all(await chainRunner.processRequest(request.body)); return response.ok({ body: { sheet, diff --git a/src/plugins/vis_types/timelion/server/series_functions/es/es.test.js b/src/plugins/vis_types/timelion/server/series_functions/es/es.test.js index f55ee31f39799..9c0dac6f6975a 100644 --- a/src/plugins/vis_types/timelion/server/series_functions/es/es.test.js +++ b/src/plugins/vis_types/timelion/server/series_functions/es/es.test.js @@ -256,12 +256,20 @@ describe('es', () => { sandbox.restore(); }); - test('sets ignore_throttled=true on the request', () => { + test('sets ignore_throttled=false on the request', () => { + config.index = 'beer'; + tlConfig.settings[UI_SETTINGS.SEARCH_INCLUDE_FROZEN] = true; + const request = fn(config, tlConfig, emptyScriptFields); + + expect(request.params.ignore_throttled).toEqual(false); + }); + + test('sets no ignore_throttled if SEARCH_INCLUDE_FROZEN is false', () => { config.index = 'beer'; tlConfig.settings[UI_SETTINGS.SEARCH_INCLUDE_FROZEN] = false; const request = fn(config, tlConfig, emptyScriptFields); - expect(request.params.ignore_throttled).toEqual(true); + expect(request.params).not.toHaveProperty('ignore_throttled'); }); test('sets no timeout if elasticsearch.shardTimeout is set to 0', () => { diff --git a/src/plugins/vis_types/timelion/server/series_functions/es/lib/build_request.js b/src/plugins/vis_types/timelion/server/series_functions/es/lib/build_request.js index 20e3f71801854..99b5d0bacd858 100644 --- a/src/plugins/vis_types/timelion/server/series_functions/es/lib/build_request.js +++ b/src/plugins/vis_types/timelion/server/series_functions/es/lib/build_request.js @@ -66,9 +66,10 @@ export default function buildRequest(config, tlConfig, scriptFields, runtimeFiel _.assign(aggCursor, createDateAgg(config, tlConfig, scriptFields)); + const includeFrozen = Boolean(tlConfig.settings[UI_SETTINGS.SEARCH_INCLUDE_FROZEN]); const request = { index: config.index, - ignore_throttled: !tlConfig.settings[UI_SETTINGS.SEARCH_INCLUDE_FROZEN], + ...(includeFrozen ? { ignore_throttled: false } : {}), body: { query: { bool: bool, diff --git a/src/plugins/vis_types/timelion/server/series_functions/quandl.js b/src/plugins/vis_types/timelion/server/series_functions/quandl.js index 7e3a0f6de9aba..3c209879d7a4c 100644 --- a/src/plugins/vis_types/timelion/server/series_functions/quandl.js +++ b/src/plugins/vis_types/timelion/server/series_functions/quandl.js @@ -10,7 +10,6 @@ import { i18n } from '@kbn/i18n'; import _ from 'lodash'; import fetch from 'node-fetch'; import moment from 'moment'; -fetch.Promise = require('bluebird'); import Datasource from '../lib/classes/datasource'; diff --git a/src/plugins/vis_types/timelion/server/series_functions/static.js b/src/plugins/vis_types/timelion/server/series_functions/static.js index b5e8dd6df6682..afc1bd5afbbb8 100644 --- a/src/plugins/vis_types/timelion/server/series_functions/static.js +++ b/src/plugins/vis_types/timelion/server/series_functions/static.js @@ -9,7 +9,6 @@ import { i18n } from '@kbn/i18n'; import _ from 'lodash'; import Datasource from '../lib/classes/datasource'; -import Bluebird from 'bluebird'; export default new Datasource('static', { aliases: ['value'], @@ -51,7 +50,7 @@ export default new Datasource('static', { }); } - return Bluebird.resolve({ + return Promise.resolve({ type: 'seriesList', list: [ { diff --git a/src/plugins/vis_types/timelion/server/series_functions/worldbank_indicators.js b/src/plugins/vis_types/timelion/server/series_functions/worldbank_indicators.js index ba28a82345522..c2eb07890a102 100644 --- a/src/plugins/vis_types/timelion/server/series_functions/worldbank_indicators.js +++ b/src/plugins/vis_types/timelion/server/series_functions/worldbank_indicators.js @@ -9,7 +9,6 @@ import { i18n } from '@kbn/i18n'; import _ from 'lodash'; import worldbank from './worldbank.js'; -import Bluebird from 'bluebird'; import Datasource from '../lib/classes/datasource'; export default new Datasource('worldbank_indicators', { @@ -61,9 +60,11 @@ export default new Datasource('worldbank_indicators', { return worldbank.timelionFn(wbArgs, tlConfig); }); - return Bluebird.map(seriesLists, function (seriesList) { - return seriesList.list[0]; - }).then(function (list) { + return Promise.all( + seriesLists.map(function (seriesList) { + return seriesList.list[0]; + }) + ).then(function (list) { return { type: 'seriesList', list: list, diff --git a/src/plugins/vis_types/timelion/server/series_functions/yaxis.test.js b/src/plugins/vis_types/timelion/server/series_functions/yaxis.test.js index 6d627832544de..d68aa7ac70117 100644 --- a/src/plugins/vis_types/timelion/server/series_functions/yaxis.test.js +++ b/src/plugins/vis_types/timelion/server/series_functions/yaxis.test.js @@ -7,7 +7,6 @@ */ import fn from './yaxis'; -import Bluebird from 'bluebird'; const expect = require('chai').expect; import invoke from './helpers/invoke_series_fn.js'; @@ -25,7 +24,7 @@ describe('yaxis.js', () => { }); it('puts odd numbers of the left, even on the right, by default', () => { - return Bluebird.all([ + return Promise.all([ invoke(fn, [seriesList, 1]).then((r) => { expect(r.output.list[0]._global.yaxes[0].position).to.equal('left'); }), @@ -39,7 +38,7 @@ describe('yaxis.js', () => { }); it('it lets you override default positions', () => { - return Bluebird.all([ + return Promise.all([ invoke(fn, [seriesList, 1, null, null, 'right']).then((r) => { expect(r.output.list[0]._global.yaxes[0].position).to.equal('right'); }), @@ -50,7 +49,7 @@ describe('yaxis.js', () => { }); it('sets the minimum (default: no min)', () => { - return Bluebird.all([ + return Promise.all([ invoke(fn, [seriesList, 1, null]).then((r) => { expect(r.output.list[0]._global.yaxes[0].min).to.equal(null); }), @@ -61,7 +60,7 @@ describe('yaxis.js', () => { }); it('sets the max (default: no max)', () => { - return Bluebird.all([ + return Promise.all([ invoke(fn, [seriesList, 1, null]).then((r) => { expect(r.output.list[0]._global.yaxes[0].max).to.equal(undefined); }), @@ -72,7 +71,7 @@ describe('yaxis.js', () => { }); it('sets the units (default: no unit', () => { - return Bluebird.all([ + return Promise.all([ invoke(fn, [seriesList, 1, null, null, null, null, null, null]).then((r) => { expect(r.output.list[0]._global.yaxes[0].units).to.equal(undefined); }), diff --git a/src/plugins/vis_types/timeseries/public/application/components/timeseries_loading.tsx b/src/plugins/vis_types/timeseries/public/application/components/timeseries_loading.tsx new file mode 100644 index 0000000000000..ae0088d22cf76 --- /dev/null +++ b/src/plugins/vis_types/timeseries/public/application/components/timeseries_loading.tsx @@ -0,0 +1,16 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 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 { EuiLoadingChart } from '@elastic/eui'; + +export const TimeseriesLoading = () => ( +
    + +
    +); diff --git a/src/plugins/vis_types/timeseries/public/application/components/timeseries_visualization.tsx b/src/plugins/vis_types/timeseries/public/application/components/timeseries_visualization.tsx index 0916892cfda46..ae699880784a9 100644 --- a/src/plugins/vis_types/timeseries/public/application/components/timeseries_visualization.tsx +++ b/src/plugins/vis_types/timeseries/public/application/components/timeseries_visualization.tsx @@ -8,7 +8,7 @@ import './timeseries_visualization.scss'; -import React, { Suspense, useCallback, useEffect } from 'react'; +import React, { Suspense, useCallback, useEffect, useState } from 'react'; import { EuiFlexGroup, EuiFlexItem, EuiLoadingChart } from '@elastic/eui'; import { XYChartSeriesIdentifier, GeometryValue } from '@elastic/charts'; import { IUiSettingsClient } from 'src/core/public'; @@ -16,8 +16,9 @@ import { IInterpreterRenderHandlers } from 'src/plugins/expressions'; import { PersistedState } from 'src/plugins/visualizations/public'; import { PaletteRegistry } from 'src/plugins/charts/public'; +import { TimeseriesLoading } from './timeseries_loading'; import { TimeseriesVisTypes } from './vis_types'; -import type { PanelData, TimeseriesVisData } from '../../../common/types'; +import type { FetchedIndexPattern, PanelData, TimeseriesVisData } from '../../../common/types'; import { isVisTableData } from '../../../common/vis_data_utils'; import { TimeseriesVisParams } from '../../types'; import { convertSeriesToDataTable } from './lib/convert_series_to_datatable'; @@ -27,32 +28,41 @@ import { LastValueModeIndicator } from './last_value_mode_indicator'; import { getInterval } from './lib/get_interval'; import { AUTO_INTERVAL } from '../../../common/constants'; import { TIME_RANGE_DATA_MODES, PANEL_TYPES } from '../../../common/enums'; -import type { IndexPattern } from '../../../../../data/common'; -import '../index.scss'; +import { fetchIndexPattern } from '../../../common/index_patterns_utils'; +import { getCharts, getDataStart } from '../../services'; interface TimeseriesVisualizationProps { - className?: string; getConfig: IUiSettingsClient['get']; handlers: IInterpreterRenderHandlers; model: TimeseriesVisParams; visData: TimeseriesVisData; uiState: PersistedState; syncColors: boolean; - palettesService: PaletteRegistry; - indexPattern?: IndexPattern | null; } function TimeseriesVisualization({ - className = 'tvbVis', visData, model, handlers, uiState, getConfig, syncColors, - palettesService, - indexPattern, }: TimeseriesVisualizationProps) { + const [indexPattern, setIndexPattern] = useState(null); + const [palettesService, setPalettesService] = useState(null); + + useEffect(() => { + getCharts() + .palettes.getPalettes() + .then((paletteRegistry) => setPalettesService(paletteRegistry)); + }, []); + + useEffect(() => { + fetchIndexPattern(model.index_pattern, getDataStart().indexPatterns).then( + (fetchedIndexPattern) => setIndexPattern(fetchedIndexPattern.indexPattern) + ); + }, [model.index_pattern]); + const onBrush = useCallback( async (gte: string, lte: string, series: PanelData[]) => { let event; @@ -136,10 +146,6 @@ function TimeseriesVisualization({ [uiState] ); - useEffect(() => { - handlers.done(); - }); - const VisComponent = TimeseriesVisTypes[model.type]; const isLastValueMode = @@ -150,46 +156,46 @@ function TimeseriesVisualization({ const [firstSeries] = (isVisTableData(visData) ? visData.series : visData[model.id]?.series) ?? []; - if (VisComponent) { - return ( - - {shouldDisplayLastValueIndicator && ( - - - - )} - - - -
    - } - > - - - - - ); + if (!VisComponent || palettesService === null || indexPattern === null) { + return ; } - return
    ; + return ( + + {shouldDisplayLastValueIndicator && ( + + + + )} + + + +
    + } + > + + + + + ); } // default export required for React.Lazy diff --git a/src/plugins/vis_types/timeseries/public/timeseries_vis_renderer.tsx b/src/plugins/vis_types/timeseries/public/timeseries_vis_renderer.tsx index ad069a4d7e2cc..9edc05893e24f 100644 --- a/src/plugins/vis_types/timeseries/public/timeseries_vis_renderer.tsx +++ b/src/plugins/vis_types/timeseries/public/timeseries_vis_renderer.tsx @@ -13,13 +13,10 @@ import { render, unmountComponentAtNode } from 'react-dom'; import { I18nProvider } from '@kbn/i18n/react'; import { IUiSettingsClient } from 'kibana/public'; -import { EuiLoadingChart } from '@elastic/eui'; -import { fetchIndexPattern } from '../common/index_patterns_utils'; import { VisualizationContainer, PersistedState } from '../../../visualizations/public'; import type { TimeseriesVisData } from '../common/types'; import { isVisTableData } from '../common/vis_data_utils'; -import { getCharts, getDataStart } from './services'; import type { TimeseriesVisParams } from './types'; import type { ExpressionRenderDefinition } from '../../../expressions/common'; @@ -44,57 +41,40 @@ export const getTimeseriesVisRenderer: (deps: { name: 'timeseries_vis', reuseDomNode: true, render: async (domNode, config, handlers) => { + // Build optimization. Move app styles from main bundle + // @ts-expect-error TS error, cannot find type declaration for scss + import('./application/index.scss'); + handlers.onDestroy(() => { unmountComponentAtNode(domNode); }); const { visParams: model, visData, syncColors } = config; - const { palettes } = getCharts(); - const { indexPatterns } = getDataStart(); const showNoResult = !checkIfDataExists(visData, model); - let servicesLoaded; - - Promise.all([ - palettes.getPalettes(), - fetchIndexPattern(model.index_pattern, indexPatterns), - ]).then(([palettesService, { indexPattern }]) => { - servicesLoaded = true; - - unmountComponentAtNode(domNode); - - render( - - + + - - - , - domNode - ); - }); - - if (!servicesLoaded) { - render( -
    - -
    , - domNode - ); - } + model={model} + visData={visData as TimeseriesVisData} + syncColors={syncColors} + uiState={handlers.uiState! as PersistedState} + /> + + , + domNode, + () => { + handlers.done(); + } + ); }, }); diff --git a/src/plugins/vis_types/timeseries/server/lib/vis_data/get_series_data.ts b/src/plugins/vis_types/timeseries/server/lib/vis_data/get_series_data.ts index a9a3825f5a9df..6f96c0d23cad7 100644 --- a/src/plugins/vis_types/timeseries/server/lib/vis_data/get_series_data.ts +++ b/src/plugins/vis_types/timeseries/server/lib/vis_data/get_series_data.ts @@ -36,7 +36,10 @@ export async function getSeriesData( fieldFormatService, } = services; - const panelIndex = await cachedIndexPatternFetcher(panel.index_pattern); + const panelIndex = await cachedIndexPatternFetcher( + panel.index_pattern, + !panel.use_kibana_indexes + ); const strategy = await searchStrategyRegistry.getViableStrategy(requestContext, req, panelIndex); diff --git a/src/plugins/vis_types/timeseries/server/lib/vis_data/get_table_data.ts b/src/plugins/vis_types/timeseries/server/lib/vis_data/get_table_data.ts index 3b53147dc6f93..35b6a78d0579b 100644 --- a/src/plugins/vis_types/timeseries/server/lib/vis_data/get_table_data.ts +++ b/src/plugins/vis_types/timeseries/server/lib/vis_data/get_table_data.ts @@ -31,7 +31,10 @@ export async function getTableData( panel: Panel, services: VisTypeTimeseriesRequestServices ) { - const panelIndex = await services.cachedIndexPatternFetcher(panel.index_pattern); + const panelIndex = await services.cachedIndexPatternFetcher( + panel.index_pattern, + !panel.use_kibana_indexes + ); const strategy = await services.searchStrategyRegistry.getViableStrategy( requestContext, diff --git a/src/plugins/vis_types/vega/kibana.json b/src/plugins/vis_types/vega/kibana.json index 1a499e284c1a8..cedd73cc6d398 100644 --- a/src/plugins/vis_types/vega/kibana.json +++ b/src/plugins/vis_types/vega/kibana.json @@ -5,7 +5,7 @@ "ui": true, "requiredPlugins": ["data", "visualizations", "mapsEms", "expressions", "inspector"], "optionalPlugins": ["home","usageCollection"], - "requiredBundles": ["kibanaUtils", "kibanaReact", "visDefaultEditor", "esUiShared"], + "requiredBundles": ["kibanaUtils", "kibanaReact", "visDefaultEditor"], "owner": { "name": "Vis Editors", "githubTeam": "kibana-vis-editors" diff --git a/src/plugins/vis_types/vega/public/components/vega_editor.scss b/src/plugins/vis_types/vega/public/components/vega_editor.scss index 709aaa2030f68..4381c0097f96d 100644 --- a/src/plugins/vis_types/vega/public/components/vega_editor.scss +++ b/src/plugins/vis_types/vega/public/components/vega_editor.scss @@ -1,18 +1,28 @@ .visEditor--vega { .visEditorSidebar__config { padding: 0; + display: flex; + flex-direction: row; + overflow: hidden; + + min-height: $euiSize * 15; + + @include euiBreakpoint('xs', 's', 'm') { + max-height: $euiSize * 15; + } } } .vgaEditor { - @include euiBreakpoint('xs', 's', 'm') { - @include euiScrollBar; - max-height: $euiSize * 15; - overflow-y: auto; + width: 100%; + flex-grow: 1; + + .kibanaCodeEditor { + width: 100%; } } -.vgaEditor__aceEditorActions { +.vgaEditor__editorActions { position: absolute; z-index: $euiZLevel1; top: $euiSizeS; diff --git a/src/plugins/vis_types/vega/public/components/vega_vis_editor.tsx b/src/plugins/vis_types/vega/public/components/vega_vis_editor.tsx index d2f586eac9885..46e5e331c9455 100644 --- a/src/plugins/vis_types/vega/public/components/vega_vis_editor.tsx +++ b/src/plugins/vis_types/vega/public/components/vega_vis_editor.tsx @@ -6,14 +6,16 @@ * Side Public License, v 1. */ -import React, { useCallback } from 'react'; -import compactStringify from 'json-stringify-pretty-compact'; +import { XJsonLang } from '@kbn/monaco'; +import useMount from 'react-use/lib/useMount'; import hjson from 'hjson'; -import 'brace/mode/hjson'; + +import React, { useCallback, useState } from 'react'; +import compactStringify from 'json-stringify-pretty-compact'; import { i18n } from '@kbn/i18n'; import { VisEditorOptionsProps } from 'src/plugins/visualizations/public'; -import { EuiCodeEditor } from '../../../../es_ui_shared/public'; +import { CodeEditor, HJsonLang } from '../../../../kibana_react/public'; import { getNotifications } from '../services'; import { VisParams } from '../vega_fn'; import { VegaHelpMenu } from './vega_help_menu'; @@ -21,20 +23,6 @@ import { VegaActionsMenu } from './vega_actions_menu'; import './vega_editor.scss'; -const aceOptions = { - maxLines: Infinity, - highlightActiveLine: false, - showPrintMargin: false, - tabSize: 2, - useSoftTabs: true, - wrap: true, -}; - -const hjsonStringifyOptions = { - bracesSameLine: true, - keepWsc: true, -}; - function format( value: string, stringify: typeof hjson.stringify | typeof compactStringify, @@ -42,7 +30,11 @@ function format( ) { try { const spec = hjson.parse(value, { legacyRoot: false, keepWsc: true }); - return stringify(spec, options); + + return { + value: stringify(spec, options), + isValid: true, + }; } catch (err) { // This is a common case - user tries to format an invalid HJSON text getNotifications().toasts.addError(err, { @@ -51,44 +43,82 @@ function format( }), }); - return value; + return { value, isValid: false }; } } function VegaVisEditor({ stateParams, setValue }: VisEditorOptionsProps) { - const onChange = useCallback( - (value: string) => { + const [languageId, setLanguageId] = useState(); + + useMount(() => { + let specLang = XJsonLang.ID; + try { + JSON.parse(stateParams.spec); + } catch { + specLang = HJsonLang; + } + setLanguageId(specLang); + }); + + const setSpec = useCallback( + (value: string, specLang?: string) => { setValue('spec', value); + if (specLang) { + setLanguageId(specLang); + } }, [setValue] ); - const formatJson = useCallback( - () => setValue('spec', format(stateParams.spec, compactStringify)), - [setValue, stateParams.spec] - ); + const onChange = useCallback((value: string) => setSpec(value), [setSpec]); - const formatHJson = useCallback( - () => setValue('spec', format(stateParams.spec, hjson.stringify, hjsonStringifyOptions)), - [setValue, stateParams.spec] - ); + const formatJson = useCallback(() => { + const { value, isValid } = format(stateParams.spec, compactStringify); + + if (isValid) { + setSpec(value, XJsonLang.ID); + } + }, [setSpec, stateParams.spec]); + + const formatHJson = useCallback(() => { + const { value, isValid } = format(stateParams.spec, hjson.stringify, { + bracesSameLine: true, + keepWsc: true, + }); + + if (isValid) { + setSpec(value, HJsonLang); + } + }, [setSpec, stateParams.spec]); + + if (!languageId) { + return null; + } return ( -
    - -
    +
    +
    +
    ); } diff --git a/src/plugins/vis_types/vega/public/vega_view/vega_base_view.js b/src/plugins/vis_types/vega/public/vega_view/vega_base_view.js index 1c444e7528d44..8c725ba0a75a2 100644 --- a/src/plugins/vis_types/vega/public/vega_view/vega_base_view.js +++ b/src/plugins/vis_types/vega/public/vega_view/vega_base_view.js @@ -247,11 +247,16 @@ export class VegaBaseView { if (!this._$messages) { this._$messages = $(`
      `).appendTo(this._$parentEl); } - this._$messages.append( - $(`
    • `).append( - $(`
      `).text(text)
      -      )
      -    );
      +    const isMessageAlreadyDisplayed = this._$messages
      +      .find(`pre.vgaVis__messageCode`)
      +      .filter((index, element) => element.textContent === text).length;
      +    if (!isMessageAlreadyDisplayed) {
      +      this._$messages.append(
      +        $(`
    • `).append( + $(`
      `).text(text)
      +        )
      +      );
      +    }
         }
       
         resize() {
      diff --git a/src/plugins/vis_types/vislib/public/vislib/components/tooltip/tooltip.js b/src/plugins/vis_types/vislib/public/vislib/components/tooltip/tooltip.js
      index e2decb86c9032..1faebdf0ce89c 100644
      --- a/src/plugins/vis_types/vislib/public/vislib/components/tooltip/tooltip.js
      +++ b/src/plugins/vis_types/vislib/public/vislib/components/tooltip/tooltip.js
      @@ -12,7 +12,7 @@ import $ from 'jquery';
       
       import { Binder } from '../../lib/binder';
       import { positionTooltip } from './position_tooltip';
      -import theme from '@elastic/eui/dist/eui_theme_light.json';
      +import { euiLightVars as theme } from '@kbn/ui-shared-deps-src/theme';
       
       let allContents = [];
       
      diff --git a/src/plugins/vis_types/xy/public/editor/components/options/point_series/point_series.mocks.ts b/src/plugins/vis_types/xy/public/editor/components/options/point_series/point_series.mocks.ts
      index e51b47bc4c7fa..b01a04c162375 100644
      --- a/src/plugins/vis_types/xy/public/editor/components/options/point_series/point_series.mocks.ts
      +++ b/src/plugins/vis_types/xy/public/editor/components/options/point_series/point_series.mocks.ts
      @@ -389,7 +389,7 @@ export const getVis = (bucketType: string) => {
                     labels: {
                       show: true,
                       rotate: 0,
      -                filter: false,
      +                filter: true,
                       truncate: 100,
                     },
                     title: {
      @@ -822,7 +822,7 @@ export const getStateParams = (type: string, thresholdPanelOn: boolean) => {
               labels: {
                 show: true,
                 rotate: 0,
      -          filter: false,
      +          filter: true,
                 truncate: 100,
               },
               title: {
      diff --git a/src/plugins/vis_types/xy/public/mocks.ts b/src/plugins/vis_types/xy/public/mocks.ts
      index bb74035485723..6c0de8de1ac36 100644
      --- a/src/plugins/vis_types/xy/public/mocks.ts
      +++ b/src/plugins/vis_types/xy/public/mocks.ts
      @@ -118,7 +118,7 @@ export const visParamsWithTwoYAxes = {
             },
             labels: {
               type: 'label',
      -        filter: false,
      +        filter: true,
               rotate: 0,
               show: true,
               truncate: 100,
      @@ -138,7 +138,7 @@ export const visParamsWithTwoYAxes = {
               mode: 'normal',
             },
             labels: {
      -        filter: false,
      +        filter: true,
               rotate: 0,
               show: true,
               truncate: 100,
      diff --git a/src/plugins/vis_types/xy/public/utils/get_legend_actions.tsx b/src/plugins/vis_types/xy/public/utils/get_legend_actions.tsx
      index 98ace7dd57a39..d52e3a457f8e9 100644
      --- a/src/plugins/vis_types/xy/public/utils/get_legend_actions.tsx
      +++ b/src/plugins/vis_types/xy/public/utils/get_legend_actions.tsx
      @@ -10,7 +10,12 @@ import React, { useState, useEffect, useMemo } from 'react';
       
       import { i18n } from '@kbn/i18n';
       import { EuiContextMenuPanelDescriptor, EuiIcon, EuiPopover, EuiContextMenu } from '@elastic/eui';
      -import { LegendAction, XYChartSeriesIdentifier, SeriesName } from '@elastic/charts';
      +import {
      +  LegendAction,
      +  XYChartSeriesIdentifier,
      +  SeriesName,
      +  useLegendAction,
      +} from '@elastic/charts';
       
       import { ClickTriggerEvent } from '../../../../charts/public';
       
      @@ -25,6 +30,7 @@ export const getLegendActions = (
           const [isfilterable, setIsfilterable] = useState(false);
           const series = xySeries as XYChartSeriesIdentifier;
           const filterData = useMemo(() => getFilterEventData(series), [series]);
      +    const [ref, onClose] = useLegendAction();
       
           useEffect(() => {
             (async () => setIsfilterable(await canFilter(filterData)))();
      @@ -69,6 +75,7 @@ export const getLegendActions = (
           const Button = (
             
      setPopoverOpen(false)} + closePopover={() => { + setPopoverOpen(false); + onClose(); + }} panelPaddingSize="none" anchorPosition="upLeft" title={i18n.translate('visTypeXy.legend.filterOptionsLegend', { diff --git a/src/plugins/vis_types/xy/public/vis_types/area.ts b/src/plugins/vis_types/xy/public/vis_types/area.ts index 3ff840f1817e9..a140164cf2eb8 100644 --- a/src/plugins/vis_types/xy/public/vis_types/area.ts +++ b/src/plugins/vis_types/xy/public/vis_types/area.ts @@ -74,7 +74,7 @@ export const areaVisTypeDefinition = { labels: { show: true, rotate: LabelRotation.Horizontal, - filter: false, + filter: true, truncate: 100, }, title: { diff --git a/src/plugins/vis_types/xy/public/vis_types/histogram.ts b/src/plugins/vis_types/xy/public/vis_types/histogram.ts index dd65d6f31cb80..c9d17c8ed4501 100644 --- a/src/plugins/vis_types/xy/public/vis_types/histogram.ts +++ b/src/plugins/vis_types/xy/public/vis_types/histogram.ts @@ -76,7 +76,7 @@ export const histogramVisTypeDefinition = { labels: { show: true, rotate: LabelRotation.Horizontal, - filter: false, + filter: true, truncate: 100, }, title: { diff --git a/src/plugins/vis_types/xy/public/vis_types/horizontal_bar.ts b/src/plugins/vis_types/xy/public/vis_types/horizontal_bar.ts index c8494024d1d0a..f6d2a6e0e429a 100644 --- a/src/plugins/vis_types/xy/public/vis_types/horizontal_bar.ts +++ b/src/plugins/vis_types/xy/public/vis_types/horizontal_bar.ts @@ -56,7 +56,7 @@ export const horizontalBarVisTypeDefinition = { labels: { show: true, rotate: LabelRotation.Horizontal, - filter: false, + filter: true, truncate: 200, }, title: {}, diff --git a/src/plugins/vis_types/xy/public/vis_types/line.ts b/src/plugins/vis_types/xy/public/vis_types/line.ts index 08e17f7e97d46..3b6c9dc1a2084 100644 --- a/src/plugins/vis_types/xy/public/vis_types/line.ts +++ b/src/plugins/vis_types/xy/public/vis_types/line.ts @@ -74,7 +74,7 @@ export const lineVisTypeDefinition = { labels: { show: true, rotate: LabelRotation.Horizontal, - filter: false, + filter: true, truncate: 100, }, title: { diff --git a/test/api_integration/apis/console/proxy_route.ts b/test/api_integration/apis/console/proxy_route.ts index d8a5f57a41a6e..a208ef405306f 100644 --- a/test/api_integration/apis/console/proxy_route.ts +++ b/test/api_integration/apis/console/proxy_route.ts @@ -12,7 +12,8 @@ import { FtrProviderContext } from '../../ftr_provider_context'; export default function ({ getService }: FtrProviderContext) { const supertest = getService('supertest'); - describe('POST /api/console/proxy', () => { + // Failing: See https://github.com/elastic/kibana/issues/117674 + describe.skip('POST /api/console/proxy', () => { describe('system indices behavior', () => { it('returns warning header when making requests to .kibana index', async () => { return await supertest diff --git a/test/api_integration/apis/custom_integration/integrations.ts b/test/api_integration/apis/custom_integration/integrations.ts index 0784a86e4b546..036eb2ef33c78 100644 --- a/test/api_integration/apis/custom_integration/integrations.ts +++ b/test/api_integration/apis/custom_integration/integrations.ts @@ -22,7 +22,7 @@ export default function ({ getService }: FtrProviderContext) { expect(resp.body).to.be.an('array'); - expect(resp.body.length).to.be(33); + expect(resp.body.length).to.be(34); // Test for sample data card expect(resp.body.findIndex((c: { id: string }) => c.id === 'sample_data_all')).to.be.above( @@ -40,7 +40,7 @@ export default function ({ getService }: FtrProviderContext) { expect(resp.body).to.be.an('array'); - expect(resp.body.length).to.be.above(109); // at least the beats + apm + expect(resp.body.length).to.be(109); // the beats }); }); }); diff --git a/test/api_integration/apis/index_pattern_field_editor/field_preview.ts b/test/api_integration/apis/index_pattern_field_editor/field_preview.ts index 7123be1deb18a..c687f3094b6fd 100644 --- a/test/api_integration/apis/index_pattern_field_editor/field_preview.ts +++ b/test/api_integration/apis/index_pattern_field_editor/field_preview.ts @@ -8,6 +8,7 @@ import expect from '@kbn/expect'; +import { getErrorCodeFromErrorReason } from '../../../../src/plugins/index_pattern_field_editor/public/lib/runtime_field_validation'; import { FtrProviderContext } from '../../ftr_provider_context'; import { API_BASE_PATH } from './constants'; @@ -140,5 +141,26 @@ export default function ({ getService }: FtrProviderContext) { .expect(400); }); }); + + describe('Error messages', () => { + // As ES does not return error codes we will add a test to make sure its error message string + // does not change overtime as we rely on it to extract our own error code. + // If this test fail we'll need to update the "getErrorCodeFromErrorReason()" handler + it('should detect a script casting error', async () => { + const { body: response } = await supertest + .post(`${API_BASE_PATH}/field_preview`) + .send({ + script: { source: 'emit(123)' }, // We send a long but the type is "keyword" + context: 'keyword_field', + index: INDEX_NAME, + documentId: DOC_ID, + }) + .set('kbn-xsrf', 'xxx'); + + const errorCode = getErrorCodeFromErrorReason(response.error?.caused_by?.reason); + + expect(errorCode).be('CAST_ERROR'); + }); + }); }); } diff --git a/test/api_integration/apis/kql_telemetry/kql_telemetry.ts b/test/api_integration/apis/kql_telemetry/kql_telemetry.ts index 4825b454bc42f..310b99a5fb781 100644 --- a/test/api_integration/apis/kql_telemetry/kql_telemetry.ts +++ b/test/api_integration/apis/kql_telemetry/kql_telemetry.ts @@ -7,7 +7,6 @@ */ import expect from '@kbn/expect'; -import Bluebird from 'bluebird'; import { get } from 'lodash'; import { FtrProviderContext } from '../../ftr_provider_context'; @@ -89,7 +88,7 @@ export default function ({ getService }: FtrProviderContext) { }); it('should only accept literal boolean values for the opt_in POST body param', function () { - return Bluebird.all([ + return Promise.all([ supertest .post('/api/kibana/kql_opt_in_stats') .set('content-type', 'application/json') diff --git a/test/api_integration/apis/saved_objects/index.ts b/test/api_integration/apis/saved_objects/index.ts index 12189bce302b8..44ee3d8d7d76b 100644 --- a/test/api_integration/apis/saved_objects/index.ts +++ b/test/api_integration/apis/saved_objects/index.ts @@ -19,7 +19,6 @@ export default function ({ loadTestFile }: FtrProviderContext) { loadTestFile(require.resolve('./find')); loadTestFile(require.resolve('./get')); loadTestFile(require.resolve('./import')); - loadTestFile(require.resolve('./migrations')); loadTestFile(require.resolve('./resolve')); loadTestFile(require.resolve('./resolve_import_errors')); loadTestFile(require.resolve('./update')); diff --git a/test/api_integration/apis/saved_objects/migrations.ts b/test/api_integration/apis/saved_objects/migrations.ts deleted file mode 100644 index cba62ee51763d..0000000000000 --- a/test/api_integration/apis/saved_objects/migrations.ts +++ /dev/null @@ -1,763 +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. - */ - -/* - * Smokescreen tests for core migration logic - */ - -import uuidv5 from 'uuid/v5'; -import { set } from '@elastic/safer-lodash-set'; -import _ from 'lodash'; -import expect from '@kbn/expect'; -import { SavedObjectsType } from 'src/core/server'; -import { Client as ElasticsearchClient } from '@elastic/elasticsearch'; - -import { - DocumentMigrator, - IndexMigrator, - createMigrationEsClient, -} from '../../../../src/core/server/saved_objects/migrations/core'; -import { SavedObjectsTypeMappingDefinitions } from '../../../../src/core/server/saved_objects/mappings'; - -import { - SavedObjectsSerializer, - SavedObjectTypeRegistry, -} from '../../../../src/core/server/saved_objects'; -import { FtrProviderContext } from '../../ftr_provider_context'; - -const KIBANA_VERSION = '99.9.9'; -const FOO_TYPE: SavedObjectsType = { - name: 'foo', - hidden: false, - namespaceType: 'single', - mappings: { properties: {} }, -}; -const BAR_TYPE: SavedObjectsType = { - name: 'bar', - hidden: false, - namespaceType: 'single', - mappings: { properties: {} }, -}; -const BAZ_TYPE: SavedObjectsType = { - name: 'baz', - hidden: false, - namespaceType: 'single', - mappings: { properties: {} }, -}; -const FLEET_AGENT_EVENT_TYPE: SavedObjectsType = { - name: 'fleet-agent-event', - hidden: false, - namespaceType: 'single', - mappings: { properties: {} }, -}; - -function getLogMock() { - return { - debug() {}, - error() {}, - fatal() {}, - info() {}, - log() {}, - trace() {}, - warn() {}, - get: getLogMock, - }; -} -export default ({ getService }: FtrProviderContext) => { - const esClient = getService('es'); - const esDeleteAllIndices = getService('esDeleteAllIndices'); - - describe('Kibana index migration', () => { - before(() => esDeleteAllIndices('.migrate-*')); - - it('Migrates an existing index that has never been migrated before', async () => { - const index = '.migration-a'; - const originalDocs = [ - { id: 'foo:a', type: 'foo', foo: { name: 'Foo A' } }, - { id: 'foo:e', type: 'foo', foo: { name: 'Fooey' } }, - { id: 'bar:i', type: 'bar', bar: { nomnom: 33 } }, - { id: 'bar:o', type: 'bar', bar: { nomnom: 2 } }, - { id: 'baz:u', type: 'baz', baz: { title: 'Terrific!' } }, - ]; - - const mappingProperties = { - foo: { properties: { name: { type: 'text' } } }, - bar: { properties: { mynum: { type: 'integer' } } }, - } as const; - - const savedObjectTypes: SavedObjectsType[] = [ - { - ...FOO_TYPE, - migrations: { - '1.0.0': (doc) => set(doc, 'attributes.name', doc.attributes.name.toUpperCase()), - }, - }, - { - ...BAR_TYPE, - migrations: { - '1.0.0': (doc) => set(doc, 'attributes.nomnom', doc.attributes.nomnom + 1), - '1.3.0': (doc) => set(doc, 'attributes', { mynum: doc.attributes.nomnom }), - '1.9.0': (doc) => set(doc, 'attributes.mynum', doc.attributes.mynum * 2), - }, - }, - ]; - - await createIndex({ esClient, index, esDeleteAllIndices }); - await createDocs({ esClient, index, docs: originalDocs }); - - // Test that unrelated index templates are unaffected - await esClient.indices.putTemplate({ - name: 'migration_test_a_template', - body: { - index_patterns: ['migration_test_a'], - mappings: { - dynamic: 'strict', - properties: { baz: { type: 'text' } }, - }, - }, - }); - - // Test that obsolete index templates get removed - await esClient.indices.putTemplate({ - name: 'migration_a_template', - body: { - index_patterns: [index], - mappings: { - dynamic: 'strict', - properties: { baz: { type: 'text' } }, - }, - }, - }); - - const migrationATemplate = await esClient.indices.existsTemplate({ - name: 'migration_a_template', - }); - expect(migrationATemplate).to.be.ok(); - - const result = await migrateIndex({ - esClient, - index, - savedObjectTypes, - mappingProperties, - obsoleteIndexTemplatePattern: 'migration_a*', - }); - - const migrationATemplateAfter = await esClient.indices.existsTemplate({ - name: 'migration_a_template', - }); - - expect(migrationATemplateAfter).not.to.be.ok(); - const migrationTestATemplateAfter = await esClient.indices.existsTemplate({ - name: 'migration_test_a_template', - }); - - expect(migrationTestATemplateAfter).to.be.ok(); - expect(_.omit(result, 'elapsedMs')).to.eql({ - destIndex: '.migration-a_2', - sourceIndex: '.migration-a_1', - status: 'migrated', - }); - - // The docs in the original index are unchanged - expect(await fetchDocs(esClient, `${index}_1`)).to.eql(originalDocs.sort(sortByTypeAndId)); - - // The docs in the alias have been migrated - expect(await fetchDocs(esClient, index)).to.eql([ - { - id: 'bar:i', - type: 'bar', - migrationVersion: { bar: '1.9.0' }, - bar: { mynum: 68 }, - references: [], - coreMigrationVersion: KIBANA_VERSION, - }, - { - id: 'bar:o', - type: 'bar', - migrationVersion: { bar: '1.9.0' }, - bar: { mynum: 6 }, - references: [], - coreMigrationVersion: KIBANA_VERSION, - }, - { - id: 'baz:u', - type: 'baz', - baz: { title: 'Terrific!' }, - references: [], - coreMigrationVersion: KIBANA_VERSION, - }, - { - id: 'foo:a', - type: 'foo', - migrationVersion: { foo: '1.0.0' }, - foo: { name: 'FOO A' }, - references: [], - coreMigrationVersion: KIBANA_VERSION, - }, - { - id: 'foo:e', - type: 'foo', - migrationVersion: { foo: '1.0.0' }, - foo: { name: 'FOOEY' }, - references: [], - coreMigrationVersion: KIBANA_VERSION, - }, - ]); - }); - - it('migrates a previously migrated index, if migrations change', async () => { - const index = '.migration-b'; - const originalDocs = [ - { id: 'foo:a', type: 'foo', foo: { name: 'Foo A' } }, - { id: 'foo:e', type: 'foo', foo: { name: 'Fooey' } }, - { id: 'bar:i', type: 'bar', bar: { nomnom: 33 } }, - { id: 'bar:o', type: 'bar', bar: { nomnom: 2 } }, - ]; - - const mappingProperties = { - foo: { properties: { name: { type: 'text' } } }, - bar: { properties: { mynum: { type: 'integer' } } }, - } as const; - - let savedObjectTypes: SavedObjectsType[] = [ - { - ...FOO_TYPE, - migrations: { - '1.0.0': (doc) => set(doc, 'attributes.name', doc.attributes.name.toUpperCase()), - }, - }, - { - ...BAR_TYPE, - migrations: { - '1.0.0': (doc) => set(doc, 'attributes.nomnom', doc.attributes.nomnom + 1), - '1.3.0': (doc) => set(doc, 'attributes', { mynum: doc.attributes.nomnom }), - '1.9.0': (doc) => set(doc, 'attributes.mynum', doc.attributes.mynum * 2), - }, - }, - ]; - - await createIndex({ esClient, index, esDeleteAllIndices }); - await createDocs({ esClient, index, docs: originalDocs }); - - await migrateIndex({ esClient, index, savedObjectTypes, mappingProperties }); - - // @ts-expect-error name doesn't exist on mynum type - mappingProperties.bar.properties.name = { type: 'keyword' }; - savedObjectTypes = [ - { - ...FOO_TYPE, - migrations: { - '2.0.1': (doc) => set(doc, 'attributes.name', `${doc.attributes.name}v2`), - }, - }, - { - ...BAR_TYPE, - migrations: { - '2.3.4': (doc) => set(doc, 'attributes.name', `NAME ${doc.id}`), - }, - }, - ]; - - await migrateIndex({ esClient, index, savedObjectTypes, mappingProperties }); - - // The index for the initial migration has not been destroyed... - expect(await fetchDocs(esClient, `${index}_2`)).to.eql([ - { - id: 'bar:i', - type: 'bar', - migrationVersion: { bar: '1.9.0' }, - bar: { mynum: 68 }, - references: [], - coreMigrationVersion: KIBANA_VERSION, - }, - { - id: 'bar:o', - type: 'bar', - migrationVersion: { bar: '1.9.0' }, - bar: { mynum: 6 }, - references: [], - coreMigrationVersion: KIBANA_VERSION, - }, - { - id: 'foo:a', - type: 'foo', - migrationVersion: { foo: '1.0.0' }, - foo: { name: 'FOO A' }, - references: [], - coreMigrationVersion: KIBANA_VERSION, - }, - { - id: 'foo:e', - type: 'foo', - migrationVersion: { foo: '1.0.0' }, - foo: { name: 'FOOEY' }, - references: [], - coreMigrationVersion: KIBANA_VERSION, - }, - ]); - - // The docs were migrated again... - expect(await fetchDocs(esClient, index)).to.eql([ - { - id: 'bar:i', - type: 'bar', - migrationVersion: { bar: '2.3.4' }, - bar: { mynum: 68, name: 'NAME i' }, - references: [], - coreMigrationVersion: KIBANA_VERSION, - }, - { - id: 'bar:o', - type: 'bar', - migrationVersion: { bar: '2.3.4' }, - bar: { mynum: 6, name: 'NAME o' }, - references: [], - coreMigrationVersion: KIBANA_VERSION, - }, - { - id: 'foo:a', - type: 'foo', - migrationVersion: { foo: '2.0.1' }, - foo: { name: 'FOO Av2' }, - references: [], - coreMigrationVersion: KIBANA_VERSION, - }, - { - id: 'foo:e', - type: 'foo', - migrationVersion: { foo: '2.0.1' }, - foo: { name: 'FOOEYv2' }, - references: [], - coreMigrationVersion: KIBANA_VERSION, - }, - ]); - }); - - it('drops fleet-agent-event saved object types when doing a migration', async () => { - const index = '.migration-b'; - const originalDocs = [ - { - id: 'fleet-agent-event:a', - type: 'fleet-agent-event', - 'fleet-agent-event': { name: 'Foo A' }, - }, - { - id: 'fleet-agent-event:e', - type: 'fleet-agent-event', - 'fleet-agent-event': { name: 'Fooey' }, - }, - { id: 'bar:i', type: 'bar', bar: { nomnom: 33 } }, - { id: 'bar:o', type: 'bar', bar: { nomnom: 2 } }, - ]; - - const mappingProperties = { - 'fleet-agent-event': { properties: { name: { type: 'text' } } }, - bar: { properties: { mynum: { type: 'integer' } } }, - } as const; - - let savedObjectTypes: SavedObjectsType[] = [ - FLEET_AGENT_EVENT_TYPE, - { - ...BAR_TYPE, - migrations: { - '1.0.0': (doc) => set(doc, 'attributes.nomnom', doc.attributes.nomnom + 1), - '1.3.0': (doc) => set(doc, 'attributes', { mynum: doc.attributes.nomnom }), - '1.9.0': (doc) => set(doc, 'attributes.mynum', doc.attributes.mynum * 2), - }, - }, - ]; - - await createIndex({ esClient, index, esDeleteAllIndices }); - await createDocs({ esClient, index, docs: originalDocs }); - - await migrateIndex({ esClient, index, savedObjectTypes, mappingProperties }); - - // @ts-expect-error name doesn't exist on mynum type - mappingProperties.bar.properties.name = { type: 'keyword' }; - savedObjectTypes = [ - FLEET_AGENT_EVENT_TYPE, - { - ...BAR_TYPE, - migrations: { - '2.3.4': (doc) => set(doc, 'attributes.name', `NAME ${doc.id}`), - }, - }, - ]; - - await migrateIndex({ esClient, index, savedObjectTypes, mappingProperties }); - - // Assert that fleet-agent-events were dropped - expect(await fetchDocs(esClient, index)).to.eql([ - { - id: 'bar:i', - type: 'bar', - migrationVersion: { bar: '2.3.4' }, - bar: { mynum: 68, name: 'NAME i' }, - references: [], - coreMigrationVersion: KIBANA_VERSION, - }, - { - id: 'bar:o', - type: 'bar', - migrationVersion: { bar: '2.3.4' }, - bar: { mynum: 6, name: 'NAME o' }, - references: [], - coreMigrationVersion: KIBANA_VERSION, - }, - ]); - }); - - it('Coordinates migrations across the Kibana cluster', async () => { - const index = '.migration-c'; - const originalDocs = [{ id: 'foo:lotr', type: 'foo', foo: { name: 'Lord of the Rings' } }]; - - const mappingProperties = { - foo: { properties: { name: { type: 'text' } } }, - } as const; - - const savedObjectTypes: SavedObjectsType[] = [ - { - ...FOO_TYPE, - migrations: { - '1.0.0': (doc) => set(doc, 'attributes.name', 'LOTR'), - }, - }, - ]; - - await createIndex({ esClient, index, esDeleteAllIndices }); - await createDocs({ esClient, index, docs: originalDocs }); - - const result = await Promise.all([ - migrateIndex({ esClient, index, savedObjectTypes, mappingProperties }), - migrateIndex({ esClient, index, savedObjectTypes, mappingProperties }), - ]); - - // The polling instance and the migrating instance should both - // return a similar migration result. - expect( - result - // @ts-expect-error destIndex exists only on MigrationResult status: 'migrated'; - .map(({ status, destIndex }) => ({ status, destIndex })) - .sort(({ destIndex: a }, { destIndex: b }) => - // sort by destIndex in ascending order, keeping falsy values at the end - (a && !b) || a < b ? -1 : (!a && b) || a > b ? 1 : 0 - ) - ).to.eql([ - { status: 'migrated', destIndex: '.migration-c_2' }, - { status: 'skipped', destIndex: undefined }, - ]); - - const body = await esClient.cat.indices({ index: '.migration-c*', format: 'json' }); - // It only created the original and the dest - expect(_.map(body, 'index').sort()).to.eql(['.migration-c_1', '.migration-c_2']); - - // The docs in the original index are unchanged - expect(await fetchDocs(esClient, `${index}_1`)).to.eql([ - { id: 'foo:lotr', type: 'foo', foo: { name: 'Lord of the Rings' } }, - ]); - - // The docs in the alias have been migrated - expect(await fetchDocs(esClient, index)).to.eql([ - { - id: 'foo:lotr', - type: 'foo', - migrationVersion: { foo: '1.0.0' }, - foo: { name: 'LOTR' }, - references: [], - coreMigrationVersion: KIBANA_VERSION, - }, - ]); - }); - - it('Correctly applies reference transforms and conversion transforms', async () => { - const index = '.migration-d'; - const originalDocs = [ - { id: 'foo:1', type: 'foo', foo: { name: 'Foo 1 default' } }, - { id: 'spacex:foo:1', type: 'foo', foo: { name: 'Foo 1 spacex' }, namespace: 'spacex' }, - { - id: 'bar:1', - type: 'bar', - bar: { nomnom: 1 }, - references: [{ type: 'foo', id: '1', name: 'Foo 1 default' }], - }, - { - id: 'spacex:bar:1', - type: 'bar', - bar: { nomnom: 2 }, - references: [{ type: 'foo', id: '1', name: 'Foo 1 spacex' }], - namespace: 'spacex', - }, - { - id: 'baz:1', - type: 'baz', - baz: { title: 'Baz 1 default' }, - references: [{ type: 'bar', id: '1', name: 'Bar 1 default' }], - }, - { - id: 'spacex:baz:1', - type: 'baz', - baz: { title: 'Baz 1 spacex' }, - references: [{ type: 'bar', id: '1', name: 'Bar 1 spacex' }], - namespace: 'spacex', - }, - ]; - - const mappingProperties = { - foo: { properties: { name: { type: 'text' } } }, - bar: { properties: { nomnom: { type: 'integer' } } }, - baz: { properties: { title: { type: 'keyword' } } }, - } as const; - - const savedObjectTypes: SavedObjectsType[] = [ - { - ...FOO_TYPE, - namespaceType: 'multiple', - convertToMultiNamespaceTypeVersion: '1.0.0', - }, - { - ...BAR_TYPE, - namespaceType: 'multiple-isolated', - convertToMultiNamespaceTypeVersion: '2.0.0', - }, - BAZ_TYPE, // must be registered for reference transforms to be applied to objects of this type - ]; - - await createIndex({ esClient, index, esDeleteAllIndices }); - await createDocs({ esClient, index, docs: originalDocs }); - - await migrateIndex({ - esClient, - index, - savedObjectTypes, - mappingProperties, - obsoleteIndexTemplatePattern: 'migration_a*', - }); - - // The docs in the original index are unchanged - expect(await fetchDocs(esClient, `${index}_1`)).to.eql(originalDocs.sort(sortByTypeAndId)); - - // The docs in the alias have been migrated - const migratedDocs = await fetchDocs(esClient, index); - - // each newly converted multi-namespace object in a non-default space has its ID deterministically regenerated, and a legacy-url-alias - // object is created which links the old ID to the new ID - const newFooId = uuidv5('spacex:foo:1', uuidv5.DNS); - const newBarId = uuidv5('spacex:bar:1', uuidv5.DNS); - - expect(migratedDocs).to.eql( - [ - { - id: 'foo:1', - type: 'foo', - foo: { name: 'Foo 1 default' }, - references: [], - namespaces: ['default'], - migrationVersion: { foo: '1.0.0' }, - coreMigrationVersion: KIBANA_VERSION, - }, - { - id: `foo:${newFooId}`, - type: 'foo', - foo: { name: 'Foo 1 spacex' }, - references: [], - namespaces: ['spacex'], - originId: '1', - migrationVersion: { foo: '1.0.0' }, - coreMigrationVersion: KIBANA_VERSION, - }, - { - // new object - id: 'legacy-url-alias:spacex:foo:1', - type: 'legacy-url-alias', - 'legacy-url-alias': { - sourceId: '1', - targetId: newFooId, - targetNamespace: 'spacex', - targetType: 'foo', - }, - migrationVersion: {}, - references: [], - coreMigrationVersion: KIBANA_VERSION, - }, - { - id: 'bar:1', - type: 'bar', - bar: { nomnom: 1 }, - references: [{ type: 'foo', id: '1', name: 'Foo 1 default' }], - namespaces: ['default'], - migrationVersion: { bar: '2.0.0' }, - coreMigrationVersion: KIBANA_VERSION, - }, - { - id: `bar:${newBarId}`, - type: 'bar', - bar: { nomnom: 2 }, - references: [{ type: 'foo', id: newFooId, name: 'Foo 1 spacex' }], - namespaces: ['spacex'], - originId: '1', - migrationVersion: { bar: '2.0.0' }, - coreMigrationVersion: KIBANA_VERSION, - }, - { - // new object - id: 'legacy-url-alias:spacex:bar:1', - type: 'legacy-url-alias', - 'legacy-url-alias': { - sourceId: '1', - targetId: newBarId, - targetNamespace: 'spacex', - targetType: 'bar', - }, - migrationVersion: {}, - references: [], - coreMigrationVersion: KIBANA_VERSION, - }, - { - id: 'baz:1', - type: 'baz', - baz: { title: 'Baz 1 default' }, - references: [{ type: 'bar', id: '1', name: 'Bar 1 default' }], - coreMigrationVersion: KIBANA_VERSION, - }, - { - id: 'spacex:baz:1', - type: 'baz', - baz: { title: 'Baz 1 spacex' }, - references: [{ type: 'bar', id: newBarId, name: 'Bar 1 spacex' }], - namespace: 'spacex', - coreMigrationVersion: KIBANA_VERSION, - }, - ].sort(sortByTypeAndId) - ); - }); - }); -}; - -async function createIndex({ - esClient, - index, - esDeleteAllIndices, -}: { - esClient: ElasticsearchClient; - index: string; - esDeleteAllIndices: (pattern: string) => Promise; -}) { - await esDeleteAllIndices(`${index}*`); - - const properties = { - type: { type: 'keyword' }, - foo: { properties: { name: { type: 'keyword' } } }, - bar: { properties: { nomnom: { type: 'integer' } } }, - baz: { properties: { title: { type: 'keyword' } } }, - 'legacy-url-alias': { - properties: { - targetNamespace: { type: 'text' }, - targetType: { type: 'text' }, - targetId: { type: 'text' }, - lastResolved: { type: 'date' }, - resolveCounter: { type: 'integer' }, - disabled: { type: 'boolean' }, - }, - }, - namespace: { type: 'keyword' }, - namespaces: { type: 'keyword' }, - originId: { type: 'keyword' }, - references: { - type: 'nested', - properties: { - name: { type: 'keyword' }, - type: { type: 'keyword' }, - id: { type: 'keyword' }, - }, - }, - coreMigrationVersion: { - type: 'keyword', - }, - } as const; - await esClient.indices.create({ - index, - body: { mappings: { dynamic: 'strict', properties } }, - }); -} - -async function createDocs({ - esClient, - index, - docs, -}: { - esClient: ElasticsearchClient; - index: string; - docs: any[]; -}) { - await esClient.bulk({ - body: docs.reduce((acc, doc) => { - acc.push({ index: { _id: doc.id, _index: index } }); - acc.push(_.omit(doc, 'id')); - return acc; - }, []), - }); - await esClient.indices.refresh({ index }); -} - -async function migrateIndex({ - esClient, - index, - savedObjectTypes, - mappingProperties, - obsoleteIndexTemplatePattern, -}: { - esClient: ElasticsearchClient; - index: string; - savedObjectTypes: SavedObjectsType[]; - mappingProperties: SavedObjectsTypeMappingDefinitions; - obsoleteIndexTemplatePattern?: string; -}) { - const typeRegistry = new SavedObjectTypeRegistry(); - savedObjectTypes.forEach((type) => typeRegistry.registerType(type)); - - const documentMigrator = new DocumentMigrator({ - kibanaVersion: KIBANA_VERSION, - typeRegistry, - minimumConvertVersion: '0.0.0', // bypass the restriction of a minimum version of 8.0.0 for these integration tests - log: getLogMock(), - }); - - documentMigrator.prepareMigrations(); - - const migrator = new IndexMigrator({ - client: createMigrationEsClient(esClient, getLogMock()), - documentMigrator, - index, - kibanaVersion: KIBANA_VERSION, - obsoleteIndexTemplatePattern, - mappingProperties, - batchSize: 10, - log: getLogMock(), - setStatus: () => {}, - pollInterval: 50, - scrollDuration: '5m', - serializer: new SavedObjectsSerializer(typeRegistry), - }); - - return await migrator.migrate(); -} - -async function fetchDocs(esClient: ElasticsearchClient, index: string) { - const body = await esClient.search({ index }); - - return body.hits.hits - .map((h) => ({ - ...h._source, - id: h._id, - })) - .sort(sortByTypeAndId); -} - -function sortByTypeAndId(a: { type: string; id: string }, b: { type: string; id: string }) { - return a.type.localeCompare(b.type) || a.id.localeCompare(b.id); -} diff --git a/test/examples/embeddables/dashboard.ts b/test/examples/embeddables/dashboard.ts index 5c255b136c666..23f221c40d4da 100644 --- a/test/examples/embeddables/dashboard.ts +++ b/test/examples/embeddables/dashboard.ts @@ -97,11 +97,10 @@ export default function ({ getService, getPageObjects }: PluginFunctionalProvide const pieChart = getService('pieChart'); const dashboardExpect = getService('dashboardExpect'); const elasticChart = getService('elasticChart'); - const PageObjects = getPageObjects(['common', 'visChart']); + const PageObjects = getPageObjects(['common', 'visChart', 'dashboard']); const monacoEditor = getService('monacoEditor'); - // FLAKY: https://github.com/elastic/kibana/issues/116414 - describe.skip('dashboard container', () => { + describe('dashboard container', () => { before(async () => { await esArchiver.loadIfNeeded('test/functional/fixtures/es_archiver/dashboard/current/data'); await esArchiver.loadIfNeeded( @@ -109,6 +108,8 @@ export default function ({ getService, getPageObjects }: PluginFunctionalProvide ); await PageObjects.common.navigateToApp('dashboardEmbeddableExamples'); await testSubjects.click('dashboardEmbeddableByValue'); + await PageObjects.dashboard.waitForRenderComplete(); + await updateInput(JSON.stringify(testDashboardInput, null, 4)); }); diff --git a/test/examples/index_pattern_field_editor_example/index.ts b/test/examples/index_pattern_field_editor_example/index.ts index 0cd23a33c8476..06460a268624f 100644 --- a/test/examples/index_pattern_field_editor_example/index.ts +++ b/test/examples/index_pattern_field_editor_example/index.ts @@ -14,6 +14,7 @@ export default function ({ getPageObjects, loadTestFile, }: PluginFunctionalProviderContext) { + const esArchiver = getService('esArchiver'); const browser = getService('browser'); const es = getService('es'); const PageObjects = getPageObjects(['common', 'header', 'settings']); @@ -21,6 +22,7 @@ export default function ({ describe('index pattern field editor example', function () { this.tags('ciGroup2'); before(async () => { + await esArchiver.emptyKibanaIndex(); await browser.setWindowSize(1300, 900); await es.transport.request({ path: '/blogs/_doc', diff --git a/test/examples/index_pattern_field_editor_example/index_pattern_field_editor_example.ts b/test/examples/index_pattern_field_editor_example/index_pattern_field_editor_example.ts index fa4308ae72883..5744c8e64f5c1 100644 --- a/test/examples/index_pattern_field_editor_example/index_pattern_field_editor_example.ts +++ b/test/examples/index_pattern_field_editor_example/index_pattern_field_editor_example.ts @@ -12,8 +12,7 @@ import { PluginFunctionalProviderContext } from 'test/plugin_functional/services export default function ({ getService }: PluginFunctionalProviderContext) { const testSubjects = getService('testSubjects'); - // FAILING: https://github.com/elastic/kibana/issues/116463 - describe.skip('', () => { + describe('', () => { it('finds an index pattern', async () => { await testSubjects.existOrFail('indexPatternTitle'); }); diff --git a/test/functional/apps/context/_discover_navigation.ts b/test/functional/apps/context/_discover_navigation.ts index 60745bd64b8be..9a807293c8148 100644 --- a/test/functional/apps/context/_discover_navigation.ts +++ b/test/functional/apps/context/_discover_navigation.ts @@ -55,20 +55,39 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await kibanaServer.uiSettings.replace({}); }); - it('should open the context view with the selected document as anchor', async () => { + it('should open the context view with the selected document as anchor and allows selecting next anchor', async () => { + /** + * Helper function to get the first timestamp of the document table + * @param isAnchorRow - determins if just the anchor row of context should be selected + */ + const getTimestamp = async (isAnchorRow: boolean = false) => { + const contextFields = await docTable.getFields({ isAnchorRow }); + return contextFields[0][0]; + }; + // get the timestamp of the first row + + const firstDiscoverTimestamp = await getTimestamp(); + // check the anchor timestamp in the context view await retry.waitFor('selected document timestamp matches anchor timestamp ', async () => { - // get the timestamp of the first row - const discoverFields = await docTable.getFields(); - const firstTimestamp = discoverFields[0][0]; - // navigate to the context view await docTable.clickRowToggle({ rowIndex: 0 }); const rowActions = await docTable.getRowActions({ rowIndex: 0 }); await rowActions[0].click(); - const contextFields = await docTable.getFields({ isAnchorRow: true }); - const anchorTimestamp = contextFields[0][0]; - return anchorTimestamp === firstTimestamp; + await PageObjects.context.waitUntilContextLoadingHasFinished(); + const anchorTimestamp = await getTimestamp(true); + return anchorTimestamp === firstDiscoverTimestamp; + }); + + await retry.waitFor('next anchor timestamp matches previous anchor timestamp', async () => { + // get the timestamp of the first row + const firstContextTimestamp = await getTimestamp(false); + await docTable.clickRowToggle({ rowIndex: 0 }); + const rowActions = await docTable.getRowActions({ rowIndex: 0 }); + await rowActions[0].click(); + await PageObjects.context.waitUntilContextLoadingHasFinished(); + const anchorTimestamp = await getTimestamp(true); + return anchorTimestamp === firstContextTimestamp; }); }); diff --git a/test/functional/apps/dashboard/dashboard_filtering.ts b/test/functional/apps/dashboard/dashboard_filtering.ts index acb04b4946fad..73a53281df16d 100644 --- a/test/functional/apps/dashboard/dashboard_filtering.ts +++ b/test/functional/apps/dashboard/dashboard_filtering.ts @@ -28,8 +28,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const dashboardPanelActions = getService('dashboardPanelActions'); const PageObjects = getPageObjects(['common', 'dashboard', 'header', 'visualize', 'timePicker']); - // Failing: See https://github.com/elastic/kibana/issues/92522 - describe.skip('dashboard filtering', function () { + describe('dashboard filtering', function () { this.tags('includeFirefox'); const populateDashboard = async () => { @@ -115,13 +114,12 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { }); it('saved search is filtered', async () => { - await dashboardExpect.savedSearchRowCount(0); + await dashboardExpect.savedSearchRowsMissing(); }); - // TODO: Uncomment once https://github.com/elastic/kibana/issues/22561 is fixed - // it('timelion is filtered', async () => { - // await dashboardExpect.timelionLegendCount(0); - // }); + it('timelion is filtered', async () => { + await dashboardExpect.timelionLegendCount(0); + }); it('vega is filtered', async () => { await dashboardExpect.vegaTextsDoNotExist(['5,000']); @@ -177,13 +175,12 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { }); it('saved search is filtered', async () => { - await dashboardExpect.savedSearchRowCount(0); + await dashboardExpect.savedSearchRowsMissing(); }); - // TODO: Uncomment once https://github.com/elastic/kibana/issues/22561 is fixed - // it('timelion is filtered', async () => { - // await dashboardExpect.timelionLegendCount(0); - // }); + it('timelion is filtered', async () => { + await dashboardExpect.timelionLegendCount(0); + }); it('vega is filtered', async () => { await dashboardExpect.vegaTextsDoNotExist(['5,000']); @@ -206,7 +203,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { }); it('area, bar and heatmap charts', async () => { - await dashboardExpect.seriesElementCount(3); + await dashboardExpect.seriesElementCount(2); }); it('data tables', async () => { @@ -238,7 +235,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { }); it('saved searches', async () => { - await dashboardExpect.savedSearchRowCount(1); + await dashboardExpect.savedSearchRowsExist(); }); it('vega', async () => { diff --git a/test/functional/apps/management/_index_pattern_create_delete.js b/test/functional/apps/management/_index_pattern_create_delete.js index acb554fa7310b..62612ad5a9080 100644 --- a/test/functional/apps/management/_index_pattern_create_delete.js +++ b/test/functional/apps/management/_index_pattern_create_delete.js @@ -9,6 +9,7 @@ import expect from '@kbn/expect'; export default function ({ getService, getPageObjects }) { + const esArchiver = getService('esArchiver'); const kibanaServer = getService('kibanaServer'); const browser = getService('browser'); const log = getService('log'); @@ -18,16 +19,11 @@ export default function ({ getService, getPageObjects }) { const PageObjects = getPageObjects(['settings', 'common', 'header']); describe('creating and deleting default index', function describeIndexTests() { - before(function () { - // Delete .kibana index and then wait for Kibana to re-create it - return kibanaServer.uiSettings - .replace({}) - .then(function () { - return PageObjects.settings.navigateTo(); - }) - .then(function () { - return PageObjects.settings.clickKibanaIndexPatterns(); - }); + before(async function () { + await esArchiver.emptyKibanaIndex(); + await kibanaServer.uiSettings.replace({}); + await PageObjects.settings.navigateTo(); + await PageObjects.settings.clickKibanaIndexPatterns(); }); describe('can open and close editor', function () { @@ -39,17 +35,16 @@ export default function ({ getService, getPageObjects }) { }); }); - // FLAKY: https://github.com/elastic/kibana/issues/107831 - describe.skip('validation', function () { + describe('validation', function () { it('can display errors', async function () { await PageObjects.settings.clickAddNewIndexPatternButton(); - await PageObjects.settings.setIndexPatternField('log*'); + await PageObjects.settings.setIndexPatternField('log-fake*'); await (await PageObjects.settings.getSaveIndexPatternButton()).click(); await find.byClassName('euiFormErrorText'); }); it('can resolve errors and submit', async function () { - await PageObjects.settings.selectTimeFieldOption('@timestamp'); + await PageObjects.settings.setIndexPatternField('log*'); await (await PageObjects.settings.getSaveIndexPatternButton()).click(); await PageObjects.settings.removeIndexPattern(); }); diff --git a/test/functional/apps/management/_index_pattern_popularity.js b/test/functional/apps/management/_index_pattern_popularity.js index 0618dd79e272e..1a71e4c5fbc68 100644 --- a/test/functional/apps/management/_index_pattern_popularity.js +++ b/test/functional/apps/management/_index_pattern_popularity.js @@ -33,7 +33,7 @@ export default function ({ getService, getPageObjects }) { }); afterEach(async () => { - await testSubjects.click('closeFlyoutButton'); + await PageObjects.settings.closeIndexPatternFieldEditor(); await PageObjects.settings.removeIndexPattern(); // Cancel saving the popularity change (we didn't make a change in this case, just checking the value) }); @@ -46,7 +46,7 @@ export default function ({ getService, getPageObjects }) { it('should be reset on cancel', async function () { // Cancel saving the popularity change - await testSubjects.click('closeFlyoutButton'); + await PageObjects.settings.closeIndexPatternFieldEditor(); await PageObjects.settings.openControlsByName(fieldName); // check that it is 0 (previous increase was cancelled const popularity = await PageObjects.settings.getPopularity(); diff --git a/test/functional/apps/management/_runtime_fields.js b/test/functional/apps/management/_runtime_fields.js index 09fa924b0b870..3a70df81b55d9 100644 --- a/test/functional/apps/management/_runtime_fields.js +++ b/test/functional/apps/management/_runtime_fields.js @@ -16,8 +16,7 @@ export default function ({ getService, getPageObjects }) { const PageObjects = getPageObjects(['settings']); const testSubjects = getService('testSubjects'); - // FLAKY: https://github.com/elastic/kibana/issues/95376 - describe.skip('runtime fields', function () { + describe('runtime fields', function () { this.tags(['skipFirefox']); before(async function () { @@ -60,7 +59,7 @@ export default function ({ getService, getPageObjects }) { await PageObjects.settings.filterField(fieldName); await testSubjects.click('editFieldFormat'); await PageObjects.settings.setFieldType('Long'); - await PageObjects.settings.changeFieldScript('emit(6);'); + await PageObjects.settings.setFieldScript('emit(6);'); await testSubjects.find('changeWarning'); await PageObjects.settings.clickSaveField(); await PageObjects.settings.confirmSave(); diff --git a/test/functional/apps/management/_scripted_fields_preview.js b/test/functional/apps/management/_scripted_fields_preview.js index 6347531b0cda8..12a6cb9537c8d 100644 --- a/test/functional/apps/management/_scripted_fields_preview.js +++ b/test/functional/apps/management/_scripted_fields_preview.js @@ -13,10 +13,11 @@ export default function ({ getService, getPageObjects }) { const PageObjects = getPageObjects(['settings']); const SCRIPTED_FIELD_NAME = 'myScriptedField'; - // FLAKY: https://github.com/elastic/kibana/issues/89475 - describe.skip('scripted fields preview', () => { + describe('scripted fields preview', () => { before(async function () { await browser.setWindowSize(1200, 800); + await PageObjects.settings.navigateTo(); + await PageObjects.settings.clickKibanaIndexPatterns(); await PageObjects.settings.createIndexPattern(); await PageObjects.settings.navigateTo(); diff --git a/test/functional/apps/management/index.ts b/test/functional/apps/management/index.ts index 4787d7b9ee532..c906697021ecf 100644 --- a/test/functional/apps/management/index.ts +++ b/test/functional/apps/management/index.ts @@ -22,7 +22,7 @@ export default function ({ getService, loadTestFile }: FtrProviderContext) { }); describe('', function () { - this.tags('ciGroup7'); + this.tags('ciGroup9'); loadTestFile(require.resolve('./_create_index_pattern_wizard')); loadTestFile(require.resolve('./_index_pattern_create_delete')); diff --git a/test/functional/apps/visualize/_timelion.ts b/test/functional/apps/visualize/_timelion.ts index afbcba7df5216..f8991e17319bd 100644 --- a/test/functional/apps/visualize/_timelion.ts +++ b/test/functional/apps/visualize/_timelion.ts @@ -265,8 +265,11 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { await monacoEditor.typeCodeEditorValue('.es(index=', 'timelionCodeEditor'); // wait for index patterns will be loaded await common.sleep(500); - const suggestions = await timelion.getSuggestionItemsText(); - expect(suggestions[0].includes('log')).to.eql(true); + // other suggestions might be shown for a short amount of time - retry until metric suggestions show up + await retry.try(async () => { + const suggestions = await timelion.getSuggestionItemsText(); + expect(suggestions[0].includes('log')).to.eql(true); + }); }); it('should show field suggestions for timefield argument when index pattern set', async () => { @@ -275,9 +278,12 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { '.es(index=logstash-*, timefield=', 'timelionCodeEditor' ); - const suggestions = await timelion.getSuggestionItemsText(); - expect(suggestions.length).to.eql(4); - expect(suggestions[0].includes('@timestamp')).to.eql(true); + // other suggestions might be shown for a short amount of time - retry until metric suggestions show up + await retry.try(async () => { + const suggestions = await timelion.getSuggestionItemsText(); + expect(suggestions.length).to.eql(4); + expect(suggestions[0].includes('@timestamp')).to.eql(true); + }); }); it('should show field suggestions for split argument when index pattern set', async () => { @@ -288,9 +294,12 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { ); // wait for split fields to load await common.sleep(300); - const suggestions = await timelion.getSuggestionItemsText(); + // other suggestions might be shown for a short amount of time - retry until metric suggestions show up + await retry.try(async () => { + const suggestions = await timelion.getSuggestionItemsText(); - expect(suggestions[0].includes('@message.raw')).to.eql(true); + expect(suggestions[0].includes('@message.raw')).to.eql(true); + }); }); it('should show field suggestions for metric argument when index pattern set', async () => { diff --git a/test/functional/apps/visualize/_tsvb_time_series.ts b/test/functional/apps/visualize/_tsvb_time_series.ts index 009e4a07cd42a..69cc764c39b21 100644 --- a/test/functional/apps/visualize/_tsvb_time_series.ts +++ b/test/functional/apps/visualize/_tsvb_time_series.ts @@ -361,6 +361,40 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { expect(chartData).to.eql(expectedChartData); }); + describe('Hiding series', () => { + it('should hide series by legend item click', async () => { + await visualBuilder.clickDataTab('timeSeries'); + await visualBuilder.setMetricsGroupByTerms('@tags.raw'); + + let areasCount = (await visualBuilder.getChartItems())?.length; + expect(areasCount).to.be(6); + + await visualBuilder.clickSeriesLegendItem('success'); + await visualBuilder.clickSeriesLegendItem('info'); + await visualBuilder.clickSeriesLegendItem('error'); + + areasCount = (await visualBuilder.getChartItems())?.length; + expect(areasCount).to.be(3); + }); + + it('should keep series hidden after refresh', async () => { + await visualBuilder.clickDataTab('timeSeries'); + await visualBuilder.setMetricsGroupByTerms('extension.raw'); + + let legendNames = await visualBuilder.getLegendNames(); + expect(legendNames).to.eql(['jpg', 'css', 'png', 'gif', 'php']); + + await visualBuilder.clickSeriesLegendItem('png'); + await visualBuilder.clickSeriesLegendItem('php'); + legendNames = await visualBuilder.getLegendNames(); + expect(legendNames).to.eql(['jpg', 'css', 'gif']); + + await visualize.clickRefresh(true); + legendNames = await visualBuilder.getLegendNames(); + expect(legendNames).to.eql(['jpg', 'css', 'gif']); + }); + }); + describe('Query filter', () => { it('should display correct chart data for applied series filter', async () => { const expectedChartData = [ diff --git a/test/functional/apps/visualize/index.ts b/test/functional/apps/visualize/index.ts index 3bc4da0163909..68b95f3521a24 100644 --- a/test/functional/apps/visualize/index.ts +++ b/test/functional/apps/visualize/index.ts @@ -74,8 +74,8 @@ export default function ({ getService, loadTestFile }: FtrProviderContext) { loadTestFile(require.resolve('./_metric_chart')); }); - describe('visualize ciGroup4', function () { - this.tags('ciGroup4'); + describe('visualize ciGroup1', function () { + this.tags('ciGroup1'); loadTestFile(require.resolve('./_pie_chart')); loadTestFile(require.resolve('./_shared_item')); diff --git a/test/functional/fixtures/es_archiver/dashboard/current/kibana/data.json b/test/functional/fixtures/es_archiver/dashboard/current/kibana/data.json index 599ffbadaaea7..45e26f1599b6c 100644 --- a/test/functional/fixtures/es_archiver/dashboard/current/kibana/data.json +++ b/test/functional/fixtures/es_archiver/dashboard/current/kibana/data.json @@ -2316,7 +2316,7 @@ "title": "Filter Bytes Test: tsvb top n with bytes filter", "uiStateJSON": "{}", "version": 1, - "visState": "{\"title\":\"Filter Bytes Test: tsvb top n with bytes filter\",\"type\":\"metrics\",\"params\":{\"id\":\"61ca57f0-469d-11e7-af02-69e470af7417\",\"type\":\"top_n\",\"series\":[{\"id\":\"61ca57f1-469d-11e7-af02-69e470af7417\",\"color\":\"#68BC00\",\"split_mode\":\"filters\",\"metrics\":[{\"id\":\"482d6560-4194-11e8-a461-7d278185cba4\",\"type\":\"avg\",\"field\":\"bytes\"}],\"seperate_axis\":0,\"axis_position\":\"right\",\"formatter\":\"number\",\"chart_type\":\"line\",\"line_width\":1,\"point_size\":1,\"fill\":0.5,\"stacked\":\"none\",\"terms_field\":\"clientip\",\"filter\":{\"query\":\"Filter Bytes Test:>1000\",\"language\":\"lucene\"},\"override_index_pattern\":0,\"series_index_pattern\":\"logstash-*\",\"series_time_field\":\"utc_time\",\"series_interval\":\"1m\",\"value_template\":\"\",\"split_filters\":[{\"filter\":{\"query\":\"Filter Bytes Test:>100\",\"language\":\"lucene\"},\"label\":\"\",\"color\":\"#68BC00\",\"id\":\"39a107e0-4194-11e8-a461-7d278185cba4\"}],\"split_color_mode\":\"gradient\"},{\"id\":\"4fd5b150-4194-11e8-a461-7d278185cba4\",\"color\":\"#68BC00\",\"split_mode\":\"everything\",\"metrics\":[{\"id\":\"4fd5b151-4194-11e8-a461-7d278185cba4\",\"type\":\"avg\",\"field\":\"bytes\"}],\"seperate_axis\":0,\"axis_position\":\"right\",\"formatter\":\"number\",\"chart_type\":\"line\",\"line_width\":1,\"point_size\":1,\"fill\":0.5,\"stacked\":\"none\",\"filter\":{\"query\":\"Filter Bytes Test:>3000\",\"language\":\"lucene\"},\"split_color_mode\":\"gradient\"}],\"time_field\":\"@timestamp\",\"index_pattern\":\"logstash-*\",\"interval\":\"auto\",\"axis_position\":\"left\",\"axis_formatter\":\"number\",\"show_legend\":1,\"show_grid\":1,\"background_color_rules\":[{\"id\":\"06893260-4194-11e8-a461-7d278185cba4\"}],\"bar_color_rules\":[{\"id\":\"36a0e740-4194-11e8-a461-7d278185cba4\"}],\"use_kibana_indexes\":false,\"hide_last_value_indicator\":true},\"aggs\":[]}" + "visState":"{\"title\":\"Filter Bytes Test: tsvb top n with bytes filter\",\"type\":\"metrics\",\"aggs\":[],\"params\":{\"time_range_mode\":\"last_value\",\"id\":\"61ca57f0-469d-11e7-af02-69e470af7417\",\"type\":\"top_n\",\"series\":[{\"time_range_mode\":\"entire_time_range\",\"id\":\"61ca57f1-469d-11e7-af02-69e470af7417\",\"color\":\"#68BC00\",\"split_mode\":\"filters\",\"metrics\":[{\"id\":\"482d6560-4194-11e8-a461-7d278185cba4\",\"type\":\"avg\",\"field\":\"bytes\"}],\"seperate_axis\":0,\"axis_position\":\"right\",\"formatter\":\"number\",\"chart_type\":\"line\",\"line_width\":1,\"point_size\":1,\"fill\":0.5,\"stacked\":\"none\",\"terms_field\":\"clientip\",\"filter\":{\"query\":\"Filter Bytes Test:>1000\",\"language\":\"lucene\"},\"override_index_pattern\":0,\"series_index_pattern\":\"logstash-*\",\"series_time_field\":\"utc_time\",\"series_interval\":\"1m\",\"value_template\":\"\",\"split_filters\":[{\"filter\":{\"query\":\"Filter Bytes Test:>100\",\"language\":\"lucene\"},\"label\":\"\",\"color\":\"#68BC00\",\"id\":\"39a107e0-4194-11e8-a461-7d278185cba4\"}],\"split_color_mode\":\"gradient\"},{\"time_range_mode\":\"entire_time_range\",\"id\":\"4fd5b150-4194-11e8-a461-7d278185cba4\",\"color\":\"#68BC00\",\"split_mode\":\"everything\",\"metrics\":[{\"id\":\"4fd5b151-4194-11e8-a461-7d278185cba4\",\"type\":\"avg\",\"field\":\"bytes\"}],\"seperate_axis\":0,\"axis_position\":\"right\",\"formatter\":\"number\",\"chart_type\":\"line\",\"line_width\":1,\"point_size\":1,\"fill\":0.5,\"stacked\":\"none\",\"filter\":{\"query\":\"Filter Bytes Test:>3000\",\"language\":\"lucene\"},\"split_color_mode\":\"gradient\"}],\"time_field\":\"@timestamp\",\"index_pattern\":\"logstash-*\",\"interval\":\"auto\",\"axis_position\":\"left\",\"axis_formatter\":\"number\",\"show_legend\":1,\"show_grid\":1,\"background_color_rules\":[{\"id\":\"06893260-4194-11e8-a461-7d278185cba4\"}],\"bar_color_rules\":[{\"id\":\"36a0e740-4194-11e8-a461-7d278185cba4\"}],\"use_kibana_indexes\":false,\"hide_last_value_indicator\":true,\"axis_scale\":\"normal\",\"truncate_legend\":1,\"max_lines_legend\":1,\"tooltip_mode\":\"show_all\",\"drop_last_bucket\":1,\"isModelInvalid\":false}}" } }, "type": "_doc" @@ -2379,7 +2379,7 @@ "title": "Filter Bytes Test: tsvb time series with bytes filter split by clientip", "uiStateJSON": "{}", "version": 1, - "visState": "{\"title\":\"Filter Bytes Test: tsvb time series with bytes filter split by clientip\",\"type\":\"metrics\",\"params\":{\"id\":\"61ca57f0-469d-11e7-af02-69e470af7417\",\"type\":\"timeseries\",\"series\":[{\"id\":\"61ca57f1-469d-11e7-af02-69e470af7417\",\"color\":\"#68BC00\",\"split_mode\":\"terms\",\"metrics\":[{\"value\":\"\",\"id\":\"61ca57f2-469d-11e7-af02-69e470af7417\",\"type\":\"sum\",\"field\":\"bytes\"}],\"seperate_axis\":0,\"axis_position\":\"right\",\"formatter\":\"number\",\"chart_type\":\"line\",\"line_width\":1,\"point_size\":1,\"fill\":0.5,\"stacked\":\"none\",\"terms_field\":\"clientip\",\"filter\":{\"query\":\"Filter Bytes Test:>1000\",\"language\":\"lucene\"},\"split_color_mode\":\"gradient\"}],\"time_field\":\"@timestamp\",\"index_pattern\":\"logstash-*\",\"interval\":\"auto\",\"axis_position\":\"left\",\"axis_formatter\":\"number\",\"show_legend\":1,\"show_grid\":1,\"use_kibana_indexes\":false},\"aggs\":[]}" + "visState":"{\"title\":\"Filter Bytes Test: tsvb time series with bytes filter split by clientip\",\"type\":\"metrics\",\"aggs\":[],\"params\":{\"time_range_mode\":\"entire_time_range\",\"id\":\"61ca57f0-469d-11e7-af02-69e470af7417\",\"type\":\"timeseries\",\"series\":[{\"time_range_mode\":\"entire_time_range\",\"id\":\"61ca57f1-469d-11e7-af02-69e470af7417\",\"color\":\"#68BC00\",\"split_mode\":\"terms\",\"metrics\":[{\"value\":\"\",\"id\":\"61ca57f2-469d-11e7-af02-69e470af7417\",\"type\":\"sum\",\"field\":\"bytes\"}],\"seperate_axis\":0,\"axis_position\":\"right\",\"formatter\":\"number\",\"chart_type\":\"line\",\"line_width\":1,\"point_size\":1,\"fill\":0.5,\"stacked\":\"none\",\"terms_field\":\"clientip\",\"filter\":{\"query\":\"Filter Bytes Test:>1000\",\"language\":\"lucene\"},\"split_color_mode\":\"gradient\"}],\"time_field\":\"@timestamp\",\"index_pattern\":\"logstash-*\",\"interval\":\"auto\",\"axis_position\":\"left\",\"axis_formatter\":\"number\",\"show_legend\":1,\"show_grid\":1,\"use_kibana_indexes\":false,\"axis_scale\":\"normal\",\"truncate_legend\":1,\"max_lines_legend\":1,\"tooltip_mode\":\"show_all\",\"drop_last_bucket\":1,\"isModelInvalid\":false}}" } }, "type": "_doc" @@ -2600,45 +2600,6 @@ } } -{ - "type": "doc", - "value": { - "id": "visualization:63983430-4192-11e8-bb13-d53698fb349a", - "index": ".kibana", - "source": { - "coreMigrationVersion": "7.14.0", - "migrationVersion": { - "visualization": "7.14.0" - }, - "references": [ - { - "id": "0bf35f60-3dc9-11e8-8660-4d65aa086b3c", - "name": "kibanaSavedObjectMeta.searchSourceJSON.index", - "type": "index-pattern" - }, - { - "id": "0bf35f60-3dc9-11e8-8660-4d65aa086b3c", - "name": "kibanaSavedObjectMeta.searchSourceJSON.filter[0].meta.index", - "type": "index-pattern" - } - ], - "type": "visualization", - "updated_at": "2018-04-17T15:06:36.275Z", - "visualization": { - "description": "", - "kibanaSavedObjectMeta": { - "searchSourceJSON": "{\"filter\":[{\"meta\":{\"negate\":false,\"disabled\":false,\"alias\":null,\"type\":\"phrase\",\"key\":\"geo.src\",\"value\":\"US\",\"params\":{\"query\":\"US\",\"type\":\"phrase\"},\"indexRefName\":\"kibanaSavedObjectMeta.searchSourceJSON.filter[0].meta.index\"},\"query\":{\"match\":{\"geo.src\":{\"query\":\"US\",\"type\":\"phrase\"}}},\"$state\":{\"store\":\"appState\"}}],\"query\":{\"query\":\"Filter Bytes Test:>5000\",\"language\":\"lucene\"},\"indexRefName\":\"kibanaSavedObjectMeta.searchSourceJSON.index\"}" - }, - "title": "Filter Bytes Test: geo map with filter and query bytes > 5000 in US geo.src, heatmap setting", - "uiStateJSON": "{\"mapZoom\":7,\"mapCenter\":[42.98857645832184,-75.49804687500001]}", - "version": 1, - "visState": "{\"title\":\"Filter Bytes Test: geo map with filter and query bytes > 5000 in US geo.src, heatmap setting\",\"type\":\"tile_map\",\"params\":{\"mapType\":\"Heatmap\",\"isDesaturated\":true,\"addTooltip\":true,\"heatClusterSize\":1.5,\"legendPosition\":\"bottomright\",\"mapZoom\":2,\"mapCenter\":[0,0],\"wms\":{\"enabled\":false,\"options\":{\"format\":\"image/png\",\"transparent\":true},\"baseLayersAreLoaded\":{},\"tmsLayers\":[{\"id\":\"road_map\",\"url\":\"https://tiles-stage.elastic.co/v2/default/{z}/{x}/{y}.png?elastic_tile_service_tos=agree&my_app_name=kibana&my_app_version=6.3.0\",\"minZoom\":0,\"maxZoom\":10,\"attribution\":\"

      © OpenStreetMap contributors | Elastic Maps Service

      \",\"subdomains\":[]}],\"selectedTmsLayer\":{\"id\":\"road_map\",\"url\":\"https://tiles-stage.elastic.co/v2/default/{z}/{x}/{y}.png?elastic_tile_service_tos=agree&my_app_name=kibana&my_app_version=6.3.0\",\"minZoom\":0,\"maxZoom\":10,\"attribution\":\"

      © OpenStreetMap contributors | Elastic Maps Service

      \",\"subdomains\":[]}}},\"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,\"precision\":4}}]}" - } - }, - "type": "_doc" - } -} - { "type": "doc", "value": { @@ -2817,7 +2778,7 @@ "title": "Filter Bytes Test: tsvb metric with custom interval and bytes filter", "uiStateJSON": "{}", "version": 1, - "visState": "{\"title\":\"Filter Bytes Test: tsvb metric with custom interval and bytes filter\",\"type\":\"metrics\",\"params\":{\"id\":\"61ca57f0-469d-11e7-af02-69e470af7417\",\"type\":\"metric\",\"series\":[{\"id\":\"61ca57f1-469d-11e7-af02-69e470af7417\",\"color\":\"#68BC00\",\"split_mode\":\"everything\",\"metrics\":[{\"value\":\"\",\"id\":\"61ca57f2-469d-11e7-af02-69e470af7417\",\"type\":\"sum\",\"field\":\"bytes\"}],\"seperate_axis\":0,\"axis_position\":\"right\",\"formatter\":\"number\",\"chart_type\":\"line\",\"line_width\":1,\"point_size\":1,\"fill\":0.5,\"stacked\":\"none\",\"terms_field\":\"clientip\",\"filter\":{\"query\":\"Filter Bytes Test:>1000\",\"language\":\"lucene\"},\"override_index_pattern\":1,\"series_index_pattern\":\"logstash-*\",\"series_time_field\":\"utc_time\",\"series_interval\":\"1d\",\"value_template\":\"{{value}} custom template\",\"split_color_mode\":\"gradient\"}],\"time_field\":\"@timestamp\",\"index_pattern\":\"logstash-*\",\"interval\":\"auto\",\"axis_position\":\"left\",\"axis_formatter\":\"number\",\"show_legend\":1,\"show_grid\":1,\"background_color_rules\":[{\"id\":\"06893260-4194-11e8-a461-7d278185cba4\"}],\"use_kibana_indexes\":false,\"hide_last_value_indicator\":true},\"aggs\":[]}" + "visState":"{\"title\":\"Filter Bytes Test: tsvb metric with custom interval and bytes filter\",\"type\":\"metrics\",\"aggs\":[],\"params\":{\"time_range_mode\":\"last_value\",\"id\":\"61ca57f0-469d-11e7-af02-69e470af7417\",\"type\":\"metric\",\"series\":[{\"time_range_mode\":\"last_value\",\"id\":\"61ca57f1-469d-11e7-af02-69e470af7417\",\"color\":\"#68BC00\",\"split_mode\":\"everything\",\"metrics\":[{\"value\":\"\",\"id\":\"61ca57f2-469d-11e7-af02-69e470af7417\",\"type\":\"sum\",\"field\":\"bytes\"}],\"seperate_axis\":0,\"axis_position\":\"right\",\"formatter\":\"number\",\"chart_type\":\"line\",\"line_width\":1,\"point_size\":1,\"fill\":0.5,\"stacked\":\"none\",\"terms_field\":\"clientip\",\"filter\":{\"query\":\"Filter Bytes Test:>1000\",\"language\":\"lucene\"},\"override_index_pattern\":1,\"series_index_pattern\":\"logstash-*\",\"series_time_field\":\"utc_time\",\"series_interval\":\"1d\",\"value_template\":\"{{value}} custom template\",\"split_color_mode\":\"gradient\",\"series_drop_last_bucket\":1}],\"time_field\":\"@timestamp\",\"index_pattern\":\"logstash-*\",\"interval\":\"auto\",\"axis_position\":\"left\",\"axis_formatter\":\"number\",\"show_legend\":1,\"show_grid\":1,\"background_color_rules\":[{\"id\":\"06893260-4194-11e8-a461-7d278185cba4\"}],\"use_kibana_indexes\":false,\"hide_last_value_indicator\":true,\"axis_scale\":\"normal\",\"truncate_legend\":1,\"max_lines_legend\":1,\"tooltip_mode\":\"show_all\",\"drop_last_bucket\":0,\"isModelInvalid\":false,\"bar_color_rules\":[{\"id\":\"71f4e260-4186-11ec-8262-619fbabeae59\"}]}}" } }, "type": "_doc" @@ -2846,7 +2807,7 @@ "title": "Filter Bytes Test: tsvb markdown", "uiStateJSON": "{}", "version": 1, - "visState": "{\"title\":\"Filter Bytes Test: tsvb markdown\",\"type\":\"metrics\",\"params\":{\"id\":\"61ca57f0-469d-11e7-af02-69e470af7417\",\"type\":\"markdown\",\"series\":[{\"id\":\"61ca57f1-469d-11e7-af02-69e470af7417\",\"color\":\"#68BC00\",\"split_mode\":\"filters\",\"metrics\":[{\"id\":\"482d6560-4194-11e8-a461-7d278185cba4\",\"type\":\"avg\",\"field\":\"bytes\"}],\"seperate_axis\":0,\"axis_position\":\"right\",\"formatter\":\"number\",\"chart_type\":\"line\",\"line_width\":1,\"point_size\":1,\"fill\":0.5,\"stacked\":\"none\",\"terms_field\":\"clientip\",\"filter\":{\"query\":\"Filter Bytes Test:>1000\",\"language\":\"lucene\"},\"override_index_pattern\":0,\"series_index_pattern\":\"logstash-*\",\"series_time_field\":\"utc_time\",\"series_interval\":\"1m\",\"value_template\":\"\",\"split_filters\":[{\"filter\":{\"query\":\"bytes:>1000\",\"language\":\"lucene\"},\"label\":\"\",\"color\":\"#68BC00\",\"id\":\"39a107e0-4194-11e8-a461-7d278185cba4\"}],\"label\":\"\",\"var_name\":\"\",\"split_color_mode\":\"gradient\"}],\"time_field\":\"@timestamp\",\"index_pattern\":\"logstash-*\",\"interval\":\"auto\",\"axis_position\":\"left\",\"axis_formatter\":\"number\",\"show_legend\":1,\"show_grid\":1,\"background_color_rules\":[{\"id\":\"06893260-4194-11e8-a461-7d278185cba4\"}],\"bar_color_rules\":[{\"id\":\"36a0e740-4194-11e8-a461-7d278185cba4\"}],\"markdown\":\"{{bytes_1000.last.formatted}}\",\"use_kibana_indexes\":false,\"hide_last_value_indicator\":true},\"aggs\":[]}" + "visState":"{\"title\":\"Filter Bytes Test: tsvb markdown\",\"type\":\"metrics\",\"aggs\":[],\"params\":{\"time_range_mode\":\"last_value\",\"id\":\"61ca57f0-469d-11e7-af02-69e470af7417\",\"type\":\"markdown\",\"series\":[{\"time_range_mode\":\"last_value\",\"id\":\"61ca57f1-469d-11e7-af02-69e470af7417\",\"color\":\"#68BC00\",\"split_mode\":\"filters\",\"metrics\":[{\"id\":\"482d6560-4194-11e8-a461-7d278185cba4\",\"type\":\"avg\",\"field\":\"bytes\"}],\"seperate_axis\":0,\"axis_position\":\"right\",\"formatter\":\"number\",\"chart_type\":\"line\",\"line_width\":1,\"point_size\":1,\"fill\":0.5,\"stacked\":\"none\",\"terms_field\":\"clientip\",\"filter\":{\"query\":\"Filter Bytes Test:>1000\",\"language\":\"lucene\"},\"override_index_pattern\":0,\"series_index_pattern\":\"logstash-*\",\"series_time_field\":\"utc_time\",\"series_interval\":\"1m\",\"value_template\":\"\",\"split_filters\":[{\"filter\":{\"query\":\"bytes:>1000\",\"language\":\"lucene\"},\"label\":\"\",\"color\":\"#68BC00\",\"id\":\"39a107e0-4194-11e8-a461-7d278185cba4\"}],\"label\":\"\",\"var_name\":\"\",\"split_color_mode\":\"gradient\",\"series_drop_last_bucket\":1}],\"time_field\":\"@timestamp\",\"index_pattern\":\"logstash-*\",\"interval\":\"auto\",\"axis_position\":\"left\",\"axis_formatter\":\"number\",\"show_legend\":1,\"show_grid\":1,\"background_color_rules\":[{\"id\":\"06893260-4194-11e8-a461-7d278185cba4\"}],\"bar_color_rules\":[{\"id\":\"36a0e740-4194-11e8-a461-7d278185cba4\"}],\"markdown\":\"{{bytes_1000.last.formatted}}\",\"use_kibana_indexes\":false,\"hide_last_value_indicator\":true,\"axis_scale\":\"normal\",\"truncate_legend\":1,\"max_lines_legend\":1,\"tooltip_mode\":\"show_all\",\"drop_last_bucket\":1,\"isModelInvalid\":false}}" } }, "type": "_doc" diff --git a/test/functional/fixtures/es_archiver/date_nested/data.json b/test/functional/fixtures/es_archiver/date_nested/data.json index 0bdb3fc510a63..bb623f93627c7 100644 --- a/test/functional/fixtures/es_archiver/date_nested/data.json +++ b/test/functional/fixtures/es_archiver/date_nested/data.json @@ -6,7 +6,7 @@ "source": { "index-pattern": { "fields":"[]", - "timeFieldName": "@timestamp", + "timeFieldName": "nested.timestamp", "title": "date-nested" }, "type": "index-pattern" diff --git a/test/functional/page_objects/common_page.ts b/test/functional/page_objects/common_page.ts index 69fbf7e49df3c..0150daec3afb5 100644 --- a/test/functional/page_objects/common_page.ts +++ b/test/functional/page_objects/common_page.ts @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import { delay } from 'bluebird'; +import { setTimeout as setTimeoutAsync } from 'timers/promises'; import expect from '@kbn/expect'; // @ts-ignore import fetch from 'node-fetch'; @@ -214,7 +214,7 @@ export class CommonPageObject extends FtrService { async sleep(sleepMilliseconds: number) { this.log.debug(`... sleep(${sleepMilliseconds}) start`); - await delay(sleepMilliseconds); + await setTimeoutAsync(sleepMilliseconds); this.log.debug(`... sleep(${sleepMilliseconds}) end`); } @@ -279,6 +279,9 @@ export class CommonPageObject extends FtrService { this.log.debug(msg); throw new Error(msg); } + if (appName === 'discover') { + await this.browser.setLocalStorageItem('data.autocompleteFtuePopover', 'true'); + } return currentUrl; }); diff --git a/test/functional/page_objects/dashboard_page.ts b/test/functional/page_objects/dashboard_page.ts index 3d2ba53e7ba98..77ea098c76878 100644 --- a/test/functional/page_objects/dashboard_page.ts +++ b/test/functional/page_objects/dashboard_page.ts @@ -292,6 +292,15 @@ export class DashboardPageObject extends FtrService { } public async clickNewDashboard(continueEditing = false) { + const discardButtonExists = await this.testSubjects.exists('discardDashboardPromptButton'); + if (!continueEditing && discardButtonExists) { + this.log.debug('found discard button'); + await this.testSubjects.click('discardDashboardPromptButton'); + const confirmation = await this.testSubjects.exists('confirmModalTitleText'); + if (confirmation) { + await this.common.clickConfirmOnModal(); + } + } await this.listingTable.clickNewButton('createDashboardPromptButton'); if (await this.testSubjects.exists('dashboardCreateConfirm')) { if (continueEditing) { @@ -305,6 +314,15 @@ export class DashboardPageObject extends FtrService { } public async clickNewDashboardExpectWarning(continueEditing = false) { + const discardButtonExists = await this.testSubjects.exists('discardDashboardPromptButton'); + if (!continueEditing && discardButtonExists) { + this.log.debug('found discard button'); + await this.testSubjects.click('discardDashboardPromptButton'); + const confirmation = await this.testSubjects.exists('confirmModalTitleText'); + if (confirmation) { + await this.common.clickConfirmOnModal(); + } + } await this.listingTable.clickNewButton('createDashboardPromptButton'); await this.testSubjects.existOrFail('dashboardCreateConfirm'); if (continueEditing) { diff --git a/test/functional/page_objects/discover_page.ts b/test/functional/page_objects/discover_page.ts index fa7aee4e3c54c..f9328e89cd19e 100644 --- a/test/functional/page_objects/discover_page.ts +++ b/test/functional/page_objects/discover_page.ts @@ -48,7 +48,7 @@ export class DiscoverPageObject extends FtrService { await fieldSearch.clearValue(); } - public async saveSearch(searchName: string) { + public async saveSearch(searchName: string, saveAsNew?: boolean) { await this.clickSaveSearchButton(); // preventing an occasional flakiness when the saved object wasn't set and the form can't be submitted await this.retry.waitFor( @@ -59,6 +59,14 @@ export class DiscoverPageObject extends FtrService { return (await saveButton.getAttribute('disabled')) !== 'true'; } ); + + if (saveAsNew !== undefined) { + await this.retry.waitFor(`save as new switch is set`, async () => { + await this.testSubjects.setEuiSwitch('saveAsNewCheckbox', saveAsNew ? 'check' : 'uncheck'); + return (await this.testSubjects.isEuiSwitchChecked('saveAsNewCheckbox')) === saveAsNew; + }); + } + await this.testSubjects.click('confirmSaveSavedObjectButton'); await this.header.waitUntilLoadingHasFinished(); // LeeDr - this additional checking for the saved search name was an attempt diff --git a/test/functional/page_objects/login_page.ts b/test/functional/page_objects/login_page.ts index 5318a2b2d0c15..74e85e60d1a69 100644 --- a/test/functional/page_objects/login_page.ts +++ b/test/functional/page_objects/login_page.ts @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import { delay } from 'bluebird'; +import { setTimeout as setTimeoutAsync } from 'timers/promises'; import { FtrService } from '../ftr_provider_context'; export class LoginPageObject extends FtrService { @@ -40,7 +40,7 @@ export class LoginPageObject extends FtrService { async sleep(sleepMilliseconds: number) { this.log.debug(`... sleep(${sleepMilliseconds}) start`); - await delay(sleepMilliseconds); + await setTimeoutAsync(sleepMilliseconds); this.log.debug(`... sleep(${sleepMilliseconds}) end`); } diff --git a/test/functional/page_objects/management/saved_objects_page.ts b/test/functional/page_objects/management/saved_objects_page.ts index 21af7aa477abd..87d5537d53ca3 100644 --- a/test/functional/page_objects/management/saved_objects_page.ts +++ b/test/functional/page_objects/management/saved_objects_page.ts @@ -7,7 +7,6 @@ */ import { keyBy } from 'lodash'; -import { map as mapAsync } from 'bluebird'; import { FtrService } from '../../ftr_provider_context'; export class SavedObjectsPageObject extends FtrService { @@ -201,51 +200,55 @@ export class SavedObjectsPageObject extends FtrService { async getElementsInTable() { const rows = await this.testSubjects.findAll('~savedObjectsTableRow'); - return mapAsync(rows, async (row) => { - const checkbox = await row.findByCssSelector('[data-test-subj*="checkboxSelectRow"]'); - // return the object type aria-label="index patterns" - const objectType = await row.findByTestSubject('objectType'); - const titleElement = await row.findByTestSubject('savedObjectsTableRowTitle'); - // not all rows have inspect button - Advanced Settings objects don't - // Advanced Settings has 2 actions, - // data-test-subj="savedObjectsTableAction-relationships" - // data-test-subj="savedObjectsTableAction-copy_saved_objects_to_space" - // Some other objects have the ... - // data-test-subj="euiCollapsedItemActionsButton" - // Maybe some objects still have the inspect element visible? - // !!! Also note that since we don't have spaces on OSS, the actions for the same object can be different depending on OSS or not - let menuElement = null; - let inspectElement = null; - let relationshipsElement = null; - let copySaveObjectsElement = null; - const actions = await row.findByClassName('euiTableRowCell--hasActions'); - // getting the innerHTML and checking if it 'includes' a string is faster than a timeout looking for each element - const actionsHTML = await actions.getAttribute('innerHTML'); - if (actionsHTML.includes('euiCollapsedItemActionsButton')) { - menuElement = await row.findByTestSubject('euiCollapsedItemActionsButton'); - } - if (actionsHTML.includes('savedObjectsTableAction-inspect')) { - inspectElement = await row.findByTestSubject('savedObjectsTableAction-inspect'); - } - if (actionsHTML.includes('savedObjectsTableAction-relationships')) { - relationshipsElement = await row.findByTestSubject('savedObjectsTableAction-relationships'); - } - if (actionsHTML.includes('savedObjectsTableAction-copy_saved_objects_to_space')) { - copySaveObjectsElement = await row.findByTestSubject( - 'savedObjectsTableAction-copy_saved_objects_to_space' - ); - } - return { - checkbox, - objectType: await objectType.getAttribute('aria-label'), - titleElement, - title: await titleElement.getVisibleText(), - menuElement, - inspectElement, - relationshipsElement, - copySaveObjectsElement, - }; - }); + return await Promise.all( + rows.map(async (row) => { + const checkbox = await row.findByCssSelector('[data-test-subj*="checkboxSelectRow"]'); + // return the object type aria-label="index patterns" + const objectType = await row.findByTestSubject('objectType'); + const titleElement = await row.findByTestSubject('savedObjectsTableRowTitle'); + // not all rows have inspect button - Advanced Settings objects don't + // Advanced Settings has 2 actions, + // data-test-subj="savedObjectsTableAction-relationships" + // data-test-subj="savedObjectsTableAction-copy_saved_objects_to_space" + // Some other objects have the ... + // data-test-subj="euiCollapsedItemActionsButton" + // Maybe some objects still have the inspect element visible? + // !!! Also note that since we don't have spaces on OSS, the actions for the same object can be different depending on OSS or not + let menuElement = null; + let inspectElement = null; + let relationshipsElement = null; + let copySaveObjectsElement = null; + const actions = await row.findByClassName('euiTableRowCell--hasActions'); + // getting the innerHTML and checking if it 'includes' a string is faster than a timeout looking for each element + const actionsHTML = await actions.getAttribute('innerHTML'); + if (actionsHTML.includes('euiCollapsedItemActionsButton')) { + menuElement = await row.findByTestSubject('euiCollapsedItemActionsButton'); + } + if (actionsHTML.includes('savedObjectsTableAction-inspect')) { + inspectElement = await row.findByTestSubject('savedObjectsTableAction-inspect'); + } + if (actionsHTML.includes('savedObjectsTableAction-relationships')) { + relationshipsElement = await row.findByTestSubject( + 'savedObjectsTableAction-relationships' + ); + } + if (actionsHTML.includes('savedObjectsTableAction-copy_saved_objects_to_space')) { + copySaveObjectsElement = await row.findByTestSubject( + 'savedObjectsTableAction-copy_saved_objects_to_space' + ); + } + return { + checkbox, + objectType: await objectType.getAttribute('aria-label'), + titleElement, + title: await titleElement.getVisibleText(), + menuElement, + inspectElement, + relationshipsElement, + copySaveObjectsElement, + }; + }) + ); } async getRowTitles() { @@ -259,35 +262,39 @@ export class SavedObjectsPageObject extends FtrService { async getRelationshipFlyout() { const rows = await this.testSubjects.findAll('relationshipsTableRow'); - return mapAsync(rows, async (row) => { - const objectType = await row.findByTestSubject('relationshipsObjectType'); - const relationship = await row.findByTestSubject('directRelationship'); - const titleElement = await row.findByTestSubject('relationshipsTitle'); - const inspectElement = await row.findByTestSubject('relationshipsTableAction-inspect'); - return { - objectType: await objectType.getAttribute('aria-label'), - relationship: await relationship.getVisibleText(), - titleElement, - title: await titleElement.getVisibleText(), - inspectElement, - }; - }); + return await Promise.all( + rows.map(async (row) => { + const objectType = await row.findByTestSubject('relationshipsObjectType'); + const relationship = await row.findByTestSubject('directRelationship'); + const titleElement = await row.findByTestSubject('relationshipsTitle'); + const inspectElement = await row.findByTestSubject('relationshipsTableAction-inspect'); + return { + objectType: await objectType.getAttribute('aria-label'), + relationship: await relationship.getVisibleText(), + titleElement, + title: await titleElement.getVisibleText(), + inspectElement, + }; + }) + ); } async getInvalidRelations() { const rows = await this.testSubjects.findAll('invalidRelationshipsTableRow'); - return mapAsync(rows, async (row) => { - const objectType = await row.findByTestSubject('relationshipsObjectType'); - const objectId = await row.findByTestSubject('relationshipsObjectId'); - const relationship = await row.findByTestSubject('directRelationship'); - const error = await row.findByTestSubject('relationshipsError'); - return { - type: await objectType.getVisibleText(), - id: await objectId.getVisibleText(), - relationship: await relationship.getVisibleText(), - error: await error.getVisibleText(), - }; - }); + return await Promise.all( + rows.map(async (row) => { + const objectType = await row.findByTestSubject('relationshipsObjectType'); + const objectId = await row.findByTestSubject('relationshipsObjectId'); + const relationship = await row.findByTestSubject('directRelationship'); + const error = await row.findByTestSubject('relationshipsError'); + return { + type: await objectType.getVisibleText(), + id: await objectId.getVisibleText(), + relationship: await relationship.getVisibleText(), + error: await error.getVisibleText(), + }; + }) + ); } async getTableSummary() { diff --git a/test/functional/page_objects/settings_page.ts b/test/functional/page_objects/settings_page.ts index d3443f9cf4925..54728e1db3f55 100644 --- a/test/functional/page_objects/settings_page.ts +++ b/test/functional/page_objects/settings_page.ts @@ -6,7 +6,6 @@ * Side Public License, v 1. */ -import { map as mapAsync } from 'bluebird'; import expect from '@kbn/expect'; import { FtrService } from '../ftr_provider_context'; @@ -234,23 +233,29 @@ export class SettingsPageObject extends FtrService { async getFieldNames() { const fieldNameCells = await this.testSubjects.findAll('editIndexPattern > indexedFieldName'); - return await mapAsync(fieldNameCells, async (cell) => { - return (await cell.getVisibleText()).trim(); - }); + return await Promise.all( + fieldNameCells.map(async (cell) => { + return (await cell.getVisibleText()).trim(); + }) + ); } async getFieldTypes() { const fieldNameCells = await this.testSubjects.findAll('editIndexPattern > indexedFieldType'); - return await mapAsync(fieldNameCells, async (cell) => { - return (await cell.getVisibleText()).trim(); - }); + return await Promise.all( + fieldNameCells.map(async (cell) => { + return (await cell.getVisibleText()).trim(); + }) + ); } async getScriptedFieldLangs() { const fieldNameCells = await this.testSubjects.findAll('editIndexPattern > scriptedFieldLang'); - return await mapAsync(fieldNameCells, async (cell) => { - return (await cell.getVisibleText()).trim(); - }); + return await Promise.all( + fieldNameCells.map(async (cell) => { + return (await cell.getVisibleText()).trim(); + }) + ); } async setFieldTypeFilter(type: string) { @@ -327,9 +332,11 @@ export class SettingsPageObject extends FtrService { async getAllIndexPatternNames() { const indexPatterns = await this.getIndexPatternList(); - return await mapAsync(indexPatterns, async (index) => { - return await index.getVisibleText(); - }); + return await Promise.all( + indexPatterns.map(async (index) => { + return await index.getVisibleText(); + }) + ); } async isIndexPatternListEmpty() { @@ -437,7 +444,8 @@ export class SettingsPageObject extends FtrService { async setIndexPatternField(indexPatternName = 'logstash-*') { this.log.debug(`setIndexPatternField(${indexPatternName})`); const field = await this.getIndexPatternField(); - await field.clearValue(); + await field.clearValueWithKeyboard(); + if ( indexPatternName.charAt(0) === '*' && indexPatternName.charAt(indexPatternName.length - 1) === '*' @@ -565,9 +573,11 @@ export class SettingsPageObject extends FtrService { const table = await this.find.byClassName('euiTable'); await this.retry.waitFor('field filter to be added', async () => { const tableCells = await table.findAllByCssSelector('td'); - const fieldNames = await mapAsync(tableCells, async (cell) => { - return (await cell.getVisibleText()).trim(); - }); + const fieldNames = await Promise.all( + tableCells.map(async (cell) => { + return (await cell.getVisibleText()).trim(); + }) + ); return fieldNames.includes(name); }); } @@ -618,23 +628,9 @@ export class SettingsPageObject extends FtrService { async setFieldScript(script: string) { this.log.debug('set script = ' + script); - const valueRow = await this.toggleRow('valueRow'); - const getMonacoTextArea = async () => (await valueRow.findAllByCssSelector('textarea'))[0]; - this.retry.waitFor('monaco editor is ready', async () => !!(await getMonacoTextArea())); - const monacoTextArea = await getMonacoTextArea(); - await monacoTextArea.focus(); - this.browser.pressKeys(script); - } - - async changeFieldScript(script: string) { - this.log.debug('set script = ' + script); - const valueRow = await this.testSubjects.find('valueRow'); - const getMonacoTextArea = async () => (await valueRow.findAllByCssSelector('textarea'))[0]; - this.retry.waitFor('monaco editor is ready', async () => !!(await getMonacoTextArea())); - const monacoTextArea = await getMonacoTextArea(); - await monacoTextArea.focus(); - this.browser.pressKeys(this.browser.keys.DELETE.repeat(30)); - this.browser.pressKeys(script); + await this.toggleRow('valueRow'); + await this.monacoEditor.waitCodeEditorReady('valueRow'); + await this.monacoEditor.setCodeEditorValue(script); } async clickAddScriptedField() { diff --git a/test/functional/page_objects/vega_chart_page.ts b/test/functional/page_objects/vega_chart_page.ts index f83c5e193034e..045e5eedb86f0 100644 --- a/test/functional/page_objects/vega_chart_page.ts +++ b/test/functional/page_objects/vega_chart_page.ts @@ -19,6 +19,7 @@ export class VegaChartPageObject extends FtrService { private readonly testSubjects = this.ctx.getService('testSubjects'); private readonly browser = this.ctx.getService('browser'); private readonly retry = this.ctx.getService('retry'); + private readonly monacoEditor = this.ctx.getService('monacoEditor'); public getEditor() { return this.testSubjects.find('vega-editor'); @@ -36,63 +37,31 @@ export class VegaChartPageObject extends FtrService { return this.find.byCssSelector('[aria-label^="Y-axis"]'); } - public async getAceGutterContainer() { - const editor = await this.getEditor(); - return editor.findByClassName('ace_gutter'); - } - - public async getRawSpec() { - // Adapted from console_page.js:getVisibleTextFromAceEditor(). Is there a common utilities file? - const editor = await this.getEditor(); - const lines = await editor.findAllByClassName('ace_line_group'); - - return await Promise.all( - lines.map(async (line) => { - return await line.getVisibleText(); - }) - ); - } - public async getSpec() { - return (await this.getRawSpec()).join('\n'); - } - - public async focusEditor() { - const editor = await this.getEditor(); - const textarea = await editor.findByClassName('ace_content'); - - await textarea.click(); + return this.monacoEditor.getCodeEditorValue(); } public async fillSpec(newSpec: string) { await this.retry.try(async () => { await this.cleanSpec(); - await this.focusEditor(); - await this.browser.pressKeys(newSpec); + await this.monacoEditor.setCodeEditorValue(newSpec); expect(compareSpecs(await this.getSpec(), newSpec)).to.be(true); }); } public async typeInSpec(text: string) { - const aceGutter = await this.getAceGutterContainer(); + const editor = await this.testSubjects.find('vega-editor'); + const textarea = await editor.findByCssSelector('textarea'); - await aceGutter.doubleClick(); + await textarea.focus(); + await this.browser.pressKeys(this.browser.keys.RIGHT); await this.browser.pressKeys(this.browser.keys.RIGHT); - await this.browser.pressKeys(this.browser.keys.LEFT); - await this.browser.pressKeys(this.browser.keys.LEFT); - await this.browser.pressKeys(text); + await textarea.type(text); } public async cleanSpec() { - const aceGutter = await this.getAceGutterContainer(); - - await this.retry.try(async () => { - await aceGutter.doubleClick(); - await this.browser.pressKeys(this.browser.keys.BACK_SPACE); - - expect(await this.getSpec()).to.be(''); - }); + await this.monacoEditor.setCodeEditorValue(''); } public async getYAxisLabels() { diff --git a/test/functional/page_objects/visual_builder_page.ts b/test/functional/page_objects/visual_builder_page.ts index b87962b34291c..082bee1f973fa 100644 --- a/test/functional/page_objects/visual_builder_page.ts +++ b/test/functional/page_objects/visual_builder_page.ts @@ -878,6 +878,10 @@ export class VisualBuilderPageObject extends FtrService { await optionInput.type(query); } + public async clickSeriesLegendItem(name: string) { + await this.find.clickByCssSelector(`[data-ech-series-name="${name}"] .echLegendItem__label`); + } + public async toggleNewChartsLibraryWithDebug(enabled: boolean) { await this.elasticChart.setNewChartUiDebugFlag(enabled); } diff --git a/test/functional/services/common/browser.ts b/test/functional/services/common/browser.ts index 73d92f8ff722b..7581c17a58ebf 100644 --- a/test/functional/services/common/browser.ts +++ b/test/functional/services/common/browser.ts @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import { delay } from 'bluebird'; +import { setTimeout as setTimeoutAsync } from 'timers/promises'; import { cloneDeepWith } from 'lodash'; import { Key, Origin, WebDriver } from 'selenium-webdriver'; // @ts-ignore internal modules are not typed @@ -303,7 +303,7 @@ class BrowserService extends FtrService { to ); // wait for 150ms to make sure the script has run - await delay(150); + await setTimeoutAsync(150); } /** diff --git a/test/functional/services/common/test_subjects.ts b/test/functional/services/common/test_subjects.ts index 3f47c6155f175..09c54af7b8811 100644 --- a/test/functional/services/common/test_subjects.ts +++ b/test/functional/services/common/test_subjects.ts @@ -7,7 +7,6 @@ */ import testSubjSelector from '@kbn/test-subj-selector'; -import { map as mapAsync } from 'bluebird'; import { WebElementWrapper } from '../lib/web_element_wrapper'; import { FtrService } from '../../ftr_provider_context'; @@ -271,11 +270,11 @@ export class TestSubjects extends FtrService { private async _mapAll( selectorAll: string, - mapFn: (element: WebElementWrapper, index?: number, arrayLength?: number) => Promise + mapFn: (element: WebElementWrapper, index: number, array: WebElementWrapper[]) => Promise ): Promise { return await this.retry.try(async () => { const elements = await this.findAll(selectorAll); - return await mapAsync(elements, mapFn); + return await Promise.all(elements.map(mapFn)); }); } diff --git a/test/functional/services/dashboard/expectations.ts b/test/functional/services/dashboard/expectations.ts index 0a689c0091edc..d4b462d2a68f4 100644 --- a/test/functional/services/dashboard/expectations.ts +++ b/test/functional/services/dashboard/expectations.ts @@ -155,30 +155,34 @@ export class DashboardExpectService extends FtrService { async emptyTagCloudFound() { this.log.debug(`DashboardExpect.emptyTagCloudFound()`); const tagCloudVisualizations = await this.testSubjects.findAll('tagCloudVisualization'); - const tagCloudsHaveContent = await Promise.all( - tagCloudVisualizations.map(async (tagCloud) => { - return await this.find.descendantExistsByCssSelector('text', tagCloud); - }) - ); - expect(tagCloudsHaveContent.indexOf(false)).to.be.greaterThan(-1); + if (tagCloudVisualizations.length > 0) { + const tagCloudsHaveContent = await Promise.all( + tagCloudVisualizations.map(async (tagCloud) => { + return await this.find.descendantExistsByCssSelector('text', tagCloud); + }) + ); + expect(tagCloudsHaveContent.indexOf(false)).to.be.greaterThan(-1); + } } async tagCloudWithValuesFound(values: string[]) { this.log.debug(`DashboardExpect.tagCloudWithValuesFound(${values})`); const tagCloudVisualizations = await this.testSubjects.findAll('tagCloudVisualization'); - const matches = await Promise.all( - tagCloudVisualizations.map(async (tagCloud) => { - const tagCloudData = await this.tagCloud.getTextTagByElement(tagCloud); - for (let i = 0; i < values.length; i++) { - const valueExists = tagCloudData.includes(values[i]); - if (!valueExists) { - return false; + if (tagCloudVisualizations.length > 0) { + const matches = await Promise.all( + tagCloudVisualizations.map(async (tagCloud) => { + const tagCloudData = await this.tagCloud.getTextTagByElement(tagCloud); + for (let i = 0; i < values.length; i++) { + const valueExists = tagCloudData.includes(values[i]); + if (!valueExists) { + return false; + } } - } - return true; - }) - ); - expect(matches.indexOf(true)).to.be.greaterThan(-1); + return true; + }) + ); + expect(matches.indexOf(true)).to.be.greaterThan(-1); + } } async goalAndGuageLabelsExist(labels: string[]) { @@ -232,6 +236,14 @@ export class DashboardExpectService extends FtrService { }); } + async savedSearchRowsExist() { + this.testSubjects.existOrFail('docTableExpandToggleColumn'); + } + + async savedSearchRowsMissing() { + this.testSubjects.missingOrFail('docTableExpandToggleColumn'); + } + async dataTableRowCount(expectedCount: number) { this.log.debug(`DashboardExpect.dataTableRowCount(${expectedCount})`); await this.retry.try(async () => { diff --git a/test/functional/services/field_editor.ts b/test/functional/services/field_editor.ts index 27cb8cf010d92..d6d2f2606e29d 100644 --- a/test/functional/services/field_editor.ts +++ b/test/functional/services/field_editor.ts @@ -37,7 +37,13 @@ export class FieldEditorService extends FtrService { const textarea = await editor.findByClassName('monaco-mouse-cursor-text'); await textarea.click(); - await this.browser.pressKeys(script); + + // To avoid issue with the timing needed for Selenium to write the script and the monaco editor + // syntax validation kicking in, we loop through all the chars of the script and enter + // them one by one (instead of calling "await this.browser.pressKeys(script);"). + for (const letter of script.split('')) { + await this.browser.pressKeys(letter); + } } public async save() { await this.testSubjects.click('fieldSaveButton'); diff --git a/test/functional/services/lib/web_element_wrapper/web_element_wrapper.ts b/test/functional/services/lib/web_element_wrapper/web_element_wrapper.ts index 4b164402bfb70..d4fe5080bdfef 100644 --- a/test/functional/services/lib/web_element_wrapper/web_element_wrapper.ts +++ b/test/functional/services/lib/web_element_wrapper/web_element_wrapper.ts @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import { delay } from 'bluebird'; +import { setTimeout as setTimeoutAsync } from 'timers/promises'; import { WebElement, WebDriver, By, Key } from 'selenium-webdriver'; import { PNG } from 'pngjs'; import cheerio from 'cheerio'; @@ -121,7 +121,7 @@ export class WebElementWrapper { `finding element '${this.locator.toString()}' again, ${attemptsRemaining - 1} attempts left` ); - await delay(200); + await setTimeoutAsync(200); this._webElement = await this.driver.findElement(this.locator); return await this.retryCall(fn, attemptsRemaining - 1); } @@ -240,7 +240,7 @@ export class WebElementWrapper { const value = await this.getAttribute('value'); for (let i = 0; i <= value.length; i++) { await this.pressKeys(this.Keys.BACK_SPACE); - await delay(100); + await setTimeoutAsync(100); } } else { if (this.isChromium) { @@ -279,7 +279,7 @@ export class WebElementWrapper { for (const char of value) { await this.retryCall(async function type(wrapper) { await wrapper._webElement.sendKeys(char); - await delay(100); + await setTimeoutAsync(100); }); } } else { diff --git a/test/functional/services/monaco_editor.ts b/test/functional/services/monaco_editor.ts index 63a5a7105ddb8..821c334b01c09 100644 --- a/test/functional/services/monaco_editor.ts +++ b/test/functional/services/monaco_editor.ts @@ -13,6 +13,11 @@ export class MonacoEditorService extends FtrService { private readonly browser = this.ctx.getService('browser'); private readonly testSubjects = this.ctx.getService('testSubjects'); + public async waitCodeEditorReady(containerTestSubjId: string) { + const editorContainer = await this.testSubjects.find(containerTestSubjId); + await editorContainer.findByCssSelector('textarea'); + } + public async getCodeEditorValue(nthIndex: number = 0) { let values: string[] = []; @@ -31,7 +36,7 @@ export class MonacoEditorService extends FtrService { public async typeCodeEditorValue(value: string, testSubjId: string) { const editor = await this.testSubjects.find(testSubjId); const textarea = await editor.findByCssSelector('textarea'); - textarea.type(value); + await textarea.type(value); } public async setCodeEditorValue(value: string, nthIndex = 0) { diff --git a/test/interactive_setup_api_integration/fixtures/test_helpers.ts b/test/interactive_setup_api_integration/fixtures/test_helpers.ts index f1e72785af02d..6001f12b0a551 100644 --- a/test/interactive_setup_api_integration/fixtures/test_helpers.ts +++ b/test/interactive_setup_api_integration/fixtures/test_helpers.ts @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import { delay } from 'bluebird'; +import { setTimeout as setTimeoutAsync } from 'timers/promises'; import expect from '@kbn/expect'; @@ -19,7 +19,7 @@ export async function hasKibanaBooted(context: FtrProviderContext) { // Run 30 consecutive requests with 1.5s delay to check if Kibana is up and running. let kibanaHasBooted = false; for (const counter of [...Array(30).keys()]) { - await delay(1500); + await setTimeoutAsync(1500); try { expect((await supertest.get('/api/status').expect(200)).body).to.have.keys([ diff --git a/test/interpreter_functional/screenshots/baseline/combined_test.png b/test/interpreter_functional/screenshots/baseline/combined_test.png index 9cb5e255ec99b..5b8709d8a5388 100644 Binary files a/test/interpreter_functional/screenshots/baseline/combined_test.png and b/test/interpreter_functional/screenshots/baseline/combined_test.png differ diff --git a/test/interpreter_functional/screenshots/baseline/final_screenshot_test.png b/test/interpreter_functional/screenshots/baseline/final_screenshot_test.png index 9cb5e255ec99b..87ca2668f06ea 100644 Binary files a/test/interpreter_functional/screenshots/baseline/final_screenshot_test.png and b/test/interpreter_functional/screenshots/baseline/final_screenshot_test.png differ diff --git a/test/interpreter_functional/screenshots/baseline/metric_all_data.png b/test/interpreter_functional/screenshots/baseline/metric_all_data.png index 18dca6c2c39c2..6d2b809cbd897 100644 Binary files a/test/interpreter_functional/screenshots/baseline/metric_all_data.png and b/test/interpreter_functional/screenshots/baseline/metric_all_data.png differ diff --git a/test/interpreter_functional/screenshots/baseline/metric_multi_metric_data.png b/test/interpreter_functional/screenshots/baseline/metric_multi_metric_data.png index 1e85944250156..55e320f24524f 100644 Binary files a/test/interpreter_functional/screenshots/baseline/metric_multi_metric_data.png and b/test/interpreter_functional/screenshots/baseline/metric_multi_metric_data.png differ diff --git a/test/interpreter_functional/screenshots/baseline/metric_percentage_mode.png b/test/interpreter_functional/screenshots/baseline/metric_percentage_mode.png index bcf33d9171193..55dc5a1fa76ea 100644 Binary files a/test/interpreter_functional/screenshots/baseline/metric_percentage_mode.png and b/test/interpreter_functional/screenshots/baseline/metric_percentage_mode.png differ diff --git a/test/interpreter_functional/screenshots/baseline/metric_single_metric_data.png b/test/interpreter_functional/screenshots/baseline/metric_single_metric_data.png index a82654240e374..d405646c3bc75 100644 Binary files a/test/interpreter_functional/screenshots/baseline/metric_single_metric_data.png and b/test/interpreter_functional/screenshots/baseline/metric_single_metric_data.png differ diff --git a/test/interpreter_functional/screenshots/baseline/partial_test_1.png b/test/interpreter_functional/screenshots/baseline/partial_test_1.png index 5e43b52099d15..1a1f396799851 100644 Binary files a/test/interpreter_functional/screenshots/baseline/partial_test_1.png and b/test/interpreter_functional/screenshots/baseline/partial_test_1.png differ diff --git a/test/interpreter_functional/screenshots/baseline/partial_test_2.png b/test/interpreter_functional/screenshots/baseline/partial_test_2.png index 9cb5e255ec99b..5b8709d8a5388 100644 Binary files a/test/interpreter_functional/screenshots/baseline/partial_test_2.png and b/test/interpreter_functional/screenshots/baseline/partial_test_2.png differ diff --git a/test/interpreter_functional/snapshots/baseline/combined_test2.json b/test/interpreter_functional/snapshots/baseline/combined_test2.json index 4870694e6adbc..3b030ec8fb597 100644 --- a/test/interpreter_functional/snapshots/baseline/combined_test2.json +++ b/test/interpreter_functional/snapshots/baseline/combined_test2.json @@ -1 +1 @@ -{"columns":[{"id":"col-0-2","meta":{"field":"response.raw","index":"logstash-*","params":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"id":"2","indexPatternId":"logstash-*","params":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"schema":"segment","type":"terms"},"type":"string"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"field":null,"index":"logstash-*","params":{"id":"number"},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"id":"1","indexPatternId":"logstash-*","params":{},"schema":"metric","type":"count"},"type":"number"},"name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"datatable"} \ No newline at end of file +{"columns":[{"id":"col-0-2","meta":{"field":"response.raw","index":"logstash-*","params":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"hasPrecisionError":false,"id":"2","indexPatternId":"logstash-*","params":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"schema":"segment","type":"terms"},"type":"string"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"field":null,"index":"logstash-*","params":{"id":"number"},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"hasPrecisionError":false,"id":"1","indexPatternId":"logstash-*","params":{},"schema":"metric","type":"count"},"type":"number"},"name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"datatable"} \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/baseline/combined_test3.json b/test/interpreter_functional/snapshots/baseline/combined_test3.json index 107d3fcbc5c54..2ddf40eb79006 100644 --- a/test/interpreter_functional/snapshots/baseline/combined_test3.json +++ b/test/interpreter_functional/snapshots/baseline/combined_test3.json @@ -1 +1 @@ -{"as":"metricVis","type":"render","value":{"visConfig":{"dimensions":{"bucket":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"metrics":[{"accessor":1,"format":{"id":"number","params":{}},"type":"vis_dimension"}]},"metric":{"colorSchema":"Green to Red","colorsRange":[{"from":0,"to":10000,"type":"range"}],"invertColors":false,"labels":{"show":true},"metricColorMode":"None","percentageMode":false,"style":{"bgColor":false,"bgFill":"#000","fontSize":60,"labelColor":false,"subText":""},"useRanges":false}},"visData":{"columns":[{"id":"col-0-2","meta":{"field":"response.raw","index":"logstash-*","params":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"id":"2","indexPatternId":"logstash-*","params":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"schema":"segment","type":"terms"},"type":"string"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"field":null,"index":"logstash-*","params":{"id":"number"},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"id":"1","indexPatternId":"logstash-*","params":{},"schema":"metric","type":"count"},"type":"number"},"name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"datatable"},"visType":"metric"}} \ No newline at end of file +{"as":"metricVis","type":"render","value":{"visConfig":{"dimensions":{"bucket":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"metrics":[{"accessor":1,"format":{"id":"number","params":{}},"type":"vis_dimension"}]},"metric":{"labels":{"show":true},"metricColorMode":"None","palette":null,"percentageMode":false,"style":{"bgColor":false,"css":"font-family:'Open Sans', Helvetica, Arial, sans-serif;font-weight:normal;font-style:normal;text-decoration:none;text-align:center;font-size:60px;line-height:1","labelColor":false,"spec":{"fontFamily":"'Open Sans', Helvetica, Arial, sans-serif","fontSize":"60px","fontStyle":"normal","fontWeight":"normal","lineHeight":"1","textAlign":"center","textDecoration":"none"},"type":"style"}}},"visData":{"columns":[{"id":"col-0-2","meta":{"field":"response.raw","index":"logstash-*","params":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"hasPrecisionError":false,"id":"2","indexPatternId":"logstash-*","params":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"schema":"segment","type":"terms"},"type":"string"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"field":null,"index":"logstash-*","params":{"id":"number"},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"hasPrecisionError":false,"id":"1","indexPatternId":"logstash-*","params":{},"schema":"metric","type":"count"},"type":"number"},"name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"datatable"},"visType":"metric"}} \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/baseline/final_output_test.json b/test/interpreter_functional/snapshots/baseline/final_output_test.json index 107d3fcbc5c54..2ddf40eb79006 100644 --- a/test/interpreter_functional/snapshots/baseline/final_output_test.json +++ b/test/interpreter_functional/snapshots/baseline/final_output_test.json @@ -1 +1 @@ -{"as":"metricVis","type":"render","value":{"visConfig":{"dimensions":{"bucket":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"metrics":[{"accessor":1,"format":{"id":"number","params":{}},"type":"vis_dimension"}]},"metric":{"colorSchema":"Green to Red","colorsRange":[{"from":0,"to":10000,"type":"range"}],"invertColors":false,"labels":{"show":true},"metricColorMode":"None","percentageMode":false,"style":{"bgColor":false,"bgFill":"#000","fontSize":60,"labelColor":false,"subText":""},"useRanges":false}},"visData":{"columns":[{"id":"col-0-2","meta":{"field":"response.raw","index":"logstash-*","params":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"id":"2","indexPatternId":"logstash-*","params":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"schema":"segment","type":"terms"},"type":"string"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"field":null,"index":"logstash-*","params":{"id":"number"},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"id":"1","indexPatternId":"logstash-*","params":{},"schema":"metric","type":"count"},"type":"number"},"name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"datatable"},"visType":"metric"}} \ No newline at end of file +{"as":"metricVis","type":"render","value":{"visConfig":{"dimensions":{"bucket":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"metrics":[{"accessor":1,"format":{"id":"number","params":{}},"type":"vis_dimension"}]},"metric":{"labels":{"show":true},"metricColorMode":"None","palette":null,"percentageMode":false,"style":{"bgColor":false,"css":"font-family:'Open Sans', Helvetica, Arial, sans-serif;font-weight:normal;font-style:normal;text-decoration:none;text-align:center;font-size:60px;line-height:1","labelColor":false,"spec":{"fontFamily":"'Open Sans', Helvetica, Arial, sans-serif","fontSize":"60px","fontStyle":"normal","fontWeight":"normal","lineHeight":"1","textAlign":"center","textDecoration":"none"},"type":"style"}}},"visData":{"columns":[{"id":"col-0-2","meta":{"field":"response.raw","index":"logstash-*","params":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"hasPrecisionError":false,"id":"2","indexPatternId":"logstash-*","params":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"schema":"segment","type":"terms"},"type":"string"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"field":null,"index":"logstash-*","params":{"id":"number"},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"hasPrecisionError":false,"id":"1","indexPatternId":"logstash-*","params":{},"schema":"metric","type":"count"},"type":"number"},"name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"datatable"},"visType":"metric"}} \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/baseline/metric_all_data.json b/test/interpreter_functional/snapshots/baseline/metric_all_data.json index 9c10b53ce8604..fb16bf98ce761 100644 --- a/test/interpreter_functional/snapshots/baseline/metric_all_data.json +++ b/test/interpreter_functional/snapshots/baseline/metric_all_data.json @@ -1 +1 @@ -{"as":"metricVis","type":"render","value":{"visConfig":{"dimensions":{"bucket":{"accessor":2,"format":{"id":"string","params":{}},"type":"vis_dimension"},"metrics":[{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"}]},"metric":{"colorSchema":"Green to Red","colorsRange":[{"from":0,"to":10000,"type":"range"}],"invertColors":false,"labels":{"show":true},"metricColorMode":"None","percentageMode":false,"style":{"bgColor":false,"bgFill":"#000","fontSize":60,"labelColor":false,"subText":""},"useRanges":false}},"visData":{"columns":[{"id":"col-0-2","meta":{"field":"response.raw","index":"logstash-*","params":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"id":"2","indexPatternId":"logstash-*","params":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"schema":"segment","type":"terms"},"type":"string"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"field":null,"index":"logstash-*","params":{"id":"number"},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"id":"1","indexPatternId":"logstash-*","params":{},"schema":"metric","type":"count"},"type":"number"},"name":"Count"},{"id":"col-2-1","meta":{"field":"bytes","index":"logstash-*","params":{"id":"bytes","params":null},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"id":"1","indexPatternId":"logstash-*","params":{"field":"bytes"},"schema":"metric","type":"max"},"type":"number"},"name":"Max bytes"}],"rows":[{"col-0-2":"200","col-1-1":12891,"col-2-1":19986},{"col-0-2":"404","col-1-1":696,"col-2-1":19881},{"col-0-2":"503","col-1-1":417,"col-2-1":0}],"type":"datatable"},"visType":"metric"}} \ No newline at end of file +{"as":"metricVis","type":"render","value":{"visConfig":{"dimensions":{"bucket":{"accessor":2,"format":{"id":"string","params":{}},"type":"vis_dimension"},"metrics":[{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"}]},"metric":{"labels":{"show":true},"metricColorMode":"None","palette":null,"percentageMode":false,"style":{"bgColor":false,"css":"font-family:'Open Sans', Helvetica, Arial, sans-serif;font-weight:normal;font-style:normal;text-decoration:none;text-align:center;font-size:60px;line-height:1","labelColor":false,"spec":{"fontFamily":"'Open Sans', Helvetica, Arial, sans-serif","fontSize":"60px","fontStyle":"normal","fontWeight":"normal","lineHeight":"1","textAlign":"center","textDecoration":"none"},"type":"style"}}},"visData":{"columns":[{"id":"col-0-2","meta":{"field":"response.raw","index":"logstash-*","params":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"hasPrecisionError":false,"id":"2","indexPatternId":"logstash-*","params":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"schema":"segment","type":"terms"},"type":"string"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"field":null,"index":"logstash-*","params":{"id":"number"},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"hasPrecisionError":false,"id":"1","indexPatternId":"logstash-*","params":{},"schema":"metric","type":"count"},"type":"number"},"name":"Count"},{"id":"col-2-1","meta":{"field":"bytes","index":"logstash-*","params":{"id":"bytes","params":null},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"hasPrecisionError":false,"id":"1","indexPatternId":"logstash-*","params":{"field":"bytes"},"schema":"metric","type":"max"},"type":"number"},"name":"Max bytes"}],"rows":[{"col-0-2":"200","col-1-1":12891,"col-2-1":19986},{"col-0-2":"404","col-1-1":696,"col-2-1":19881},{"col-0-2":"503","col-1-1":417,"col-2-1":0}],"type":"datatable"},"visType":"metric"}} \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/baseline/metric_empty_data.json b/test/interpreter_functional/snapshots/baseline/metric_empty_data.json index 6fa08239f422d..d667cc6088a3a 100644 --- a/test/interpreter_functional/snapshots/baseline/metric_empty_data.json +++ b/test/interpreter_functional/snapshots/baseline/metric_empty_data.json @@ -1 +1 @@ -{"as":"metricVis","type":"render","value":{"visConfig":{"dimensions":{"metrics":[{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"}]},"metric":{"colorSchema":"Green to Red","colorsRange":[{"from":0,"to":10000,"type":"range"}],"invertColors":false,"labels":{"show":true},"metricColorMode":"None","percentageMode":false,"style":{"bgColor":false,"bgFill":"#000","fontSize":60,"labelColor":false,"subText":""},"useRanges":false}},"visData":{"columns":[{"id":"col-0-2","meta":{"field":"response.raw","index":"logstash-*","params":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"id":"2","indexPatternId":"logstash-*","params":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"schema":"segment","type":"terms"},"type":"string"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"field":null,"index":"logstash-*","params":{"id":"number"},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"id":"1","indexPatternId":"logstash-*","params":{},"schema":"metric","type":"count"},"type":"number"},"name":"Count"},{"id":"col-2-1","meta":{"field":"bytes","index":"logstash-*","params":{"id":"bytes","params":null},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"id":"1","indexPatternId":"logstash-*","params":{"field":"bytes"},"schema":"metric","type":"max"},"type":"number"},"name":"Max bytes"}],"rows":[],"type":"datatable"},"visType":"metric"}} \ No newline at end of file +{"as":"metricVis","type":"render","value":{"visConfig":{"dimensions":{"metrics":[{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"}]},"metric":{"labels":{"show":true},"metricColorMode":"None","palette":null,"percentageMode":false,"style":{"bgColor":false,"css":"font-family:'Open Sans', Helvetica, Arial, sans-serif;font-weight:normal;font-style:normal;text-decoration:none;text-align:center;font-size:60px;line-height:1","labelColor":false,"spec":{"fontFamily":"'Open Sans', Helvetica, Arial, sans-serif","fontSize":"60px","fontStyle":"normal","fontWeight":"normal","lineHeight":"1","textAlign":"center","textDecoration":"none"},"type":"style"}}},"visData":{"columns":[{"id":"col-0-2","meta":{"field":"response.raw","index":"logstash-*","params":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"hasPrecisionError":false,"id":"2","indexPatternId":"logstash-*","params":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"schema":"segment","type":"terms"},"type":"string"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"field":null,"index":"logstash-*","params":{"id":"number"},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"hasPrecisionError":false,"id":"1","indexPatternId":"logstash-*","params":{},"schema":"metric","type":"count"},"type":"number"},"name":"Count"},{"id":"col-2-1","meta":{"field":"bytes","index":"logstash-*","params":{"id":"bytes","params":null},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"hasPrecisionError":false,"id":"1","indexPatternId":"logstash-*","params":{"field":"bytes"},"schema":"metric","type":"max"},"type":"number"},"name":"Max bytes"}],"rows":[],"type":"datatable"},"visType":"metric"}} \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/baseline/metric_multi_metric_data.json b/test/interpreter_functional/snapshots/baseline/metric_multi_metric_data.json index 4410447d2bb20..6ef90caf3da3e 100644 --- a/test/interpreter_functional/snapshots/baseline/metric_multi_metric_data.json +++ b/test/interpreter_functional/snapshots/baseline/metric_multi_metric_data.json @@ -1 +1 @@ -{"as":"metricVis","type":"render","value":{"visConfig":{"dimensions":{"metrics":[{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},{"accessor":1,"format":{"id":"string","params":{}},"type":"vis_dimension"}]},"metric":{"colorSchema":"Green to Red","colorsRange":[{"from":0,"to":10000,"type":"range"}],"invertColors":false,"labels":{"show":true},"metricColorMode":"None","percentageMode":false,"style":{"bgColor":false,"bgFill":"#000","fontSize":60,"labelColor":false,"subText":""},"useRanges":false}},"visData":{"columns":[{"id":"col-0-2","meta":{"field":"response.raw","index":"logstash-*","params":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"id":"2","indexPatternId":"logstash-*","params":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"schema":"segment","type":"terms"},"type":"string"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"field":null,"index":"logstash-*","params":{"id":"number"},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"id":"1","indexPatternId":"logstash-*","params":{},"schema":"metric","type":"count"},"type":"number"},"name":"Count"},{"id":"col-2-1","meta":{"field":"bytes","index":"logstash-*","params":{"id":"bytes","params":null},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"id":"1","indexPatternId":"logstash-*","params":{"field":"bytes"},"schema":"metric","type":"max"},"type":"number"},"name":"Max bytes"}],"rows":[{"col-0-2":"200","col-1-1":12891,"col-2-1":19986},{"col-0-2":"404","col-1-1":696,"col-2-1":19881},{"col-0-2":"503","col-1-1":417,"col-2-1":0}],"type":"datatable"},"visType":"metric"}} \ No newline at end of file +{"as":"metricVis","type":"render","value":{"visConfig":{"dimensions":{"metrics":[{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},{"accessor":1,"format":{"id":"string","params":{}},"type":"vis_dimension"}]},"metric":{"labels":{"show":true},"metricColorMode":"None","palette":null,"percentageMode":false,"style":{"bgColor":false,"css":"font-family:'Open Sans', Helvetica, Arial, sans-serif;font-weight:normal;font-style:normal;text-decoration:none;text-align:center;font-size:60px;line-height:1","labelColor":false,"spec":{"fontFamily":"'Open Sans', Helvetica, Arial, sans-serif","fontSize":"60px","fontStyle":"normal","fontWeight":"normal","lineHeight":"1","textAlign":"center","textDecoration":"none"},"type":"style"}}},"visData":{"columns":[{"id":"col-0-2","meta":{"field":"response.raw","index":"logstash-*","params":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"hasPrecisionError":false,"id":"2","indexPatternId":"logstash-*","params":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"schema":"segment","type":"terms"},"type":"string"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"field":null,"index":"logstash-*","params":{"id":"number"},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"hasPrecisionError":false,"id":"1","indexPatternId":"logstash-*","params":{},"schema":"metric","type":"count"},"type":"number"},"name":"Count"},{"id":"col-2-1","meta":{"field":"bytes","index":"logstash-*","params":{"id":"bytes","params":null},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"hasPrecisionError":false,"id":"1","indexPatternId":"logstash-*","params":{"field":"bytes"},"schema":"metric","type":"max"},"type":"number"},"name":"Max bytes"}],"rows":[{"col-0-2":"200","col-1-1":12891,"col-2-1":19986},{"col-0-2":"404","col-1-1":696,"col-2-1":19881},{"col-0-2":"503","col-1-1":417,"col-2-1":0}],"type":"datatable"},"visType":"metric"}} \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/baseline/metric_percentage_mode.json b/test/interpreter_functional/snapshots/baseline/metric_percentage_mode.json index 2abb3070c3d05..bc1ec6278dc32 100644 --- a/test/interpreter_functional/snapshots/baseline/metric_percentage_mode.json +++ b/test/interpreter_functional/snapshots/baseline/metric_percentage_mode.json @@ -1 +1 @@ -{"as":"metricVis","type":"render","value":{"visConfig":{"dimensions":{"metrics":[{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"}]},"metric":{"colorSchema":"Green to Red","colorsRange":[{"from":0,"to":1000,"type":"range"}],"invertColors":false,"labels":{"show":true},"metricColorMode":"None","percentageMode":true,"style":{"bgColor":false,"bgFill":"#000","fontSize":60,"labelColor":false,"subText":""},"useRanges":false}},"visData":{"columns":[{"id":"col-0-2","meta":{"field":"response.raw","index":"logstash-*","params":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"id":"2","indexPatternId":"logstash-*","params":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"schema":"segment","type":"terms"},"type":"string"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"field":null,"index":"logstash-*","params":{"id":"number"},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"id":"1","indexPatternId":"logstash-*","params":{},"schema":"metric","type":"count"},"type":"number"},"name":"Count"},{"id":"col-2-1","meta":{"field":"bytes","index":"logstash-*","params":{"id":"bytes","params":null},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"id":"1","indexPatternId":"logstash-*","params":{"field":"bytes"},"schema":"metric","type":"max"},"type":"number"},"name":"Max bytes"}],"rows":[{"col-0-2":"200","col-1-1":12891,"col-2-1":19986},{"col-0-2":"404","col-1-1":696,"col-2-1":19881},{"col-0-2":"503","col-1-1":417,"col-2-1":0}],"type":"datatable"},"visType":"metric"}} \ No newline at end of file +{"as":"metricVis","type":"render","value":{"visConfig":{"dimensions":{"metrics":[{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"}]},"metric":{"labels":{"show":true},"metricColorMode":"None","palette":{"colors":["rgb(0,0,0,0)","rgb(100, 100, 100)"],"continuity":"none","gradient":false,"range":"number","rangeMax":10000,"rangeMin":0,"stops":[0,10000]},"percentageMode":true,"style":{"bgColor":false,"css":"font-family:'Open Sans', Helvetica, Arial, sans-serif;font-weight:normal;font-style:normal;text-decoration:none;text-align:center;font-size:60px;line-height:1","labelColor":false,"spec":{"fontFamily":"'Open Sans', Helvetica, Arial, sans-serif","fontSize":"60px","fontStyle":"normal","fontWeight":"normal","lineHeight":"1","textAlign":"center","textDecoration":"none"},"type":"style"}}},"visData":{"columns":[{"id":"col-0-2","meta":{"field":"response.raw","index":"logstash-*","params":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"hasPrecisionError":false,"id":"2","indexPatternId":"logstash-*","params":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"schema":"segment","type":"terms"},"type":"string"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"field":null,"index":"logstash-*","params":{"id":"number"},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"hasPrecisionError":false,"id":"1","indexPatternId":"logstash-*","params":{},"schema":"metric","type":"count"},"type":"number"},"name":"Count"},{"id":"col-2-1","meta":{"field":"bytes","index":"logstash-*","params":{"id":"bytes","params":null},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"hasPrecisionError":false,"id":"1","indexPatternId":"logstash-*","params":{"field":"bytes"},"schema":"metric","type":"max"},"type":"number"},"name":"Max bytes"}],"rows":[{"col-0-2":"200","col-1-1":12891,"col-2-1":19986},{"col-0-2":"404","col-1-1":696,"col-2-1":19881},{"col-0-2":"503","col-1-1":417,"col-2-1":0}],"type":"datatable"},"visType":"metric"}} \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/baseline/metric_single_metric_data.json b/test/interpreter_functional/snapshots/baseline/metric_single_metric_data.json index cce892a2f8c6f..b5cc75694b4ba 100644 --- a/test/interpreter_functional/snapshots/baseline/metric_single_metric_data.json +++ b/test/interpreter_functional/snapshots/baseline/metric_single_metric_data.json @@ -1 +1 @@ -{"as":"metricVis","type":"render","value":{"visConfig":{"dimensions":{"metrics":[{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"}]},"metric":{"colorSchema":"Green to Red","colorsRange":[{"from":0,"to":10000,"type":"range"}],"invertColors":false,"labels":{"show":true},"metricColorMode":"None","percentageMode":false,"style":{"bgColor":false,"bgFill":"#000","fontSize":60,"labelColor":false,"subText":""},"useRanges":false}},"visData":{"columns":[{"id":"col-0-2","meta":{"field":"response.raw","index":"logstash-*","params":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"id":"2","indexPatternId":"logstash-*","params":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"schema":"segment","type":"terms"},"type":"string"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"field":null,"index":"logstash-*","params":{"id":"number"},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"id":"1","indexPatternId":"logstash-*","params":{},"schema":"metric","type":"count"},"type":"number"},"name":"Count"},{"id":"col-2-1","meta":{"field":"bytes","index":"logstash-*","params":{"id":"bytes","params":null},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"id":"1","indexPatternId":"logstash-*","params":{"field":"bytes"},"schema":"metric","type":"max"},"type":"number"},"name":"Max bytes"}],"rows":[{"col-0-2":"200","col-1-1":12891,"col-2-1":19986},{"col-0-2":"404","col-1-1":696,"col-2-1":19881},{"col-0-2":"503","col-1-1":417,"col-2-1":0}],"type":"datatable"},"visType":"metric"}} \ No newline at end of file +{"as":"metricVis","type":"render","value":{"visConfig":{"dimensions":{"metrics":[{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"}]},"metric":{"labels":{"show":true},"metricColorMode":"None","palette":null,"percentageMode":false,"style":{"bgColor":false,"css":"font-family:'Open Sans', Helvetica, Arial, sans-serif;font-weight:normal;font-style:normal;text-decoration:none;text-align:center;font-size:60px;line-height:1","labelColor":false,"spec":{"fontFamily":"'Open Sans', Helvetica, Arial, sans-serif","fontSize":"60px","fontStyle":"normal","fontWeight":"normal","lineHeight":"1","textAlign":"center","textDecoration":"none"},"type":"style"}}},"visData":{"columns":[{"id":"col-0-2","meta":{"field":"response.raw","index":"logstash-*","params":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"hasPrecisionError":false,"id":"2","indexPatternId":"logstash-*","params":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"schema":"segment","type":"terms"},"type":"string"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"field":null,"index":"logstash-*","params":{"id":"number"},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"hasPrecisionError":false,"id":"1","indexPatternId":"logstash-*","params":{},"schema":"metric","type":"count"},"type":"number"},"name":"Count"},{"id":"col-2-1","meta":{"field":"bytes","index":"logstash-*","params":{"id":"bytes","params":null},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"hasPrecisionError":false,"id":"1","indexPatternId":"logstash-*","params":{"field":"bytes"},"schema":"metric","type":"max"},"type":"number"},"name":"Max bytes"}],"rows":[{"col-0-2":"200","col-1-1":12891,"col-2-1":19986},{"col-0-2":"404","col-1-1":696,"col-2-1":19881},{"col-0-2":"503","col-1-1":417,"col-2-1":0}],"type":"datatable"},"visType":"metric"}} \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/baseline/partial_test_1.json b/test/interpreter_functional/snapshots/baseline/partial_test_1.json index 9877a0d3138c0..5b081f4d0713e 100644 --- a/test/interpreter_functional/snapshots/baseline/partial_test_1.json +++ b/test/interpreter_functional/snapshots/baseline/partial_test_1.json @@ -1 +1 @@ -{"as":"tagcloud","type":"render","value":{"syncColors":false,"visData":{"columns":[{"id":"col-0-2","meta":{"field":"response.raw","index":"logstash-*","params":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"id":"2","indexPatternId":"logstash-*","params":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"schema":"segment","type":"terms"},"type":"string"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"field":null,"index":"logstash-*","params":{"id":"number"},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"id":"1","indexPatternId":"logstash-*","params":{},"schema":"metric","type":"count"},"type":"number"},"name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"datatable"},"visParams":{"bucket":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"maxFontSize":72,"metric":{"accessor":1,"format":{"id":"number","params":{}},"type":"vis_dimension"},"minFontSize":18,"orientation":"single","palette":{"name":"custom","params":{"colors":["#882E72","#B178A6","#D6C1DE","#1965B0","#5289C7","#7BAFDE","#4EB265","#90C987","#CAE0AB","#F7EE55","#F6C141","#F1932D","#E8601C","#DC050C"],"continuity":"above","gradient":false,"range":"percent","rangeMax":100,"rangeMin":0,"stops":[]},"type":"palette"},"scale":"linear","showLabel":true},"visType":"tagcloud"}} \ No newline at end of file +{"as":"tagcloud","type":"render","value":{"syncColors":false,"visData":{"columns":[{"id":"col-0-2","meta":{"field":"response.raw","index":"logstash-*","params":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"hasPrecisionError":false,"id":"2","indexPatternId":"logstash-*","params":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"schema":"segment","type":"terms"},"type":"string"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"field":null,"index":"logstash-*","params":{"id":"number"},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"hasPrecisionError":false,"id":"1","indexPatternId":"logstash-*","params":{},"schema":"metric","type":"count"},"type":"number"},"name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"datatable"},"visParams":{"bucket":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"maxFontSize":72,"metric":{"accessor":1,"format":{"id":"number","params":{}},"type":"vis_dimension"},"minFontSize":18,"orientation":"single","palette":{"name":"custom","params":{"colors":["#882E72","#B178A6","#D6C1DE","#1965B0","#5289C7","#7BAFDE","#4EB265","#90C987","#CAE0AB","#F7EE55","#F6C141","#F1932D","#E8601C","#DC050C"],"continuity":"above","gradient":false,"range":"percent","rangeMax":100,"rangeMin":0,"stops":[]},"type":"palette"},"scale":"linear","showLabel":true},"visType":"tagcloud"}} \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/baseline/partial_test_2.json b/test/interpreter_functional/snapshots/baseline/partial_test_2.json index 107d3fcbc5c54..2ddf40eb79006 100644 --- a/test/interpreter_functional/snapshots/baseline/partial_test_2.json +++ b/test/interpreter_functional/snapshots/baseline/partial_test_2.json @@ -1 +1 @@ -{"as":"metricVis","type":"render","value":{"visConfig":{"dimensions":{"bucket":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"metrics":[{"accessor":1,"format":{"id":"number","params":{}},"type":"vis_dimension"}]},"metric":{"colorSchema":"Green to Red","colorsRange":[{"from":0,"to":10000,"type":"range"}],"invertColors":false,"labels":{"show":true},"metricColorMode":"None","percentageMode":false,"style":{"bgColor":false,"bgFill":"#000","fontSize":60,"labelColor":false,"subText":""},"useRanges":false}},"visData":{"columns":[{"id":"col-0-2","meta":{"field":"response.raw","index":"logstash-*","params":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"id":"2","indexPatternId":"logstash-*","params":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"schema":"segment","type":"terms"},"type":"string"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"field":null,"index":"logstash-*","params":{"id":"number"},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"id":"1","indexPatternId":"logstash-*","params":{},"schema":"metric","type":"count"},"type":"number"},"name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"datatable"},"visType":"metric"}} \ No newline at end of file +{"as":"metricVis","type":"render","value":{"visConfig":{"dimensions":{"bucket":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"metrics":[{"accessor":1,"format":{"id":"number","params":{}},"type":"vis_dimension"}]},"metric":{"labels":{"show":true},"metricColorMode":"None","palette":null,"percentageMode":false,"style":{"bgColor":false,"css":"font-family:'Open Sans', Helvetica, Arial, sans-serif;font-weight:normal;font-style:normal;text-decoration:none;text-align:center;font-size:60px;line-height:1","labelColor":false,"spec":{"fontFamily":"'Open Sans', Helvetica, Arial, sans-serif","fontSize":"60px","fontStyle":"normal","fontWeight":"normal","lineHeight":"1","textAlign":"center","textDecoration":"none"},"type":"style"}}},"visData":{"columns":[{"id":"col-0-2","meta":{"field":"response.raw","index":"logstash-*","params":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"hasPrecisionError":false,"id":"2","indexPatternId":"logstash-*","params":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"schema":"segment","type":"terms"},"type":"string"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"field":null,"index":"logstash-*","params":{"id":"number"},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"hasPrecisionError":false,"id":"1","indexPatternId":"logstash-*","params":{},"schema":"metric","type":"count"},"type":"number"},"name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"datatable"},"visType":"metric"}} \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/baseline/step_output_test2.json b/test/interpreter_functional/snapshots/baseline/step_output_test2.json index 4870694e6adbc..3b030ec8fb597 100644 --- a/test/interpreter_functional/snapshots/baseline/step_output_test2.json +++ b/test/interpreter_functional/snapshots/baseline/step_output_test2.json @@ -1 +1 @@ -{"columns":[{"id":"col-0-2","meta":{"field":"response.raw","index":"logstash-*","params":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"id":"2","indexPatternId":"logstash-*","params":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"schema":"segment","type":"terms"},"type":"string"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"field":null,"index":"logstash-*","params":{"id":"number"},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"id":"1","indexPatternId":"logstash-*","params":{},"schema":"metric","type":"count"},"type":"number"},"name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"datatable"} \ No newline at end of file +{"columns":[{"id":"col-0-2","meta":{"field":"response.raw","index":"logstash-*","params":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"hasPrecisionError":false,"id":"2","indexPatternId":"logstash-*","params":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"schema":"segment","type":"terms"},"type":"string"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"field":null,"index":"logstash-*","params":{"id":"number"},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"hasPrecisionError":false,"id":"1","indexPatternId":"logstash-*","params":{},"schema":"metric","type":"count"},"type":"number"},"name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"datatable"} \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/baseline/step_output_test3.json b/test/interpreter_functional/snapshots/baseline/step_output_test3.json index 107d3fcbc5c54..2ddf40eb79006 100644 --- a/test/interpreter_functional/snapshots/baseline/step_output_test3.json +++ b/test/interpreter_functional/snapshots/baseline/step_output_test3.json @@ -1 +1 @@ -{"as":"metricVis","type":"render","value":{"visConfig":{"dimensions":{"bucket":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"metrics":[{"accessor":1,"format":{"id":"number","params":{}},"type":"vis_dimension"}]},"metric":{"colorSchema":"Green to Red","colorsRange":[{"from":0,"to":10000,"type":"range"}],"invertColors":false,"labels":{"show":true},"metricColorMode":"None","percentageMode":false,"style":{"bgColor":false,"bgFill":"#000","fontSize":60,"labelColor":false,"subText":""},"useRanges":false}},"visData":{"columns":[{"id":"col-0-2","meta":{"field":"response.raw","index":"logstash-*","params":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"id":"2","indexPatternId":"logstash-*","params":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"schema":"segment","type":"terms"},"type":"string"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"field":null,"index":"logstash-*","params":{"id":"number"},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"id":"1","indexPatternId":"logstash-*","params":{},"schema":"metric","type":"count"},"type":"number"},"name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"datatable"},"visType":"metric"}} \ No newline at end of file +{"as":"metricVis","type":"render","value":{"visConfig":{"dimensions":{"bucket":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"metrics":[{"accessor":1,"format":{"id":"number","params":{}},"type":"vis_dimension"}]},"metric":{"labels":{"show":true},"metricColorMode":"None","palette":null,"percentageMode":false,"style":{"bgColor":false,"css":"font-family:'Open Sans', Helvetica, Arial, sans-serif;font-weight:normal;font-style:normal;text-decoration:none;text-align:center;font-size:60px;line-height:1","labelColor":false,"spec":{"fontFamily":"'Open Sans', Helvetica, Arial, sans-serif","fontSize":"60px","fontStyle":"normal","fontWeight":"normal","lineHeight":"1","textAlign":"center","textDecoration":"none"},"type":"style"}}},"visData":{"columns":[{"id":"col-0-2","meta":{"field":"response.raw","index":"logstash-*","params":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"hasPrecisionError":false,"id":"2","indexPatternId":"logstash-*","params":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"schema":"segment","type":"terms"},"type":"string"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"field":null,"index":"logstash-*","params":{"id":"number"},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"hasPrecisionError":false,"id":"1","indexPatternId":"logstash-*","params":{},"schema":"metric","type":"count"},"type":"number"},"name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"datatable"},"visType":"metric"}} \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/baseline/tagcloud_all_data.json b/test/interpreter_functional/snapshots/baseline/tagcloud_all_data.json index 5ddf081c54d95..8f079b49ed98d 100644 --- a/test/interpreter_functional/snapshots/baseline/tagcloud_all_data.json +++ b/test/interpreter_functional/snapshots/baseline/tagcloud_all_data.json @@ -1 +1 @@ -{"as":"tagcloud","type":"render","value":{"syncColors":false,"visData":{"columns":[{"id":"col-0-2","meta":{"field":"response.raw","index":"logstash-*","params":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"id":"2","indexPatternId":"logstash-*","params":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"schema":"segment","type":"terms"},"type":"string"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"field":null,"index":"logstash-*","params":{"id":"number"},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"id":"1","indexPatternId":"logstash-*","params":{},"schema":"metric","type":"count"},"type":"number"},"name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"datatable"},"visParams":{"bucket":{"accessor":1,"format":{"id":"string","params":{}},"type":"vis_dimension"},"maxFontSize":72,"metric":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"minFontSize":18,"orientation":"single","palette":{"name":"custom","params":{"colors":["#882E72","#B178A6","#D6C1DE","#1965B0","#5289C7","#7BAFDE","#4EB265","#90C987","#CAE0AB","#F7EE55","#F6C141","#F1932D","#E8601C","#DC050C"],"continuity":"above","gradient":false,"range":"percent","rangeMax":100,"rangeMin":0,"stops":[]},"type":"palette"},"scale":"linear","showLabel":true},"visType":"tagcloud"}} \ No newline at end of file +{"as":"tagcloud","type":"render","value":{"syncColors":false,"visData":{"columns":[{"id":"col-0-2","meta":{"field":"response.raw","index":"logstash-*","params":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"hasPrecisionError":false,"id":"2","indexPatternId":"logstash-*","params":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"schema":"segment","type":"terms"},"type":"string"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"field":null,"index":"logstash-*","params":{"id":"number"},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"hasPrecisionError":false,"id":"1","indexPatternId":"logstash-*","params":{},"schema":"metric","type":"count"},"type":"number"},"name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"datatable"},"visParams":{"bucket":{"accessor":1,"format":{"id":"string","params":{}},"type":"vis_dimension"},"maxFontSize":72,"metric":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"minFontSize":18,"orientation":"single","palette":{"name":"custom","params":{"colors":["#882E72","#B178A6","#D6C1DE","#1965B0","#5289C7","#7BAFDE","#4EB265","#90C987","#CAE0AB","#F7EE55","#F6C141","#F1932D","#E8601C","#DC050C"],"continuity":"above","gradient":false,"range":"percent","rangeMax":100,"rangeMin":0,"stops":[]},"type":"palette"},"scale":"linear","showLabel":true},"visType":"tagcloud"}} \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/baseline/tagcloud_empty_data.json b/test/interpreter_functional/snapshots/baseline/tagcloud_empty_data.json index 723ebb6e9f460..e0026b189949d 100644 --- a/test/interpreter_functional/snapshots/baseline/tagcloud_empty_data.json +++ b/test/interpreter_functional/snapshots/baseline/tagcloud_empty_data.json @@ -1 +1 @@ -{"as":"tagcloud","type":"render","value":{"syncColors":false,"visData":{"columns":[{"id":"col-0-2","meta":{"field":"response.raw","index":"logstash-*","params":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"id":"2","indexPatternId":"logstash-*","params":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"schema":"segment","type":"terms"},"type":"string"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"field":null,"index":"logstash-*","params":{"id":"number"},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"id":"1","indexPatternId":"logstash-*","params":{},"schema":"metric","type":"count"},"type":"number"},"name":"Count"}],"rows":[],"type":"datatable"},"visParams":{"maxFontSize":72,"metric":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"minFontSize":18,"orientation":"single","palette":{"name":"custom","params":{"colors":["#882E72","#B178A6","#D6C1DE","#1965B0","#5289C7","#7BAFDE","#4EB265","#90C987","#CAE0AB","#F7EE55","#F6C141","#F1932D","#E8601C","#DC050C"],"continuity":"above","gradient":false,"range":"percent","rangeMax":100,"rangeMin":0,"stops":[]},"type":"palette"},"scale":"linear","showLabel":true},"visType":"tagcloud"}} \ No newline at end of file +{"as":"tagcloud","type":"render","value":{"syncColors":false,"visData":{"columns":[{"id":"col-0-2","meta":{"field":"response.raw","index":"logstash-*","params":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"hasPrecisionError":false,"id":"2","indexPatternId":"logstash-*","params":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"schema":"segment","type":"terms"},"type":"string"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"field":null,"index":"logstash-*","params":{"id":"number"},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"hasPrecisionError":false,"id":"1","indexPatternId":"logstash-*","params":{},"schema":"metric","type":"count"},"type":"number"},"name":"Count"}],"rows":[],"type":"datatable"},"visParams":{"maxFontSize":72,"metric":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"minFontSize":18,"orientation":"single","palette":{"name":"custom","params":{"colors":["#882E72","#B178A6","#D6C1DE","#1965B0","#5289C7","#7BAFDE","#4EB265","#90C987","#CAE0AB","#F7EE55","#F6C141","#F1932D","#E8601C","#DC050C"],"continuity":"above","gradient":false,"range":"percent","rangeMax":100,"rangeMin":0,"stops":[]},"type":"palette"},"scale":"linear","showLabel":true},"visType":"tagcloud"}} \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/baseline/tagcloud_fontsize.json b/test/interpreter_functional/snapshots/baseline/tagcloud_fontsize.json index 1655451d41d03..4eef2bcb1fc48 100644 --- a/test/interpreter_functional/snapshots/baseline/tagcloud_fontsize.json +++ b/test/interpreter_functional/snapshots/baseline/tagcloud_fontsize.json @@ -1 +1 @@ -{"as":"tagcloud","type":"render","value":{"syncColors":false,"visData":{"columns":[{"id":"col-0-2","meta":{"field":"response.raw","index":"logstash-*","params":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"id":"2","indexPatternId":"logstash-*","params":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"schema":"segment","type":"terms"},"type":"string"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"field":null,"index":"logstash-*","params":{"id":"number"},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"id":"1","indexPatternId":"logstash-*","params":{},"schema":"metric","type":"count"},"type":"number"},"name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"datatable"},"visParams":{"bucket":{"accessor":1,"format":{"id":"string","params":{}},"type":"vis_dimension"},"maxFontSize":40,"metric":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"minFontSize":20,"orientation":"single","palette":{"name":"custom","params":{"colors":["#882E72","#B178A6","#D6C1DE","#1965B0","#5289C7","#7BAFDE","#4EB265","#90C987","#CAE0AB","#F7EE55","#F6C141","#F1932D","#E8601C","#DC050C"],"continuity":"above","gradient":false,"range":"percent","rangeMax":100,"rangeMin":0,"stops":[]},"type":"palette"},"scale":"linear","showLabel":true},"visType":"tagcloud"}} \ No newline at end of file +{"as":"tagcloud","type":"render","value":{"syncColors":false,"visData":{"columns":[{"id":"col-0-2","meta":{"field":"response.raw","index":"logstash-*","params":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"hasPrecisionError":false,"id":"2","indexPatternId":"logstash-*","params":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"schema":"segment","type":"terms"},"type":"string"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"field":null,"index":"logstash-*","params":{"id":"number"},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"hasPrecisionError":false,"id":"1","indexPatternId":"logstash-*","params":{},"schema":"metric","type":"count"},"type":"number"},"name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"datatable"},"visParams":{"bucket":{"accessor":1,"format":{"id":"string","params":{}},"type":"vis_dimension"},"maxFontSize":40,"metric":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"minFontSize":20,"orientation":"single","palette":{"name":"custom","params":{"colors":["#882E72","#B178A6","#D6C1DE","#1965B0","#5289C7","#7BAFDE","#4EB265","#90C987","#CAE0AB","#F7EE55","#F6C141","#F1932D","#E8601C","#DC050C"],"continuity":"above","gradient":false,"range":"percent","rangeMax":100,"rangeMin":0,"stops":[]},"type":"palette"},"scale":"linear","showLabel":true},"visType":"tagcloud"}} \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/baseline/tagcloud_metric_data.json b/test/interpreter_functional/snapshots/baseline/tagcloud_metric_data.json index f0bfd56ac99b8..26ca82acd7563 100644 --- a/test/interpreter_functional/snapshots/baseline/tagcloud_metric_data.json +++ b/test/interpreter_functional/snapshots/baseline/tagcloud_metric_data.json @@ -1 +1 @@ -{"as":"tagcloud","type":"render","value":{"syncColors":false,"visData":{"columns":[{"id":"col-0-2","meta":{"field":"response.raw","index":"logstash-*","params":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"id":"2","indexPatternId":"logstash-*","params":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"schema":"segment","type":"terms"},"type":"string"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"field":null,"index":"logstash-*","params":{"id":"number"},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"id":"1","indexPatternId":"logstash-*","params":{},"schema":"metric","type":"count"},"type":"number"},"name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"datatable"},"visParams":{"maxFontSize":72,"metric":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"minFontSize":18,"orientation":"single","palette":{"name":"custom","params":{"colors":["#882E72","#B178A6","#D6C1DE","#1965B0","#5289C7","#7BAFDE","#4EB265","#90C987","#CAE0AB","#F7EE55","#F6C141","#F1932D","#E8601C","#DC050C"],"continuity":"above","gradient":false,"range":"percent","rangeMax":100,"rangeMin":0,"stops":[]},"type":"palette"},"scale":"linear","showLabel":true},"visType":"tagcloud"}} \ No newline at end of file +{"as":"tagcloud","type":"render","value":{"syncColors":false,"visData":{"columns":[{"id":"col-0-2","meta":{"field":"response.raw","index":"logstash-*","params":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"hasPrecisionError":false,"id":"2","indexPatternId":"logstash-*","params":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"schema":"segment","type":"terms"},"type":"string"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"field":null,"index":"logstash-*","params":{"id":"number"},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"hasPrecisionError":false,"id":"1","indexPatternId":"logstash-*","params":{},"schema":"metric","type":"count"},"type":"number"},"name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"datatable"},"visParams":{"maxFontSize":72,"metric":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"minFontSize":18,"orientation":"single","palette":{"name":"custom","params":{"colors":["#882E72","#B178A6","#D6C1DE","#1965B0","#5289C7","#7BAFDE","#4EB265","#90C987","#CAE0AB","#F7EE55","#F6C141","#F1932D","#E8601C","#DC050C"],"continuity":"above","gradient":false,"range":"percent","rangeMax":100,"rangeMin":0,"stops":[]},"type":"palette"},"scale":"linear","showLabel":true},"visType":"tagcloud"}} \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/baseline/tagcloud_options.json b/test/interpreter_functional/snapshots/baseline/tagcloud_options.json index ba034fa2e435a..d13cc180e1e7d 100644 --- a/test/interpreter_functional/snapshots/baseline/tagcloud_options.json +++ b/test/interpreter_functional/snapshots/baseline/tagcloud_options.json @@ -1 +1 @@ -{"as":"tagcloud","type":"render","value":{"syncColors":false,"visData":{"columns":[{"id":"col-0-2","meta":{"field":"response.raw","index":"logstash-*","params":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"id":"2","indexPatternId":"logstash-*","params":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"schema":"segment","type":"terms"},"type":"string"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"field":null,"index":"logstash-*","params":{"id":"number"},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"id":"1","indexPatternId":"logstash-*","params":{},"schema":"metric","type":"count"},"type":"number"},"name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"datatable"},"visParams":{"bucket":{"accessor":1,"format":{"id":"string","params":{}},"type":"vis_dimension"},"maxFontSize":72,"metric":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"minFontSize":18,"orientation":"multiple","palette":{"name":"custom","params":{"colors":["#882E72","#B178A6","#D6C1DE","#1965B0","#5289C7","#7BAFDE","#4EB265","#90C987","#CAE0AB","#F7EE55","#F6C141","#F1932D","#E8601C","#DC050C"],"continuity":"above","gradient":false,"range":"percent","rangeMax":100,"rangeMin":0,"stops":[]},"type":"palette"},"scale":"log","showLabel":true},"visType":"tagcloud"}} \ No newline at end of file +{"as":"tagcloud","type":"render","value":{"syncColors":false,"visData":{"columns":[{"id":"col-0-2","meta":{"field":"response.raw","index":"logstash-*","params":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"hasPrecisionError":false,"id":"2","indexPatternId":"logstash-*","params":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"schema":"segment","type":"terms"},"type":"string"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"field":null,"index":"logstash-*","params":{"id":"number"},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"hasPrecisionError":false,"id":"1","indexPatternId":"logstash-*","params":{},"schema":"metric","type":"count"},"type":"number"},"name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"datatable"},"visParams":{"bucket":{"accessor":1,"format":{"id":"string","params":{}},"type":"vis_dimension"},"maxFontSize":72,"metric":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"minFontSize":18,"orientation":"multiple","palette":{"name":"custom","params":{"colors":["#882E72","#B178A6","#D6C1DE","#1965B0","#5289C7","#7BAFDE","#4EB265","#90C987","#CAE0AB","#F7EE55","#F6C141","#F1932D","#E8601C","#DC050C"],"continuity":"above","gradient":false,"range":"percent","rangeMax":100,"rangeMin":0,"stops":[]},"type":"palette"},"scale":"log","showLabel":true},"visType":"tagcloud"}} \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/session/combined_test0.json b/test/interpreter_functional/snapshots/session/combined_test0.json deleted file mode 100644 index 8f00d72df8ab3..0000000000000 --- a/test/interpreter_functional/snapshots/session/combined_test0.json +++ /dev/null @@ -1 +0,0 @@ -{"filters":[],"query":[],"timeRange":null,"type":"kibana_context"} \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/session/combined_test1.json b/test/interpreter_functional/snapshots/session/combined_test1.json deleted file mode 100644 index 8f00d72df8ab3..0000000000000 --- a/test/interpreter_functional/snapshots/session/combined_test1.json +++ /dev/null @@ -1 +0,0 @@ -{"filters":[],"query":[],"timeRange":null,"type":"kibana_context"} \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/session/combined_test2.json b/test/interpreter_functional/snapshots/session/combined_test2.json deleted file mode 100644 index 4870694e6adbc..0000000000000 --- a/test/interpreter_functional/snapshots/session/combined_test2.json +++ /dev/null @@ -1 +0,0 @@ -{"columns":[{"id":"col-0-2","meta":{"field":"response.raw","index":"logstash-*","params":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"id":"2","indexPatternId":"logstash-*","params":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"schema":"segment","type":"terms"},"type":"string"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"field":null,"index":"logstash-*","params":{"id":"number"},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"id":"1","indexPatternId":"logstash-*","params":{},"schema":"metric","type":"count"},"type":"number"},"name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"datatable"} \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/session/combined_test3.json b/test/interpreter_functional/snapshots/session/combined_test3.json deleted file mode 100644 index 107d3fcbc5c54..0000000000000 --- a/test/interpreter_functional/snapshots/session/combined_test3.json +++ /dev/null @@ -1 +0,0 @@ -{"as":"metricVis","type":"render","value":{"visConfig":{"dimensions":{"bucket":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"metrics":[{"accessor":1,"format":{"id":"number","params":{}},"type":"vis_dimension"}]},"metric":{"colorSchema":"Green to Red","colorsRange":[{"from":0,"to":10000,"type":"range"}],"invertColors":false,"labels":{"show":true},"metricColorMode":"None","percentageMode":false,"style":{"bgColor":false,"bgFill":"#000","fontSize":60,"labelColor":false,"subText":""},"useRanges":false}},"visData":{"columns":[{"id":"col-0-2","meta":{"field":"response.raw","index":"logstash-*","params":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"id":"2","indexPatternId":"logstash-*","params":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"schema":"segment","type":"terms"},"type":"string"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"field":null,"index":"logstash-*","params":{"id":"number"},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"id":"1","indexPatternId":"logstash-*","params":{},"schema":"metric","type":"count"},"type":"number"},"name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"datatable"},"visType":"metric"}} \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/session/final_output_test.json b/test/interpreter_functional/snapshots/session/final_output_test.json deleted file mode 100644 index 107d3fcbc5c54..0000000000000 --- a/test/interpreter_functional/snapshots/session/final_output_test.json +++ /dev/null @@ -1 +0,0 @@ -{"as":"metricVis","type":"render","value":{"visConfig":{"dimensions":{"bucket":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"metrics":[{"accessor":1,"format":{"id":"number","params":{}},"type":"vis_dimension"}]},"metric":{"colorSchema":"Green to Red","colorsRange":[{"from":0,"to":10000,"type":"range"}],"invertColors":false,"labels":{"show":true},"metricColorMode":"None","percentageMode":false,"style":{"bgColor":false,"bgFill":"#000","fontSize":60,"labelColor":false,"subText":""},"useRanges":false}},"visData":{"columns":[{"id":"col-0-2","meta":{"field":"response.raw","index":"logstash-*","params":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"id":"2","indexPatternId":"logstash-*","params":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"schema":"segment","type":"terms"},"type":"string"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"field":null,"index":"logstash-*","params":{"id":"number"},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"id":"1","indexPatternId":"logstash-*","params":{},"schema":"metric","type":"count"},"type":"number"},"name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"datatable"},"visType":"metric"}} \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/session/metric_all_data.json b/test/interpreter_functional/snapshots/session/metric_all_data.json deleted file mode 100644 index 9c10b53ce8604..0000000000000 --- a/test/interpreter_functional/snapshots/session/metric_all_data.json +++ /dev/null @@ -1 +0,0 @@ -{"as":"metricVis","type":"render","value":{"visConfig":{"dimensions":{"bucket":{"accessor":2,"format":{"id":"string","params":{}},"type":"vis_dimension"},"metrics":[{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"}]},"metric":{"colorSchema":"Green to Red","colorsRange":[{"from":0,"to":10000,"type":"range"}],"invertColors":false,"labels":{"show":true},"metricColorMode":"None","percentageMode":false,"style":{"bgColor":false,"bgFill":"#000","fontSize":60,"labelColor":false,"subText":""},"useRanges":false}},"visData":{"columns":[{"id":"col-0-2","meta":{"field":"response.raw","index":"logstash-*","params":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"id":"2","indexPatternId":"logstash-*","params":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"schema":"segment","type":"terms"},"type":"string"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"field":null,"index":"logstash-*","params":{"id":"number"},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"id":"1","indexPatternId":"logstash-*","params":{},"schema":"metric","type":"count"},"type":"number"},"name":"Count"},{"id":"col-2-1","meta":{"field":"bytes","index":"logstash-*","params":{"id":"bytes","params":null},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"id":"1","indexPatternId":"logstash-*","params":{"field":"bytes"},"schema":"metric","type":"max"},"type":"number"},"name":"Max bytes"}],"rows":[{"col-0-2":"200","col-1-1":12891,"col-2-1":19986},{"col-0-2":"404","col-1-1":696,"col-2-1":19881},{"col-0-2":"503","col-1-1":417,"col-2-1":0}],"type":"datatable"},"visType":"metric"}} \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/session/metric_empty_data.json b/test/interpreter_functional/snapshots/session/metric_empty_data.json deleted file mode 100644 index 6fa08239f422d..0000000000000 --- a/test/interpreter_functional/snapshots/session/metric_empty_data.json +++ /dev/null @@ -1 +0,0 @@ -{"as":"metricVis","type":"render","value":{"visConfig":{"dimensions":{"metrics":[{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"}]},"metric":{"colorSchema":"Green to Red","colorsRange":[{"from":0,"to":10000,"type":"range"}],"invertColors":false,"labels":{"show":true},"metricColorMode":"None","percentageMode":false,"style":{"bgColor":false,"bgFill":"#000","fontSize":60,"labelColor":false,"subText":""},"useRanges":false}},"visData":{"columns":[{"id":"col-0-2","meta":{"field":"response.raw","index":"logstash-*","params":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"id":"2","indexPatternId":"logstash-*","params":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"schema":"segment","type":"terms"},"type":"string"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"field":null,"index":"logstash-*","params":{"id":"number"},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"id":"1","indexPatternId":"logstash-*","params":{},"schema":"metric","type":"count"},"type":"number"},"name":"Count"},{"id":"col-2-1","meta":{"field":"bytes","index":"logstash-*","params":{"id":"bytes","params":null},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"id":"1","indexPatternId":"logstash-*","params":{"field":"bytes"},"schema":"metric","type":"max"},"type":"number"},"name":"Max bytes"}],"rows":[],"type":"datatable"},"visType":"metric"}} \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/session/metric_invalid_data.json b/test/interpreter_functional/snapshots/session/metric_invalid_data.json deleted file mode 100644 index f23b9b0915774..0000000000000 --- a/test/interpreter_functional/snapshots/session/metric_invalid_data.json +++ /dev/null @@ -1 +0,0 @@ -"[metricVis] > [visdimension] > Column name or index provided is invalid" \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/session/metric_multi_metric_data.json b/test/interpreter_functional/snapshots/session/metric_multi_metric_data.json deleted file mode 100644 index 4410447d2bb20..0000000000000 --- a/test/interpreter_functional/snapshots/session/metric_multi_metric_data.json +++ /dev/null @@ -1 +0,0 @@ -{"as":"metricVis","type":"render","value":{"visConfig":{"dimensions":{"metrics":[{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},{"accessor":1,"format":{"id":"string","params":{}},"type":"vis_dimension"}]},"metric":{"colorSchema":"Green to Red","colorsRange":[{"from":0,"to":10000,"type":"range"}],"invertColors":false,"labels":{"show":true},"metricColorMode":"None","percentageMode":false,"style":{"bgColor":false,"bgFill":"#000","fontSize":60,"labelColor":false,"subText":""},"useRanges":false}},"visData":{"columns":[{"id":"col-0-2","meta":{"field":"response.raw","index":"logstash-*","params":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"id":"2","indexPatternId":"logstash-*","params":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"schema":"segment","type":"terms"},"type":"string"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"field":null,"index":"logstash-*","params":{"id":"number"},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"id":"1","indexPatternId":"logstash-*","params":{},"schema":"metric","type":"count"},"type":"number"},"name":"Count"},{"id":"col-2-1","meta":{"field":"bytes","index":"logstash-*","params":{"id":"bytes","params":null},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"id":"1","indexPatternId":"logstash-*","params":{"field":"bytes"},"schema":"metric","type":"max"},"type":"number"},"name":"Max bytes"}],"rows":[{"col-0-2":"200","col-1-1":12891,"col-2-1":19986},{"col-0-2":"404","col-1-1":696,"col-2-1":19881},{"col-0-2":"503","col-1-1":417,"col-2-1":0}],"type":"datatable"},"visType":"metric"}} \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/session/metric_percentage_mode.json b/test/interpreter_functional/snapshots/session/metric_percentage_mode.json deleted file mode 100644 index 2abb3070c3d05..0000000000000 --- a/test/interpreter_functional/snapshots/session/metric_percentage_mode.json +++ /dev/null @@ -1 +0,0 @@ -{"as":"metricVis","type":"render","value":{"visConfig":{"dimensions":{"metrics":[{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"}]},"metric":{"colorSchema":"Green to Red","colorsRange":[{"from":0,"to":1000,"type":"range"}],"invertColors":false,"labels":{"show":true},"metricColorMode":"None","percentageMode":true,"style":{"bgColor":false,"bgFill":"#000","fontSize":60,"labelColor":false,"subText":""},"useRanges":false}},"visData":{"columns":[{"id":"col-0-2","meta":{"field":"response.raw","index":"logstash-*","params":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"id":"2","indexPatternId":"logstash-*","params":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"schema":"segment","type":"terms"},"type":"string"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"field":null,"index":"logstash-*","params":{"id":"number"},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"id":"1","indexPatternId":"logstash-*","params":{},"schema":"metric","type":"count"},"type":"number"},"name":"Count"},{"id":"col-2-1","meta":{"field":"bytes","index":"logstash-*","params":{"id":"bytes","params":null},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"id":"1","indexPatternId":"logstash-*","params":{"field":"bytes"},"schema":"metric","type":"max"},"type":"number"},"name":"Max bytes"}],"rows":[{"col-0-2":"200","col-1-1":12891,"col-2-1":19986},{"col-0-2":"404","col-1-1":696,"col-2-1":19881},{"col-0-2":"503","col-1-1":417,"col-2-1":0}],"type":"datatable"},"visType":"metric"}} \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/session/metric_single_metric_data.json b/test/interpreter_functional/snapshots/session/metric_single_metric_data.json deleted file mode 100644 index cce892a2f8c6f..0000000000000 --- a/test/interpreter_functional/snapshots/session/metric_single_metric_data.json +++ /dev/null @@ -1 +0,0 @@ -{"as":"metricVis","type":"render","value":{"visConfig":{"dimensions":{"metrics":[{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"}]},"metric":{"colorSchema":"Green to Red","colorsRange":[{"from":0,"to":10000,"type":"range"}],"invertColors":false,"labels":{"show":true},"metricColorMode":"None","percentageMode":false,"style":{"bgColor":false,"bgFill":"#000","fontSize":60,"labelColor":false,"subText":""},"useRanges":false}},"visData":{"columns":[{"id":"col-0-2","meta":{"field":"response.raw","index":"logstash-*","params":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"id":"2","indexPatternId":"logstash-*","params":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"schema":"segment","type":"terms"},"type":"string"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"field":null,"index":"logstash-*","params":{"id":"number"},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"id":"1","indexPatternId":"logstash-*","params":{},"schema":"metric","type":"count"},"type":"number"},"name":"Count"},{"id":"col-2-1","meta":{"field":"bytes","index":"logstash-*","params":{"id":"bytes","params":null},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"id":"1","indexPatternId":"logstash-*","params":{"field":"bytes"},"schema":"metric","type":"max"},"type":"number"},"name":"Max bytes"}],"rows":[{"col-0-2":"200","col-1-1":12891,"col-2-1":19986},{"col-0-2":"404","col-1-1":696,"col-2-1":19881},{"col-0-2":"503","col-1-1":417,"col-2-1":0}],"type":"datatable"},"visType":"metric"}} \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/session/partial_test_1.json b/test/interpreter_functional/snapshots/session/partial_test_1.json deleted file mode 100644 index 9877a0d3138c0..0000000000000 --- a/test/interpreter_functional/snapshots/session/partial_test_1.json +++ /dev/null @@ -1 +0,0 @@ -{"as":"tagcloud","type":"render","value":{"syncColors":false,"visData":{"columns":[{"id":"col-0-2","meta":{"field":"response.raw","index":"logstash-*","params":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"id":"2","indexPatternId":"logstash-*","params":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"schema":"segment","type":"terms"},"type":"string"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"field":null,"index":"logstash-*","params":{"id":"number"},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"id":"1","indexPatternId":"logstash-*","params":{},"schema":"metric","type":"count"},"type":"number"},"name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"datatable"},"visParams":{"bucket":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"maxFontSize":72,"metric":{"accessor":1,"format":{"id":"number","params":{}},"type":"vis_dimension"},"minFontSize":18,"orientation":"single","palette":{"name":"custom","params":{"colors":["#882E72","#B178A6","#D6C1DE","#1965B0","#5289C7","#7BAFDE","#4EB265","#90C987","#CAE0AB","#F7EE55","#F6C141","#F1932D","#E8601C","#DC050C"],"continuity":"above","gradient":false,"range":"percent","rangeMax":100,"rangeMin":0,"stops":[]},"type":"palette"},"scale":"linear","showLabel":true},"visType":"tagcloud"}} \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/session/partial_test_2.json b/test/interpreter_functional/snapshots/session/partial_test_2.json deleted file mode 100644 index 107d3fcbc5c54..0000000000000 --- a/test/interpreter_functional/snapshots/session/partial_test_2.json +++ /dev/null @@ -1 +0,0 @@ -{"as":"metricVis","type":"render","value":{"visConfig":{"dimensions":{"bucket":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"metrics":[{"accessor":1,"format":{"id":"number","params":{}},"type":"vis_dimension"}]},"metric":{"colorSchema":"Green to Red","colorsRange":[{"from":0,"to":10000,"type":"range"}],"invertColors":false,"labels":{"show":true},"metricColorMode":"None","percentageMode":false,"style":{"bgColor":false,"bgFill":"#000","fontSize":60,"labelColor":false,"subText":""},"useRanges":false}},"visData":{"columns":[{"id":"col-0-2","meta":{"field":"response.raw","index":"logstash-*","params":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"id":"2","indexPatternId":"logstash-*","params":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"schema":"segment","type":"terms"},"type":"string"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"field":null,"index":"logstash-*","params":{"id":"number"},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"id":"1","indexPatternId":"logstash-*","params":{},"schema":"metric","type":"count"},"type":"number"},"name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"datatable"},"visType":"metric"}} \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/session/step_output_test0.json b/test/interpreter_functional/snapshots/session/step_output_test0.json deleted file mode 100644 index 8f00d72df8ab3..0000000000000 --- a/test/interpreter_functional/snapshots/session/step_output_test0.json +++ /dev/null @@ -1 +0,0 @@ -{"filters":[],"query":[],"timeRange":null,"type":"kibana_context"} \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/session/step_output_test1.json b/test/interpreter_functional/snapshots/session/step_output_test1.json deleted file mode 100644 index 8f00d72df8ab3..0000000000000 --- a/test/interpreter_functional/snapshots/session/step_output_test1.json +++ /dev/null @@ -1 +0,0 @@ -{"filters":[],"query":[],"timeRange":null,"type":"kibana_context"} \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/session/step_output_test2.json b/test/interpreter_functional/snapshots/session/step_output_test2.json deleted file mode 100644 index 4870694e6adbc..0000000000000 --- a/test/interpreter_functional/snapshots/session/step_output_test2.json +++ /dev/null @@ -1 +0,0 @@ -{"columns":[{"id":"col-0-2","meta":{"field":"response.raw","index":"logstash-*","params":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"id":"2","indexPatternId":"logstash-*","params":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"schema":"segment","type":"terms"},"type":"string"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"field":null,"index":"logstash-*","params":{"id":"number"},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"id":"1","indexPatternId":"logstash-*","params":{},"schema":"metric","type":"count"},"type":"number"},"name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"datatable"} \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/session/step_output_test3.json b/test/interpreter_functional/snapshots/session/step_output_test3.json deleted file mode 100644 index 107d3fcbc5c54..0000000000000 --- a/test/interpreter_functional/snapshots/session/step_output_test3.json +++ /dev/null @@ -1 +0,0 @@ -{"as":"metricVis","type":"render","value":{"visConfig":{"dimensions":{"bucket":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"metrics":[{"accessor":1,"format":{"id":"number","params":{}},"type":"vis_dimension"}]},"metric":{"colorSchema":"Green to Red","colorsRange":[{"from":0,"to":10000,"type":"range"}],"invertColors":false,"labels":{"show":true},"metricColorMode":"None","percentageMode":false,"style":{"bgColor":false,"bgFill":"#000","fontSize":60,"labelColor":false,"subText":""},"useRanges":false}},"visData":{"columns":[{"id":"col-0-2","meta":{"field":"response.raw","index":"logstash-*","params":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"id":"2","indexPatternId":"logstash-*","params":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"schema":"segment","type":"terms"},"type":"string"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"field":null,"index":"logstash-*","params":{"id":"number"},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"id":"1","indexPatternId":"logstash-*","params":{},"schema":"metric","type":"count"},"type":"number"},"name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"datatable"},"visType":"metric"}} \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/session/tagcloud_all_data.json b/test/interpreter_functional/snapshots/session/tagcloud_all_data.json deleted file mode 100644 index 5ddf081c54d95..0000000000000 --- a/test/interpreter_functional/snapshots/session/tagcloud_all_data.json +++ /dev/null @@ -1 +0,0 @@ -{"as":"tagcloud","type":"render","value":{"syncColors":false,"visData":{"columns":[{"id":"col-0-2","meta":{"field":"response.raw","index":"logstash-*","params":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"id":"2","indexPatternId":"logstash-*","params":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"schema":"segment","type":"terms"},"type":"string"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"field":null,"index":"logstash-*","params":{"id":"number"},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"id":"1","indexPatternId":"logstash-*","params":{},"schema":"metric","type":"count"},"type":"number"},"name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"datatable"},"visParams":{"bucket":{"accessor":1,"format":{"id":"string","params":{}},"type":"vis_dimension"},"maxFontSize":72,"metric":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"minFontSize":18,"orientation":"single","palette":{"name":"custom","params":{"colors":["#882E72","#B178A6","#D6C1DE","#1965B0","#5289C7","#7BAFDE","#4EB265","#90C987","#CAE0AB","#F7EE55","#F6C141","#F1932D","#E8601C","#DC050C"],"continuity":"above","gradient":false,"range":"percent","rangeMax":100,"rangeMin":0,"stops":[]},"type":"palette"},"scale":"linear","showLabel":true},"visType":"tagcloud"}} \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/session/tagcloud_empty_data.json b/test/interpreter_functional/snapshots/session/tagcloud_empty_data.json deleted file mode 100644 index 723ebb6e9f460..0000000000000 --- a/test/interpreter_functional/snapshots/session/tagcloud_empty_data.json +++ /dev/null @@ -1 +0,0 @@ -{"as":"tagcloud","type":"render","value":{"syncColors":false,"visData":{"columns":[{"id":"col-0-2","meta":{"field":"response.raw","index":"logstash-*","params":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"id":"2","indexPatternId":"logstash-*","params":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"schema":"segment","type":"terms"},"type":"string"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"field":null,"index":"logstash-*","params":{"id":"number"},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"id":"1","indexPatternId":"logstash-*","params":{},"schema":"metric","type":"count"},"type":"number"},"name":"Count"}],"rows":[],"type":"datatable"},"visParams":{"maxFontSize":72,"metric":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"minFontSize":18,"orientation":"single","palette":{"name":"custom","params":{"colors":["#882E72","#B178A6","#D6C1DE","#1965B0","#5289C7","#7BAFDE","#4EB265","#90C987","#CAE0AB","#F7EE55","#F6C141","#F1932D","#E8601C","#DC050C"],"continuity":"above","gradient":false,"range":"percent","rangeMax":100,"rangeMin":0,"stops":[]},"type":"palette"},"scale":"linear","showLabel":true},"visType":"tagcloud"}} \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/session/tagcloud_fontsize.json b/test/interpreter_functional/snapshots/session/tagcloud_fontsize.json deleted file mode 100644 index 1655451d41d03..0000000000000 --- a/test/interpreter_functional/snapshots/session/tagcloud_fontsize.json +++ /dev/null @@ -1 +0,0 @@ -{"as":"tagcloud","type":"render","value":{"syncColors":false,"visData":{"columns":[{"id":"col-0-2","meta":{"field":"response.raw","index":"logstash-*","params":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"id":"2","indexPatternId":"logstash-*","params":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"schema":"segment","type":"terms"},"type":"string"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"field":null,"index":"logstash-*","params":{"id":"number"},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"id":"1","indexPatternId":"logstash-*","params":{},"schema":"metric","type":"count"},"type":"number"},"name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"datatable"},"visParams":{"bucket":{"accessor":1,"format":{"id":"string","params":{}},"type":"vis_dimension"},"maxFontSize":40,"metric":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"minFontSize":20,"orientation":"single","palette":{"name":"custom","params":{"colors":["#882E72","#B178A6","#D6C1DE","#1965B0","#5289C7","#7BAFDE","#4EB265","#90C987","#CAE0AB","#F7EE55","#F6C141","#F1932D","#E8601C","#DC050C"],"continuity":"above","gradient":false,"range":"percent","rangeMax":100,"rangeMin":0,"stops":[]},"type":"palette"},"scale":"linear","showLabel":true},"visType":"tagcloud"}} \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/session/tagcloud_invalid_data.json b/test/interpreter_functional/snapshots/session/tagcloud_invalid_data.json deleted file mode 100644 index b5ae1a2cb59fc..0000000000000 --- a/test/interpreter_functional/snapshots/session/tagcloud_invalid_data.json +++ /dev/null @@ -1 +0,0 @@ -"[tagcloud] > [visdimension] > Column name or index provided is invalid" \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/session/tagcloud_metric_data.json b/test/interpreter_functional/snapshots/session/tagcloud_metric_data.json deleted file mode 100644 index f0bfd56ac99b8..0000000000000 --- a/test/interpreter_functional/snapshots/session/tagcloud_metric_data.json +++ /dev/null @@ -1 +0,0 @@ -{"as":"tagcloud","type":"render","value":{"syncColors":false,"visData":{"columns":[{"id":"col-0-2","meta":{"field":"response.raw","index":"logstash-*","params":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"id":"2","indexPatternId":"logstash-*","params":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"schema":"segment","type":"terms"},"type":"string"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"field":null,"index":"logstash-*","params":{"id":"number"},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"id":"1","indexPatternId":"logstash-*","params":{},"schema":"metric","type":"count"},"type":"number"},"name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"datatable"},"visParams":{"maxFontSize":72,"metric":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"minFontSize":18,"orientation":"single","palette":{"name":"custom","params":{"colors":["#882E72","#B178A6","#D6C1DE","#1965B0","#5289C7","#7BAFDE","#4EB265","#90C987","#CAE0AB","#F7EE55","#F6C141","#F1932D","#E8601C","#DC050C"],"continuity":"above","gradient":false,"range":"percent","rangeMax":100,"rangeMin":0,"stops":[]},"type":"palette"},"scale":"linear","showLabel":true},"visType":"tagcloud"}} \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/session/tagcloud_options.json b/test/interpreter_functional/snapshots/session/tagcloud_options.json deleted file mode 100644 index ba034fa2e435a..0000000000000 --- a/test/interpreter_functional/snapshots/session/tagcloud_options.json +++ /dev/null @@ -1 +0,0 @@ -{"as":"tagcloud","type":"render","value":{"syncColors":false,"visData":{"columns":[{"id":"col-0-2","meta":{"field":"response.raw","index":"logstash-*","params":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"id":"2","indexPatternId":"logstash-*","params":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"schema":"segment","type":"terms"},"type":"string"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"field":null,"index":"logstash-*","params":{"id":"number"},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"id":"1","indexPatternId":"logstash-*","params":{},"schema":"metric","type":"count"},"type":"number"},"name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"datatable"},"visParams":{"bucket":{"accessor":1,"format":{"id":"string","params":{}},"type":"vis_dimension"},"maxFontSize":72,"metric":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"minFontSize":18,"orientation":"multiple","palette":{"name":"custom","params":{"colors":["#882E72","#B178A6","#D6C1DE","#1965B0","#5289C7","#7BAFDE","#4EB265","#90C987","#CAE0AB","#F7EE55","#F6C141","#F1932D","#E8601C","#DC050C"],"continuity":"above","gradient":false,"range":"percent","rangeMax":100,"rangeMin":0,"stops":[]},"type":"palette"},"scale":"log","showLabel":true},"visType":"tagcloud"}} \ No newline at end of file diff --git a/test/interpreter_functional/test_suites/run_pipeline/metric.ts b/test/interpreter_functional/test_suites/run_pipeline/metric.ts index 5483e09d6671b..09d0e076d9868 100644 --- a/test/interpreter_functional/test_suites/run_pipeline/metric.ts +++ b/test/interpreter_functional/test_suites/run_pipeline/metric.ts @@ -71,7 +71,8 @@ export default function ({ it('with percentageMode option', async () => { const expression = - 'metricVis metric={visdimension 0} percentageMode=true colorRange={range from=0 to=1000}'; + 'metricVis metric={visdimension 0} percentageMode=true \ + palette={palette stop=0 color="rgb(0,0,0,0)" stop=10000 color="rgb(100, 100, 100)" range="number" continuity="none"}'; await ( await expectExpression( 'metric_percentage_mode', diff --git a/typings/@elastic/eui/index.d.ts b/typings/@elastic/eui/index.d.ts index f5baf73df7057..814a386a3b5e7 100644 --- a/typings/@elastic/eui/index.d.ts +++ b/typings/@elastic/eui/index.d.ts @@ -11,7 +11,3 @@ declare module '@elastic/eui/lib/services' { export const RIGHT_ALIGNMENT: any; } - -declare module '@elastic/eui/lib/services/format' { - export const dateFormatAliases: any; -} diff --git a/typings/accept.d.ts b/typings/accept.d.ts deleted file mode 100644 index e868063c7f7c0..0000000000000 --- a/typings/accept.d.ts +++ /dev/null @@ -1,12 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -declare module 'accept' { - // @types/accept does not include the `preferences` argument so we override the type to include it - export function encodings(encodingHeader?: string, preferences?: string[]): string[]; -} diff --git a/typings/global_fetch.d.ts b/typings/global_fetch.d.ts deleted file mode 100644 index 597bc7e89497c..0000000000000 --- a/typings/global_fetch.d.ts +++ /dev/null @@ -1,11 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -// This type needs to still exist due to apollo-link-http-common hasn't yet updated -// it's usage (https://github.com/apollographql/apollo-link/issues/1131) -declare type GlobalFetch = WindowOrWorkerGlobalScope; diff --git a/typings/js_levenshtein.d.ts b/typings/js_levenshtein.d.ts deleted file mode 100644 index 7c934333dbc7b..0000000000000 --- a/typings/js_levenshtein.d.ts +++ /dev/null @@ -1,12 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -declare module 'js-levenshtein' { - const levenshtein: (a: string, b: string) => number; - export = levenshtein; -} diff --git a/typings/react_vis.d.ts b/typings/react_vis.d.ts deleted file mode 100644 index 209dd398e86f4..0000000000000 --- a/typings/react_vis.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. - */ - -declare module 'react-vis'; diff --git a/vars/kibanaPipeline.groovy b/vars/kibanaPipeline.groovy index daf6ca7a8e993..8a7596a591175 100644 --- a/vars/kibanaPipeline.groovy +++ b/vars/kibanaPipeline.groovy @@ -93,7 +93,7 @@ def withFunctionalTestEnv(List additionalEnvs = [], Closure closure) { def corsTestServerPort = "64${parallelId}3" // needed for https://github.com/elastic/kibana/issues/107246 def proxyTestServerPort = "64${parallelId}4" - def apmActive = githubPr.isPr() ? "false" : "true" + def contextPropagationOnly = githubPr.isPr() ? "true" : "false" withEnv([ "CI_GROUP=${parallelId}", @@ -109,7 +109,8 @@ def withFunctionalTestEnv(List additionalEnvs = [], Closure closure) { "KBN_NP_PLUGINS_BUILT=true", "FLEET_PACKAGE_REGISTRY_PORT=${fleetPackageRegistryPort}", "ALERTING_PROXY_PORT=${alertingProxyPort}", - "ELASTIC_APM_ACTIVE=${apmActive}", + "ELASTIC_APM_ACTIVE=true", + "ELASTIC_APM_CONTEXT_PROPAGATION_ONLY=${contextPropagationOnly}", "ELASTIC_APM_TRANSACTION_SAMPLE_RATE=0.1", ] + additionalEnvs) { closure() diff --git a/x-pack/plugins/actions/server/builtin_action_types/index.ts b/x-pack/plugins/actions/server/builtin_action_types/index.ts index 9988e951ae86d..9f48a45fc4664 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/index.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/index.ts @@ -24,7 +24,6 @@ import { import { getActionType as getJiraActionType } from './jira'; import { getActionType as getResilientActionType } from './resilient'; import { getActionType as getTeamsActionType } from './teams'; -import { ENABLE_ITOM } from '../constants/connectors'; export type { ActionParamsType as EmailActionParams } from './email'; export { ActionTypeId as EmailActionTypeId } from './email'; export type { ActionParamsType as IndexActionParams } from './es_index'; @@ -72,12 +71,8 @@ export function registerBuiltInActionTypes({ actionTypeRegistry.register(getWebhookActionType({ logger, configurationUtilities })); actionTypeRegistry.register(getServiceNowITSMActionType({ logger, configurationUtilities })); actionTypeRegistry.register(getServiceNowSIRActionType({ logger, configurationUtilities })); + actionTypeRegistry.register(getServiceNowITOMActionType({ logger, configurationUtilities })); actionTypeRegistry.register(getJiraActionType({ logger, configurationUtilities })); actionTypeRegistry.register(getResilientActionType({ logger, configurationUtilities })); actionTypeRegistry.register(getTeamsActionType({ logger, configurationUtilities })); - - // TODO: Remove when ITOM is ready - if (ENABLE_ITOM) { - actionTypeRegistry.register(getServiceNowITOMActionType({ logger, configurationUtilities })); - } } diff --git a/x-pack/plugins/actions/server/builtin_action_types/servicenow/config.test.ts b/x-pack/plugins/actions/server/builtin_action_types/servicenow/config.test.ts index 41f723bc9e2aa..8830b2cc23c38 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/servicenow/config.test.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/servicenow/config.test.ts @@ -24,6 +24,7 @@ describe('config', () => { table: 'incident', useImportAPI: true, commentFieldKey: 'work_notes', + appId: '7148dbc91bf1f450ced060a7234bcb88', }); }); @@ -35,6 +36,7 @@ describe('config', () => { table: 'sn_si_incident', useImportAPI: true, commentFieldKey: 'work_notes', + appId: '2f0746801baeb01019ae54e4604bcb0f', }); }); @@ -44,7 +46,7 @@ describe('config', () => { importSetTable: 'x_elas2_inc_int_elastic_incident', appScope: 'x_elas2_inc_int', table: 'em_event', - useImportAPI: true, + useImportAPI: false, commentFieldKey: 'work_notes', }); }); diff --git a/x-pack/plugins/actions/server/builtin_action_types/servicenow/config.ts b/x-pack/plugins/actions/server/builtin_action_types/servicenow/config.ts index 52d2eb7662f53..11f18f0407fdf 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/servicenow/config.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/servicenow/config.ts @@ -5,11 +5,6 @@ * 2.0. */ -import { - ENABLE_ITOM, - ENABLE_NEW_SN_ITSM_CONNECTOR, - ENABLE_NEW_SN_SIR_CONNECTOR, -} from '../../constants/connectors'; import { SNProductsConfig } from './types'; export const serviceNowITSMTable = 'incident'; @@ -19,26 +14,31 @@ export const ServiceNowITSMActionTypeId = '.servicenow'; export const ServiceNowSIRActionTypeId = '.servicenow-sir'; export const ServiceNowITOMActionTypeId = '.servicenow-itom'; +const SN_ITSM_APP_ID = '7148dbc91bf1f450ced060a7234bcb88'; +const SN_SIR_APP_ID = '2f0746801baeb01019ae54e4604bcb0f'; + export const snExternalServiceConfig: SNProductsConfig = { '.servicenow': { importSetTable: 'x_elas2_inc_int_elastic_incident', appScope: 'x_elas2_inc_int', table: 'incident', - useImportAPI: ENABLE_NEW_SN_ITSM_CONNECTOR, + useImportAPI: true, commentFieldKey: 'work_notes', + appId: SN_ITSM_APP_ID, }, '.servicenow-sir': { importSetTable: 'x_elas2_sir_int_elastic_si_incident', appScope: 'x_elas2_sir_int', table: 'sn_si_incident', - useImportAPI: ENABLE_NEW_SN_SIR_CONNECTOR, + useImportAPI: true, commentFieldKey: 'work_notes', + appId: SN_SIR_APP_ID, }, '.servicenow-itom': { importSetTable: 'x_elas2_inc_int_elastic_incident', appScope: 'x_elas2_inc_int', table: 'em_event', - useImportAPI: ENABLE_ITOM, + useImportAPI: false, commentFieldKey: 'work_notes', }, }; diff --git a/x-pack/plugins/actions/server/builtin_action_types/servicenow/types.ts b/x-pack/plugins/actions/server/builtin_action_types/servicenow/types.ts index 31af3781c6b04..352a0c22c7ec8 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/servicenow/types.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/servicenow/types.ts @@ -253,6 +253,7 @@ export interface SNProductsConfigValue { useImportAPI: boolean; importSetTable: string; commentFieldKey: string; + appId?: string; } export type SNProductsConfig = Record; diff --git a/x-pack/plugins/actions/server/constants/connectors.ts b/x-pack/plugins/actions/server/constants/connectors.ts deleted file mode 100644 index 94324e4d82bc2..0000000000000 --- a/x-pack/plugins/actions/server/constants/connectors.ts +++ /dev/null @@ -1,15 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -// TODO: Remove when Elastic for ITSM is published. -export const ENABLE_NEW_SN_ITSM_CONNECTOR = true; - -// TODO: Remove when Elastic for Security Operations is published. -export const ENABLE_NEW_SN_SIR_CONNECTOR = true; - -// TODO: Remove when ready -export const ENABLE_ITOM = true; diff --git a/x-pack/plugins/alerting/README.md b/x-pack/plugins/alerting/README.md index 343960aee9dfb..9c4f27fa945be 100644 --- a/x-pack/plugins/alerting/README.md +++ b/x-pack/plugins/alerting/README.md @@ -75,29 +75,6 @@ To change the schedule for the invalidation task, use the kibana.yml configurati To change the default delay for the API key invalidation, use the kibana.yml configuration option `xpack.alerting.invalidateApiKeysTask.removalDelay`. -## Plugin Status - -The plugin status of the Alerting Framework is customized by including information about checking for failures during framework decryption: - -```js -core.status.set( - combineLatest([ - core.status.derivedStatus$, - getHealthStatusStream(startPlugins.taskManager), - ]).pipe( - map(([derivedStatus, healthStatus]) => { - if (healthStatus.level > derivedStatus.level) { - return healthStatus as ServiceStatus; - } else { - return derivedStatus; - } - }) - ) - ); -``` - -To check for framework decryption failures, we use the task `alerting_health_check`, which runs every 60 minutes by default. To change the default schedule, use the kibana.yml configuration option `xpack.alerting.healthCheck.interval`. - ## Rule Types ### Methods diff --git a/x-pack/plugins/alerting/common/alert_instance_summary.ts b/x-pack/plugins/alerting/common/alert_instance_summary.ts deleted file mode 100644 index 462d20b8fb2ea..0000000000000 --- a/x-pack/plugins/alerting/common/alert_instance_summary.ts +++ /dev/null @@ -1,38 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -export type AlertStatusValues = 'OK' | 'Active' | 'Error'; -export type AlertInstanceStatusValues = 'OK' | 'Active'; - -export interface AlertInstanceSummary { - id: string; - name: string; - tags: string[]; - alertTypeId: string; - consumer: string; - muteAll: boolean; - throttle: string | null; - enabled: boolean; - statusStartDate: string; - statusEndDate: string; - status: AlertStatusValues; - lastRun?: string; - errorMessages: Array<{ date: string; message: string }>; - instances: Record; - executionDuration: { - average: number; - values: number[]; - }; -} - -export interface AlertInstanceStatus { - status: AlertInstanceStatusValues; - muted: boolean; - actionGroupId?: string; - actionSubgroup?: string; - activeStartDate?: string; -} diff --git a/x-pack/plugins/alerting/common/alert_summary.ts b/x-pack/plugins/alerting/common/alert_summary.ts new file mode 100644 index 0000000000000..87d05dce7c958 --- /dev/null +++ b/x-pack/plugins/alerting/common/alert_summary.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. + */ + +export type RuleStatusValues = 'OK' | 'Active' | 'Error'; +export type AlertStatusValues = 'OK' | 'Active'; + +export interface AlertSummary { + id: string; + name: string; + tags: string[]; + ruleTypeId: string; + consumer: string; + muteAll: boolean; + throttle: string | null; + enabled: boolean; + statusStartDate: string; + statusEndDate: string; + status: RuleStatusValues; + lastRun?: string; + errorMessages: Array<{ date: string; message: string }>; + alerts: Record; + executionDuration: { + average: number; + values: number[]; + }; +} + +export interface AlertStatus { + status: AlertStatusValues; + muted: boolean; + actionGroupId?: string; + actionSubgroup?: string; + activeStartDate?: string; +} diff --git a/x-pack/plugins/alerting/common/index.ts b/x-pack/plugins/alerting/common/index.ts index 61e38941d0233..1c7525a065760 100644 --- a/x-pack/plugins/alerting/common/index.ts +++ b/x-pack/plugins/alerting/common/index.ts @@ -15,7 +15,7 @@ export * from './alert_type'; export * from './alert_instance'; export * from './alert_task_instance'; export * from './alert_navigation'; -export * from './alert_instance_summary'; +export * from './alert_summary'; export * from './builtin_action_groups'; export * from './disabled_action_groups'; export * from './alert_notify_when_type'; diff --git a/x-pack/plugins/alerting/server/health/get_state.test.ts b/x-pack/plugins/alerting/server/health/get_state.test.ts deleted file mode 100644 index f4306b8250b81..0000000000000 --- a/x-pack/plugins/alerting/server/health/get_state.test.ts +++ /dev/null @@ -1,328 +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 { ServiceStatusLevels } from '../../../../../src/core/server'; -import { taskManagerMock } from '../../../task_manager/server/mocks'; -import { - getHealthStatusStream, - getHealthServiceStatusWithRetryAndErrorHandling, - MAX_RETRY_ATTEMPTS, -} from './get_state'; -import { ConcreteTaskInstance, TaskStatus } from '../../../task_manager/server'; -import { HealthStatus } from '../types'; -import { loggingSystemMock, savedObjectsServiceMock } from 'src/core/server/mocks'; - -jest.mock('./get_health', () => ({ - getAlertingHealthStatus: jest.fn().mockReturnValue({ - state: { - runs: 0, - health_status: 'warn', - }, - }), -})); - -const tick = () => new Promise((resolve) => setImmediate(resolve)); - -const getHealthCheckTask = (overrides = {}): ConcreteTaskInstance => ({ - id: 'test', - attempts: 0, - status: TaskStatus.Running, - version: '123', - runAt: new Date(), - scheduledAt: new Date(), - startedAt: new Date(), - retryAt: new Date(Date.now() + 5 * 60 * 1000), - state: { - runs: 1, - health_status: HealthStatus.OK, - }, - taskType: 'alerting:alerting_health_check', - params: { - alertId: '1', - }, - ownerId: null, - ...overrides, -}); - -const logger = loggingSystemMock.create().get(); -const savedObjects = savedObjectsServiceMock.createStartContract(); - -describe('getHealthServiceStatusWithRetryAndErrorHandling', () => { - beforeEach(() => jest.useFakeTimers()); - - it('should get status at each interval', async () => { - const mockTaskManager = taskManagerMock.createStart(); - mockTaskManager.get.mockResolvedValue(getHealthCheckTask()); - const pollInterval = 100; - - getHealthStatusStream( - mockTaskManager, - logger, - savedObjects, - Promise.resolve({ - healthCheck: { - interval: '5m', - }, - invalidateApiKeysTask: { - interval: '5m', - removalDelay: '1h', - }, - maxEphemeralActionsPerAlert: 100, - defaultRuleTaskTimeout: '20m', - }), - pollInterval - ).subscribe(); - - // should fire before poll interval passes - // should fire once each poll interval - expect(mockTaskManager.get).toHaveBeenCalledTimes(1); - jest.advanceTimersByTime(pollInterval); - expect(mockTaskManager.get).toHaveBeenCalledTimes(2); - jest.advanceTimersByTime(pollInterval); - expect(mockTaskManager.get).toHaveBeenCalledTimes(3); - jest.advanceTimersByTime(pollInterval); - expect(mockTaskManager.get).toHaveBeenCalledTimes(4); - }); - - it('should retry on error', async () => { - const mockTaskManager = taskManagerMock.createStart(); - mockTaskManager.get.mockRejectedValue(new Error('Failure')); - const retryDelay = 10; - const pollInterval = 100; - - getHealthStatusStream( - mockTaskManager, - logger, - savedObjects, - Promise.resolve({ - healthCheck: { - interval: '5m', - }, - invalidateApiKeysTask: { - interval: '5m', - removalDelay: '1h', - }, - maxEphemeralActionsPerAlert: 100, - defaultRuleTaskTimeout: '20m', - }), - pollInterval, - retryDelay - ).subscribe(); - - expect(mockTaskManager.get).toHaveBeenCalledTimes(1); - jest.advanceTimersByTime(pollInterval); - expect(mockTaskManager.get).toHaveBeenCalledTimes(2); - - // Retry on failure - let numTimesCalled = 1; - for (let i = 0; i < MAX_RETRY_ATTEMPTS; i++) { - await tick(); - jest.advanceTimersByTime(retryDelay); - expect(mockTaskManager.get).toHaveBeenCalledTimes(numTimesCalled++ + 2); - } - - // Once we've exceeded max retries, should not try again - await tick(); - jest.advanceTimersByTime(retryDelay); - expect(mockTaskManager.get).toHaveBeenCalledTimes(numTimesCalled + 1); - - // Once another poll interval passes, should call fn again - await tick(); - jest.advanceTimersByTime(pollInterval - MAX_RETRY_ATTEMPTS * retryDelay); - expect(mockTaskManager.get).toHaveBeenCalledTimes(numTimesCalled + 2); - }); - - it('should return healthy status when health status is "ok"', async () => { - const mockTaskManager = taskManagerMock.createStart(); - mockTaskManager.get.mockResolvedValue(getHealthCheckTask()); - - const status = await getHealthServiceStatusWithRetryAndErrorHandling( - mockTaskManager, - logger, - savedObjects, - Promise.resolve({ - healthCheck: { - interval: '5m', - }, - invalidateApiKeysTask: { - interval: '5m', - removalDelay: '1h', - }, - maxEphemeralActionsPerAlert: 100, - defaultRuleTaskTimeout: '20m', - }) - ).toPromise(); - - expect(status.level).toEqual(ServiceStatusLevels.available); - expect(status.summary).toEqual('Alerting framework is available'); - }); - - it('should return degraded status when health status is "warn"', async () => { - const mockTaskManager = taskManagerMock.createStart(); - mockTaskManager.get.mockResolvedValue( - getHealthCheckTask({ - state: { - runs: 1, - health_status: HealthStatus.Warning, - }, - }) - ); - - const status = await getHealthServiceStatusWithRetryAndErrorHandling( - mockTaskManager, - logger, - savedObjects, - Promise.resolve({ - healthCheck: { - interval: '5m', - }, - invalidateApiKeysTask: { - interval: '5m', - removalDelay: '1h', - }, - maxEphemeralActionsPerAlert: 100, - defaultRuleTaskTimeout: '20m', - }) - ).toPromise(); - - expect(status.level).toEqual(ServiceStatusLevels.degraded); - expect(status.summary).toEqual('Alerting framework is degraded'); - }); - - it('should return unavailable status when health status is "error"', async () => { - const mockTaskManager = taskManagerMock.createStart(); - mockTaskManager.get.mockResolvedValue( - getHealthCheckTask({ - state: { - runs: 1, - health_status: HealthStatus.Error, - }, - }) - ); - - const status = await getHealthServiceStatusWithRetryAndErrorHandling( - mockTaskManager, - logger, - savedObjects, - Promise.resolve({ - healthCheck: { - interval: '5m', - }, - invalidateApiKeysTask: { - interval: '5m', - removalDelay: '1h', - }, - maxEphemeralActionsPerAlert: 100, - defaultRuleTaskTimeout: '20m', - }) - ).toPromise(); - - expect(status.level).toEqual(ServiceStatusLevels.degraded); - expect(status.summary).toEqual('Alerting framework is degraded'); - expect(status.meta).toBeUndefined(); - }); - - it('should retry on error and return healthy status if retry succeeds', async () => { - const retryDelay = 10; - const mockTaskManager = taskManagerMock.createStart(); - mockTaskManager.get - .mockRejectedValueOnce(new Error('Failure')) - .mockResolvedValue(getHealthCheckTask()); - - getHealthServiceStatusWithRetryAndErrorHandling( - mockTaskManager, - logger, - savedObjects, - Promise.resolve({ - healthCheck: { - interval: '5m', - }, - invalidateApiKeysTask: { - interval: '5m', - removalDelay: '1h', - }, - maxEphemeralActionsPerAlert: 100, - defaultRuleTaskTimeout: '20m', - }), - retryDelay - ).subscribe((status) => { - expect(status.level).toEqual(ServiceStatusLevels.available); - expect(logger.warn).toHaveBeenCalledTimes(1); - expect(status.summary).toEqual('Alerting framework is available'); - }); - - await tick(); - jest.advanceTimersByTime(retryDelay * 2); - }); - - it('should retry on error and return unavailable status if retry fails', async () => { - const retryDelay = 10; - const err = new Error('Failure'); - const mockTaskManager = taskManagerMock.createStart(); - mockTaskManager.get.mockRejectedValue(err); - - getHealthServiceStatusWithRetryAndErrorHandling( - mockTaskManager, - logger, - savedObjects, - Promise.resolve({ - healthCheck: { - interval: '5m', - }, - invalidateApiKeysTask: { - interval: '5m', - removalDelay: '1h', - }, - maxEphemeralActionsPerAlert: 100, - defaultRuleTaskTimeout: '20m', - }), - retryDelay - ).subscribe((status) => { - expect(status.level).toEqual(ServiceStatusLevels.degraded); - expect(status.summary).toEqual('Alerting framework is degraded'); - expect(status.meta).toEqual({ error: err }); - }); - - for (let i = 0; i < MAX_RETRY_ATTEMPTS + 1; i++) { - await tick(); - jest.advanceTimersByTime(retryDelay); - } - expect(mockTaskManager.get).toHaveBeenCalledTimes(MAX_RETRY_ATTEMPTS + 1); - }); - - it('should schedule a new health check task if it does not exist without throwing an error', async () => { - const mockTaskManager = taskManagerMock.createStart(); - mockTaskManager.get.mockRejectedValue({ - output: { - statusCode: 404, - message: 'Not Found', - }, - }); - - const status = await getHealthServiceStatusWithRetryAndErrorHandling( - mockTaskManager, - logger, - savedObjects, - Promise.resolve({ - healthCheck: { - interval: '5m', - }, - invalidateApiKeysTask: { - interval: '5m', - removalDelay: '1h', - }, - maxEphemeralActionsPerAlert: 100, - defaultRuleTaskTimeout: '20m', - }) - ).toPromise(); - - expect(mockTaskManager.ensureScheduled).toHaveBeenCalledTimes(1); - expect(status.level).toEqual(ServiceStatusLevels.degraded); - expect(status.summary).toEqual('Alerting framework is degraded'); - expect(status.meta).toBeUndefined(); - }); -}); diff --git a/x-pack/plugins/alerting/server/health/get_state.ts b/x-pack/plugins/alerting/server/health/get_state.ts deleted file mode 100644 index 34f897ad5b73c..0000000000000 --- a/x-pack/plugins/alerting/server/health/get_state.ts +++ /dev/null @@ -1,142 +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 { i18n } from '@kbn/i18n'; -import { defer, of, interval, Observable, throwError, timer } from 'rxjs'; -import { catchError, mergeMap, retryWhen, startWith, switchMap } from 'rxjs/operators'; -import { - Logger, - SavedObjectsServiceStart, - ServiceStatus, - ServiceStatusLevels, -} from '../../../../../src/core/server'; -import { TaskManagerStartContract } from '../../../task_manager/server'; -import { HEALTH_TASK_ID, scheduleAlertingHealthCheck } from './task'; -import { HealthStatus } from '../types'; -import { getAlertingHealthStatus } from './get_health'; -import { AlertsConfig } from '../config'; - -export const MAX_RETRY_ATTEMPTS = 3; -const HEALTH_STATUS_INTERVAL = 60000 * 5; // Five minutes -const RETRY_DELAY = 5000; // Wait 5 seconds before retrying on errors - -async function getLatestTaskState( - taskManager: TaskManagerStartContract, - logger: Logger, - savedObjects: SavedObjectsServiceStart, - config: Promise -) { - try { - return await taskManager.get(HEALTH_TASK_ID); - } catch (err) { - // if task is not found - if (err?.output?.statusCode === 404) { - await scheduleAlertingHealthCheck(logger, config, taskManager); - return await getAlertingHealthStatus(savedObjects); - } - throw err; - } -} - -const LEVEL_SUMMARY = { - [ServiceStatusLevels.available.toString()]: i18n.translate( - 'xpack.alerting.server.healthStatus.available', - { - defaultMessage: 'Alerting framework is available', - } - ), - [ServiceStatusLevels.degraded.toString()]: i18n.translate( - 'xpack.alerting.server.healthStatus.degraded', - { - defaultMessage: 'Alerting framework is degraded', - } - ), - [ServiceStatusLevels.unavailable.toString()]: i18n.translate( - 'xpack.alerting.server.healthStatus.unavailable', - { - defaultMessage: 'Alerting framework is unavailable', - } - ), -}; - -const getHealthServiceStatus = async ( - taskManager: TaskManagerStartContract, - logger: Logger, - savedObjects: SavedObjectsServiceStart, - config: Promise -): Promise> => { - const doc = await getLatestTaskState(taskManager, logger, savedObjects, config); - const level = - doc.state?.health_status === HealthStatus.OK - ? ServiceStatusLevels.available - : ServiceStatusLevels.degraded; - return { - level, - summary: LEVEL_SUMMARY[level.toString()], - }; -}; - -export const getHealthServiceStatusWithRetryAndErrorHandling = ( - taskManager: TaskManagerStartContract, - logger: Logger, - savedObjects: SavedObjectsServiceStart, - config: Promise, - retryDelay?: number -): Observable> => { - return defer(() => getHealthServiceStatus(taskManager, logger, savedObjects, config)).pipe( - retryWhen((errors) => { - return errors.pipe( - mergeMap((error, i) => { - const retryAttempt = i + 1; - if (retryAttempt > MAX_RETRY_ATTEMPTS) { - return throwError(error); - } - return timer(retryDelay ?? RETRY_DELAY); - }) - ); - }), - catchError((error) => { - logger.warn(`Alerting framework is degraded due to the error: ${error}`); - return of({ - level: ServiceStatusLevels.degraded, - summary: LEVEL_SUMMARY[ServiceStatusLevels.degraded.toString()], - meta: { error }, - }); - }) - ); -}; - -export const getHealthStatusStream = ( - taskManager: TaskManagerStartContract, - logger: Logger, - savedObjects: SavedObjectsServiceStart, - config: Promise, - healthStatusInterval?: number, - retryDelay?: number -): Observable> => - interval(healthStatusInterval ?? HEALTH_STATUS_INTERVAL).pipe( - // Emit an initial check - startWith( - getHealthServiceStatusWithRetryAndErrorHandling( - taskManager, - logger, - savedObjects, - config, - retryDelay - ) - ), - // On each interval do a new check - switchMap(() => - getHealthServiceStatusWithRetryAndErrorHandling( - taskManager, - logger, - savedObjects, - config, - retryDelay - ) - ) - ); diff --git a/x-pack/plugins/alerting/server/health/index.ts b/x-pack/plugins/alerting/server/health/index.ts index 8ac0e7be6867c..f7f8e15b6f36f 100644 --- a/x-pack/plugins/alerting/server/health/index.ts +++ b/x-pack/plugins/alerting/server/health/index.ts @@ -5,5 +5,4 @@ * 2.0. */ -export { getHealthStatusStream } from './get_state'; export { scheduleAlertingHealthCheck, initializeAlertingHealth } from './task'; diff --git a/x-pack/plugins/alerting/server/lib/alert_instance_summary_from_event_log.test.ts b/x-pack/plugins/alerting/server/lib/alert_instance_summary_from_event_log.test.ts deleted file mode 100644 index a6529f4c30a7b..0000000000000 --- a/x-pack/plugins/alerting/server/lib/alert_instance_summary_from_event_log.test.ts +++ /dev/null @@ -1,714 +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 { random, mean } from 'lodash'; -import { SanitizedAlert, AlertInstanceSummary } from '../types'; -import { IValidatedEvent } from '../../../event_log/server'; -import { EVENT_LOG_ACTIONS, EVENT_LOG_PROVIDER, LEGACY_EVENT_LOG_ACTIONS } from '../plugin'; -import { alertInstanceSummaryFromEventLog } from './alert_instance_summary_from_event_log'; - -const ONE_HOUR_IN_MILLIS = 60 * 60 * 1000; -const dateStart = '2020-06-18T00:00:00.000Z'; -const dateEnd = dateString(dateStart, ONE_HOUR_IN_MILLIS); - -describe('alertInstanceSummaryFromEventLog', () => { - test('no events and muted ids', async () => { - const alert = createAlert({}); - const events: IValidatedEvent[] = []; - const summary: AlertInstanceSummary = alertInstanceSummaryFromEventLog({ - alert, - events, - dateStart, - dateEnd, - }); - - expect(summary).toMatchInlineSnapshot(` - Object { - "alertTypeId": "123", - "consumer": "alert-consumer", - "enabled": false, - "errorMessages": Array [], - "executionDuration": Object { - "average": 0, - "values": Array [], - }, - "id": "alert-123", - "instances": Object {}, - "lastRun": undefined, - "muteAll": false, - "name": "alert-name", - "status": "OK", - "statusEndDate": "2020-06-18T01:00:00.000Z", - "statusStartDate": "2020-06-18T00:00:00.000Z", - "tags": Array [], - "throttle": null, - } - `); - }); - - test('different alert properties', async () => { - const alert = createAlert({ - id: 'alert-456', - alertTypeId: '456', - schedule: { interval: '100s' }, - enabled: true, - name: 'alert-name-2', - tags: ['tag-1', 'tag-2'], - consumer: 'alert-consumer-2', - throttle: '1h', - muteAll: true, - }); - const events: IValidatedEvent[] = []; - const summary: AlertInstanceSummary = alertInstanceSummaryFromEventLog({ - alert, - events, - dateStart: dateString(dateEnd, ONE_HOUR_IN_MILLIS), - dateEnd: dateString(dateEnd, ONE_HOUR_IN_MILLIS * 2), - }); - - expect(summary).toMatchInlineSnapshot(` - Object { - "alertTypeId": "456", - "consumer": "alert-consumer-2", - "enabled": true, - "errorMessages": Array [], - "executionDuration": Object { - "average": 0, - "values": Array [], - }, - "id": "alert-456", - "instances": Object {}, - "lastRun": undefined, - "muteAll": true, - "name": "alert-name-2", - "status": "OK", - "statusEndDate": "2020-06-18T03:00:00.000Z", - "statusStartDate": "2020-06-18T02:00:00.000Z", - "tags": Array [ - "tag-1", - "tag-2", - ], - "throttle": "1h", - } - `); - }); - - test('two muted instances', async () => { - const alert = createAlert({ - mutedInstanceIds: ['instance-1', 'instance-2'], - }); - const events: IValidatedEvent[] = []; - const summary: AlertInstanceSummary = alertInstanceSummaryFromEventLog({ - alert, - events, - dateStart, - dateEnd, - }); - - const { lastRun, status, instances } = summary; - expect({ lastRun, status, instances }).toMatchInlineSnapshot(` - Object { - "instances": Object { - "instance-1": Object { - "actionGroupId": undefined, - "actionSubgroup": undefined, - "activeStartDate": undefined, - "muted": true, - "status": "OK", - }, - "instance-2": Object { - "actionGroupId": undefined, - "actionSubgroup": undefined, - "activeStartDate": undefined, - "muted": true, - "status": "OK", - }, - }, - "lastRun": undefined, - "status": "OK", - } - `); - }); - - test('active alert but no instances', async () => { - const alert = createAlert({}); - const eventsFactory = new EventsFactory(); - const events = eventsFactory.addExecute().advanceTime(10000).addExecute().getEvents(); - - const summary: AlertInstanceSummary = alertInstanceSummaryFromEventLog({ - alert, - events, - dateStart, - dateEnd, - }); - - const { lastRun, status, instances, executionDuration } = summary; - expect({ lastRun, status, instances }).toMatchInlineSnapshot(` - Object { - "instances": Object {}, - "lastRun": "2020-06-18T00:00:10.000Z", - "status": "OK", - } - `); - - testExecutionDurations(eventsFactory.getExecutionDurations(), executionDuration); - }); - - test('active alert with no instances but has errors', async () => { - const alert = createAlert({}); - const eventsFactory = new EventsFactory(); - const events = eventsFactory - .addExecute('oof!') - .advanceTime(10000) - .addExecute('rut roh!') - .getEvents(); - - const summary: AlertInstanceSummary = alertInstanceSummaryFromEventLog({ - alert, - events, - dateStart, - dateEnd, - }); - - const { lastRun, status, errorMessages, instances, executionDuration } = summary; - expect({ lastRun, status, errorMessages, instances }).toMatchInlineSnapshot(` - Object { - "errorMessages": Array [ - Object { - "date": "2020-06-18T00:00:00.000Z", - "message": "oof!", - }, - Object { - "date": "2020-06-18T00:00:10.000Z", - "message": "rut roh!", - }, - ], - "instances": Object {}, - "lastRun": "2020-06-18T00:00:10.000Z", - "status": "Error", - } - `); - - testExecutionDurations(eventsFactory.getExecutionDurations(), executionDuration); - }); - - test('alert with currently inactive instance', async () => { - const alert = createAlert({}); - const eventsFactory = new EventsFactory(); - const events = eventsFactory - .addExecute() - .addNewInstance('instance-1') - .addActiveInstance('instance-1', 'action group A') - .advanceTime(10000) - .addExecute() - .addRecoveredInstance('instance-1') - .getEvents(); - - const summary: AlertInstanceSummary = alertInstanceSummaryFromEventLog({ - alert, - events, - dateStart, - dateEnd, - }); - - const { lastRun, status, instances, executionDuration } = summary; - expect({ lastRun, status, instances }).toMatchInlineSnapshot(` - Object { - "instances": Object { - "instance-1": Object { - "actionGroupId": undefined, - "actionSubgroup": undefined, - "activeStartDate": undefined, - "muted": false, - "status": "OK", - }, - }, - "lastRun": "2020-06-18T00:00:10.000Z", - "status": "OK", - } - `); - - testExecutionDurations(eventsFactory.getExecutionDurations(), executionDuration); - }); - - test('legacy alert with currently inactive instance', async () => { - const alert = createAlert({}); - const eventsFactory = new EventsFactory(); - const events = eventsFactory - .addExecute() - .addNewInstance('instance-1') - .addActiveInstance('instance-1', 'action group A') - .advanceTime(10000) - .addExecute() - .addLegacyResolvedInstance('instance-1') - .getEvents(); - - const summary: AlertInstanceSummary = alertInstanceSummaryFromEventLog({ - alert, - events, - dateStart, - dateEnd, - }); - - const { lastRun, status, instances, executionDuration } = summary; - expect({ lastRun, status, instances }).toMatchInlineSnapshot(` - Object { - "instances": Object { - "instance-1": Object { - "actionGroupId": undefined, - "actionSubgroup": undefined, - "activeStartDate": undefined, - "muted": false, - "status": "OK", - }, - }, - "lastRun": "2020-06-18T00:00:10.000Z", - "status": "OK", - } - `); - - testExecutionDurations(eventsFactory.getExecutionDurations(), executionDuration); - }); - - test('alert with currently inactive instance, no new-instance', async () => { - const alert = createAlert({}); - const eventsFactory = new EventsFactory(); - const events = eventsFactory - .addExecute() - .addActiveInstance('instance-1', 'action group A') - .advanceTime(10000) - .addExecute() - .addRecoveredInstance('instance-1') - .getEvents(); - - const summary: AlertInstanceSummary = alertInstanceSummaryFromEventLog({ - alert, - events, - dateStart, - dateEnd, - }); - - const { lastRun, status, instances, executionDuration } = summary; - expect({ lastRun, status, instances }).toMatchInlineSnapshot(` - Object { - "instances": Object { - "instance-1": Object { - "actionGroupId": undefined, - "actionSubgroup": undefined, - "activeStartDate": undefined, - "muted": false, - "status": "OK", - }, - }, - "lastRun": "2020-06-18T00:00:10.000Z", - "status": "OK", - } - `); - - testExecutionDurations(eventsFactory.getExecutionDurations(), executionDuration); - }); - - test('alert with currently active instance', async () => { - const alert = createAlert({}); - const eventsFactory = new EventsFactory(); - const events = eventsFactory - .addExecute() - .addNewInstance('instance-1') - .addActiveInstance('instance-1', 'action group A') - .advanceTime(10000) - .addExecute() - .addActiveInstance('instance-1', 'action group A') - .getEvents(); - - const summary: AlertInstanceSummary = alertInstanceSummaryFromEventLog({ - alert, - events, - dateStart, - dateEnd, - }); - - const { lastRun, status, instances, executionDuration } = summary; - expect({ lastRun, status, instances }).toMatchInlineSnapshot(` - Object { - "instances": Object { - "instance-1": Object { - "actionGroupId": "action group A", - "actionSubgroup": undefined, - "activeStartDate": "2020-06-18T00:00:00.000Z", - "muted": false, - "status": "Active", - }, - }, - "lastRun": "2020-06-18T00:00:10.000Z", - "status": "Active", - } - `); - - testExecutionDurations(eventsFactory.getExecutionDurations(), executionDuration); - }); - - test('alert with currently active instance with no action group in event log', async () => { - const alert = createAlert({}); - const eventsFactory = new EventsFactory(); - const events = eventsFactory - .addExecute() - .addNewInstance('instance-1') - .addActiveInstance('instance-1', undefined) - .advanceTime(10000) - .addExecute() - .addActiveInstance('instance-1', undefined) - .getEvents(); - - const summary: AlertInstanceSummary = alertInstanceSummaryFromEventLog({ - alert, - events, - dateStart, - dateEnd, - }); - - const { lastRun, status, instances, executionDuration } = summary; - expect({ lastRun, status, instances }).toMatchInlineSnapshot(` - Object { - "instances": Object { - "instance-1": Object { - "actionGroupId": undefined, - "actionSubgroup": undefined, - "activeStartDate": "2020-06-18T00:00:00.000Z", - "muted": false, - "status": "Active", - }, - }, - "lastRun": "2020-06-18T00:00:10.000Z", - "status": "Active", - } - `); - - testExecutionDurations(eventsFactory.getExecutionDurations(), executionDuration); - }); - - test('alert with currently active instance that switched action groups', async () => { - const alert = createAlert({}); - const eventsFactory = new EventsFactory(); - const events = eventsFactory - .addExecute() - .addNewInstance('instance-1') - .addActiveInstance('instance-1', 'action group A') - .advanceTime(10000) - .addExecute() - .addActiveInstance('instance-1', 'action group B') - .getEvents(); - - const summary: AlertInstanceSummary = alertInstanceSummaryFromEventLog({ - alert, - events, - dateStart, - dateEnd, - }); - - const { lastRun, status, instances, executionDuration } = summary; - expect({ lastRun, status, instances }).toMatchInlineSnapshot(` - Object { - "instances": Object { - "instance-1": Object { - "actionGroupId": "action group B", - "actionSubgroup": undefined, - "activeStartDate": "2020-06-18T00:00:00.000Z", - "muted": false, - "status": "Active", - }, - }, - "lastRun": "2020-06-18T00:00:10.000Z", - "status": "Active", - } - `); - - testExecutionDurations(eventsFactory.getExecutionDurations(), executionDuration); - }); - - test('alert with currently active instance, no new-instance', async () => { - const alert = createAlert({}); - const eventsFactory = new EventsFactory(); - const events = eventsFactory - .addExecute() - .addActiveInstance('instance-1', 'action group A') - .advanceTime(10000) - .addExecute() - .addActiveInstance('instance-1', 'action group A') - .getEvents(); - - const summary: AlertInstanceSummary = alertInstanceSummaryFromEventLog({ - alert, - events, - dateStart, - dateEnd, - }); - - const { lastRun, status, instances, executionDuration } = summary; - expect({ lastRun, status, instances }).toMatchInlineSnapshot(` - Object { - "instances": Object { - "instance-1": Object { - "actionGroupId": "action group A", - "actionSubgroup": undefined, - "activeStartDate": undefined, - "muted": false, - "status": "Active", - }, - }, - "lastRun": "2020-06-18T00:00:10.000Z", - "status": "Active", - } - `); - - testExecutionDurations(eventsFactory.getExecutionDurations(), executionDuration); - }); - - test('alert with active and inactive muted alerts', async () => { - const alert = createAlert({ mutedInstanceIds: ['instance-1', 'instance-2'] }); - const eventsFactory = new EventsFactory(); - const events = eventsFactory - .addExecute() - .addNewInstance('instance-1') - .addActiveInstance('instance-1', 'action group A') - .addNewInstance('instance-2') - .addActiveInstance('instance-2', 'action group B') - .advanceTime(10000) - .addExecute() - .addActiveInstance('instance-1', 'action group A') - .addRecoveredInstance('instance-2') - .getEvents(); - - const summary: AlertInstanceSummary = alertInstanceSummaryFromEventLog({ - alert, - events, - dateStart, - dateEnd, - }); - - const { lastRun, status, instances, executionDuration } = summary; - expect({ lastRun, status, instances }).toMatchInlineSnapshot(` - Object { - "instances": Object { - "instance-1": Object { - "actionGroupId": "action group A", - "actionSubgroup": undefined, - "activeStartDate": "2020-06-18T00:00:00.000Z", - "muted": true, - "status": "Active", - }, - "instance-2": Object { - "actionGroupId": undefined, - "actionSubgroup": undefined, - "activeStartDate": undefined, - "muted": true, - "status": "OK", - }, - }, - "lastRun": "2020-06-18T00:00:10.000Z", - "status": "Active", - } - `); - - testExecutionDurations(eventsFactory.getExecutionDurations(), executionDuration); - }); - - test('alert with active and inactive alerts over many executes', async () => { - const alert = createAlert({}); - const eventsFactory = new EventsFactory(); - const events = eventsFactory - .addExecute() - .addNewInstance('instance-1') - .addActiveInstance('instance-1', 'action group A') - .addNewInstance('instance-2') - .addActiveInstance('instance-2', 'action group B') - .advanceTime(10000) - .addExecute() - .addActiveInstance('instance-1', 'action group A') - .addRecoveredInstance('instance-2') - .advanceTime(10000) - .addExecute() - .addActiveInstance('instance-1', 'action group B') - .advanceTime(10000) - .addExecute() - .addActiveInstance('instance-1', 'action group B') - .getEvents(); - - const summary: AlertInstanceSummary = alertInstanceSummaryFromEventLog({ - alert, - events, - dateStart, - dateEnd, - }); - - const { lastRun, status, instances, executionDuration } = summary; - expect({ lastRun, status, instances }).toMatchInlineSnapshot(` - Object { - "instances": Object { - "instance-1": Object { - "actionGroupId": "action group B", - "actionSubgroup": undefined, - "activeStartDate": "2020-06-18T00:00:00.000Z", - "muted": false, - "status": "Active", - }, - "instance-2": Object { - "actionGroupId": undefined, - "actionSubgroup": undefined, - "activeStartDate": undefined, - "muted": false, - "status": "OK", - }, - }, - "lastRun": "2020-06-18T00:00:30.000Z", - "status": "Active", - } - `); - - testExecutionDurations(eventsFactory.getExecutionDurations(), executionDuration); - }); - - const testExecutionDurations = ( - actualDurations: number[], - executionDuration?: { average?: number; values?: number[] } - ) => { - expect(executionDuration).toEqual({ - average: Math.round(mean(actualDurations)), - values: actualDurations, - }); - }; -}); - -function dateString(isoBaseDate: string, offsetMillis = 0): string { - return new Date(Date.parse(isoBaseDate) + offsetMillis).toISOString(); -} - -export class EventsFactory { - private events: IValidatedEvent[] = []; - - constructor(private date: string = dateStart) {} - - getEvents(): IValidatedEvent[] { - // ES normally returns events sorted newest to oldest, so we need to sort - // that way also - const events = this.events.slice(); - events.sort((a, b) => -a!['@timestamp']!.localeCompare(b!['@timestamp']!)); - return events; - } - - getTime(): string { - return this.date; - } - - advanceTime(millis: number): EventsFactory { - this.date = dateString(this.date, millis); - return this; - } - - addExecute(errorMessage?: string): EventsFactory { - let event: IValidatedEvent = { - '@timestamp': this.date, - event: { - provider: EVENT_LOG_PROVIDER, - action: EVENT_LOG_ACTIONS.execute, - duration: random(2000, 180000) * 1000 * 1000, - }, - }; - - if (errorMessage) { - event = { ...event, error: { message: errorMessage } }; - } - - this.events.push(event); - return this; - } - - addActiveInstance(instanceId: string, actionGroupId: string | undefined): EventsFactory { - const kibanaAlerting = actionGroupId - ? { instance_id: instanceId, action_group_id: actionGroupId } - : { instance_id: instanceId }; - this.events.push({ - '@timestamp': this.date, - event: { - provider: EVENT_LOG_PROVIDER, - action: EVENT_LOG_ACTIONS.activeInstance, - }, - kibana: { alerting: kibanaAlerting }, - }); - return this; - } - - addNewInstance(instanceId: string): EventsFactory { - this.events.push({ - '@timestamp': this.date, - event: { - provider: EVENT_LOG_PROVIDER, - action: EVENT_LOG_ACTIONS.newInstance, - }, - kibana: { alerting: { instance_id: instanceId } }, - }); - return this; - } - - addRecoveredInstance(instanceId: string): EventsFactory { - this.events.push({ - '@timestamp': this.date, - event: { - provider: EVENT_LOG_PROVIDER, - action: EVENT_LOG_ACTIONS.recoveredInstance, - }, - kibana: { alerting: { instance_id: instanceId } }, - }); - return this; - } - - addLegacyResolvedInstance(instanceId: string): EventsFactory { - this.events.push({ - '@timestamp': this.date, - event: { - provider: EVENT_LOG_PROVIDER, - action: LEGACY_EVENT_LOG_ACTIONS.resolvedInstance, - }, - kibana: { alerting: { instance_id: instanceId } }, - }); - return this; - } - - getExecutionDurations(): number[] { - return this.events - .filter((ev) => ev?.event?.action === 'execute' && ev?.event?.duration !== undefined) - .map((ev) => ev?.event?.duration! / (1000 * 1000)); - } -} - -function createAlert(overrides: Partial): SanitizedAlert<{ bar: boolean }> { - return { ...BaseAlert, ...overrides }; -} - -const BaseAlert: SanitizedAlert<{ bar: boolean }> = { - id: 'alert-123', - alertTypeId: '123', - schedule: { interval: '10s' }, - enabled: false, - name: 'alert-name', - tags: [], - consumer: 'alert-consumer', - throttle: null, - notifyWhen: null, - muteAll: false, - mutedInstanceIds: [], - params: { bar: true }, - actions: [], - createdBy: null, - updatedBy: null, - createdAt: new Date(), - updatedAt: new Date(), - apiKeyOwner: null, - executionStatus: { - status: 'unknown', - lastExecutionDate: new Date('2020-08-20T19:23:38Z'), - }, -}; diff --git a/x-pack/plugins/alerting/server/lib/alert_instance_summary_from_event_log.ts b/x-pack/plugins/alerting/server/lib/alert_instance_summary_from_event_log.ts deleted file mode 100644 index 40fae121df51d..0000000000000 --- a/x-pack/plugins/alerting/server/lib/alert_instance_summary_from_event_log.ts +++ /dev/null @@ -1,152 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { mean } from 'lodash'; -import { SanitizedAlert, AlertInstanceSummary, AlertInstanceStatus } from '../types'; -import { IEvent } from '../../../event_log/server'; -import { EVENT_LOG_ACTIONS, EVENT_LOG_PROVIDER, LEGACY_EVENT_LOG_ACTIONS } from '../plugin'; - -const Millis2Nanos = 1000 * 1000; - -export interface AlertInstanceSummaryFromEventLogParams { - alert: SanitizedAlert<{ bar: boolean }>; - events: IEvent[]; - dateStart: string; - dateEnd: string; -} - -export function alertInstanceSummaryFromEventLog( - params: AlertInstanceSummaryFromEventLogParams -): AlertInstanceSummary { - // initialize the result - const { alert, events, dateStart, dateEnd } = params; - const alertInstanceSummary: AlertInstanceSummary = { - id: alert.id, - name: alert.name, - tags: alert.tags, - alertTypeId: alert.alertTypeId, - consumer: alert.consumer, - statusStartDate: dateStart, - statusEndDate: dateEnd, - status: 'OK', - muteAll: alert.muteAll, - throttle: alert.throttle, - enabled: alert.enabled, - lastRun: undefined, - errorMessages: [], - instances: {}, - executionDuration: { - average: 0, - values: [], - }, - }; - - const instances = new Map(); - const eventDurations: number[] = []; - - // loop through the events - // should be sorted newest to oldest, we want oldest to newest, so reverse - for (const event of events.reverse()) { - const timeStamp = event?.['@timestamp']; - if (timeStamp === undefined) continue; - - const provider = event?.event?.provider; - if (provider !== EVENT_LOG_PROVIDER) continue; - - const action = event?.event?.action; - if (action === undefined) continue; - - if (action === EVENT_LOG_ACTIONS.execute) { - alertInstanceSummary.lastRun = timeStamp; - - const errorMessage = event?.error?.message; - if (errorMessage !== undefined) { - alertInstanceSummary.status = 'Error'; - alertInstanceSummary.errorMessages.push({ - date: timeStamp, - message: errorMessage, - }); - } else { - alertInstanceSummary.status = 'OK'; - } - - if (event?.event?.duration) { - eventDurations.push(event?.event?.duration / Millis2Nanos); - } - - continue; - } - - const instanceId = event?.kibana?.alerting?.instance_id; - if (instanceId === undefined) continue; - - const status = getAlertInstanceStatus(instances, instanceId); - switch (action) { - case EVENT_LOG_ACTIONS.newInstance: - status.activeStartDate = timeStamp; - // intentionally no break here - case EVENT_LOG_ACTIONS.activeInstance: - status.status = 'Active'; - status.actionGroupId = event?.kibana?.alerting?.action_group_id; - status.actionSubgroup = event?.kibana?.alerting?.action_subgroup; - break; - case LEGACY_EVENT_LOG_ACTIONS.resolvedInstance: - case EVENT_LOG_ACTIONS.recoveredInstance: - status.status = 'OK'; - status.activeStartDate = undefined; - status.actionGroupId = undefined; - status.actionSubgroup = undefined; - } - } - - // set the muted status of instances - for (const instanceId of alert.mutedInstanceIds) { - getAlertInstanceStatus(instances, instanceId).muted = true; - } - - // convert the instances map to object form - const instanceIds = Array.from(instances.keys()).sort(); - for (const instanceId of instanceIds) { - alertInstanceSummary.instances[instanceId] = instances.get(instanceId)!; - } - - // set the overall alert status to Active if appropriate - if (alertInstanceSummary.status !== 'Error') { - if (Array.from(instances.values()).some((instance) => instance.status === 'Active')) { - alertInstanceSummary.status = 'Active'; - } - } - - alertInstanceSummary.errorMessages.sort((a, b) => a.date.localeCompare(b.date)); - - if (eventDurations.length > 0) { - alertInstanceSummary.executionDuration = { - average: Math.round(mean(eventDurations)), - values: eventDurations, - }; - } - - return alertInstanceSummary; -} - -// return an instance status object, creating and adding to the map if needed -function getAlertInstanceStatus( - instances: Map, - instanceId: string -): AlertInstanceStatus { - if (instances.has(instanceId)) return instances.get(instanceId)!; - - const status: AlertInstanceStatus = { - status: 'OK', - muted: false, - actionGroupId: undefined, - actionSubgroup: undefined, - activeStartDate: undefined, - }; - instances.set(instanceId, status); - return status; -} diff --git a/x-pack/plugins/alerting/server/lib/alert_summary_from_event_log.test.ts b/x-pack/plugins/alerting/server/lib/alert_summary_from_event_log.test.ts new file mode 100644 index 0000000000000..e243a4dc0ad5b --- /dev/null +++ b/x-pack/plugins/alerting/server/lib/alert_summary_from_event_log.test.ts @@ -0,0 +1,714 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { random, mean } from 'lodash'; +import { SanitizedAlert, AlertSummary } from '../types'; +import { IValidatedEvent } from '../../../event_log/server'; +import { EVENT_LOG_ACTIONS, EVENT_LOG_PROVIDER, LEGACY_EVENT_LOG_ACTIONS } from '../plugin'; +import { alertSummaryFromEventLog } from './alert_summary_from_event_log'; + +const ONE_HOUR_IN_MILLIS = 60 * 60 * 1000; +const dateStart = '2020-06-18T00:00:00.000Z'; +const dateEnd = dateString(dateStart, ONE_HOUR_IN_MILLIS); + +describe('alertSummaryFromEventLog', () => { + test('no events and muted ids', async () => { + const rule = createRule({}); + const events: IValidatedEvent[] = []; + const summary: AlertSummary = alertSummaryFromEventLog({ + rule, + events, + dateStart, + dateEnd, + }); + + expect(summary).toMatchInlineSnapshot(` + Object { + "alerts": Object {}, + "consumer": "rule-consumer", + "enabled": false, + "errorMessages": Array [], + "executionDuration": Object { + "average": 0, + "values": Array [], + }, + "id": "rule-123", + "lastRun": undefined, + "muteAll": false, + "name": "rule-name", + "ruleTypeId": "123", + "status": "OK", + "statusEndDate": "2020-06-18T01:00:00.000Z", + "statusStartDate": "2020-06-18T00:00:00.000Z", + "tags": Array [], + "throttle": null, + } + `); + }); + + test('different rule properties', async () => { + const rule = createRule({ + id: 'rule-456', + alertTypeId: '456', + schedule: { interval: '100s' }, + enabled: true, + name: 'rule-name-2', + tags: ['tag-1', 'tag-2'], + consumer: 'rule-consumer-2', + throttle: '1h', + muteAll: true, + }); + const events: IValidatedEvent[] = []; + const summary: AlertSummary = alertSummaryFromEventLog({ + rule, + events, + dateStart: dateString(dateEnd, ONE_HOUR_IN_MILLIS), + dateEnd: dateString(dateEnd, ONE_HOUR_IN_MILLIS * 2), + }); + + expect(summary).toMatchInlineSnapshot(` + Object { + "alerts": Object {}, + "consumer": "rule-consumer-2", + "enabled": true, + "errorMessages": Array [], + "executionDuration": Object { + "average": 0, + "values": Array [], + }, + "id": "rule-456", + "lastRun": undefined, + "muteAll": true, + "name": "rule-name-2", + "ruleTypeId": "456", + "status": "OK", + "statusEndDate": "2020-06-18T03:00:00.000Z", + "statusStartDate": "2020-06-18T02:00:00.000Z", + "tags": Array [ + "tag-1", + "tag-2", + ], + "throttle": "1h", + } + `); + }); + + test('two muted alerts', async () => { + const rule = createRule({ + mutedInstanceIds: ['alert-1', 'alert-2'], + }); + const events: IValidatedEvent[] = []; + const summary: AlertSummary = alertSummaryFromEventLog({ + rule, + events, + dateStart, + dateEnd, + }); + + const { lastRun, status, alerts } = summary; + expect({ lastRun, status, alerts }).toMatchInlineSnapshot(` + Object { + "alerts": Object { + "alert-1": Object { + "actionGroupId": undefined, + "actionSubgroup": undefined, + "activeStartDate": undefined, + "muted": true, + "status": "OK", + }, + "alert-2": Object { + "actionGroupId": undefined, + "actionSubgroup": undefined, + "activeStartDate": undefined, + "muted": true, + "status": "OK", + }, + }, + "lastRun": undefined, + "status": "OK", + } + `); + }); + + test('active rule but no alerts', async () => { + const rule = createRule({}); + const eventsFactory = new EventsFactory(); + const events = eventsFactory.addExecute().advanceTime(10000).addExecute().getEvents(); + + const summary: AlertSummary = alertSummaryFromEventLog({ + rule, + events, + dateStart, + dateEnd, + }); + + const { lastRun, status, alerts, executionDuration } = summary; + expect({ lastRun, status, alerts }).toMatchInlineSnapshot(` + Object { + "alerts": Object {}, + "lastRun": "2020-06-18T00:00:10.000Z", + "status": "OK", + } + `); + + testExecutionDurations(eventsFactory.getExecutionDurations(), executionDuration); + }); + + test('active rule with no alerts but has errors', async () => { + const rule = createRule({}); + const eventsFactory = new EventsFactory(); + const events = eventsFactory + .addExecute('oof!') + .advanceTime(10000) + .addExecute('rut roh!') + .getEvents(); + + const summary: AlertSummary = alertSummaryFromEventLog({ + rule, + events, + dateStart, + dateEnd, + }); + + const { lastRun, status, errorMessages, alerts, executionDuration } = summary; + expect({ lastRun, status, errorMessages, alerts }).toMatchInlineSnapshot(` + Object { + "alerts": Object {}, + "errorMessages": Array [ + Object { + "date": "2020-06-18T00:00:00.000Z", + "message": "oof!", + }, + Object { + "date": "2020-06-18T00:00:10.000Z", + "message": "rut roh!", + }, + ], + "lastRun": "2020-06-18T00:00:10.000Z", + "status": "Error", + } + `); + + testExecutionDurations(eventsFactory.getExecutionDurations(), executionDuration); + }); + + test('rule with currently inactive alert', async () => { + const rule = createRule({}); + const eventsFactory = new EventsFactory(); + const events = eventsFactory + .addExecute() + .addNewAlert('alert-1') + .addActiveAlert('alert-1', 'action group A') + .advanceTime(10000) + .addExecute() + .addRecoveredAlert('alert-1') + .getEvents(); + + const summary: AlertSummary = alertSummaryFromEventLog({ + rule, + events, + dateStart, + dateEnd, + }); + + const { lastRun, status, alerts, executionDuration } = summary; + expect({ lastRun, status, alerts }).toMatchInlineSnapshot(` + Object { + "alerts": Object { + "alert-1": Object { + "actionGroupId": undefined, + "actionSubgroup": undefined, + "activeStartDate": undefined, + "muted": false, + "status": "OK", + }, + }, + "lastRun": "2020-06-18T00:00:10.000Z", + "status": "OK", + } + `); + + testExecutionDurations(eventsFactory.getExecutionDurations(), executionDuration); + }); + + test('legacy rule with currently inactive alert', async () => { + const rule = createRule({}); + const eventsFactory = new EventsFactory(); + const events = eventsFactory + .addExecute() + .addNewAlert('alert-1') + .addActiveAlert('alert-1', 'action group A') + .advanceTime(10000) + .addExecute() + .addLegacyResolvedAlert('alert-1') + .getEvents(); + + const summary: AlertSummary = alertSummaryFromEventLog({ + rule, + events, + dateStart, + dateEnd, + }); + + const { lastRun, status, alerts, executionDuration } = summary; + expect({ lastRun, status, alerts }).toMatchInlineSnapshot(` + Object { + "alerts": Object { + "alert-1": Object { + "actionGroupId": undefined, + "actionSubgroup": undefined, + "activeStartDate": undefined, + "muted": false, + "status": "OK", + }, + }, + "lastRun": "2020-06-18T00:00:10.000Z", + "status": "OK", + } + `); + + testExecutionDurations(eventsFactory.getExecutionDurations(), executionDuration); + }); + + test('rule with currently inactive alert, no new-instance', async () => { + const rule = createRule({}); + const eventsFactory = new EventsFactory(); + const events = eventsFactory + .addExecute() + .addActiveAlert('alert-1', 'action group A') + .advanceTime(10000) + .addExecute() + .addRecoveredAlert('alert-1') + .getEvents(); + + const summary: AlertSummary = alertSummaryFromEventLog({ + rule, + events, + dateStart, + dateEnd, + }); + + const { lastRun, status, alerts, executionDuration } = summary; + expect({ lastRun, status, alerts }).toMatchInlineSnapshot(` + Object { + "alerts": Object { + "alert-1": Object { + "actionGroupId": undefined, + "actionSubgroup": undefined, + "activeStartDate": undefined, + "muted": false, + "status": "OK", + }, + }, + "lastRun": "2020-06-18T00:00:10.000Z", + "status": "OK", + } + `); + + testExecutionDurations(eventsFactory.getExecutionDurations(), executionDuration); + }); + + test('rule with currently active alert', async () => { + const rule = createRule({}); + const eventsFactory = new EventsFactory(); + const events = eventsFactory + .addExecute() + .addNewAlert('alert-1') + .addActiveAlert('alert-1', 'action group A') + .advanceTime(10000) + .addExecute() + .addActiveAlert('alert-1', 'action group A') + .getEvents(); + + const summary: AlertSummary = alertSummaryFromEventLog({ + rule, + events, + dateStart, + dateEnd, + }); + + const { lastRun, status, alerts, executionDuration } = summary; + expect({ lastRun, status, alerts }).toMatchInlineSnapshot(` + Object { + "alerts": Object { + "alert-1": Object { + "actionGroupId": "action group A", + "actionSubgroup": undefined, + "activeStartDate": "2020-06-18T00:00:00.000Z", + "muted": false, + "status": "Active", + }, + }, + "lastRun": "2020-06-18T00:00:10.000Z", + "status": "Active", + } + `); + + testExecutionDurations(eventsFactory.getExecutionDurations(), executionDuration); + }); + + test('rule with currently active alert with no action group in event log', async () => { + const rule = createRule({}); + const eventsFactory = new EventsFactory(); + const events = eventsFactory + .addExecute() + .addNewAlert('alert-1') + .addActiveAlert('alert-1', undefined) + .advanceTime(10000) + .addExecute() + .addActiveAlert('alert-1', undefined) + .getEvents(); + + const summary: AlertSummary = alertSummaryFromEventLog({ + rule, + events, + dateStart, + dateEnd, + }); + + const { lastRun, status, alerts, executionDuration } = summary; + expect({ lastRun, status, alerts }).toMatchInlineSnapshot(` + Object { + "alerts": Object { + "alert-1": Object { + "actionGroupId": undefined, + "actionSubgroup": undefined, + "activeStartDate": "2020-06-18T00:00:00.000Z", + "muted": false, + "status": "Active", + }, + }, + "lastRun": "2020-06-18T00:00:10.000Z", + "status": "Active", + } + `); + + testExecutionDurations(eventsFactory.getExecutionDurations(), executionDuration); + }); + + test('rule with currently active alert that switched action groups', async () => { + const rule = createRule({}); + const eventsFactory = new EventsFactory(); + const events = eventsFactory + .addExecute() + .addNewAlert('alert-1') + .addActiveAlert('alert-1', 'action group A') + .advanceTime(10000) + .addExecute() + .addActiveAlert('alert-1', 'action group B') + .getEvents(); + + const summary: AlertSummary = alertSummaryFromEventLog({ + rule, + events, + dateStart, + dateEnd, + }); + + const { lastRun, status, alerts, executionDuration } = summary; + expect({ lastRun, status, alerts }).toMatchInlineSnapshot(` + Object { + "alerts": Object { + "alert-1": Object { + "actionGroupId": "action group B", + "actionSubgroup": undefined, + "activeStartDate": "2020-06-18T00:00:00.000Z", + "muted": false, + "status": "Active", + }, + }, + "lastRun": "2020-06-18T00:00:10.000Z", + "status": "Active", + } + `); + + testExecutionDurations(eventsFactory.getExecutionDurations(), executionDuration); + }); + + test('rule with currently active alert, no new-instance', async () => { + const rule = createRule({}); + const eventsFactory = new EventsFactory(); + const events = eventsFactory + .addExecute() + .addActiveAlert('alert-1', 'action group A') + .advanceTime(10000) + .addExecute() + .addActiveAlert('alert-1', 'action group A') + .getEvents(); + + const summary: AlertSummary = alertSummaryFromEventLog({ + rule, + events, + dateStart, + dateEnd, + }); + + const { lastRun, status, alerts, executionDuration } = summary; + expect({ lastRun, status, alerts }).toMatchInlineSnapshot(` + Object { + "alerts": Object { + "alert-1": Object { + "actionGroupId": "action group A", + "actionSubgroup": undefined, + "activeStartDate": undefined, + "muted": false, + "status": "Active", + }, + }, + "lastRun": "2020-06-18T00:00:10.000Z", + "status": "Active", + } + `); + + testExecutionDurations(eventsFactory.getExecutionDurations(), executionDuration); + }); + + test('rule with active and inactive muted alerts', async () => { + const rule = createRule({ mutedInstanceIds: ['alert-1', 'alert-2'] }); + const eventsFactory = new EventsFactory(); + const events = eventsFactory + .addExecute() + .addNewAlert('alert-1') + .addActiveAlert('alert-1', 'action group A') + .addNewAlert('alert-2') + .addActiveAlert('alert-2', 'action group B') + .advanceTime(10000) + .addExecute() + .addActiveAlert('alert-1', 'action group A') + .addRecoveredAlert('alert-2') + .getEvents(); + + const summary: AlertSummary = alertSummaryFromEventLog({ + rule, + events, + dateStart, + dateEnd, + }); + + const { lastRun, status, alerts, executionDuration } = summary; + expect({ lastRun, status, alerts }).toMatchInlineSnapshot(` + Object { + "alerts": Object { + "alert-1": Object { + "actionGroupId": "action group A", + "actionSubgroup": undefined, + "activeStartDate": "2020-06-18T00:00:00.000Z", + "muted": true, + "status": "Active", + }, + "alert-2": Object { + "actionGroupId": undefined, + "actionSubgroup": undefined, + "activeStartDate": undefined, + "muted": true, + "status": "OK", + }, + }, + "lastRun": "2020-06-18T00:00:10.000Z", + "status": "Active", + } + `); + + testExecutionDurations(eventsFactory.getExecutionDurations(), executionDuration); + }); + + test('rule with active and inactive alerts over many executes', async () => { + const rule = createRule({}); + const eventsFactory = new EventsFactory(); + const events = eventsFactory + .addExecute() + .addNewAlert('alert-1') + .addActiveAlert('alert-1', 'action group A') + .addNewAlert('alert-2') + .addActiveAlert('alert-2', 'action group B') + .advanceTime(10000) + .addExecute() + .addActiveAlert('alert-1', 'action group A') + .addRecoveredAlert('alert-2') + .advanceTime(10000) + .addExecute() + .addActiveAlert('alert-1', 'action group B') + .advanceTime(10000) + .addExecute() + .addActiveAlert('alert-1', 'action group B') + .getEvents(); + + const summary: AlertSummary = alertSummaryFromEventLog({ + rule, + events, + dateStart, + dateEnd, + }); + + const { lastRun, status, alerts, executionDuration } = summary; + expect({ lastRun, status, alerts }).toMatchInlineSnapshot(` + Object { + "alerts": Object { + "alert-1": Object { + "actionGroupId": "action group B", + "actionSubgroup": undefined, + "activeStartDate": "2020-06-18T00:00:00.000Z", + "muted": false, + "status": "Active", + }, + "alert-2": Object { + "actionGroupId": undefined, + "actionSubgroup": undefined, + "activeStartDate": undefined, + "muted": false, + "status": "OK", + }, + }, + "lastRun": "2020-06-18T00:00:30.000Z", + "status": "Active", + } + `); + + testExecutionDurations(eventsFactory.getExecutionDurations(), executionDuration); + }); + + const testExecutionDurations = ( + actualDurations: number[], + executionDuration?: { average?: number; values?: number[] } + ) => { + expect(executionDuration).toEqual({ + average: Math.round(mean(actualDurations)), + values: actualDurations, + }); + }; +}); + +function dateString(isoBaseDate: string, offsetMillis = 0): string { + return new Date(Date.parse(isoBaseDate) + offsetMillis).toISOString(); +} + +export class EventsFactory { + private events: IValidatedEvent[] = []; + + constructor(private date: string = dateStart) {} + + getEvents(): IValidatedEvent[] { + // ES normally returns events sorted newest to oldest, so we need to sort + // that way also + const events = this.events.slice(); + events.sort((a, b) => -a!['@timestamp']!.localeCompare(b!['@timestamp']!)); + return events; + } + + getTime(): string { + return this.date; + } + + advanceTime(millis: number): EventsFactory { + this.date = dateString(this.date, millis); + return this; + } + + addExecute(errorMessage?: string): EventsFactory { + let event: IValidatedEvent = { + '@timestamp': this.date, + event: { + provider: EVENT_LOG_PROVIDER, + action: EVENT_LOG_ACTIONS.execute, + duration: random(2000, 180000) * 1000 * 1000, + }, + }; + + if (errorMessage) { + event = { ...event, error: { message: errorMessage } }; + } + + this.events.push(event); + return this; + } + + addActiveAlert(alertId: string, actionGroupId: string | undefined): EventsFactory { + const kibanaAlerting = actionGroupId + ? { instance_id: alertId, action_group_id: actionGroupId } + : { instance_id: alertId }; + this.events.push({ + '@timestamp': this.date, + event: { + provider: EVENT_LOG_PROVIDER, + action: EVENT_LOG_ACTIONS.activeInstance, + }, + kibana: { alerting: kibanaAlerting }, + }); + return this; + } + + addNewAlert(alertId: string): EventsFactory { + this.events.push({ + '@timestamp': this.date, + event: { + provider: EVENT_LOG_PROVIDER, + action: EVENT_LOG_ACTIONS.newInstance, + }, + kibana: { alerting: { instance_id: alertId } }, + }); + return this; + } + + addRecoveredAlert(alertId: string): EventsFactory { + this.events.push({ + '@timestamp': this.date, + event: { + provider: EVENT_LOG_PROVIDER, + action: EVENT_LOG_ACTIONS.recoveredInstance, + }, + kibana: { alerting: { instance_id: alertId } }, + }); + return this; + } + + addLegacyResolvedAlert(alertId: string): EventsFactory { + this.events.push({ + '@timestamp': this.date, + event: { + provider: EVENT_LOG_PROVIDER, + action: LEGACY_EVENT_LOG_ACTIONS.resolvedInstance, + }, + kibana: { alerting: { instance_id: alertId } }, + }); + return this; + } + + getExecutionDurations(): number[] { + return this.events + .filter((ev) => ev?.event?.action === 'execute' && ev?.event?.duration !== undefined) + .map((ev) => ev?.event?.duration! / (1000 * 1000)); + } +} + +function createRule(overrides: Partial): SanitizedAlert<{ bar: boolean }> { + return { ...BaseRule, ...overrides }; +} + +const BaseRule: SanitizedAlert<{ bar: boolean }> = { + id: 'rule-123', + alertTypeId: '123', + schedule: { interval: '10s' }, + enabled: false, + name: 'rule-name', + tags: [], + consumer: 'rule-consumer', + throttle: null, + notifyWhen: null, + muteAll: false, + mutedInstanceIds: [], + params: { bar: true }, + actions: [], + createdBy: null, + updatedBy: null, + createdAt: new Date(), + updatedAt: new Date(), + apiKeyOwner: null, + executionStatus: { + status: 'unknown', + lastExecutionDate: new Date('2020-08-20T19:23:38Z'), + }, +}; diff --git a/x-pack/plugins/alerting/server/lib/alert_summary_from_event_log.ts b/x-pack/plugins/alerting/server/lib/alert_summary_from_event_log.ts new file mode 100644 index 0000000000000..65c29407a8a01 --- /dev/null +++ b/x-pack/plugins/alerting/server/lib/alert_summary_from_event_log.ts @@ -0,0 +1,147 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { mean } from 'lodash'; +import { SanitizedAlert, AlertSummary, AlertStatus } from '../types'; +import { IEvent } from '../../../event_log/server'; +import { EVENT_LOG_ACTIONS, EVENT_LOG_PROVIDER, LEGACY_EVENT_LOG_ACTIONS } from '../plugin'; + +const Millis2Nanos = 1000 * 1000; + +export interface AlertSummaryFromEventLogParams { + rule: SanitizedAlert<{ bar: boolean }>; + events: IEvent[]; + dateStart: string; + dateEnd: string; +} + +export function alertSummaryFromEventLog(params: AlertSummaryFromEventLogParams): AlertSummary { + // initialize the result + const { rule, events, dateStart, dateEnd } = params; + const alertSummary: AlertSummary = { + id: rule.id, + name: rule.name, + tags: rule.tags, + ruleTypeId: rule.alertTypeId, + consumer: rule.consumer, + statusStartDate: dateStart, + statusEndDate: dateEnd, + status: 'OK', + muteAll: rule.muteAll, + throttle: rule.throttle, + enabled: rule.enabled, + lastRun: undefined, + errorMessages: [], + alerts: {}, + executionDuration: { + average: 0, + values: [], + }, + }; + + const alerts = new Map(); + const eventDurations: number[] = []; + + // loop through the events + // should be sorted newest to oldest, we want oldest to newest, so reverse + for (const event of events.reverse()) { + const timeStamp = event?.['@timestamp']; + if (timeStamp === undefined) continue; + + const provider = event?.event?.provider; + if (provider !== EVENT_LOG_PROVIDER) continue; + + const action = event?.event?.action; + if (action === undefined) continue; + + if (action === EVENT_LOG_ACTIONS.execute) { + alertSummary.lastRun = timeStamp; + + const errorMessage = event?.error?.message; + if (errorMessage !== undefined) { + alertSummary.status = 'Error'; + alertSummary.errorMessages.push({ + date: timeStamp, + message: errorMessage, + }); + } else { + alertSummary.status = 'OK'; + } + + if (event?.event?.duration) { + eventDurations.push(event?.event?.duration / Millis2Nanos); + } + + continue; + } + + const alertId = event?.kibana?.alerting?.instance_id; + if (alertId === undefined) continue; + + const status = getAlertStatus(alerts, alertId); + switch (action) { + case EVENT_LOG_ACTIONS.newInstance: + status.activeStartDate = timeStamp; + // intentionally no break here + case EVENT_LOG_ACTIONS.activeInstance: + status.status = 'Active'; + status.actionGroupId = event?.kibana?.alerting?.action_group_id; + status.actionSubgroup = event?.kibana?.alerting?.action_subgroup; + break; + case LEGACY_EVENT_LOG_ACTIONS.resolvedInstance: + case EVENT_LOG_ACTIONS.recoveredInstance: + status.status = 'OK'; + status.activeStartDate = undefined; + status.actionGroupId = undefined; + status.actionSubgroup = undefined; + } + } + + // set the muted status of alerts + for (const alertId of rule.mutedInstanceIds) { + getAlertStatus(alerts, alertId).muted = true; + } + + // convert the alerts map to object form + const alertIds = Array.from(alerts.keys()).sort(); + for (const alertId of alertIds) { + alertSummary.alerts[alertId] = alerts.get(alertId)!; + } + + // set the overall alert status to Active if appropriatea + if (alertSummary.status !== 'Error') { + if (Array.from(alerts.values()).some((a) => a.status === 'Active')) { + alertSummary.status = 'Active'; + } + } + + alertSummary.errorMessages.sort((a, b) => a.date.localeCompare(b.date)); + + if (eventDurations.length > 0) { + alertSummary.executionDuration = { + average: Math.round(mean(eventDurations)), + values: eventDurations, + }; + } + + return alertSummary; +} + +// return an alert status object, creating and adding to the map if needed +function getAlertStatus(alerts: Map, alertId: string): AlertStatus { + if (alerts.has(alertId)) return alerts.get(alertId)!; + + const status: AlertStatus = { + status: 'OK', + muted: false, + actionGroupId: undefined, + actionSubgroup: undefined, + activeStartDate: undefined, + }; + alerts.set(alertId, status); + return status; +} diff --git a/x-pack/plugins/alerting/server/lib/get_security_health.test.ts b/x-pack/plugins/alerting/server/lib/get_security_health.test.ts new file mode 100644 index 0000000000000..1253e0c3379b5 --- /dev/null +++ b/x-pack/plugins/alerting/server/lib/get_security_health.test.ts @@ -0,0 +1,84 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { getSecurityHealth } from './get_security_health'; + +const createDependencies = ( + isSecurityEnabled: boolean | null, + canEncrypt: boolean, + apiKeysEnabled: boolean +) => { + const isEsSecurityEnabled = async () => isSecurityEnabled; + const isAbleToEncrypt = async () => canEncrypt; + const areApikeysEnabled = async () => apiKeysEnabled; + + const deps: [() => Promise, () => Promise, () => Promise] = [ + isEsSecurityEnabled, + isAbleToEncrypt, + areApikeysEnabled, + ]; + + return deps; +}; + +describe('Get security health', () => { + describe('Correctly returns the overall security health', () => { + test('When ES security enabled status cannot be determined', async () => { + const deps = createDependencies(null, true, true); + const securityHealth = await getSecurityHealth(...deps); + expect(securityHealth).toEqual({ + isSufficientlySecure: false, + hasPermanentEncryptionKey: true, + }); + }); + + test('When ES security is disabled', async () => { + const deps = createDependencies(false, true, true); + const securityHealth = await getSecurityHealth(...deps); + expect(securityHealth).toEqual({ + isSufficientlySecure: true, + hasPermanentEncryptionKey: true, + }); + }); + + test('When ES security is enabled, and API keys are disabled', async () => { + const deps = createDependencies(true, true, false); + const securityHealth = await getSecurityHealth(...deps); + expect(securityHealth).toEqual({ + isSufficientlySecure: false, + hasPermanentEncryptionKey: true, + }); + }); + + test('When ES security is enabled, and API keys are enabled', async () => { + const deps = createDependencies(true, true, true); + const securityHealth = await getSecurityHealth(...deps); + expect(securityHealth).toEqual({ + isSufficientlySecure: true, + hasPermanentEncryptionKey: true, + }); + }); + + test('With encryption enabled', async () => { + const deps = createDependencies(true, true, true); + const securityHealth = await getSecurityHealth(...deps); + expect(securityHealth).toEqual({ + isSufficientlySecure: true, + hasPermanentEncryptionKey: true, + }); + }); + + test('With encryption disabled', async () => { + const deps = createDependencies(true, false, true); + const securityHealth = await getSecurityHealth(...deps); + expect(securityHealth).toEqual({ + isSufficientlySecure: true, + hasPermanentEncryptionKey: false, + }); + }); + }); +}); diff --git a/x-pack/plugins/alerting/server/lib/get_security_health.ts b/x-pack/plugins/alerting/server/lib/get_security_health.ts new file mode 100644 index 0000000000000..1a2097221433b --- /dev/null +++ b/x-pack/plugins/alerting/server/lib/get_security_health.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. + */ + +export interface SecurityHealth { + isSufficientlySecure: boolean; + hasPermanentEncryptionKey: boolean; +} + +export const getSecurityHealth = async ( + isEsSecurityEnabled: () => Promise, + isAbleToEncrypt: () => Promise, + areApiKeysEnabled: () => Promise +) => { + const esSecurityIsEnabled = await isEsSecurityEnabled(); + const apiKeysAreEnabled = await areApiKeysEnabled(); + const ableToEncrypt = await isAbleToEncrypt(); + + let isSufficientlySecure: boolean; + + if (esSecurityIsEnabled === null) { + isSufficientlySecure = false; + } else { + // if esSecurityIsEnabled = true, then areApiKeysEnabled must be true to enable alerting + // if esSecurityIsEnabled = false, then it does not matter what areApiKeysEnabled is + isSufficientlySecure = !esSecurityIsEnabled || (esSecurityIsEnabled && apiKeysAreEnabled); + } + + const securityHealth: SecurityHealth = { + isSufficientlySecure, + hasPermanentEncryptionKey: ableToEncrypt, + }; + + return securityHealth; +}; diff --git a/x-pack/plugins/alerting/server/mocks.ts b/x-pack/plugins/alerting/server/mocks.ts index 639ba166e00a8..7fb748a305037 100644 --- a/x-pack/plugins/alerting/server/mocks.ts +++ b/x-pack/plugins/alerting/server/mocks.ts @@ -19,6 +19,7 @@ export { rulesClientMock }; const createSetupMock = () => { const mock: jest.Mocked = { registerType: jest.fn(), + getSecurityHealth: jest.fn(), }; return mock; }; diff --git a/x-pack/plugins/alerting/server/plugin.ts b/x-pack/plugins/alerting/server/plugin.ts index f0703defbca3d..bd3eab19d220d 100644 --- a/x-pack/plugins/alerting/server/plugin.ts +++ b/x-pack/plugins/alerting/server/plugin.ts @@ -65,6 +65,7 @@ import { AlertsConfig } from './config'; import { getHealth } from './health/get_health'; import { AlertingAuthorizationClientFactory } from './alerting_authorization_client_factory'; import { AlertingAuthorization } from './authorization'; +import { getSecurityHealth, SecurityHealth } from './lib/get_security_health'; export const EVENT_LOG_PROVIDER = 'alerting'; export const EVENT_LOG_ACTIONS = { @@ -99,6 +100,7 @@ export interface PluginSetupContract { RecoveryActionGroupId > ): void; + getSecurityHealth: () => Promise; } export interface PluginStartContract { @@ -242,29 +244,6 @@ export class AlertingPlugin { }); core.status.set(serviceStatus$); - // core.getStartServices().then(async ([coreStart, startPlugins]) => { - // combineLatest([ - // core.status.derivedStatus$, - // getHealthStatusStream( - // startPlugins.taskManager, - // this.logger, - // coreStart.savedObjects, - // this.config - // ), - // ]) - // .pipe( - // map(([derivedStatus, healthStatus]) => { - // if (healthStatus.level > derivedStatus.level) { - // return healthStatus as ServiceStatus; - // } else { - // return derivedStatus; - // } - // }), - // share() - // ) - // .subscribe(serviceStatus$); - // }); - initializeAlertingHealth(this.logger, plugins.taskManager, core.getStartServices()); core.http.registerRouteHandlerContext( @@ -315,6 +294,16 @@ export class AlertingPlugin { ruleTypeRegistry.register(alertType); } }, + getSecurityHealth: async () => { + return await getSecurityHealth( + async () => (this.licenseState ? this.licenseState.getIsSecurityEnabled() : null), + async () => plugins.encryptedSavedObjects.canEncrypt, + async () => { + const [, { security }] = await core.getStartServices(); + return security?.authc.apiKeys.areAPIKeysEnabled() ?? false; + } + ); + }, }; } diff --git a/x-pack/plugins/alerting/server/routes/get_rule_alert_summary.test.ts b/x-pack/plugins/alerting/server/routes/get_rule_alert_summary.test.ts index 5044c5c8617a0..7b7088a127491 100644 --- a/x-pack/plugins/alerting/server/routes/get_rule_alert_summary.test.ts +++ b/x-pack/plugins/alerting/server/routes/get_rule_alert_summary.test.ts @@ -11,7 +11,7 @@ import { licenseStateMock } from '../lib/license_state.mock'; import { mockHandlerArguments } from './_mock_handler_arguments'; import { SavedObjectsErrorHelpers } from 'src/core/server'; import { rulesClientMock } from '../rules_client.mock'; -import { AlertInstanceSummary } from '../types'; +import { AlertSummary } from '../types'; const rulesClient = rulesClientMock.create(); jest.mock('../lib/license_api_access.ts', () => ({ @@ -24,11 +24,11 @@ beforeEach(() => { describe('getRuleAlertSummaryRoute', () => { const dateString = new Date().toISOString(); - const mockedAlertInstanceSummary: AlertInstanceSummary = { + const mockedAlertSummary: AlertSummary = { id: '', name: '', tags: [], - alertTypeId: '', + ruleTypeId: '', consumer: '', muteAll: false, throttle: null, @@ -37,7 +37,7 @@ describe('getRuleAlertSummaryRoute', () => { statusEndDate: dateString, status: 'OK', errorMessages: [], - instances: {}, + alerts: {}, executionDuration: { average: 1, values: [3, 5, 5], @@ -54,7 +54,7 @@ describe('getRuleAlertSummaryRoute', () => { expect(config.path).toMatchInlineSnapshot(`"/internal/alerting/rule/{id}/_alert_summary"`); - rulesClient.getAlertInstanceSummary.mockResolvedValueOnce(mockedAlertInstanceSummary); + rulesClient.getAlertSummary.mockResolvedValueOnce(mockedAlertSummary); const [context, req, res] = mockHandlerArguments( { rulesClient }, @@ -69,8 +69,8 @@ describe('getRuleAlertSummaryRoute', () => { await handler(context, req, res); - expect(rulesClient.getAlertInstanceSummary).toHaveBeenCalledTimes(1); - expect(rulesClient.getAlertInstanceSummary.mock.calls[0]).toMatchInlineSnapshot(` + expect(rulesClient.getAlertSummary).toHaveBeenCalledTimes(1); + expect(rulesClient.getAlertSummary.mock.calls[0]).toMatchInlineSnapshot(` Array [ Object { "dateStart": undefined, @@ -90,7 +90,7 @@ describe('getRuleAlertSummaryRoute', () => { const [, handler] = router.get.mock.calls[0]; - rulesClient.getAlertInstanceSummary = jest + rulesClient.getAlertSummary = jest .fn() .mockResolvedValueOnce(SavedObjectsErrorHelpers.createGenericNotFoundError('alert', '1')); diff --git a/x-pack/plugins/alerting/server/routes/get_rule_alert_summary.ts b/x-pack/plugins/alerting/server/routes/get_rule_alert_summary.ts index 131bddcce049d..dbe71c09d7402 100644 --- a/x-pack/plugins/alerting/server/routes/get_rule_alert_summary.ts +++ b/x-pack/plugins/alerting/server/routes/get_rule_alert_summary.ts @@ -8,12 +8,12 @@ import { IRouter } from 'kibana/server'; import { schema } from '@kbn/config-schema'; import { ILicenseState } from '../lib'; -import { GetAlertInstanceSummaryParams } from '../rules_client'; +import { GetAlertSummaryParams } from '../rules_client'; import { RewriteRequestCase, RewriteResponseCase, verifyAccessAndContext } from './lib'; import { AlertingRequestHandlerContext, INTERNAL_BASE_ALERTING_API_PATH, - AlertInstanceSummary, + AlertSummary, } from '../types'; const paramSchema = schema.object({ @@ -24,27 +24,25 @@ const querySchema = schema.object({ date_start: schema.maybe(schema.string()), }); -const rewriteReq: RewriteRequestCase = ({ +const rewriteReq: RewriteRequestCase = ({ date_start: dateStart, ...rest }) => ({ ...rest, dateStart, }); -const rewriteBodyRes: RewriteResponseCase = ({ - alertTypeId, +const rewriteBodyRes: RewriteResponseCase = ({ + ruleTypeId, muteAll, statusStartDate, statusEndDate, errorMessages, lastRun, - instances: alerts, executionDuration, ...rest }) => ({ ...rest, - alerts, - rule_type_id: alertTypeId, + rule_type_id: ruleTypeId, mute_all: muteAll, status_start_date: statusStartDate, status_end_date: statusEndDate, @@ -69,7 +67,7 @@ export const getRuleAlertSummaryRoute = ( verifyAccessAndContext(licenseState, async function (context, req, res) { const rulesClient = context.alerting.getRulesClient(); const { id } = req.params; - const summary = await rulesClient.getAlertInstanceSummary(rewriteReq({ id, ...req.query })); + const summary = await rulesClient.getAlertSummary(rewriteReq({ id, ...req.query })); return res.ok({ body: rewriteBodyRes(summary) }); }) ) diff --git a/x-pack/plugins/alerting/server/routes/health.ts b/x-pack/plugins/alerting/server/routes/health.ts index fa09213dada3a..4f3ed2b542611 100644 --- a/x-pack/plugins/alerting/server/routes/health.ts +++ b/x-pack/plugins/alerting/server/routes/health.ts @@ -14,6 +14,7 @@ import { BASE_ALERTING_API_PATH, AlertingFrameworkHealth, } from '../types'; +import { getSecurityHealth } from '../lib/get_security_health'; const rewriteBodyRes: RewriteResponseCase = ({ isSufficientlySecure, @@ -44,23 +45,16 @@ export const healthRoute = ( router.handleLegacyErrors( verifyAccessAndContext(licenseState, async function (context, req, res) { try { - const isEsSecurityEnabled: boolean | null = licenseState.getIsSecurityEnabled(); - const areApiKeysEnabled = await context.alerting.areApiKeysEnabled(); const alertingFrameworkHeath = await context.alerting.getFrameworkHealth(); - let isSufficientlySecure; - if (isEsSecurityEnabled === null) { - isSufficientlySecure = false; - } else { - // if isEsSecurityEnabled = true, then areApiKeysEnabled must be true to enable alerting - // if isEsSecurityEnabled = false, then it does not matter what areApiKeysEnabled is - isSufficientlySecure = - !isEsSecurityEnabled || (isEsSecurityEnabled && areApiKeysEnabled); - } + const securityHealth = await getSecurityHealth( + async () => (licenseState ? licenseState.getIsSecurityEnabled() : null), + async () => encryptedSavedObjects.canEncrypt, + context.alerting.areApiKeysEnabled + ); const frameworkHealth: AlertingFrameworkHealth = { - isSufficientlySecure, - hasPermanentEncryptionKey: encryptedSavedObjects.canEncrypt, + ...securityHealth, alertingFrameworkHeath, }; diff --git a/x-pack/plugins/alerting/server/routes/legacy/get_alert_instance_summary.test.ts b/x-pack/plugins/alerting/server/routes/legacy/get_alert_instance_summary.test.ts index ed2b3056da45e..e5ce9c5f3e285 100644 --- a/x-pack/plugins/alerting/server/routes/legacy/get_alert_instance_summary.test.ts +++ b/x-pack/plugins/alerting/server/routes/legacy/get_alert_instance_summary.test.ts @@ -11,7 +11,7 @@ import { licenseStateMock } from '../../lib/license_state.mock'; import { mockHandlerArguments } from './../_mock_handler_arguments'; import { SavedObjectsErrorHelpers } from 'src/core/server'; import { rulesClientMock } from '../../rules_client.mock'; -import { AlertInstanceSummary } from '../../types'; +import { AlertSummary } from '../../types'; import { trackLegacyRouteUsage } from '../../lib/track_legacy_route_usage'; const rulesClient = rulesClientMock.create(); @@ -29,11 +29,11 @@ beforeEach(() => { describe('getAlertInstanceSummaryRoute', () => { const dateString = new Date().toISOString(); - const mockedAlertInstanceSummary: AlertInstanceSummary = { + const mockedAlertInstanceSummary: AlertSummary = { id: '', name: '', tags: [], - alertTypeId: '', + ruleTypeId: '', consumer: '', muteAll: false, throttle: null, @@ -42,7 +42,7 @@ describe('getAlertInstanceSummaryRoute', () => { statusEndDate: dateString, status: 'OK', errorMessages: [], - instances: {}, + alerts: {}, executionDuration: { average: 0, values: [], @@ -59,7 +59,7 @@ describe('getAlertInstanceSummaryRoute', () => { expect(config.path).toMatchInlineSnapshot(`"/api/alerts/alert/{id}/_instance_summary"`); - rulesClient.getAlertInstanceSummary.mockResolvedValueOnce(mockedAlertInstanceSummary); + rulesClient.getAlertSummary.mockResolvedValueOnce(mockedAlertInstanceSummary); const [context, req, res] = mockHandlerArguments( { rulesClient }, @@ -74,8 +74,8 @@ describe('getAlertInstanceSummaryRoute', () => { await handler(context, req, res); - expect(rulesClient.getAlertInstanceSummary).toHaveBeenCalledTimes(1); - expect(rulesClient.getAlertInstanceSummary.mock.calls[0]).toMatchInlineSnapshot(` + expect(rulesClient.getAlertSummary).toHaveBeenCalledTimes(1); + expect(rulesClient.getAlertSummary.mock.calls[0]).toMatchInlineSnapshot(` Array [ Object { "dateStart": undefined, @@ -95,7 +95,7 @@ describe('getAlertInstanceSummaryRoute', () => { const [, handler] = router.get.mock.calls[0]; - rulesClient.getAlertInstanceSummary = jest + rulesClient.getAlertSummary = jest .fn() .mockResolvedValueOnce(SavedObjectsErrorHelpers.createGenericNotFoundError('alert', '1')); @@ -121,6 +121,8 @@ describe('getAlertInstanceSummaryRoute', () => { getAlertInstanceSummaryRoute(router, licenseState, mockUsageCounter); const [, handler] = router.get.mock.calls[0]; + + rulesClient.getAlertSummary.mockResolvedValueOnce(mockedAlertInstanceSummary); const [context, req, res] = mockHandlerArguments( { rulesClient }, { params: { id: '1' }, query: {} }, diff --git a/x-pack/plugins/alerting/server/routes/legacy/get_alert_instance_summary.ts b/x-pack/plugins/alerting/server/routes/legacy/get_alert_instance_summary.ts index 2eed14a913a85..e94c0a858646d 100644 --- a/x-pack/plugins/alerting/server/routes/legacy/get_alert_instance_summary.ts +++ b/x-pack/plugins/alerting/server/routes/legacy/get_alert_instance_summary.ts @@ -10,7 +10,7 @@ import { UsageCounter } from 'src/plugins/usage_collection/server'; import type { AlertingRouter } from '../../types'; import { ILicenseState } from '../../lib/license_state'; import { verifyApiAccess } from '../../lib/license_api_access'; -import { LEGACY_BASE_ALERT_API_PATH } from '../../../common'; +import { AlertSummary, LEGACY_BASE_ALERT_API_PATH } from '../../../common'; import { trackLegacyRouteUsage } from '../../lib/track_legacy_route_usage'; const paramSchema = schema.object({ @@ -21,6 +21,12 @@ const querySchema = schema.object({ dateStart: schema.maybe(schema.string()), }); +const rewriteBodyRes = ({ ruleTypeId, alerts, ...rest }: AlertSummary) => ({ + ...rest, + alertTypeId: ruleTypeId, + instances: alerts, +}); + export const getAlertInstanceSummaryRoute = ( router: AlertingRouter, licenseState: ILicenseState, @@ -43,8 +49,9 @@ export const getAlertInstanceSummaryRoute = ( const rulesClient = context.alerting.getRulesClient(); const { id } = req.params; const { dateStart } = req.query; - const summary = await rulesClient.getAlertInstanceSummary({ id, dateStart }); - return res.ok({ body: summary }); + const summary = await rulesClient.getAlertSummary({ id, dateStart }); + + return res.ok({ body: rewriteBodyRes(summary) }); }) ); }; diff --git a/x-pack/plugins/alerting/server/routes/legacy/health.ts b/x-pack/plugins/alerting/server/routes/legacy/health.ts index 8c654f103ea86..abea724b63c6f 100644 --- a/x-pack/plugins/alerting/server/routes/legacy/health.ts +++ b/x-pack/plugins/alerting/server/routes/legacy/health.ts @@ -12,6 +12,7 @@ import { verifyApiAccess } from '../../lib/license_api_access'; import { AlertingFrameworkHealth } from '../../types'; import { EncryptedSavedObjectsPluginSetup } from '../../../../encrypted_saved_objects/server'; import { trackLegacyRouteUsage } from '../../lib/track_legacy_route_usage'; +import { getSecurityHealth } from '../../lib/get_security_health'; export function healthRoute( router: AlertingRouter, @@ -31,22 +32,16 @@ export function healthRoute( } trackLegacyRouteUsage('health', usageCounter); try { - const isEsSecurityEnabled: boolean | null = licenseState.getIsSecurityEnabled(); const alertingFrameworkHeath = await context.alerting.getFrameworkHealth(); - const areApiKeysEnabled = await context.alerting.areApiKeysEnabled(); - let isSufficientlySecure; - if (isEsSecurityEnabled === null) { - isSufficientlySecure = false; - } else { - // if isEsSecurityEnabled = true, then areApiKeysEnabled must be true to enable alerting - // if isEsSecurityEnabled = false, then it does not matter what areApiKeysEnabled is - isSufficientlySecure = !isEsSecurityEnabled || (isEsSecurityEnabled && areApiKeysEnabled); - } + const securityHealth = await getSecurityHealth( + async () => (licenseState ? licenseState.getIsSecurityEnabled() : null), + async () => encryptedSavedObjects.canEncrypt, + context.alerting.areApiKeysEnabled + ); const frameworkHealth: AlertingFrameworkHealth = { - isSufficientlySecure, - hasPermanentEncryptionKey: encryptedSavedObjects.canEncrypt, + ...securityHealth, alertingFrameworkHeath, }; diff --git a/x-pack/plugins/alerting/server/rules_client.mock.ts b/x-pack/plugins/alerting/server/rules_client.mock.ts index 438331a1cd580..2395e7f041846 100644 --- a/x-pack/plugins/alerting/server/rules_client.mock.ts +++ b/x-pack/plugins/alerting/server/rules_client.mock.ts @@ -29,7 +29,7 @@ const createRulesClientMock = () => { muteInstance: jest.fn(), unmuteInstance: jest.fn(), listAlertTypes: jest.fn(), - getAlertInstanceSummary: jest.fn(), + getAlertSummary: jest.fn(), getSpaceId: jest.fn(), }; return mocked; diff --git a/x-pack/plugins/alerting/server/rules_client/rules_client.ts b/x-pack/plugins/alerting/server/rules_client/rules_client.ts index 75e56afcfd9bf..e10af37e0936b 100644 --- a/x-pack/plugins/alerting/server/rules_client/rules_client.ts +++ b/x-pack/plugins/alerting/server/rules_client/rules_client.ts @@ -30,7 +30,7 @@ import { IntervalSchedule, SanitizedAlert, AlertTaskState, - AlertInstanceSummary, + AlertSummary, AlertExecutionStatusValues, AlertNotifyWhenType, AlertTypeParams, @@ -68,7 +68,7 @@ import { SAVED_OBJECT_REL_PRIMARY, } from '../../../event_log/server'; import { parseIsoOrRelativeDate } from '../lib/iso_or_relative_date'; -import { alertInstanceSummaryFromEventLog } from '../lib/alert_instance_summary_from_event_log'; +import { alertSummaryFromEventLog } from '../lib/alert_summary_from_event_log'; import { AuditLogger } from '../../../security/server'; import { parseDuration } from '../../common/parse_duration'; import { retryIfConflicts } from '../lib/retry_if_conflicts'; @@ -193,7 +193,7 @@ export interface UpdateOptions { }; } -export interface GetAlertInstanceSummaryParams { +export interface GetAlertSummaryParams { id: string; dateStart?: string; } @@ -377,10 +377,11 @@ export class RulesClient { if (data.enabled) { let scheduledTask; try { - scheduledTask = await this.scheduleAlert( + scheduledTask = await this.scheduleRule( createdAlert.id, rawAlert.alertTypeId, - data.schedule + data.schedule, + true ); } catch (e) { // Cleanup data, something went wrong scheduling the task @@ -508,29 +509,26 @@ export class RulesClient { } } - public async getAlertInstanceSummary({ - id, - dateStart, - }: GetAlertInstanceSummaryParams): Promise { - this.logger.debug(`getAlertInstanceSummary(): getting alert ${id}`); - const alert = (await this.get({ id, includeLegacyId: true })) as SanitizedAlertWithLegacyId; + public async getAlertSummary({ id, dateStart }: GetAlertSummaryParams): Promise { + this.logger.debug(`getAlertSummary(): getting alert ${id}`); + const rule = (await this.get({ id, includeLegacyId: true })) as SanitizedAlertWithLegacyId; await this.authorization.ensureAuthorized({ - ruleTypeId: alert.alertTypeId, - consumer: alert.consumer, + ruleTypeId: rule.alertTypeId, + consumer: rule.consumer, operation: ReadOperations.GetAlertSummary, entity: AlertingAuthorizationEntity.Rule, }); - // default duration of instance summary is 60 * alert interval + // default duration of instance summary is 60 * rule interval const dateNow = new Date(); - const durationMillis = parseDuration(alert.schedule.interval) * 60; + const durationMillis = parseDuration(rule.schedule.interval) * 60; const defaultDateStart = new Date(dateNow.valueOf() - durationMillis); const parsedDateStart = parseDate(dateStart, 'dateStart', defaultDateStart); const eventLogClient = await this.getEventLogClient(); - this.logger.debug(`getAlertInstanceSummary(): search the event log for alert ${id}`); + this.logger.debug(`getAlertSummary(): search the event log for rule ${id}`); let events: IEvent[]; try { const queryResults = await eventLogClient.findEventsBySavedObjectIds( @@ -543,18 +541,18 @@ export class RulesClient { end: dateNow.toISOString(), sort_order: 'desc', }, - alert.legacyId !== null ? [alert.legacyId] : undefined + rule.legacyId !== null ? [rule.legacyId] : undefined ); events = queryResults.data; } catch (err) { this.logger.debug( - `rulesClient.getAlertInstanceSummary(): error searching event log for alert ${id}: ${err.message}` + `rulesClient.getAlertSummary(): error searching event log for rule ${id}: ${err.message}` ); events = []; } - return alertInstanceSummaryFromEventLog({ - alert, + return alertSummaryFromEventLog({ + rule, events, dateStart: parsedDateStart.toISOString(), dateEnd: dateNow.toISOString(), @@ -1152,10 +1150,11 @@ export class RulesClient { ); throw e; } - const scheduledTask = await this.scheduleAlert( + const scheduledTask = await this.scheduleRule( id, attributes.alertTypeId, - attributes.schedule as IntervalSchedule + attributes.schedule as IntervalSchedule, + false ); await this.unsecuredSavedObjectsClient.update('alert', id, { scheduledTaskId: scheduledTask.id, @@ -1566,9 +1565,15 @@ export class RulesClient { return this.spaceId; } - private async scheduleAlert(id: string, alertTypeId: string, schedule: IntervalSchedule) { - return await this.taskManager.schedule({ - taskType: `alerting:${alertTypeId}`, + private async scheduleRule( + id: string, + ruleTypeId: string, + schedule: IntervalSchedule, + throwOnConflict: boolean // whether to throw conflict errors or swallow them + ) { + const taskInstance = { + id, // use the same ID for task document as the rule + taskType: `alerting:${ruleTypeId}`, schedule, params: { alertId: id, @@ -1580,7 +1585,15 @@ export class RulesClient { alertInstances: {}, }, scope: ['alerting'], - }); + }; + try { + return await this.taskManager.schedule(taskInstance); + } catch (err) { + if (err.statusCode === 409 && !throwOnConflict) { + return taskInstance; + } + throw err; + } } private injectReferencesIntoActions( diff --git a/x-pack/plugins/alerting/server/rules_client/tests/create.test.ts b/x-pack/plugins/alerting/server/rules_client/tests/create.test.ts index fc8f272702e0d..aa8ecfd73bb61 100644 --- a/x-pack/plugins/alerting/server/rules_client/tests/create.test.ts +++ b/x-pack/plugins/alerting/server/rules_client/tests/create.test.ts @@ -441,6 +441,7 @@ describe('create()', () => { expect(taskManager.schedule.mock.calls[0]).toMatchInlineSnapshot(` Array [ Object { + "id": "1", "params": Object { "alertId": "1", "spaceId": "default", @@ -1923,6 +1924,52 @@ describe('create()', () => { }); }); + test('fails if task scheduling fails due to conflict', async () => { + const data = getMockData(); + unsecuredSavedObjectsClient.create.mockResolvedValueOnce({ + id: '1', + type: 'alert', + attributes: { + alertTypeId: '123', + schedule: { interval: '10s' }, + params: { + bar: true, + }, + actions: [ + { + group: 'default', + actionRef: 'action_0', + actionTypeId: 'test', + params: { + foo: true, + }, + }, + ], + }, + references: [ + { + name: 'action_0', + type: 'action', + id: '1', + }, + ], + }); + taskManager.schedule.mockRejectedValueOnce( + Object.assign(new Error('Conflict!'), { statusCode: 409 }) + ); + unsecuredSavedObjectsClient.delete.mockResolvedValueOnce({}); + await expect(rulesClient.create({ data })).rejects.toThrowErrorMatchingInlineSnapshot( + `"Conflict!"` + ); + expect(unsecuredSavedObjectsClient.delete).toHaveBeenCalledTimes(1); + expect(unsecuredSavedObjectsClient.delete.mock.calls[0]).toMatchInlineSnapshot(` + Array [ + "alert", + "1", + ] + `); + }); + test('attempts to remove saved object if scheduling failed', async () => { const data = getMockData(); unsecuredSavedObjectsClient.create.mockResolvedValueOnce({ diff --git a/x-pack/plugins/alerting/server/rules_client/tests/enable.test.ts b/x-pack/plugins/alerting/server/rules_client/tests/enable.test.ts index 5e3f148c2fc11..afa7db98cab08 100644 --- a/x-pack/plugins/alerting/server/rules_client/tests/enable.test.ts +++ b/x-pack/plugins/alerting/server/rules_client/tests/enable.test.ts @@ -98,7 +98,7 @@ describe('enable()', () => { }, }); taskManager.schedule.mockResolvedValue({ - id: 'task-123', + id: '1', scheduledAt: new Date(), attempts: 0, status: TaskStatus.Idle, @@ -113,27 +113,6 @@ describe('enable()', () => { }); describe('authorization', () => { - beforeEach(() => { - encryptedSavedObjects.getDecryptedAsInternalUser.mockResolvedValue(existingAlert); - unsecuredSavedObjectsClient.get.mockResolvedValue(existingAlert); - rulesClientParams.createAPIKey.mockResolvedValue({ - apiKeysEnabled: false, - }); - taskManager.schedule.mockResolvedValue({ - id: 'task-123', - scheduledAt: new Date(), - attempts: 0, - status: TaskStatus.Idle, - runAt: new Date(), - state: {}, - params: {}, - taskType: '', - startedAt: null, - retryAt: null, - ownerId: null, - }); - }); - test('ensures user is authorised to enable this type of alert under the consumer', async () => { await rulesClient.enable({ id: '1' }); @@ -203,7 +182,7 @@ describe('enable()', () => { }); }); - test('enables an alert', async () => { + test('enables a rule', async () => { const createdAt = new Date().toISOString(); unsecuredSavedObjectsClient.create.mockResolvedValueOnce({ ...existingAlert, @@ -270,6 +249,7 @@ describe('enable()', () => { } ); expect(taskManager.schedule).toHaveBeenCalledWith({ + id: '1', taskType: `alerting:myType`, params: { alertId: '1', @@ -286,7 +266,7 @@ describe('enable()', () => { scope: ['alerting'], }); expect(unsecuredSavedObjectsClient.update).toHaveBeenCalledWith('alert', '1', { - scheduledTaskId: 'task-123', + scheduledTaskId: '1', }); }); @@ -477,4 +457,95 @@ describe('enable()', () => { expect(rulesClientParams.createAPIKey).toHaveBeenCalled(); expect(unsecuredSavedObjectsClient.update).toHaveBeenCalled(); }); + + test('enables a rule if conflict errors received when scheduling a task', async () => { + const createdAt = new Date().toISOString(); + unsecuredSavedObjectsClient.create.mockResolvedValueOnce({ + ...existingAlert, + attributes: { + ...existingAlert.attributes, + enabled: true, + apiKey: null, + apiKeyOwner: null, + updatedBy: 'elastic', + }, + }); + unsecuredSavedObjectsClient.create.mockResolvedValueOnce({ + id: '1', + type: 'api_key_pending_invalidation', + attributes: { + apiKeyId: '123', + createdAt, + }, + references: [], + }); + taskManager.schedule.mockRejectedValueOnce( + Object.assign(new Error('Conflict!'), { statusCode: 409 }) + ); + + await rulesClient.enable({ id: '1' }); + expect(unsecuredSavedObjectsClient.get).not.toHaveBeenCalled(); + expect(encryptedSavedObjects.getDecryptedAsInternalUser).toHaveBeenCalledWith('alert', '1', { + namespace: 'default', + }); + expect(unsecuredSavedObjectsClient.create).not.toBeCalledWith('api_key_pending_invalidation'); + expect(rulesClientParams.createAPIKey).toHaveBeenCalled(); + expect(unsecuredSavedObjectsClient.update).toHaveBeenCalledWith( + 'alert', + '1', + { + schedule: { interval: '10s' }, + alertTypeId: 'myType', + consumer: 'myApp', + enabled: true, + meta: { + versionApiKeyLastmodified: kibanaVersion, + }, + updatedAt: '2019-02-12T21:01:22.479Z', + updatedBy: 'elastic', + apiKey: null, + apiKeyOwner: null, + actions: [ + { + group: 'default', + id: '1', + actionTypeId: '1', + actionRef: '1', + params: { + foo: true, + }, + }, + ], + executionStatus: { + status: 'pending', + lastDuration: 0, + lastExecutionDate: '2019-02-12T21:01:22.479Z', + error: null, + }, + }, + { + version: '123', + } + ); + expect(taskManager.schedule).toHaveBeenCalledWith({ + id: '1', + taskType: `alerting:myType`, + params: { + alertId: '1', + spaceId: 'default', + }, + schedule: { + interval: '10s', + }, + state: { + alertInstances: {}, + alertTypeState: {}, + previousStartedAt: null, + }, + scope: ['alerting'], + }); + expect(unsecuredSavedObjectsClient.update).toHaveBeenCalledWith('alert', '1', { + scheduledTaskId: '1', + }); + }); }); diff --git a/x-pack/plugins/alerting/server/rules_client/tests/get_alert_instance_summary.test.ts b/x-pack/plugins/alerting/server/rules_client/tests/get_alert_instance_summary.test.ts deleted file mode 100644 index bd88b0f53ab07..0000000000000 --- a/x-pack/plugins/alerting/server/rules_client/tests/get_alert_instance_summary.test.ts +++ /dev/null @@ -1,346 +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 { omit, mean } from 'lodash'; -import { RulesClient, ConstructorOptions } from '../rules_client'; -import { savedObjectsClientMock, loggingSystemMock } from '../../../../../../src/core/server/mocks'; -import { taskManagerMock } from '../../../../task_manager/server/mocks'; -import { ruleTypeRegistryMock } from '../../rule_type_registry.mock'; -import { alertingAuthorizationMock } from '../../authorization/alerting_authorization.mock'; -import { encryptedSavedObjectsMock } from '../../../../encrypted_saved_objects/server/mocks'; -import { actionsAuthorizationMock } from '../../../../actions/server/mocks'; -import { AlertingAuthorization } from '../../authorization/alerting_authorization'; -import { ActionsAuthorization } from '../../../../actions/server'; -import { eventLogClientMock } from '../../../../event_log/server/mocks'; -import { QueryEventsBySavedObjectResult } from '../../../../event_log/server'; -import { SavedObject } from 'kibana/server'; -import { EventsFactory } from '../../lib/alert_instance_summary_from_event_log.test'; -import { RawAlert } from '../../types'; -import { getBeforeSetup, mockedDateString, setGlobalDate } from './lib'; - -const taskManager = taskManagerMock.createStart(); -const ruleTypeRegistry = ruleTypeRegistryMock.create(); -const unsecuredSavedObjectsClient = savedObjectsClientMock.create(); -const eventLogClient = eventLogClientMock.create(); - -const encryptedSavedObjects = encryptedSavedObjectsMock.createClient(); -const authorization = alertingAuthorizationMock.create(); -const actionsAuthorization = actionsAuthorizationMock.create(); - -const kibanaVersion = 'v7.10.0'; -const rulesClientParams: jest.Mocked = { - taskManager, - ruleTypeRegistry, - unsecuredSavedObjectsClient, - authorization: authorization as unknown as AlertingAuthorization, - actionsAuthorization: actionsAuthorization as unknown as ActionsAuthorization, - spaceId: 'default', - namespace: 'default', - getUserName: jest.fn(), - createAPIKey: jest.fn(), - logger: loggingSystemMock.create().get(), - encryptedSavedObjectsClient: encryptedSavedObjects, - getActionsClient: jest.fn(), - getEventLogClient: jest.fn(), - kibanaVersion, -}; - -beforeEach(() => { - getBeforeSetup(rulesClientParams, taskManager, ruleTypeRegistry, eventLogClient); -}); - -setGlobalDate(); - -const AlertInstanceSummaryFindEventsResult: QueryEventsBySavedObjectResult = { - page: 1, - per_page: 10000, - total: 0, - data: [], -}; - -const AlertInstanceSummaryIntervalSeconds = 1; - -const BaseAlertInstanceSummarySavedObject: SavedObject = { - id: '1', - type: 'alert', - attributes: { - enabled: true, - name: 'alert-name', - tags: ['tag-1', 'tag-2'], - alertTypeId: '123', - consumer: 'alert-consumer', - legacyId: null, - schedule: { interval: `${AlertInstanceSummaryIntervalSeconds}s` }, - actions: [], - params: {}, - createdBy: null, - updatedBy: null, - createdAt: mockedDateString, - updatedAt: mockedDateString, - apiKey: null, - apiKeyOwner: null, - throttle: null, - notifyWhen: null, - muteAll: false, - mutedInstanceIds: [], - executionStatus: { - status: 'unknown', - lastExecutionDate: '2020-08-20T19:23:38Z', - error: null, - }, - }, - references: [], -}; - -function getAlertInstanceSummarySavedObject( - attributes: Partial = {} -): SavedObject { - return { - ...BaseAlertInstanceSummarySavedObject, - attributes: { ...BaseAlertInstanceSummarySavedObject.attributes, ...attributes }, - }; -} - -describe('getAlertInstanceSummary()', () => { - let rulesClient: RulesClient; - - beforeEach(() => { - rulesClient = new RulesClient(rulesClientParams); - }); - - test('runs as expected with some event log data', async () => { - const alertSO = getAlertInstanceSummarySavedObject({ - mutedInstanceIds: ['instance-muted-no-activity'], - }); - unsecuredSavedObjectsClient.get.mockResolvedValueOnce(alertSO); - - const eventsFactory = new EventsFactory(mockedDateString); - const events = eventsFactory - .addExecute() - .addNewInstance('instance-currently-active') - .addNewInstance('instance-previously-active') - .addActiveInstance('instance-currently-active', 'action group A') - .addActiveInstance('instance-previously-active', 'action group B') - .advanceTime(10000) - .addExecute() - .addRecoveredInstance('instance-previously-active') - .addActiveInstance('instance-currently-active', 'action group A') - .getEvents(); - const eventsResult = { - ...AlertInstanceSummaryFindEventsResult, - total: events.length, - data: events, - }; - eventLogClient.findEventsBySavedObjectIds.mockResolvedValueOnce(eventsResult); - - const dateStart = new Date(Date.now() - 60 * 1000).toISOString(); - - const durations: number[] = eventsFactory.getExecutionDurations(); - - const result = await rulesClient.getAlertInstanceSummary({ id: '1', dateStart }); - const resultWithoutExecutionDuration = omit(result, 'executionDuration'); - expect(resultWithoutExecutionDuration).toMatchInlineSnapshot(` - Object { - "alertTypeId": "123", - "consumer": "alert-consumer", - "enabled": true, - "errorMessages": Array [], - "id": "1", - "instances": Object { - "instance-currently-active": Object { - "actionGroupId": "action group A", - "actionSubgroup": undefined, - "activeStartDate": "2019-02-12T21:01:22.479Z", - "muted": false, - "status": "Active", - }, - "instance-muted-no-activity": Object { - "actionGroupId": undefined, - "actionSubgroup": undefined, - "activeStartDate": undefined, - "muted": true, - "status": "OK", - }, - "instance-previously-active": Object { - "actionGroupId": undefined, - "actionSubgroup": undefined, - "activeStartDate": undefined, - "muted": false, - "status": "OK", - }, - }, - "lastRun": "2019-02-12T21:01:32.479Z", - "muteAll": false, - "name": "alert-name", - "status": "Active", - "statusEndDate": "2019-02-12T21:01:22.479Z", - "statusStartDate": "2019-02-12T21:00:22.479Z", - "tags": Array [ - "tag-1", - "tag-2", - ], - "throttle": null, - } - `); - - expect(result.executionDuration).toEqual({ - average: Math.round(mean(durations)), - values: durations, - }); - }); - - // Further tests don't check the result of `getAlertInstanceSummary()`, as the result - // is just the result from the `alertInstanceSummaryFromEventLog()`, which itself - // has a complete set of tests. These tests just make sure the data gets - // sent into `getAlertInstanceSummary()` as appropriate. - - test('calls saved objects and event log client with default params', async () => { - unsecuredSavedObjectsClient.get.mockResolvedValueOnce(getAlertInstanceSummarySavedObject()); - eventLogClient.findEventsBySavedObjectIds.mockResolvedValueOnce( - AlertInstanceSummaryFindEventsResult - ); - - await rulesClient.getAlertInstanceSummary({ id: '1' }); - - expect(unsecuredSavedObjectsClient.get).toHaveBeenCalledTimes(1); - expect(eventLogClient.findEventsBySavedObjectIds).toHaveBeenCalledTimes(1); - expect(eventLogClient.findEventsBySavedObjectIds.mock.calls[0]).toMatchInlineSnapshot(` - Array [ - "alert", - Array [ - "1", - ], - Object { - "end": "2019-02-12T21:01:22.479Z", - "page": 1, - "per_page": 10000, - "sort_order": "desc", - "start": "2019-02-12T21:00:22.479Z", - }, - undefined, - ] - `); - // calculate the expected start/end date for one test - const { start, end } = eventLogClient.findEventsBySavedObjectIds.mock.calls[0][2]!; - expect(end).toBe(mockedDateString); - - const startMillis = Date.parse(start!); - const endMillis = Date.parse(end!); - const expectedDuration = 60 * AlertInstanceSummaryIntervalSeconds * 1000; - expect(endMillis - startMillis).toBeGreaterThan(expectedDuration - 2); - expect(endMillis - startMillis).toBeLessThan(expectedDuration + 2); - }); - - test('calls event log client with legacy ids param', async () => { - unsecuredSavedObjectsClient.get.mockResolvedValueOnce( - getAlertInstanceSummarySavedObject({ legacyId: '99999' }) - ); - eventLogClient.findEventsBySavedObjectIds.mockResolvedValueOnce( - AlertInstanceSummaryFindEventsResult - ); - - await rulesClient.getAlertInstanceSummary({ id: '1' }); - - expect(unsecuredSavedObjectsClient.get).toHaveBeenCalledTimes(1); - expect(eventLogClient.findEventsBySavedObjectIds).toHaveBeenCalledTimes(1); - expect(eventLogClient.findEventsBySavedObjectIds.mock.calls[0]).toMatchInlineSnapshot(` - Array [ - "alert", - Array [ - "1", - ], - Object { - "end": "2019-02-12T21:01:22.479Z", - "page": 1, - "per_page": 10000, - "sort_order": "desc", - "start": "2019-02-12T21:00:22.479Z", - }, - Array [ - "99999", - ], - ] - `); - }); - - test('calls event log client with start date', async () => { - unsecuredSavedObjectsClient.get.mockResolvedValueOnce(getAlertInstanceSummarySavedObject()); - eventLogClient.findEventsBySavedObjectIds.mockResolvedValueOnce( - AlertInstanceSummaryFindEventsResult - ); - - const dateStart = new Date( - Date.now() - 60 * AlertInstanceSummaryIntervalSeconds * 1000 - ).toISOString(); - await rulesClient.getAlertInstanceSummary({ id: '1', dateStart }); - - expect(unsecuredSavedObjectsClient.get).toHaveBeenCalledTimes(1); - expect(eventLogClient.findEventsBySavedObjectIds).toHaveBeenCalledTimes(1); - const { start, end } = eventLogClient.findEventsBySavedObjectIds.mock.calls[0][2]!; - - expect({ start, end }).toMatchInlineSnapshot(` - Object { - "end": "2019-02-12T21:01:22.479Z", - "start": "2019-02-12T21:00:22.479Z", - } - `); - }); - - test('calls event log client with relative start date', async () => { - unsecuredSavedObjectsClient.get.mockResolvedValueOnce(getAlertInstanceSummarySavedObject()); - eventLogClient.findEventsBySavedObjectIds.mockResolvedValueOnce( - AlertInstanceSummaryFindEventsResult - ); - - const dateStart = '2m'; - await rulesClient.getAlertInstanceSummary({ id: '1', dateStart }); - - expect(unsecuredSavedObjectsClient.get).toHaveBeenCalledTimes(1); - expect(eventLogClient.findEventsBySavedObjectIds).toHaveBeenCalledTimes(1); - const { start, end } = eventLogClient.findEventsBySavedObjectIds.mock.calls[0][2]!; - - expect({ start, end }).toMatchInlineSnapshot(` - Object { - "end": "2019-02-12T21:01:22.479Z", - "start": "2019-02-12T20:59:22.479Z", - } - `); - }); - - test('invalid start date throws an error', async () => { - unsecuredSavedObjectsClient.get.mockResolvedValueOnce(getAlertInstanceSummarySavedObject()); - eventLogClient.findEventsBySavedObjectIds.mockResolvedValueOnce( - AlertInstanceSummaryFindEventsResult - ); - - const dateStart = 'ain"t no way this will get parsed as a date'; - expect( - rulesClient.getAlertInstanceSummary({ id: '1', dateStart }) - ).rejects.toMatchInlineSnapshot( - `[Error: Invalid date for parameter dateStart: "ain"t no way this will get parsed as a date"]` - ); - }); - - test('saved object get throws an error', async () => { - unsecuredSavedObjectsClient.get.mockRejectedValueOnce(new Error('OMG!')); - eventLogClient.findEventsBySavedObjectIds.mockResolvedValueOnce( - AlertInstanceSummaryFindEventsResult - ); - - expect(rulesClient.getAlertInstanceSummary({ id: '1' })).rejects.toMatchInlineSnapshot( - `[Error: OMG!]` - ); - }); - - test('findEvents throws an error', async () => { - unsecuredSavedObjectsClient.get.mockResolvedValueOnce(getAlertInstanceSummarySavedObject()); - eventLogClient.findEventsBySavedObjectIds.mockRejectedValueOnce(new Error('OMG 2!')); - - // error eaten but logged - await rulesClient.getAlertInstanceSummary({ id: '1' }); - }); -}); diff --git a/x-pack/plugins/alerting/server/rules_client/tests/get_alert_summary.test.ts b/x-pack/plugins/alerting/server/rules_client/tests/get_alert_summary.test.ts new file mode 100644 index 0000000000000..9e34d2e027987 --- /dev/null +++ b/x-pack/plugins/alerting/server/rules_client/tests/get_alert_summary.test.ts @@ -0,0 +1,326 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { omit, mean } from 'lodash'; +import { RulesClient, ConstructorOptions } from '../rules_client'; +import { savedObjectsClientMock, loggingSystemMock } from '../../../../../../src/core/server/mocks'; +import { taskManagerMock } from '../../../../task_manager/server/mocks'; +import { ruleTypeRegistryMock } from '../../rule_type_registry.mock'; +import { alertingAuthorizationMock } from '../../authorization/alerting_authorization.mock'; +import { encryptedSavedObjectsMock } from '../../../../encrypted_saved_objects/server/mocks'; +import { actionsAuthorizationMock } from '../../../../actions/server/mocks'; +import { AlertingAuthorization } from '../../authorization/alerting_authorization'; +import { ActionsAuthorization } from '../../../../actions/server'; +import { eventLogClientMock } from '../../../../event_log/server/mocks'; +import { QueryEventsBySavedObjectResult } from '../../../../event_log/server'; +import { SavedObject } from 'kibana/server'; +import { EventsFactory } from '../../lib/alert_summary_from_event_log.test'; +import { RawAlert } from '../../types'; +import { getBeforeSetup, mockedDateString, setGlobalDate } from './lib'; + +const taskManager = taskManagerMock.createStart(); +const ruleTypeRegistry = ruleTypeRegistryMock.create(); +const unsecuredSavedObjectsClient = savedObjectsClientMock.create(); +const eventLogClient = eventLogClientMock.create(); + +const encryptedSavedObjects = encryptedSavedObjectsMock.createClient(); +const authorization = alertingAuthorizationMock.create(); +const actionsAuthorization = actionsAuthorizationMock.create(); + +const kibanaVersion = 'v7.10.0'; +const rulesClientParams: jest.Mocked = { + taskManager, + ruleTypeRegistry, + unsecuredSavedObjectsClient, + authorization: authorization as unknown as AlertingAuthorization, + actionsAuthorization: actionsAuthorization as unknown as ActionsAuthorization, + spaceId: 'default', + namespace: 'default', + getUserName: jest.fn(), + createAPIKey: jest.fn(), + logger: loggingSystemMock.create().get(), + encryptedSavedObjectsClient: encryptedSavedObjects, + getActionsClient: jest.fn(), + getEventLogClient: jest.fn(), + kibanaVersion, +}; + +beforeEach(() => { + getBeforeSetup(rulesClientParams, taskManager, ruleTypeRegistry, eventLogClient); +}); + +setGlobalDate(); + +const AlertSummaryFindEventsResult: QueryEventsBySavedObjectResult = { + page: 1, + per_page: 10000, + total: 0, + data: [], +}; + +const RuleIntervalSeconds = 1; + +const BaseRuleSavedObject: SavedObject = { + id: '1', + type: 'alert', + attributes: { + enabled: true, + name: 'rule-name', + tags: ['tag-1', 'tag-2'], + alertTypeId: '123', + consumer: 'rule-consumer', + legacyId: null, + schedule: { interval: `${RuleIntervalSeconds}s` }, + actions: [], + params: {}, + createdBy: null, + updatedBy: null, + createdAt: mockedDateString, + updatedAt: mockedDateString, + apiKey: null, + apiKeyOwner: null, + throttle: null, + notifyWhen: null, + muteAll: false, + mutedInstanceIds: [], + executionStatus: { + status: 'unknown', + lastExecutionDate: '2020-08-20T19:23:38Z', + error: null, + }, + }, + references: [], +}; + +function getRuleSavedObject(attributes: Partial = {}): SavedObject { + return { + ...BaseRuleSavedObject, + attributes: { ...BaseRuleSavedObject.attributes, ...attributes }, + }; +} + +describe('getAlertSummary()', () => { + let rulesClient: RulesClient; + + beforeEach(() => { + rulesClient = new RulesClient(rulesClientParams); + }); + + test('runs as expected with some event log data', async () => { + const ruleSO = getRuleSavedObject({ + mutedInstanceIds: ['alert-muted-no-activity'], + }); + unsecuredSavedObjectsClient.get.mockResolvedValueOnce(ruleSO); + + const eventsFactory = new EventsFactory(mockedDateString); + const events = eventsFactory + .addExecute() + .addNewAlert('alert-currently-active') + .addNewAlert('alert-previously-active') + .addActiveAlert('alert-currently-active', 'action group A') + .addActiveAlert('alert-previously-active', 'action group B') + .advanceTime(10000) + .addExecute() + .addRecoveredAlert('alert-previously-active') + .addActiveAlert('alert-currently-active', 'action group A') + .getEvents(); + const eventsResult = { + ...AlertSummaryFindEventsResult, + total: events.length, + data: events, + }; + eventLogClient.findEventsBySavedObjectIds.mockResolvedValueOnce(eventsResult); + + const dateStart = new Date(Date.now() - 60 * 1000).toISOString(); + + const durations: number[] = eventsFactory.getExecutionDurations(); + + const result = await rulesClient.getAlertSummary({ id: '1', dateStart }); + const resultWithoutExecutionDuration = omit(result, 'executionDuration'); + expect(resultWithoutExecutionDuration).toMatchInlineSnapshot(` + Object { + "alerts": Object { + "alert-currently-active": Object { + "actionGroupId": "action group A", + "actionSubgroup": undefined, + "activeStartDate": "2019-02-12T21:01:22.479Z", + "muted": false, + "status": "Active", + }, + "alert-muted-no-activity": Object { + "actionGroupId": undefined, + "actionSubgroup": undefined, + "activeStartDate": undefined, + "muted": true, + "status": "OK", + }, + "alert-previously-active": Object { + "actionGroupId": undefined, + "actionSubgroup": undefined, + "activeStartDate": undefined, + "muted": false, + "status": "OK", + }, + }, + "consumer": "rule-consumer", + "enabled": true, + "errorMessages": Array [], + "id": "1", + "lastRun": "2019-02-12T21:01:32.479Z", + "muteAll": false, + "name": "rule-name", + "ruleTypeId": "123", + "status": "Active", + "statusEndDate": "2019-02-12T21:01:22.479Z", + "statusStartDate": "2019-02-12T21:00:22.479Z", + "tags": Array [ + "tag-1", + "tag-2", + ], + "throttle": null, + } + `); + + expect(result.executionDuration).toEqual({ + average: Math.round(mean(durations)), + values: durations, + }); + }); + + // Further tests don't check the result of `getAlertSummary()`, as the result + // is just the result from the `alertSummaryFromEventLog()`, which itself + // has a complete set of tests. These tests just make sure the data gets + // sent into `getAlertSummary()` as appropriate. + + test('calls saved objects and event log client with default params', async () => { + unsecuredSavedObjectsClient.get.mockResolvedValueOnce(getRuleSavedObject()); + eventLogClient.findEventsBySavedObjectIds.mockResolvedValueOnce(AlertSummaryFindEventsResult); + + await rulesClient.getAlertSummary({ id: '1' }); + + expect(unsecuredSavedObjectsClient.get).toHaveBeenCalledTimes(1); + expect(eventLogClient.findEventsBySavedObjectIds).toHaveBeenCalledTimes(1); + expect(eventLogClient.findEventsBySavedObjectIds.mock.calls[0]).toMatchInlineSnapshot(` + Array [ + "alert", + Array [ + "1", + ], + Object { + "end": "2019-02-12T21:01:22.479Z", + "page": 1, + "per_page": 10000, + "sort_order": "desc", + "start": "2019-02-12T21:00:22.479Z", + }, + undefined, + ] + `); + // calculate the expected start/end date for one test + const { start, end } = eventLogClient.findEventsBySavedObjectIds.mock.calls[0][2]!; + expect(end).toBe(mockedDateString); + + const startMillis = Date.parse(start!); + const endMillis = Date.parse(end!); + const expectedDuration = 60 * RuleIntervalSeconds * 1000; + expect(endMillis - startMillis).toBeGreaterThan(expectedDuration - 2); + expect(endMillis - startMillis).toBeLessThan(expectedDuration + 2); + }); + + test('calls event log client with legacy ids param', async () => { + unsecuredSavedObjectsClient.get.mockResolvedValueOnce( + getRuleSavedObject({ legacyId: '99999' }) + ); + eventLogClient.findEventsBySavedObjectIds.mockResolvedValueOnce(AlertSummaryFindEventsResult); + + await rulesClient.getAlertSummary({ id: '1' }); + + expect(unsecuredSavedObjectsClient.get).toHaveBeenCalledTimes(1); + expect(eventLogClient.findEventsBySavedObjectIds).toHaveBeenCalledTimes(1); + expect(eventLogClient.findEventsBySavedObjectIds.mock.calls[0]).toMatchInlineSnapshot(` + Array [ + "alert", + Array [ + "1", + ], + Object { + "end": "2019-02-12T21:01:22.479Z", + "page": 1, + "per_page": 10000, + "sort_order": "desc", + "start": "2019-02-12T21:00:22.479Z", + }, + Array [ + "99999", + ], + ] + `); + }); + + test('calls event log client with start date', async () => { + unsecuredSavedObjectsClient.get.mockResolvedValueOnce(getRuleSavedObject()); + eventLogClient.findEventsBySavedObjectIds.mockResolvedValueOnce(AlertSummaryFindEventsResult); + + const dateStart = new Date(Date.now() - 60 * RuleIntervalSeconds * 1000).toISOString(); + await rulesClient.getAlertSummary({ id: '1', dateStart }); + + expect(unsecuredSavedObjectsClient.get).toHaveBeenCalledTimes(1); + expect(eventLogClient.findEventsBySavedObjectIds).toHaveBeenCalledTimes(1); + const { start, end } = eventLogClient.findEventsBySavedObjectIds.mock.calls[0][2]!; + + expect({ start, end }).toMatchInlineSnapshot(` + Object { + "end": "2019-02-12T21:01:22.479Z", + "start": "2019-02-12T21:00:22.479Z", + } + `); + }); + + test('calls event log client with relative start date', async () => { + unsecuredSavedObjectsClient.get.mockResolvedValueOnce(getRuleSavedObject()); + eventLogClient.findEventsBySavedObjectIds.mockResolvedValueOnce(AlertSummaryFindEventsResult); + + const dateStart = '2m'; + await rulesClient.getAlertSummary({ id: '1', dateStart }); + + expect(unsecuredSavedObjectsClient.get).toHaveBeenCalledTimes(1); + expect(eventLogClient.findEventsBySavedObjectIds).toHaveBeenCalledTimes(1); + const { start, end } = eventLogClient.findEventsBySavedObjectIds.mock.calls[0][2]!; + + expect({ start, end }).toMatchInlineSnapshot(` + Object { + "end": "2019-02-12T21:01:22.479Z", + "start": "2019-02-12T20:59:22.479Z", + } + `); + }); + + test('invalid start date throws an error', async () => { + unsecuredSavedObjectsClient.get.mockResolvedValueOnce(getRuleSavedObject()); + eventLogClient.findEventsBySavedObjectIds.mockResolvedValueOnce(AlertSummaryFindEventsResult); + + const dateStart = 'ain"t no way this will get parsed as a date'; + expect(rulesClient.getAlertSummary({ id: '1', dateStart })).rejects.toMatchInlineSnapshot( + `[Error: Invalid date for parameter dateStart: "ain"t no way this will get parsed as a date"]` + ); + }); + + test('saved object get throws an error', async () => { + unsecuredSavedObjectsClient.get.mockRejectedValueOnce(new Error('OMG!')); + eventLogClient.findEventsBySavedObjectIds.mockResolvedValueOnce(AlertSummaryFindEventsResult); + + expect(rulesClient.getAlertSummary({ id: '1' })).rejects.toMatchInlineSnapshot(`[Error: OMG!]`); + }); + + test('findEvents throws an error', async () => { + unsecuredSavedObjectsClient.get.mockResolvedValueOnce(getRuleSavedObject()); + eventLogClient.findEventsBySavedObjectIds.mockRejectedValueOnce(new Error('OMG 2!')); + + // error eaten but logged + await rulesClient.getAlertSummary({ id: '1' }); + }); +}); diff --git a/x-pack/plugins/alerting/server/usage/alerts_telemetry.test.ts b/x-pack/plugins/alerting/server/usage/alerts_telemetry.test.ts index af08c8c75c144..848c5e9b72168 100644 --- a/x-pack/plugins/alerting/server/usage/alerts_telemetry.test.ts +++ b/x-pack/plugins/alerting/server/usage/alerts_telemetry.test.ts @@ -106,11 +106,21 @@ Object { "count_rules_namespaces": 0, "count_total": 4, "schedule_time": Object { + "avg": "4.5s", + "max": "10s", + "min": "1s", + }, + "schedule_time_number_s": Object { "avg": 4.5, "max": 10, "min": 1, }, "throttle_time": Object { + "avg": "30s", + "max": "60s", + "min": "0s", + }, + "throttle_time_number_s": Object { "avg": 30, "max": 60, "min": 0, diff --git a/x-pack/plugins/alerting/server/usage/alerts_telemetry.ts b/x-pack/plugins/alerting/server/usage/alerts_telemetry.ts index 180ee4300f18c..075404e82e1a9 100644 --- a/x-pack/plugins/alerting/server/usage/alerts_telemetry.ts +++ b/x-pack/plugins/alerting/server/usage/alerts_telemetry.ts @@ -13,7 +13,7 @@ const alertTypeMetric = { init_script: 'state.ruleTypes = [:]; state.namespaces = [:]', map_script: ` String alertType = doc['alert.alertTypeId'].value; - String namespace = doc['namespaces'] !== null ? doc['namespaces'].value : 'default'; + String namespace = doc['namespaces'] !== null && doc['namespaces'].size() > 0 ? doc['namespaces'].value : 'default'; state.ruleTypes.put(alertType, state.ruleTypes.containsKey(alertType) ? state.ruleTypes.get(alertType) + 1 : 1); if (state.namespaces.containsKey(namespace) === false) { state.namespaces.put(namespace, 1); @@ -107,6 +107,8 @@ export async function getTotalCountAggregations( | 'count_by_type' | 'throttle_time' | 'schedule_time' + | 'throttle_time_number_s' + | 'schedule_time_number_s' | 'connectors_per_alert' | 'count_rules_namespaces' > @@ -253,11 +255,21 @@ export async function getTotalCountAggregations( {} ), throttle_time: { + min: `${aggregations.min_throttle_time.value}s`, + avg: `${aggregations.avg_throttle_time.value}s`, + max: `${aggregations.max_throttle_time.value}s`, + }, + schedule_time: { + min: `${aggregations.min_interval_time.value}s`, + avg: `${aggregations.avg_interval_time.value}s`, + max: `${aggregations.max_interval_time.value}s`, + }, + throttle_time_number_s: { min: aggregations.min_throttle_time.value, avg: aggregations.avg_throttle_time.value, max: aggregations.max_throttle_time.value, }, - schedule_time: { + schedule_time_number_s: { min: aggregations.min_interval_time.value, avg: aggregations.avg_interval_time.value, max: aggregations.max_interval_time.value, diff --git a/x-pack/plugins/alerting/server/usage/alerts_usage_collector.ts b/x-pack/plugins/alerting/server/usage/alerts_usage_collector.ts index e5b25ea75fc1c..327073f26bacf 100644 --- a/x-pack/plugins/alerting/server/usage/alerts_usage_collector.ts +++ b/x-pack/plugins/alerting/server/usage/alerts_usage_collector.ts @@ -95,11 +95,21 @@ export function createAlertsUsageCollector( count_active_total: 0, count_disabled_total: 0, throttle_time: { + min: '0s', + avg: '0s', + max: '0s', + }, + schedule_time: { + min: '0s', + avg: '0s', + max: '0s', + }, + throttle_time_number_s: { min: 0, avg: 0, max: 0, }, - schedule_time: { + schedule_time_number_s: { min: 0, avg: 0, max: 0, @@ -127,11 +137,21 @@ export function createAlertsUsageCollector( count_active_total: { type: 'long' }, count_disabled_total: { type: 'long' }, throttle_time: { + min: { type: 'keyword' }, + avg: { type: 'keyword' }, + max: { type: 'keyword' }, + }, + schedule_time: { + min: { type: 'keyword' }, + avg: { type: 'keyword' }, + max: { type: 'keyword' }, + }, + throttle_time_number_s: { min: { type: 'long' }, avg: { type: 'float' }, max: { type: 'long' }, }, - schedule_time: { + schedule_time_number_s: { min: { type: 'long' }, avg: { type: 'float' }, max: { type: 'long' }, diff --git a/x-pack/plugins/alerting/server/usage/types.ts b/x-pack/plugins/alerting/server/usage/types.ts index 50d9b80c44b70..546663e3ea403 100644 --- a/x-pack/plugins/alerting/server/usage/types.ts +++ b/x-pack/plugins/alerting/server/usage/types.ts @@ -20,11 +20,21 @@ export interface AlertsUsage { avg_execution_time_per_day: number; avg_execution_time_by_type_per_day: Record; throttle_time: { + min: string; + avg: string; + max: string; + }; + schedule_time: { + min: string; + avg: string; + max: string; + }; + throttle_time_number_s: { min: number; avg: number; max: number; }; - schedule_time: { + schedule_time_number_s: { min: number; avg: number; max: number; diff --git a/x-pack/plugins/apm/common/anomaly_detection.ts b/x-pack/plugins/apm/common/anomaly_detection.ts deleted file mode 100644 index 43a779407d2a4..0000000000000 --- a/x-pack/plugins/apm/common/anomaly_detection.ts +++ /dev/null @@ -1,69 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { i18n } from '@kbn/i18n'; -import { ANOMALY_SEVERITY } from './ml_constants'; -import { - getSeverityType, - getSeverityColor as mlGetSeverityColor, -} from '../../ml/common'; -import { ServiceHealthStatus } from './service_health_status'; - -export interface ServiceAnomalyStats { - transactionType?: string; - anomalyScore?: number; - actualValue?: number; - jobId?: string; - healthStatus: ServiceHealthStatus; -} - -export function getSeverity(score: number | undefined) { - if (score === undefined) { - return ANOMALY_SEVERITY.UNKNOWN; - } - - return getSeverityType(score); -} - -export function getSeverityColor(score: number) { - return mlGetSeverityColor(score); -} - -export const ML_ERRORS = { - INVALID_LICENSE: i18n.translate( - 'xpack.apm.anomaly_detection.error.invalid_license', - { - defaultMessage: `To use anomaly detection, you must be subscribed to an Elastic Platinum license. With it, you'll be able to monitor your services with the aid of machine learning.`, - } - ), - MISSING_READ_PRIVILEGES: i18n.translate( - 'xpack.apm.anomaly_detection.error.missing_read_privileges', - { - defaultMessage: - 'You must have "read" privileges to Machine Learning and APM in order to view Anomaly Detection jobs', - } - ), - MISSING_WRITE_PRIVILEGES: i18n.translate( - 'xpack.apm.anomaly_detection.error.missing_write_privileges', - { - defaultMessage: - 'You must have "write" privileges to Machine Learning and APM in order to create Anomaly Detection jobs', - } - ), - ML_NOT_AVAILABLE: i18n.translate( - 'xpack.apm.anomaly_detection.error.not_available', - { - defaultMessage: 'Machine learning is not available', - } - ), - ML_NOT_AVAILABLE_IN_SPACE: i18n.translate( - 'xpack.apm.anomaly_detection.error.not_available_in_space', - { - defaultMessage: 'Machine learning is not available in the selected space', - } - ), -}; diff --git a/x-pack/plugins/apm/common/anomaly_detection/apm_ml_anomaly_query.ts b/x-pack/plugins/apm/common/anomaly_detection/apm_ml_anomaly_query.ts new file mode 100644 index 0000000000000..00fb4b5ee4a54 --- /dev/null +++ b/x-pack/plugins/apm/common/anomaly_detection/apm_ml_anomaly_query.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 { ApmMlDetectorIndex } from './apm_ml_detectors'; + +export function apmMlAnomalyQuery(detectorIndex: ApmMlDetectorIndex) { + return [ + { + bool: { + filter: [ + { + terms: { + result_type: ['model_plot', 'record'], + }, + }, + { + term: { detector_index: detectorIndex }, + }, + ], + }, + }, + ]; +} diff --git a/x-pack/plugins/apm/common/anomaly_detection/apm_ml_detectors.ts b/x-pack/plugins/apm/common/anomaly_detection/apm_ml_detectors.ts new file mode 100644 index 0000000000000..7c68232122408 --- /dev/null +++ b/x-pack/plugins/apm/common/anomaly_detection/apm_ml_detectors.ts @@ -0,0 +1,12 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export const enum ApmMlDetectorIndex { + txLatency = 0, + txThroughput = 1, + txFailureRate = 2, +} diff --git a/x-pack/plugins/apm/common/anomaly_detection/index.ts b/x-pack/plugins/apm/common/anomaly_detection/index.ts new file mode 100644 index 0000000000000..cdb1c62b9eed5 --- /dev/null +++ b/x-pack/plugins/apm/common/anomaly_detection/index.ts @@ -0,0 +1,69 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { i18n } from '@kbn/i18n'; +import { ANOMALY_SEVERITY } from '../ml_constants'; +import { + getSeverityType, + getSeverityColor as mlGetSeverityColor, +} from '../../../ml/common'; +import { ServiceHealthStatus } from '../service_health_status'; + +export interface ServiceAnomalyStats { + transactionType?: string; + anomalyScore?: number; + actualValue?: number; + jobId?: string; + healthStatus: ServiceHealthStatus; +} + +export function getSeverity(score: number | undefined) { + if (score === undefined) { + return ANOMALY_SEVERITY.UNKNOWN; + } + + return getSeverityType(score); +} + +export function getSeverityColor(score: number) { + return mlGetSeverityColor(score); +} + +export const ML_ERRORS = { + INVALID_LICENSE: i18n.translate( + 'xpack.apm.anomaly_detection.error.invalid_license', + { + defaultMessage: `To use anomaly detection, you must be subscribed to an Elastic Platinum license. With it, you'll be able to monitor your services with the aid of machine learning.`, + } + ), + MISSING_READ_PRIVILEGES: i18n.translate( + 'xpack.apm.anomaly_detection.error.missing_read_privileges', + { + defaultMessage: + 'You must have "read" privileges to Machine Learning and APM in order to view Anomaly Detection jobs', + } + ), + MISSING_WRITE_PRIVILEGES: i18n.translate( + 'xpack.apm.anomaly_detection.error.missing_write_privileges', + { + defaultMessage: + 'You must have "write" privileges to Machine Learning and APM in order to create Anomaly Detection jobs', + } + ), + ML_NOT_AVAILABLE: i18n.translate( + 'xpack.apm.anomaly_detection.error.not_available', + { + defaultMessage: 'Machine learning is not available', + } + ), + ML_NOT_AVAILABLE_IN_SPACE: i18n.translate( + 'xpack.apm.anomaly_detection.error.not_available_in_space', + { + defaultMessage: 'Machine learning is not available in the selected space', + } + ), +}; diff --git a/x-pack/plugins/apm/common/correlations/constants.ts b/x-pack/plugins/apm/common/correlations/constants.ts new file mode 100644 index 0000000000000..11b9a9a109dbf --- /dev/null +++ b/x-pack/plugins/apm/common/correlations/constants.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. + */ + +/** + * Fields to exclude as potential field candidates + */ +export const FIELDS_TO_EXCLUDE_AS_CANDIDATE = new Set([ + // Exclude for all usage Contexts + 'parent.id', + 'trace.id', + 'transaction.id', + '@timestamp', + 'timestamp.us', + 'agent.ephemeral_id', + 'ecs.version', + 'event.ingested', + 'http.response.finished', + 'parent.id', + 'trace.id', + 'transaction.duration.us', + 'transaction.id', + 'process.pid', + 'process.ppid', + 'processor.event', + 'processor.name', + 'transaction.sampled', + 'transaction.span_count.dropped', + // Exclude for correlation on a Single Service + 'agent.name', + 'http.request.method', + 'service.framework.name', + 'service.language.name', + 'service.name', + 'service.runtime.name', + 'transaction.name', + 'transaction.type', +]); + +export const FIELD_PREFIX_TO_EXCLUDE_AS_CANDIDATE = ['observer.']; + +/** + * Fields to include/prioritize as potential field candidates + */ +export const FIELDS_TO_ADD_AS_CANDIDATE = new Set([ + 'service.version', + 'service.node.name', + 'service.framework.version', + 'service.language.version', + 'service.runtime.version', + 'kubernetes.pod.name', + 'kubernetes.pod.uid', + 'container.id', + 'source.ip', + 'client.ip', + 'host.ip', + 'service.environment', + 'process.args', + 'http.response.status_code', +]); +export const FIELD_PREFIX_TO_ADD_AS_CANDIDATE = [ + 'cloud.', + 'labels.', + 'user_agent.', +]; + +/** + * Other constants + */ +export const POPULATED_DOC_COUNT_SAMPLE_SIZE = 1000; + +export const PERCENTILES_STEP = 2; +export const TERMS_SIZE = 20; +export const SIGNIFICANT_FRACTION = 3; +export const SIGNIFICANT_VALUE_DIGITS = 3; + +export const CORRELATION_THRESHOLD = 0.3; +export const KS_TEST_THRESHOLD = 0.1; + +export const ERROR_CORRELATION_THRESHOLD = 0.02; + +export const DEFAULT_PERCENTILE_THRESHOLD = 95; +export const DEBOUNCE_INTERVAL = 100; diff --git a/x-pack/plugins/apm/common/search_strategies/failed_transactions_correlations/constants.ts b/x-pack/plugins/apm/common/correlations/failed_transactions_correlations/constants.ts similarity index 100% rename from x-pack/plugins/apm/common/search_strategies/failed_transactions_correlations/constants.ts rename to x-pack/plugins/apm/common/correlations/failed_transactions_correlations/constants.ts diff --git a/x-pack/plugins/apm/common/correlations/failed_transactions_correlations/types.ts b/x-pack/plugins/apm/common/correlations/failed_transactions_correlations/types.ts new file mode 100644 index 0000000000000..8b09d45c1e1b6 --- /dev/null +++ b/x-pack/plugins/apm/common/correlations/failed_transactions_correlations/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 { FieldValuePair, HistogramItem } from '../types'; + +import { FAILED_TRANSACTIONS_IMPACT_THRESHOLD } from './constants'; +import { FieldStats } from '../field_stats_types'; + +export interface FailedTransactionsCorrelation extends FieldValuePair { + doc_count: number; + bg_count: number; + score: number; + pValue: number | null; + normalizedScore: number; + failurePercentage: number; + successPercentage: number; + histogram: HistogramItem[]; +} + +export type FailedTransactionsCorrelationsImpactThreshold = + typeof FAILED_TRANSACTIONS_IMPACT_THRESHOLD[keyof typeof FAILED_TRANSACTIONS_IMPACT_THRESHOLD]; + +export interface FailedTransactionsCorrelationsResponse { + ccsWarning: boolean; + failedTransactionsCorrelations?: FailedTransactionsCorrelation[]; + percentileThresholdValue?: number; + overallHistogram?: HistogramItem[]; + errorHistogram?: HistogramItem[]; + fieldStats?: FieldStats[]; +} diff --git a/x-pack/plugins/apm/common/search_strategies/field_stats_types.ts b/x-pack/plugins/apm/common/correlations/field_stats_types.ts similarity index 90% rename from x-pack/plugins/apm/common/search_strategies/field_stats_types.ts rename to x-pack/plugins/apm/common/correlations/field_stats_types.ts index d63dd7f8d58a1..50dc7919fbd00 100644 --- a/x-pack/plugins/apm/common/search_strategies/field_stats_types.ts +++ b/x-pack/plugins/apm/common/correlations/field_stats_types.ts @@ -6,9 +6,9 @@ */ import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; -import { SearchStrategyParams } from './types'; +import { CorrelationsParams } from './types'; -export interface FieldStatsCommonRequestParams extends SearchStrategyParams { +export interface FieldStatsCommonRequestParams extends CorrelationsParams { samplerShardSize: number; } diff --git a/x-pack/plugins/apm/common/correlations/latency_correlations/types.ts b/x-pack/plugins/apm/common/correlations/latency_correlations/types.ts new file mode 100644 index 0000000000000..23c91554b6547 --- /dev/null +++ b/x-pack/plugins/apm/common/correlations/latency_correlations/types.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 { FieldValuePair, HistogramItem } from '../types'; +import { FieldStats } from '../field_stats_types'; + +export interface LatencyCorrelation extends FieldValuePair { + correlation: number; + histogram: HistogramItem[]; + ksTest: number; +} + +export interface LatencyCorrelationsResponse { + ccsWarning: boolean; + overallHistogram?: HistogramItem[]; + percentileThresholdValue?: number; + latencyCorrelations?: LatencyCorrelation[]; + fieldStats?: FieldStats[]; +} diff --git a/x-pack/plugins/apm/common/correlations/types.ts b/x-pack/plugins/apm/common/correlations/types.ts new file mode 100644 index 0000000000000..402750b72b2ab --- /dev/null +++ b/x-pack/plugins/apm/common/correlations/types.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. + */ + +export interface FieldValuePair { + fieldName: string; + // For dynamic fieldValues we only identify fields as `string`, + // but for example `http.response.status_code` which is part of + // of the list of predefined field candidates is of type long/number. + fieldValue: string | number; +} + +export interface HistogramItem { + key: number; + doc_count: number; +} + +export interface ResponseHitSource { + [s: string]: unknown; +} + +export interface ResponseHit { + _source: ResponseHitSource; +} + +export interface CorrelationsClientParams { + environment: string; + kuery: string; + serviceName?: string; + transactionName?: string; + transactionType?: string; + start: number; + end: number; +} + +export interface CorrelationsServerParams { + index: string; + includeFrozen?: boolean; +} + +export type CorrelationsParams = CorrelationsClientParams & + CorrelationsServerParams; diff --git a/x-pack/plugins/apm/server/lib/search_strategies/queries/get_prioritized_field_value_pairs.test.ts b/x-pack/plugins/apm/common/correlations/utils/get_prioritized_field_value_pairs.test.ts similarity index 100% rename from x-pack/plugins/apm/server/lib/search_strategies/queries/get_prioritized_field_value_pairs.test.ts rename to x-pack/plugins/apm/common/correlations/utils/get_prioritized_field_value_pairs.test.ts diff --git a/x-pack/plugins/apm/server/lib/search_strategies/queries/get_prioritized_field_value_pairs.ts b/x-pack/plugins/apm/common/correlations/utils/get_prioritized_field_value_pairs.ts similarity index 88% rename from x-pack/plugins/apm/server/lib/search_strategies/queries/get_prioritized_field_value_pairs.ts rename to x-pack/plugins/apm/common/correlations/utils/get_prioritized_field_value_pairs.ts index 6338422b022da..4a0086ba02a6d 100644 --- a/x-pack/plugins/apm/server/lib/search_strategies/queries/get_prioritized_field_value_pairs.ts +++ b/x-pack/plugins/apm/common/correlations/utils/get_prioritized_field_value_pairs.ts @@ -6,9 +6,9 @@ */ import { FIELDS_TO_ADD_AS_CANDIDATE } from '../constants'; -import { hasPrefixToInclude } from '../utils'; +import { hasPrefixToInclude } from './has_prefix_to_include'; -import type { FieldValuePair } from '../../../../common/search_strategies/types'; +import type { FieldValuePair } from '../types'; export const getPrioritizedFieldValuePairs = ( fieldValuePairs: FieldValuePair[] diff --git a/x-pack/plugins/apm/server/lib/search_strategies/utils/has_prefix_to_include.test.ts b/x-pack/plugins/apm/common/correlations/utils/has_prefix_to_include.test.ts similarity index 100% rename from x-pack/plugins/apm/server/lib/search_strategies/utils/has_prefix_to_include.test.ts rename to x-pack/plugins/apm/common/correlations/utils/has_prefix_to_include.test.ts diff --git a/x-pack/plugins/apm/server/lib/search_strategies/utils/has_prefix_to_include.ts b/x-pack/plugins/apm/common/correlations/utils/has_prefix_to_include.ts similarity index 100% rename from x-pack/plugins/apm/server/lib/search_strategies/utils/has_prefix_to_include.ts rename to x-pack/plugins/apm/common/correlations/utils/has_prefix_to_include.ts diff --git a/x-pack/plugins/apm/common/correlations/utils/index.ts b/x-pack/plugins/apm/common/correlations/utils/index.ts new file mode 100644 index 0000000000000..eb83c8ae2ed01 --- /dev/null +++ b/x-pack/plugins/apm/common/correlations/utils/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 { getPrioritizedFieldValuePairs } from './get_prioritized_field_value_pairs'; +export { hasPrefixToInclude } from './has_prefix_to_include'; diff --git a/x-pack/plugins/apm/common/environment_rt.ts b/x-pack/plugins/apm/common/environment_rt.ts index e9337da9bdcf5..4598ffa6f6681 100644 --- a/x-pack/plugins/apm/common/environment_rt.ts +++ b/x-pack/plugins/apm/common/environment_rt.ts @@ -5,7 +5,7 @@ * 2.0. */ import * as t from 'io-ts'; -import { nonEmptyStringRt } from '@kbn/io-ts-utils'; +import { nonEmptyStringRt } from '@kbn/io-ts-utils/non_empty_string_rt'; import { ENVIRONMENT_ALL, ENVIRONMENT_NOT_DEFINED, diff --git a/x-pack/plugins/apm/common/search_strategies/constants.ts b/x-pack/plugins/apm/common/search_strategies/constants.ts deleted file mode 100644 index 58203c93e5a42..0000000000000 --- a/x-pack/plugins/apm/common/search_strategies/constants.ts +++ /dev/null @@ -1,15 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -export const APM_SEARCH_STRATEGIES = { - APM_FAILED_TRANSACTIONS_CORRELATIONS: 'apmFailedTransactionsCorrelations', - APM_LATENCY_CORRELATIONS: 'apmLatencyCorrelations', -} as const; -export type ApmSearchStrategies = - typeof APM_SEARCH_STRATEGIES[keyof typeof APM_SEARCH_STRATEGIES]; - -export const DEFAULT_PERCENTILE_THRESHOLD = 95; diff --git a/x-pack/plugins/apm/common/search_strategies/failed_transactions_correlations/types.ts b/x-pack/plugins/apm/common/search_strategies/failed_transactions_correlations/types.ts deleted file mode 100644 index 28ce2ff24b961..0000000000000 --- a/x-pack/plugins/apm/common/search_strategies/failed_transactions_correlations/types.ts +++ /dev/null @@ -1,38 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { FieldValuePair, HistogramItem } from '../types'; - -import { FAILED_TRANSACTIONS_IMPACT_THRESHOLD } from './constants'; -import { FieldStats } from '../field_stats_types'; - -export interface FailedTransactionsCorrelation extends FieldValuePair { - doc_count: number; - bg_count: number; - score: number; - pValue: number | null; - normalizedScore: number; - failurePercentage: number; - successPercentage: number; - histogram: HistogramItem[]; -} - -export type FailedTransactionsCorrelationsImpactThreshold = - typeof FAILED_TRANSACTIONS_IMPACT_THRESHOLD[keyof typeof FAILED_TRANSACTIONS_IMPACT_THRESHOLD]; - -export interface FailedTransactionsCorrelationsParams { - percentileThreshold: number; -} - -export interface FailedTransactionsCorrelationsRawResponse { - log: string[]; - failedTransactionsCorrelations?: FailedTransactionsCorrelation[]; - percentileThresholdValue?: number; - overallHistogram?: HistogramItem[]; - errorHistogram?: HistogramItem[]; - fieldStats?: FieldStats[]; -} diff --git a/x-pack/plugins/apm/common/search_strategies/latency_correlations/types.ts b/x-pack/plugins/apm/common/search_strategies/latency_correlations/types.ts deleted file mode 100644 index ea74175a3dacb..0000000000000 --- a/x-pack/plugins/apm/common/search_strategies/latency_correlations/types.ts +++ /dev/null @@ -1,37 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { FieldValuePair, HistogramItem } from '../types'; -import { FieldStats } from '../field_stats_types'; - -export interface LatencyCorrelation extends FieldValuePair { - correlation: number; - histogram: HistogramItem[]; - ksTest: number; -} - -export interface LatencyCorrelationSearchServiceProgress { - started: number; - loadedHistogramStepsize: number; - loadedOverallHistogram: number; - loadedFieldCandidates: number; - loadedFieldValuePairs: number; - loadedHistograms: number; -} - -export interface LatencyCorrelationsParams { - percentileThreshold: number; - analyzeCorrelations: boolean; -} - -export interface LatencyCorrelationsRawResponse { - log: string[]; - overallHistogram?: HistogramItem[]; - percentileThresholdValue?: number; - latencyCorrelations?: LatencyCorrelation[]; - fieldStats?: FieldStats[]; -} diff --git a/x-pack/plugins/apm/common/search_strategies/types.ts b/x-pack/plugins/apm/common/search_strategies/types.ts deleted file mode 100644 index ff925f70fc9b0..0000000000000 --- a/x-pack/plugins/apm/common/search_strategies/types.ts +++ /dev/null @@ -1,60 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -export interface FieldValuePair { - fieldName: string; - // For dynamic fieldValues we only identify fields as `string`, - // but for example `http.response.status_code` which is part of - // of the list of predefined field candidates is of type long/number. - fieldValue: string | number; -} - -export interface HistogramItem { - key: number; - doc_count: number; -} - -export interface ResponseHitSource { - [s: string]: unknown; -} - -export interface ResponseHit { - _source: ResponseHitSource; -} - -export interface RawResponseBase { - ccsWarning: boolean; - took: number; -} - -export interface SearchStrategyClientParamsBase { - environment: string; - kuery: string; - serviceName?: string; - transactionName?: string; - transactionType?: string; -} - -export interface RawSearchStrategyClientParams - extends SearchStrategyClientParamsBase { - start?: string; - end?: string; -} - -export interface SearchStrategyClientParams - extends SearchStrategyClientParamsBase { - start: number; - end: number; -} - -export interface SearchStrategyServerParams { - index: string; - includeFrozen?: boolean; -} - -export type SearchStrategyParams = SearchStrategyClientParams & - SearchStrategyServerParams; diff --git a/x-pack/plugins/apm/common/viz_colors.ts b/x-pack/plugins/apm/common/viz_colors.ts index 20287f6e097bc..5b4946f346841 100644 --- a/x-pack/plugins/apm/common/viz_colors.ts +++ b/x-pack/plugins/apm/common/viz_colors.ts @@ -5,7 +5,7 @@ * 2.0. */ -import lightTheme from '@elastic/eui/dist/eui_theme_light.json'; +import { euiLightVars as lightTheme } from '@kbn/ui-shared-deps-src/theme'; function getVizColorsForTheme(theme = lightTheme) { return [ diff --git a/x-pack/plugins/apm/dev_docs/query_debugging_in_development_and_production.md b/x-pack/plugins/apm/dev_docs/query_debugging_in_development_and_production.md new file mode 100644 index 0000000000000..0dcf20d3e2fed --- /dev/null +++ b/x-pack/plugins/apm/dev_docs/query_debugging_in_development_and_production.md @@ -0,0 +1,17 @@ +# Query Debugging + +When debugging an issue with the APM UI it can be very helpful to see the exact Elasticsearch queries and responses that was made for a given API request. +To enable debugging of Elasticsearch queries in APM UI do the following: + +1. Go to "Stack Management" +2. Under "Kibana" on the left-hand side, select "Advanced Settings" +3. Search for "Observability" +4. Enable "Inspect ES queries" setting +5. Click "Save" + +When you navigate back to APM UI you can now inspect Elasticsearch queries by opening your browser's Developer Tools and selecting an api request to APM's api. +There will be an `_inspect` key containing every Elasticsearch query made during that request including both requests and responses to and from Elasticsearch. + +![image](https://user-images.githubusercontent.com/209966/140500012-b075adf0-8401-40fd-99f8-85b68711de17.png) + + diff --git a/x-pack/plugins/apm/dev_docs/routing_and_linking.md b/x-pack/plugins/apm/dev_docs/routing_and_linking.md index 1f6160a6c4a99..562af3d01ef77 100644 --- a/x-pack/plugins/apm/dev_docs/routing_and_linking.md +++ b/x-pack/plugins/apm/dev_docs/routing_and_linking.md @@ -6,7 +6,7 @@ This document describes routing in the APM plugin. ### Server-side -Route definitions for APM's server-side API are in the [server/routes directory](../server/routes). Routes are created with [the `createApmServerRoute` function](../server/routes/create_apm_server_route.ts). Routes are added to the API in [the `registerRoutes` function](../server/routes/register_routes.ts), which is initialized in the plugin `setup` lifecycle method. +Route definitions for APM's server-side API are in the [server/routes directory](../server/routes). Routes are created with [the `createApmServerRoute` function](../server/routes/apm_routes/create_apm_server_route.ts). Routes are added to the API in [the `registerRoutes` function](../server/routes/apm_routes/register_apm_server_routes.ts), which is initialized in the plugin `setup` lifecycle method. The path and query string parameters are defined in the calls to `createApmServerRoute` with io-ts types, so that each route has its parameters type checked. diff --git a/x-pack/plugins/apm/ftr_e2e/cypress/integration/read_only_user/errors/error_details.spec.ts b/x-pack/plugins/apm/ftr_e2e/cypress/integration/read_only_user/errors/error_details.spec.ts new file mode 100644 index 0000000000000..f3d3d75cad00d --- /dev/null +++ b/x-pack/plugins/apm/ftr_e2e/cypress/integration/read_only_user/errors/error_details.spec.ts @@ -0,0 +1,90 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import url from 'url'; +import { synthtrace } from '../../../../synthtrace'; +import { generateData } from './generate_data'; + +const start = '2021-10-10T00:00:00.000Z'; +const end = '2021-10-10T00:15:00.000Z'; +const errorDetailsPageHref = url.format({ + pathname: + '/app/apm/services/opbeans-java/errors/0000000000000000000000000Error%201', + query: { + rangeFrom: start, + rangeTo: end, + }, +}); + +describe('Error details', () => { + beforeEach(() => { + cy.loginAsReadOnlyUser(); + }); + + describe('when data is loaded', () => { + before(async () => { + await synthtrace.index( + generateData({ + from: new Date(start).getTime(), + to: new Date(end).getTime(), + }) + ); + }); + + after(async () => { + await synthtrace.clean(); + }); + + describe('when error has no occurrences', () => { + it('shows empty an message', () => { + cy.visit( + url.format({ + pathname: + '/app/apm/services/opbeans-java/errors/0000000000000000000000000Error%201', + query: { + rangeFrom: start, + rangeTo: end, + kuery: 'service.name: "opbeans-node"', + }, + }) + ); + cy.contains('No data to display'); + }); + }); + + describe('when error has data', () => { + it('shows errors distribution chart', () => { + cy.visit(errorDetailsPageHref); + cy.contains('Error group 00000'); + cy.get('[data-test-subj="errorDistribution"]').contains('Occurrences'); + }); + + it('shows a Stacktrace and Metadata tabs', () => { + cy.visit(errorDetailsPageHref); + cy.contains('button', 'Exception stack trace'); + cy.contains('button', 'Metadata'); + }); + + describe('when clicking on related transaction sample', () => { + it('should redirects to the transaction details page', () => { + cy.visit(errorDetailsPageHref); + cy.contains('Error group 00000'); + cy.contains('a', 'GET /apple 🍎').click(); + cy.url().should('include', 'opbeans-java/transactions/view'); + }); + }); + + describe('when clicking on View x occurences in discover', () => { + it('should redirects the user to discover', () => { + cy.visit(errorDetailsPageHref); + cy.contains('span', 'Discover').click(); + cy.url().should('include', 'app/discover'); + }); + }); + }); + }); +}); diff --git a/x-pack/plugins/apm/ftr_e2e/cypress/integration/read_only_user/errors/errors_page.spec.ts b/x-pack/plugins/apm/ftr_e2e/cypress/integration/read_only_user/errors/errors_page.spec.ts new file mode 100644 index 0000000000000..fd6890d3a7bed --- /dev/null +++ b/x-pack/plugins/apm/ftr_e2e/cypress/integration/read_only_user/errors/errors_page.spec.ts @@ -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 url from 'url'; +import { synthtrace } from '../../../../synthtrace'; +import { generateData } from './generate_data'; + +const start = '2021-10-10T00:00:00.000Z'; +const end = '2021-10-10T00:15:00.000Z'; + +const javaServiceErrorsPageHref = url.format({ + pathname: '/app/apm/services/opbeans-java/errors', + query: { rangeFrom: start, rangeTo: end }, +}); + +const nodeServiceErrorsPageHref = url.format({ + pathname: '/app/apm/services/opbeans-node/errors', + query: { rangeFrom: start, rangeTo: end }, +}); + +describe('Errors page', () => { + beforeEach(() => { + cy.loginAsReadOnlyUser(); + }); + + describe('when data is loaded', () => { + before(async () => { + await synthtrace.index( + generateData({ + from: new Date(start).getTime(), + to: new Date(end).getTime(), + }) + ); + }); + + after(async () => { + await synthtrace.clean(); + }); + + describe('when service has no errors', () => { + it('shows empty message', () => { + cy.visit(nodeServiceErrorsPageHref); + cy.contains('opbeans-node'); + cy.contains('No errors found'); + }); + }); + + describe('when service has errors', () => { + it('shows errors distribution chart', () => { + cy.visit(javaServiceErrorsPageHref); + cy.contains('Error occurrences'); + }); + + it('shows failed transaction rate chart', () => { + cy.visit(javaServiceErrorsPageHref); + cy.contains('Failed transaction rate'); + }); + + it('errors table is populated', () => { + cy.visit(javaServiceErrorsPageHref); + cy.contains('Error 0'); + }); + + it('clicking on an error in the list navigates to error detail page', () => { + cy.visit(javaServiceErrorsPageHref); + cy.contains('a', 'Error 1').click(); + cy.contains('div', 'Error 1'); + }); + + it('clicking on type adds a filter in the kuerybar', () => { + cy.visit(javaServiceErrorsPageHref); + cy.get('[data-test-subj="headerFilterKuerybar"]') + .invoke('val') + .should('be.empty'); + // `force: true` because Cypress says the element is 0x0 + cy.contains('exception 0').click({ + force: true, + }); + cy.get('[data-test-subj="headerFilterKuerybar"]') + .its('length') + .should('be.gt', 0); + cy.get('table') + .find('td:contains("exception 0")') + .should('have.length', 1); + }); + + it('sorts by ocurrences', () => { + cy.visit(javaServiceErrorsPageHref); + cy.contains('span', 'Occurrences').click(); + cy.url().should( + 'include', + '&sortField=occurrenceCount&sortDirection=asc' + ); + }); + + it('sorts by latest occurrences', () => { + cy.visit(javaServiceErrorsPageHref); + cy.contains('span', 'Latest occurrence').click(); + cy.url().should( + 'include', + '&sortField=latestOccurrenceAt&sortDirection=asc' + ); + }); + }); + }); +}); diff --git a/x-pack/plugins/apm/ftr_e2e/cypress/integration/read_only_user/errors/generate_data.ts b/x-pack/plugins/apm/ftr_e2e/cypress/integration/read_only_user/errors/generate_data.ts new file mode 100644 index 0000000000000..7f1c14ac25513 --- /dev/null +++ b/x-pack/plugins/apm/ftr_e2e/cypress/integration/read_only_user/errors/generate_data.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 { service, timerange } from '@elastic/apm-synthtrace'; + +export function generateData({ from, to }: { from: number; to: number }) { + const range = timerange(from, to); + + const opbeansJava = service('opbeans-java', 'production', 'java') + .instance('opbeans-java-prod-1') + .podId('opbeans-java-prod-1-pod'); + + const opbeansNode = service('opbeans-node', 'production', 'nodejs').instance( + 'opbeans-node-prod-1' + ); + + return [ + ...range + .interval('2m') + .rate(1) + .flatMap((timestamp, index) => [ + ...opbeansJava + .transaction('GET /apple 🍎 ') + .timestamp(timestamp) + .duration(1000) + .success() + .errors( + opbeansJava + .error(`Error ${index}`, `exception ${index}`) + .timestamp(timestamp) + ) + .serialize(), + ...opbeansNode + .transaction('GET /banana 🍌') + .timestamp(timestamp) + .duration(500) + .success() + .serialize(), + ]), + ]; +} diff --git a/x-pack/plugins/apm/ftr_e2e/cypress/integration/read_only_user/service_inventory/header_filters/generate_data.ts b/x-pack/plugins/apm/ftr_e2e/cypress/integration/read_only_user/service_inventory/header_filters/generate_data.ts new file mode 100644 index 0000000000000..9ebaa1747d909 --- /dev/null +++ b/x-pack/plugins/apm/ftr_e2e/cypress/integration/read_only_user/service_inventory/header_filters/generate_data.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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import { service, timerange } from '@elastic/apm-synthtrace'; + +export function generateData({ + from, + to, + specialServiceName, +}: { + from: number; + to: number; + specialServiceName: string; +}) { + const range = timerange(from, to); + + const service1 = service(specialServiceName, 'production', 'java') + .instance('service-1-prod-1') + .podId('service-1-prod-1-pod'); + + const opbeansNode = service('opbeans-node', 'production', 'nodejs').instance( + 'opbeans-node-prod-1' + ); + + return [ + ...range + .interval('2m') + .rate(1) + .flatMap((timestamp, index) => [ + ...service1 + .transaction('GET /apple 🍎 ') + .timestamp(timestamp) + .duration(1000) + .success() + .serialize(), + ...opbeansNode + .transaction('GET /banana 🍌') + .timestamp(timestamp) + .duration(500) + .success() + .serialize(), + ]), + ]; +} diff --git a/x-pack/plugins/apm/ftr_e2e/cypress/integration/read_only_user/service_inventory/header_filters/header_filters.spec.ts b/x-pack/plugins/apm/ftr_e2e/cypress/integration/read_only_user/service_inventory/header_filters/header_filters.spec.ts new file mode 100644 index 0000000000000..2fa8b1588a630 --- /dev/null +++ b/x-pack/plugins/apm/ftr_e2e/cypress/integration/read_only_user/service_inventory/header_filters/header_filters.spec.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 url from 'url'; +import { synthtrace } from '../../../../../synthtrace'; +import { generateData } from './generate_data'; + +const start = '2021-10-10T00:00:00.000Z'; +const end = '2021-10-10T00:15:00.000Z'; + +const serviceOverviewHref = url.format({ + pathname: '/app/apm/services', + query: { rangeFrom: start, rangeTo: end }, +}); + +const specialServiceName = + 'service 1 / ? # [ ] @ ! $ & ( ) * + , ; = < > % {} | ^ ` <>'; + +describe('Service inventory - header filters', () => { + before(async () => { + await synthtrace.index( + generateData({ + from: new Date(start).getTime(), + to: new Date(end).getTime(), + specialServiceName, + }) + ); + }); + + after(async () => { + await synthtrace.clean(); + }); + + beforeEach(() => { + cy.loginAsReadOnlyUser(); + }); + + describe('Filtering by kuerybar', () => { + it('filters by service.name with special characters', () => { + cy.visit(serviceOverviewHref); + cy.contains('Services'); + cy.contains('opbeans-node'); + cy.contains('service 1'); + cy.get('[data-test-subj="headerFilterKuerybar"]') + .type(`service.name: "${specialServiceName}"`) + .type('{enter}'); + cy.contains('service 1'); + cy.url().should('include', encodeURIComponent(specialServiceName)); + }); + }); +}); diff --git a/x-pack/plugins/apm/ftr_e2e/cypress/integration/read_only_user/service_inventory/service_inventory.spec.ts b/x-pack/plugins/apm/ftr_e2e/cypress/integration/read_only_user/service_inventory/service_inventory.spec.ts new file mode 100644 index 0000000000000..f74a1d122e426 --- /dev/null +++ b/x-pack/plugins/apm/ftr_e2e/cypress/integration/read_only_user/service_inventory/service_inventory.spec.ts @@ -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 url from 'url'; +import { synthtrace } from '../../../../synthtrace'; +import { opbeans } from '../../../fixtures/synthtrace/opbeans'; + +const timeRange = { + rangeFrom: '2021-10-10T00:00:00.000Z', + rangeTo: '2021-10-10T00:15:00.000Z', +}; + +const serviceInventoryHref = url.format({ + pathname: '/app/apm/services', + query: timeRange, +}); + +const apiRequestsToIntercept = [ + { + endpoint: '/internal/apm/services?*', + aliasName: 'servicesRequest', + }, + { + endpoint: '/internal/apm/services/detailed_statistics?*', + aliasName: 'detailedStatisticsRequest', + }, +]; + +const aliasNames = apiRequestsToIntercept.map( + ({ aliasName }) => `@${aliasName}` +); + +describe('When navigating to the service inventory', () => { + before(async () => { + const { rangeFrom, rangeTo } = timeRange; + await synthtrace.index( + opbeans({ + from: new Date(rangeFrom).getTime(), + to: new Date(rangeTo).getTime(), + }) + ); + }); + + after(async () => { + await synthtrace.clean(); + }); + + beforeEach(() => { + cy.loginAsReadOnlyUser(); + cy.visit(serviceInventoryHref); + }); + + it('has a list of services', () => { + cy.contains('opbeans-node'); + cy.contains('opbeans-java'); + cy.contains('opbeans-rum'); + }); + + it('has a list of environments', () => { + cy.get('td:contains(production)').should('have.length', 3); + }); + + it('when clicking on a service it loads the service overview for that service', () => { + cy.contains('opbeans-node').click({ force: true }); + cy.url().should('include', '/apm/services/opbeans-node/overview'); + cy.contains('h1', 'opbeans-node'); + }); + + describe('Calls APIs', () => { + beforeEach(() => { + apiRequestsToIntercept.map(({ endpoint, aliasName }) => { + cy.intercept('GET', endpoint).as(aliasName); + }); + }); + + it('with the correct environment when changing the environment', () => { + cy.wait(aliasNames); + + cy.get('[data-test-subj="environmentFilter"]').select('production'); + + cy.expectAPIsToHaveBeenCalledWith({ + apisIntercepted: aliasNames, + value: 'environment=production', + }); + }); + + it('when clicking the refresh button', () => { + cy.wait(aliasNames); + cy.contains('Refresh').click(); + cy.wait(aliasNames); + }); + + it.skip('when selecting a different time range and clicking the update button', () => { + cy.wait(aliasNames); + + cy.selectAbsoluteTimeRange( + 'Oct 10, 2021 @ 01:00:00.000', + 'Oct 10, 2021 @ 01:30:00.000' + ); + cy.contains('Update').click(); + cy.wait(aliasNames); + + cy.contains('Refresh').click(); + cy.wait(aliasNames); + }); + }); +}); diff --git a/x-pack/plugins/apm/ftr_e2e/cypress/integration/read_only_user/service_overview/errors_table.spec.ts b/x-pack/plugins/apm/ftr_e2e/cypress/integration/read_only_user/service_overview/errors_table.spec.ts new file mode 100644 index 0000000000000..9ea6ef028b805 --- /dev/null +++ b/x-pack/plugins/apm/ftr_e2e/cypress/integration/read_only_user/service_overview/errors_table.spec.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 url from 'url'; +import { synthtrace } from '../../../../synthtrace'; +import { opbeans } from '../../../fixtures/synthtrace/opbeans'; + +const start = '2021-10-10T00:00:00.000Z'; +const end = '2021-10-10T00:15:00.000Z'; + +const serviceOverviewHref = url.format({ + pathname: '/app/apm/services/opbeans-java/overview', + query: { rangeFrom: start, rangeTo: end }, +}); + +describe('Errors table', () => { + before(async () => { + await synthtrace.index( + opbeans({ + from: new Date(start).getTime(), + to: new Date(end).getTime(), + }) + ); + }); + + after(async () => { + await synthtrace.clean(); + }); + + beforeEach(() => { + cy.loginAsReadOnlyUser(); + }); + + it('errors table is populated', () => { + cy.visit(serviceOverviewHref); + cy.contains('opbeans-java'); + cy.contains('[MockError] Foo'); + }); + + it('navigates to the errors page', () => { + cy.visit(serviceOverviewHref); + cy.contains('opbeans-java'); + cy.contains('a', 'View errors').click(); + cy.url().should('include', '/opbeans-java/errors'); + }); + + it('navigates to error detail page', () => { + cy.visit(serviceOverviewHref); + cy.contains('a', '[MockError] Foo').click(); + cy.contains('div', 'Exception message'); + }); +}); diff --git a/x-pack/plugins/apm/ftr_e2e/cypress/support/commands.ts b/x-pack/plugins/apm/ftr_e2e/cypress/support/commands.ts index 519cb0aa31cdb..cb66d6db809f3 100644 --- a/x-pack/plugins/apm/ftr_e2e/cypress/support/commands.ts +++ b/x-pack/plugins/apm/ftr_e2e/cypress/support/commands.ts @@ -42,6 +42,22 @@ Cypress.Commands.add('changeTimeRange', (value: string) => { cy.contains(value).click(); }); +Cypress.Commands.add( + 'selectAbsoluteTimeRange', + (start: string, end: string) => { + cy.get('[data-test-subj="superDatePickerstartDatePopoverButton"]').click(); + cy.get('[data-test-subj="superDatePickerAbsoluteDateInput"]') + .eq(0) + .clear() + .type(start, { force: true }); + cy.get('[data-test-subj="superDatePickerendDatePopoverButton"]').click(); + cy.get('[data-test-subj="superDatePickerAbsoluteDateInput"]') + .eq(1) + .clear() + .type(end, { force: true }); + } +); + Cypress.Commands.add( 'expectAPIsToHaveBeenCalledWith', ({ diff --git a/x-pack/plugins/apm/ftr_e2e/cypress/support/types.d.ts b/x-pack/plugins/apm/ftr_e2e/cypress/support/types.d.ts index 2d9ef090eef65..413f38be892f1 100644 --- a/x-pack/plugins/apm/ftr_e2e/cypress/support/types.d.ts +++ b/x-pack/plugins/apm/ftr_e2e/cypress/support/types.d.ts @@ -11,6 +11,7 @@ declare namespace Cypress { loginAsPowerUser(): void; loginAs(params: { username: string; password: string }): void; changeTimeRange(value: string): void; + selectAbsoluteTimeRange(start: string, end: string): void; expectAPIsToHaveBeenCalledWith(params: { apisIntercepted: string[]; value: string; diff --git a/x-pack/plugins/apm/jest.config.js b/x-pack/plugins/apm/jest.config.js index 66b4b164a794c..cc985407698bf 100644 --- a/x-pack/plugins/apm/jest.config.js +++ b/x-pack/plugins/apm/jest.config.js @@ -19,5 +19,6 @@ module.exports = { coverageReporters: ['text', 'html'], collectCoverageFrom: [ '/x-pack/plugins/apm/{common,public,server}/**/*.{js,ts,tsx}', + '!/**/*.stories.*', ], }; diff --git a/x-pack/plugins/apm/public/application/application.test.tsx b/x-pack/plugins/apm/public/application/application.test.tsx index 56e7b4684acde..12170ac20b7df 100644 --- a/x-pack/plugins/apm/public/application/application.test.tsx +++ b/x-pack/plugins/apm/public/application/application.test.tsx @@ -7,24 +7,31 @@ import React from 'react'; import { act } from '@testing-library/react'; +import { EuiErrorBoundary } from '@elastic/eui'; +import { mount } from 'enzyme'; import { createMemoryHistory } from 'history'; import { Observable } from 'rxjs'; -import { CoreStart, DocLinksStart, HttpStart } from 'src/core/public'; +import { AppMountParameters, DocLinksStart, HttpStart } from 'src/core/public'; import { mockApmPluginContextValue } from '../context/apm_plugin/mock_apm_plugin_context'; import { createCallApmApi } from '../services/rest/createCallApmApi'; -import { renderApp } from './'; +import { renderApp as renderApmApp } from './'; +import { UXAppRoot } from './uxApp'; import { disableConsoleWarning } from '../utils/testHelpers'; import { dataPluginMock } from 'src/plugins/data/public/mocks'; import { embeddablePluginMock } from 'src/plugins/embeddable/public/mocks'; -import { ApmPluginStartDeps } from '../plugin'; +import { ApmPluginSetupDeps, ApmPluginStartDeps } from '../plugin'; +import { RumHome } from '../components/app/RumDashboard/RumHome'; jest.mock('../services/rest/data_view', () => ({ createStaticDataView: () => Promise.resolve(undefined), })); -describe('renderApp', () => { - let mockConsole: jest.SpyInstance; +jest.mock('../components/app/RumDashboard/RumHome', () => ({ + RumHome: () =>

      Home Mock

      , +})); +describe('renderApp (APM)', () => { + let mockConsole: jest.SpyInstance; beforeAll(() => { // The RUM agent logs an unnecessary message here. There's a couple open // issues need to be fixed to get the ability to turn off all of the logging: @@ -40,11 +47,15 @@ describe('renderApp', () => { mockConsole.mockRestore(); }); - it('renders the app', () => { - const { core, config, observabilityRuleTypeRegistry } = - mockApmPluginContextValue; + const getApmMountProps = () => { + const { + core: coreStart, + config, + observabilityRuleTypeRegistry, + corePlugins, + } = mockApmPluginContextValue; - const plugins = { + const pluginsSetup = { licensing: { license$: new Observable() }, triggersActionsUi: { actionTypeRegistry: {}, ruleTypeRegistry: {} }, data: { @@ -99,7 +110,7 @@ describe('renderApp', () => { } as unknown as ApmPluginStartDeps; jest.spyOn(window, 'scrollTo').mockReturnValueOnce(undefined); - createCallApmApi(core as unknown as CoreStart); + createCallApmApi(coreStart); jest .spyOn(window.console, 'warn') @@ -111,17 +122,24 @@ describe('renderApp', () => { } }); + return { + coreStart, + pluginsSetup: pluginsSetup as unknown as ApmPluginSetupDeps, + appMountParameters: appMountParameters as unknown as AppMountParameters, + pluginsStart, + config, + observabilityRuleTypeRegistry, + corePlugins, + }; + }; + + it('renders the app', () => { + const mountProps = getApmMountProps(); + let unmount: () => void; act(() => { - unmount = renderApp({ - coreStart: core as any, - pluginsSetup: plugins as any, - appMountParameters: appMountParameters as any, - pluginsStart, - config, - observabilityRuleTypeRegistry, - }); + unmount = renderApmApp(mountProps); }); expect(() => { @@ -129,3 +147,21 @@ describe('renderApp', () => { }).not.toThrowError(); }); }); + +describe('renderUxApp', () => { + it('has an error boundary for the UXAppRoot', async () => { + const uxMountProps = mockApmPluginContextValue; + + const wrapper = mount(); + + wrapper + .find(RumHome) + .simulateError(new Error('Oh no, an unexpected error!')); + + expect(wrapper.find(RumHome)).toHaveLength(0); + expect(wrapper.find(EuiErrorBoundary)).toHaveLength(1); + expect(wrapper.find(EuiErrorBoundary).text()).toMatch( + /Error: Oh no, an unexpected error!/ + ); + }); +}); diff --git a/x-pack/plugins/apm/public/application/uxApp.tsx b/x-pack/plugins/apm/public/application/uxApp.tsx index 2e4ba786811f8..cfb1a5c354c2d 100644 --- a/x-pack/plugins/apm/public/application/uxApp.tsx +++ b/x-pack/plugins/apm/public/application/uxApp.tsx @@ -5,8 +5,8 @@ * 2.0. */ -import euiDarkVars from '@elastic/eui/dist/eui_theme_dark.json'; -import euiLightVars from '@elastic/eui/dist/eui_theme_light.json'; +import { euiLightVars, euiDarkVars } from '@kbn/ui-shared-deps-src/theme'; +import { EuiErrorBoundary } from '@elastic/eui'; import { AppMountParameters, CoreStart } from 'kibana/public'; import React from 'react'; import ReactDOM from 'react-dom'; @@ -133,7 +133,9 @@ export function UXAppRoot({ - + + + diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/ActionMenu/index.tsx b/x-pack/plugins/apm/public/components/app/RumDashboard/ActionMenu/index.tsx index 3bf21de7487de..2a1badd0ae1d8 100644 --- a/x-pack/plugins/apm/public/components/app/RumDashboard/ActionMenu/index.tsx +++ b/x-pack/plugins/apm/public/components/app/RumDashboard/ActionMenu/index.tsx @@ -19,14 +19,14 @@ import { InspectorHeaderLink } from '../../../shared/apm_header_action_menu/insp import { SERVICE_NAME } from '../../../../../common/elasticsearch_fieldnames'; const ANALYZE_DATA = i18n.translate('xpack.apm.analyzeDataButtonLabel', { - defaultMessage: 'Analyze data', + defaultMessage: 'Explore data', }); const ANALYZE_MESSAGE = i18n.translate( 'xpack.apm.analyzeDataButtonLabel.message', { defaultMessage: - 'EXPERIMENTAL - Analyze Data allows you to select and filter result data in any dimension and look for the cause or impact of performance problems.', + 'EXPERIMENTAL - Explore Data allows you to select and filter result data in any dimension and look for the cause or impact of performance problems.', } ); diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/ClientMetrics/Metrics.tsx b/x-pack/plugins/apm/public/components/app/RumDashboard/ClientMetrics/Metrics.tsx index 3a9100a0712aa..ded242e2ce558 100644 --- a/x-pack/plugins/apm/public/components/app/RumDashboard/ClientMetrics/Metrics.tsx +++ b/x-pack/plugins/apm/public/components/app/RumDashboard/ClientMetrics/Metrics.tsx @@ -55,7 +55,7 @@ export function Metrics() { (callApmApi) => { if (uxQuery) { return callApmApi({ - endpoint: 'GET /api/apm/rum/client-metrics', + endpoint: 'GET /internal/apm/ux/client-metrics', params: { query: { ...uxQuery, diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/ImpactfulMetrics/JSErrors.tsx b/x-pack/plugins/apm/public/components/app/RumDashboard/ImpactfulMetrics/JSErrors.tsx index b8bdc36ed4e0d..7f481d1c14dc2 100644 --- a/x-pack/plugins/apm/public/components/app/RumDashboard/ImpactfulMetrics/JSErrors.tsx +++ b/x-pack/plugins/apm/public/components/app/RumDashboard/ImpactfulMetrics/JSErrors.tsx @@ -41,7 +41,7 @@ export function JSErrors() { (callApmApi) => { if (start && end && serviceName) { return callApmApi({ - endpoint: 'GET /api/apm/rum-client/js-errors', + endpoint: 'GET /internal/apm/ux/js-errors', params: { query: { start, diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/LocalUIFilters/use_data_view.test.js b/x-pack/plugins/apm/public/components/app/RumDashboard/LocalUIFilters/use_data_view.test.js new file mode 100644 index 0000000000000..d9eef896782ca --- /dev/null +++ b/x-pack/plugins/apm/public/components/app/RumDashboard/LocalUIFilters/use_data_view.test.js @@ -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 React from 'react'; +import { renderHook } from '@testing-library/react-hooks'; +import * as dynamicDataView from '../../../../hooks/use_dynamic_data_view'; +import { useDataView } from './use_data_view'; +import { MockApmPluginContextWrapper } from '../../../../context/apm_plugin/mock_apm_plugin_context'; +import { KibanaContextProvider } from '../../../../../../../../src/plugins/kibana_react/public'; + +describe('useDataView', () => { + const create = jest.fn(); + const mockDataService = { + data: { + dataViews: { + create, + }, + }, + }; + + const title = 'apm-*'; + jest + .spyOn(dynamicDataView, 'useDynamicDataViewFetcher') + .mockReturnValue({ dataView: { title } }); + + it('returns result as expected', async () => { + const { waitForNextUpdate } = renderHook(() => useDataView(), { + wrapper: ({ children }) => ( + + + {children} + + + ), + }); + + await waitForNextUpdate(); + + expect(create).toBeCalledWith({ title }); + }); +}); diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/LocalUIFilters/use_data_view.ts b/x-pack/plugins/apm/public/components/app/RumDashboard/LocalUIFilters/use_data_view.ts index ba99729293368..40d0017d8d096 100644 --- a/x-pack/plugins/apm/public/components/app/RumDashboard/LocalUIFilters/use_data_view.ts +++ b/x-pack/plugins/apm/public/components/app/RumDashboard/LocalUIFilters/use_data_view.ts @@ -6,10 +6,7 @@ */ import { useDynamicDataViewFetcher } from '../../../../hooks/use_dynamic_data_view'; -import { - DataView, - DataViewSpec, -} from '../../../../../../../../src/plugins/data/common'; +import { DataView } from '../../../../../../../../src/plugins/data/common'; import { useKibana } from '../../../../../../../../src/plugins/kibana_react/public'; import { useFetcher } from '../../../../hooks/use_fetcher'; import { DataPublicPluginStart } from '../../../../../../../../src/plugins/data/public'; @@ -26,8 +23,8 @@ export function useDataView() { const { data } = useFetcher>(async () => { if (dataView?.title) { return dataViews.create({ - pattern: dataView?.title, - } as DataViewSpec); + title: dataView?.title, + }); } }, [dataView?.title, dataViews]); diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/PageLoadDistribution/PercentileAnnotations.tsx b/x-pack/plugins/apm/public/components/app/RumDashboard/PageLoadDistribution/PercentileAnnotations.tsx index ac713ad8dd8a8..35c6fb3c634cc 100644 --- a/x-pack/plugins/apm/public/components/app/RumDashboard/PageLoadDistribution/PercentileAnnotations.tsx +++ b/x-pack/plugins/apm/public/components/app/RumDashboard/PageLoadDistribution/PercentileAnnotations.tsx @@ -13,7 +13,7 @@ import { LineAnnotationStyle, Position, } from '@elastic/charts'; -import euiLightVars from '@elastic/eui/dist/eui_theme_light.json'; +import { euiLightVars } from '@kbn/ui-shared-deps-src/theme'; import { EuiToolTip } from '@elastic/eui'; interface Props { diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/PageLoadDistribution/index.tsx b/x-pack/plugins/apm/public/components/app/RumDashboard/PageLoadDistribution/index.tsx index f75d0fd093b5a..6798fbe90e4de 100644 --- a/x-pack/plugins/apm/public/components/app/RumDashboard/PageLoadDistribution/index.tsx +++ b/x-pack/plugins/apm/public/components/app/RumDashboard/PageLoadDistribution/index.tsx @@ -51,7 +51,7 @@ export function PageLoadDistribution() { (callApmApi) => { if (start && end && serviceName) { return callApmApi({ - endpoint: 'GET /api/apm/rum-client/page-load-distribution', + endpoint: 'GET /internal/apm/ux/page-load-distribution', params: { query: { start, diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/PageLoadDistribution/use_breakdowns.ts b/x-pack/plugins/apm/public/components/app/RumDashboard/PageLoadDistribution/use_breakdowns.ts index 0cfa293c87844..ee3acac73211f 100644 --- a/x-pack/plugins/apm/public/components/app/RumDashboard/PageLoadDistribution/use_breakdowns.ts +++ b/x-pack/plugins/apm/public/components/app/RumDashboard/PageLoadDistribution/use_breakdowns.ts @@ -24,7 +24,7 @@ export const useBreakdowns = ({ percentileRange, field, value }: Props) => { (callApmApi) => { if (start && end && field && value) { return callApmApi({ - endpoint: 'GET /api/apm/rum-client/page-load-distribution/breakdown', + endpoint: 'GET /internal/apm/ux/page-load-distribution/breakdown', params: { query: { start, diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/PageViewsTrend/index.tsx b/x-pack/plugins/apm/public/components/app/RumDashboard/PageViewsTrend/index.tsx index 581260f5931e7..16605a83505ff 100644 --- a/x-pack/plugins/apm/public/components/app/RumDashboard/PageViewsTrend/index.tsx +++ b/x-pack/plugins/apm/public/components/app/RumDashboard/PageViewsTrend/index.tsx @@ -39,7 +39,7 @@ export function PageViewsTrend() { (callApmApi) => { if (start && end && serviceName) { return callApmApi({ - endpoint: 'GET /api/apm/rum-client/page-view-trends', + endpoint: 'GET /internal/apm/ux/page-view-trends', params: { query: { start, diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/Panels/WebApplicationSelect.tsx b/x-pack/plugins/apm/public/components/app/RumDashboard/Panels/WebApplicationSelect.tsx index 5b1cca0ec44fa..ecba89b2651ac 100644 --- a/x-pack/plugins/apm/public/components/app/RumDashboard/Panels/WebApplicationSelect.tsx +++ b/x-pack/plugins/apm/public/components/app/RumDashboard/Panels/WebApplicationSelect.tsx @@ -20,7 +20,7 @@ export function WebApplicationSelect() { (callApmApi) => { if (start && end) { return callApmApi({ - endpoint: 'GET /api/apm/rum-client/services', + endpoint: 'GET /internal/apm/ux/services', params: { query: { start, diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/RumHome.tsx b/x-pack/plugins/apm/public/components/app/RumDashboard/RumHome.tsx index cc4bd0d14e290..54c34121ea0cb 100644 --- a/x-pack/plugins/apm/public/components/app/RumDashboard/RumHome.tsx +++ b/x-pack/plugins/apm/public/components/app/RumDashboard/RumHome.tsx @@ -48,7 +48,7 @@ export function RumHome() { 'Enable RUM with the APM agent to collect user experience data.', } ), - href: core.http.basePath.prepend(`integrations/detail/apm`), + href: core.http.basePath.prepend(`/app/home#/tutorial/apm`), }, }, docsLink: core.docLinks.links.observability.guide, diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/URLFilter/URLSearch/render_option.tsx b/x-pack/plugins/apm/public/components/app/RumDashboard/URLFilter/URLSearch/render_option.tsx index f5f5a04353c50..4a4d8e9d3e191 100644 --- a/x-pack/plugins/apm/public/components/app/RumDashboard/URLFilter/URLSearch/render_option.tsx +++ b/x-pack/plugins/apm/public/components/app/RumDashboard/URLFilter/URLSearch/render_option.tsx @@ -8,7 +8,7 @@ import React, { ReactNode } from 'react'; import { EuiHighlight, EuiSelectableOption } from '@elastic/eui'; import styled from 'styled-components'; -import euiLightVars from '@elastic/eui/dist/eui_theme_light.json'; +import { euiLightVars } from '@kbn/ui-shared-deps-src/theme'; const StyledSpan = styled.span` color: ${euiLightVars.euiColorSecondaryText}; diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/URLFilter/URLSearch/use_url_search.tsx b/x-pack/plugins/apm/public/components/app/RumDashboard/URLFilter/URLSearch/use_url_search.tsx index 7b6b093c70367..8228ab4c6e83e 100644 --- a/x-pack/plugins/apm/public/components/app/RumDashboard/URLFilter/URLSearch/use_url_search.tsx +++ b/x-pack/plugins/apm/public/components/app/RumDashboard/URLFilter/URLSearch/use_url_search.tsx @@ -38,7 +38,7 @@ export const useUrlSearch = ({ popoverIsOpen, query }: Props) => { (callApmApi) => { if (uxQuery && popoverIsOpen) { return callApmApi({ - endpoint: 'GET /api/apm/rum-client/url-search', + endpoint: 'GET /internal/apm/ux/url-search', params: { query: { ...uxQuery, diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/UXMetrics/KeyUXMetrics.tsx b/x-pack/plugins/apm/public/components/app/RumDashboard/UXMetrics/KeyUXMetrics.tsx index b8766e8b5ce67..4eaf0dccc3225 100644 --- a/x-pack/plugins/apm/public/components/app/RumDashboard/UXMetrics/KeyUXMetrics.tsx +++ b/x-pack/plugins/apm/public/components/app/RumDashboard/UXMetrics/KeyUXMetrics.tsx @@ -56,7 +56,7 @@ export function KeyUXMetrics({ data, loading }: Props) { (callApmApi) => { if (uxQuery) { return callApmApi({ - endpoint: 'GET /api/apm/rum-client/long-task-metrics', + endpoint: 'GET /internal/apm/ux/long-task-metrics', params: { query: { ...uxQuery, diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/UXMetrics/index.tsx b/x-pack/plugins/apm/public/components/app/RumDashboard/UXMetrics/index.tsx index 673f045ecfb97..ab6843f94ee43 100644 --- a/x-pack/plugins/apm/public/components/app/RumDashboard/UXMetrics/index.tsx +++ b/x-pack/plugins/apm/public/components/app/RumDashboard/UXMetrics/index.tsx @@ -34,7 +34,7 @@ export function UXMetrics() { (callApmApi) => { if (uxQuery) { return callApmApi({ - endpoint: 'GET /api/apm/rum-client/web-core-vitals', + endpoint: 'GET /internal/apm/ux/web-core-vitals', params: { query: uxQuery, }, diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdown/index.tsx b/x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdown/index.tsx index f044890a9b649..7a19690a4582e 100644 --- a/x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdown/index.tsx +++ b/x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdown/index.tsx @@ -23,7 +23,7 @@ export function VisitorBreakdown() { if (start && end && serviceName) { return callApmApi({ - endpoint: 'GET /api/apm/rum-client/visitor-breakdown', + endpoint: 'GET /internal/apm/ux/visitor-breakdown', params: { query: { start, diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/ux_overview_fetchers.ts b/x-pack/plugins/apm/public/components/app/RumDashboard/ux_overview_fetchers.ts index 34dd2d53daf8e..61310a5c5ad2c 100644 --- a/x-pack/plugins/apm/public/components/app/RumDashboard/ux_overview_fetchers.ts +++ b/x-pack/plugins/apm/public/components/app/RumDashboard/ux_overview_fetchers.ts @@ -21,7 +21,7 @@ export const fetchUxOverviewDate = async ({ serviceName, }: FetchDataParams): Promise => { const data = await callApmApi({ - endpoint: 'GET /api/apm/rum-client/web-core-vitals', + endpoint: 'GET /internal/apm/ux/web-core-vitals', signal: null, params: { query: { diff --git a/x-pack/plugins/apm/public/components/app/Settings/schema/confirm_switch_modal.tsx b/x-pack/plugins/apm/public/components/app/Settings/schema/confirm_switch_modal.tsx index a13f31e8a6566..1847ea90bd7fa 100644 --- a/x-pack/plugins/apm/public/components/app/Settings/schema/confirm_switch_modal.tsx +++ b/x-pack/plugins/apm/public/components/app/Settings/schema/confirm_switch_modal.tsx @@ -58,12 +58,6 @@ export function ConfirmSwitchModal({ }} confirmButtonDisabled={!isConfirmChecked} > -

      - {i18n.translate('xpack.apm.settings.schema.confirm.descriptionText', { - defaultMessage: - 'Please note Stack monitoring is not currently supported with Fleet-managed APM.', - })} -

      {!hasUnsupportedConfigs && (

      {i18n.translate( diff --git a/x-pack/plugins/apm/public/components/app/correlations/context_popover/context_popover.tsx b/x-pack/plugins/apm/public/components/app/correlations/context_popover/context_popover.tsx index 4a0f7d81e24dc..7165aa67a5e5a 100644 --- a/x-pack/plugins/apm/public/components/app/correlations/context_popover/context_popover.tsx +++ b/x-pack/plugins/apm/public/components/app/correlations/context_popover/context_popover.tsx @@ -19,7 +19,7 @@ import { import React, { Fragment, useState } from 'react'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; -import { FieldStats } from '../../../../../common/search_strategies/field_stats_types'; +import { FieldStats } from '../../../../../common/correlations/field_stats_types'; import { OnAddFilter, TopValues } from './top_values'; import { useTheme } from '../../../../hooks/use_theme'; diff --git a/x-pack/plugins/apm/public/components/app/correlations/context_popover/top_values.tsx b/x-pack/plugins/apm/public/components/app/correlations/context_popover/top_values.tsx index 803b474fe7754..05b4f6d56fa45 100644 --- a/x-pack/plugins/apm/public/components/app/correlations/context_popover/top_values.tsx +++ b/x-pack/plugins/apm/public/components/app/correlations/context_popover/top_values.tsx @@ -14,7 +14,7 @@ import { EuiToolTip, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { FieldStats } from '../../../../../common/search_strategies/field_stats_types'; +import { FieldStats } from '../../../../../common/correlations/field_stats_types'; import { asPercent } from '../../../../../common/utils/formatters'; import { useTheme } from '../../../../hooks/use_theme'; diff --git a/x-pack/plugins/apm/public/components/app/correlations/correlations_log.tsx b/x-pack/plugins/apm/public/components/app/correlations/correlations_log.tsx deleted file mode 100644 index 2115918a71415..0000000000000 --- a/x-pack/plugins/apm/public/components/app/correlations/correlations_log.tsx +++ /dev/null @@ -1,38 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { EuiAccordion, EuiCode, EuiPanel } from '@elastic/eui'; -import React from 'react'; -import { i18n } from '@kbn/i18n'; -import { asAbsoluteDateTime } from '../../../../common/utils/formatters'; - -interface Props { - logMessages: string[]; -} -export function CorrelationsLog({ logMessages }: Props) { - return ( - - - {logMessages.map((logMessage, i) => { - const [timestamp, message] = logMessage.split(': '); - return ( -

      - - {asAbsoluteDateTime(timestamp)} {message} - -

      - ); - })} - - - ); -} 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 eda3b64c309cc..a2026b0a8abea 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 @@ -14,7 +14,7 @@ import type { Criteria } from '@elastic/eui/src/components/basic_table/basic_tab import { FETCH_STATUS } from '../../../hooks/use_fetcher'; import { useUiTracker } from '../../../../../observability/public'; import { useTheme } from '../../../hooks/use_theme'; -import type { FieldValuePair } from '../../../../common/search_strategies/types'; +import type { FieldValuePair } from '../../../../common/correlations/types'; const PAGINATION_SIZE_OPTIONS = [5, 10, 20, 50]; diff --git a/x-pack/plugins/apm/public/components/app/correlations/failed_transactions_correlations.tsx b/x-pack/plugins/apm/public/components/app/correlations/failed_transactions_correlations.tsx index 838671cbae7d9..f13d360444923 100644 --- a/x-pack/plugins/apm/public/components/app/correlations/failed_transactions_correlations.tsx +++ b/x-pack/plugins/apm/public/components/app/correlations/failed_transactions_correlations.tsx @@ -29,23 +29,16 @@ import type { Direction } from '@elastic/eui/src/services/sort/sort_direction'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; -import { - enableInspectEsQueries, - useUiTracker, -} from '../../../../../observability/public'; +import { useUiTracker } from '../../../../../observability/public'; import { asPercent } from '../../../../common/utils/formatters'; -import { FailedTransactionsCorrelation } from '../../../../common/search_strategies/failed_transactions_correlations/types'; -import { - APM_SEARCH_STRATEGIES, - DEFAULT_PERCENTILE_THRESHOLD, -} from '../../../../common/search_strategies/constants'; -import { FieldStats } from '../../../../common/search_strategies/field_stats_types'; +import { FailedTransactionsCorrelation } from '../../../../common/correlations/failed_transactions_correlations/types'; +import { DEFAULT_PERCENTILE_THRESHOLD } from '../../../../common/correlations/constants'; +import { FieldStats } from '../../../../common/correlations/field_stats_types'; import { useApmPluginContext } from '../../../context/apm_plugin/use_apm_plugin_context'; import { useLocalStorage } from '../../../hooks/useLocalStorage'; import { FETCH_STATUS } from '../../../hooks/use_fetcher'; -import { useSearchStrategy } from '../../../hooks/use_search_strategy'; import { useTheme } from '../../../hooks/use_theme'; import { ImpactBar } from '../../shared/ImpactBar'; @@ -53,14 +46,12 @@ import { push } from '../../shared/Links/url_helpers'; import { CorrelationsTable } from './correlations_table'; import { FailedTransactionsCorrelationsHelpPopover } from './failed_transactions_correlations_help_popover'; -import { isErrorMessage } from './utils/is_error_message'; import { getFailedTransactionsCorrelationImpactLabel } from './utils/get_failed_transactions_correlation_impact_label'; import { getOverallHistogram } from './utils/get_overall_histogram'; import { TransactionDistributionChart, TransactionDistributionChartData, } from '../../shared/charts/transaction_distribution_chart'; -import { CorrelationsLog } from './correlations_log'; import { CorrelationsEmptyStatePrompt } from './empty_state_prompt'; import { CrossClusterSearchCompatibilityWarning } from './cross_cluster_search_warning'; import { CorrelationsProgressControls } from './progress_controls'; @@ -68,6 +59,8 @@ import { useTransactionColors } from './use_transaction_colors'; import { CorrelationsContextPopover } from './context_popover'; import { OnAddFilter } from './context_popover/top_values'; +import { useFailedTransactionsCorrelations } from './use_failed_transactions_correlations'; + export function FailedTransactionsCorrelations({ onFilter, }: { @@ -77,18 +70,12 @@ export function FailedTransactionsCorrelations({ const transactionColors = useTransactionColors(); const { - core: { notifications, uiSettings }, + core: { notifications }, } = useApmPluginContext(); const trackApmEvent = useUiTracker({ app: 'apm' }); - const inspectEnabled = uiSettings.get(enableInspectEsQueries); - - const { progress, response, startFetch, cancelFetch } = useSearchStrategy( - APM_SEARCH_STRATEGIES.APM_FAILED_TRANSACTIONS_CORRELATIONS, - { - percentileThreshold: DEFAULT_PERCENTILE_THRESHOLD, - } - ); + const { progress, response, startFetch, cancelFetch } = + useFailedTransactionsCorrelations(); const fieldStats: Record | undefined = useMemo(() => { return response.fieldStats?.reduce((obj, field) => { @@ -97,7 +84,6 @@ export function FailedTransactionsCorrelations({ }, {} as Record); }, [response?.fieldStats]); - const progressNormalized = progress.loaded / progress.total; const { overallHistogram, hasData, status } = getOverallHistogram( response, progress.isRunning @@ -368,7 +354,7 @@ export function FailedTransactionsCorrelations({ }, [fieldStats, onAddFilter, showStats]); useEffect(() => { - if (isErrorMessage(progress.error)) { + if (progress.error) { notifications.toasts.addDanger({ title: i18n.translate( 'xpack.apm.correlations.failedTransactions.errorTitle', @@ -377,7 +363,7 @@ export function FailedTransactionsCorrelations({ 'An error occurred performing correlations on failed transactions', } ), - text: progress.error.toString(), + text: progress.error, }); } }, [progress.error, notifications.toasts]); @@ -439,7 +425,7 @@ export function FailedTransactionsCorrelations({ const showCorrelationsEmptyStatePrompt = correlationTerms.length < 1 && - (progressNormalized === 1 || !progress.isRunning); + (progress.loaded === 1 || !progress.isRunning); const transactionDistributionChartData: TransactionDistributionChartData[] = []; @@ -457,8 +443,8 @@ export function FailedTransactionsCorrelations({ if (Array.isArray(response.errorHistogram)) { transactionDistributionChartData.push({ id: i18n.translate( - 'xpack.apm.transactionDistribution.chart.allFailedTransactionsLabel', - { defaultMessage: 'All failed transactions' } + 'xpack.apm.transactionDistribution.chart.failedTransactionsLabel', + { defaultMessage: 'Failed transactions' } ), histogram: response.errorHistogram, }); @@ -525,7 +511,7 @@ export function FailedTransactionsCorrelations({ , allTransactions: ( @@ -536,13 +522,13 @@ export function FailedTransactionsCorrelations({ /> ), - allFailedTransactions: ( + failedTransactions: ( ), @@ -621,7 +607,7 @@ export function FailedTransactionsCorrelations({ }
      - {inspectEnabled && }
    ); } diff --git a/x-pack/plugins/apm/public/components/app/correlations/latency_correlations.test.tsx b/x-pack/plugins/apm/public/components/app/correlations/latency_correlations.test.tsx index 918f94e64ef09..b6bd267e746b3 100644 --- a/x-pack/plugins/apm/public/components/app/correlations/latency_correlations.test.tsx +++ b/x-pack/plugins/apm/public/components/app/correlations/latency_correlations.test.tsx @@ -18,8 +18,7 @@ import { dataPluginMock } from 'src/plugins/data/public/mocks'; import type { IKibanaSearchResponse } from 'src/plugins/data/public'; import { EuiThemeProvider } from 'src/plugins/kibana_react/common'; import { createKibanaReactContext } from 'src/plugins/kibana_react/public'; -import type { LatencyCorrelationsRawResponse } from '../../../../common/search_strategies/latency_correlations/types'; -import type { RawResponseBase } from '../../../../common/search_strategies/types'; +import type { LatencyCorrelationsResponse } from '../../../../common/correlations/latency_correlations/types'; import { MockUrlParamsContextProvider } from '../../../context/url_params_context/mock_url_params_context_provider'; import { ApmPluginContextValue } from '../../../context/apm_plugin/apm_plugin_context'; import { @@ -35,9 +34,7 @@ function Wrapper({ dataSearchResponse, }: { children?: ReactNode; - dataSearchResponse: IKibanaSearchResponse< - LatencyCorrelationsRawResponse & RawResponseBase - >; + dataSearchResponse: IKibanaSearchResponse; }) { const mockDataSearch = jest.fn(() => of(dataSearchResponse)); @@ -99,9 +96,7 @@ describe('correlations', () => { isRunning: true, rawResponse: { ccsWarning: false, - took: 1234, latencyCorrelations: [], - log: [], }, }} > @@ -122,9 +117,7 @@ describe('correlations', () => { isRunning: false, rawResponse: { ccsWarning: false, - took: 1234, latencyCorrelations: [], - log: [], }, }} > 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 db6f3ad63f00d..b67adc03d40e9 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 @@ -25,22 +25,15 @@ import { EuiTableSortingType } from '@elastic/eui/src/components/basic_table/tab import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; -import { - enableInspectEsQueries, - useUiTracker, -} from '../../../../../observability/public'; +import { useUiTracker } from '../../../../../observability/public'; import { asPreciseDecimal } from '../../../../common/utils/formatters'; -import { - APM_SEARCH_STRATEGIES, - DEFAULT_PERCENTILE_THRESHOLD, -} from '../../../../common/search_strategies/constants'; -import { LatencyCorrelation } from '../../../../common/search_strategies/latency_correlations/types'; -import { FieldStats } from '../../../../common/search_strategies/field_stats_types'; +import { DEFAULT_PERCENTILE_THRESHOLD } from '../../../../common/correlations/constants'; +import { LatencyCorrelation } from '../../../../common/correlations/latency_correlations/types'; +import { FieldStats } from '../../../../common/correlations/field_stats_types'; import { useApmPluginContext } from '../../../context/apm_plugin/use_apm_plugin_context'; import { FETCH_STATUS } from '../../../hooks/use_fetcher'; -import { useSearchStrategy } from '../../../hooks/use_search_strategy'; import { TransactionDistributionChart, @@ -50,33 +43,24 @@ import { push } from '../../shared/Links/url_helpers'; import { CorrelationsTable } from './correlations_table'; import { LatencyCorrelationsHelpPopover } from './latency_correlations_help_popover'; -import { isErrorMessage } from './utils/is_error_message'; import { getOverallHistogram } from './utils/get_overall_histogram'; -import { CorrelationsLog } from './correlations_log'; import { CorrelationsEmptyStatePrompt } from './empty_state_prompt'; import { CrossClusterSearchCompatibilityWarning } from './cross_cluster_search_warning'; import { CorrelationsProgressControls } from './progress_controls'; import { useTransactionColors } from './use_transaction_colors'; import { CorrelationsContextPopover } from './context_popover'; import { OnAddFilter } from './context_popover/top_values'; +import { useLatencyCorrelations } from './use_latency_correlations'; export function LatencyCorrelations({ onFilter }: { onFilter: () => void }) { const transactionColors = useTransactionColors(); const { - core: { notifications, uiSettings }, + core: { notifications }, } = useApmPluginContext(); - const displayLog = uiSettings.get(enableInspectEsQueries); - - const { progress, response, startFetch, cancelFetch } = useSearchStrategy( - APM_SEARCH_STRATEGIES.APM_LATENCY_CORRELATIONS, - { - percentileThreshold: DEFAULT_PERCENTILE_THRESHOLD, - analyzeCorrelations: true, - } - ); - const progressNormalized = progress.loaded / progress.total; + const { progress, response, startFetch, cancelFetch } = + useLatencyCorrelations(); const { overallHistogram, hasData, status } = getOverallHistogram( response, progress.isRunning @@ -90,7 +74,7 @@ export function LatencyCorrelations({ onFilter }: { onFilter: () => void }) { }, [response?.fieldStats]); useEffect(() => { - if (isErrorMessage(progress.error)) { + if (progress.error) { notifications.toasts.addDanger({ title: i18n.translate( 'xpack.apm.correlations.latencyCorrelations.errorTitle', @@ -98,7 +82,7 @@ export function LatencyCorrelations({ onFilter }: { onFilter: () => void }) { defaultMessage: 'An error occurred fetching correlations', } ), - text: progress.error.toString(), + text: progress.error, }); } }, [progress.error, notifications.toasts]); @@ -288,8 +272,7 @@ export function LatencyCorrelations({ onFilter }: { onFilter: () => void }) { const showCorrelationsTable = progress.isRunning || histogramTerms.length > 0; const showCorrelationsEmptyStatePrompt = - histogramTerms.length < 1 && - (progressNormalized === 1 || !progress.isRunning); + histogramTerms.length < 1 && (progress.loaded === 1 || !progress.isRunning); const transactionDistributionChartData: TransactionDistributionChartData[] = []; @@ -382,7 +365,7 @@ export function LatencyCorrelations({ onFilter }: { onFilter: () => void }) { void }) { )} {showCorrelationsEmptyStatePrompt && }
    - {displayLog && }
    ); } diff --git a/x-pack/plugins/apm/public/components/app/correlations/use_failed_transactions_correlations.test.tsx b/x-pack/plugins/apm/public/components/app/correlations/use_failed_transactions_correlations.test.tsx new file mode 100644 index 0000000000000..929cc4f7f4cd3 --- /dev/null +++ b/x-pack/plugins/apm/public/components/app/correlations/use_failed_transactions_correlations.test.tsx @@ -0,0 +1,399 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license 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, { ReactNode } from 'react'; +import { merge } from 'lodash'; +import { createMemoryHistory } from 'history'; +import { renderHook, act } from '@testing-library/react-hooks'; + +import { ApmPluginContextValue } from '../../../context/apm_plugin/apm_plugin_context'; +import { + mockApmPluginContextValue, + MockApmPluginContextWrapper, +} from '../../../context/apm_plugin/mock_apm_plugin_context'; +import { delay } from '../../../utils/testHelpers'; + +import { fromQuery } from '../../shared/Links/url_helpers'; + +import { useFailedTransactionsCorrelations } from './use_failed_transactions_correlations'; + +function wrapper({ + children, + error = false, +}: { + children?: ReactNode; + error: boolean; +}) { + const httpMethodMock = jest.fn().mockImplementation(async (endpoint) => { + await delay(100); + if (error) { + throw new Error('Something went wrong'); + } + switch (endpoint) { + case '/internal/apm/latency/overall_distribution': + return { + overallHistogram: [{ key: 'the-key', doc_count: 1234 }], + percentileThresholdValue: 1.234, + }; + case '/internal/apm/correlations/field_candidates': + return { fieldCandidates: ['field-1', 'field2'] }; + case '/internal/apm/correlations/field_value_pairs': + return { + fieldValuePairs: [ + { fieldName: 'field-name-1', fieldValue: 'field-value-1' }, + ], + }; + case '/internal/apm/correlations/p_values': + return { + failedTransactionsCorrelations: [ + { + fieldName: 'field-name-1', + fieldValue: 'field-value-1', + doc_count: 123, + bg_count: 1234, + score: 0.66, + pValue: 0.01, + normalizedScore: 0.85, + failurePercentage: 30, + successPercentage: 70, + histogram: [{ key: 'the-key', doc_count: 123 }], + }, + ], + }; + case '/internal/apm/correlations/field_stats': + return { + stats: [ + { fieldName: 'field-name-1', count: 123 }, + { fieldName: 'field-name-2', count: 1111 }, + ], + }; + default: + return {}; + } + }); + + const history = createMemoryHistory(); + jest.spyOn(history, 'push'); + jest.spyOn(history, 'replace'); + + history.replace({ + pathname: '/services/the-service-name/transactions/view', + search: fromQuery({ + transactionName: 'the-transaction-name', + rangeFrom: 'now-15m', + rangeTo: 'now', + }), + }); + + const mockPluginContext = merge({}, mockApmPluginContextValue, { + core: { http: { get: httpMethodMock, post: httpMethodMock } }, + }) as unknown as ApmPluginContextValue; + + return ( + + {children} + + ); +} + +describe('useFailedTransactionsCorrelations', () => { + beforeEach(async () => { + jest.useFakeTimers(); + }); + // Running all pending timers and switching to real timers using Jest + afterEach(() => { + jest.runOnlyPendingTimers(); + jest.useRealTimers(); + }); + + describe('when successfully loading results', () => { + it('should automatically start fetching results', async () => { + const { result, unmount } = renderHook( + () => useFailedTransactionsCorrelations(), + { + wrapper, + } + ); + + try { + expect(result.current.progress).toEqual({ + isRunning: true, + loaded: 0, + }); + expect(result.current.response).toEqual({ ccsWarning: false }); + expect(typeof result.current.startFetch).toEqual('function'); + expect(typeof result.current.cancelFetch).toEqual('function'); + } finally { + unmount(); + } + }); + + it('should not have received any results after 50ms', async () => { + const { result, unmount } = renderHook( + () => useFailedTransactionsCorrelations(), + { + wrapper, + } + ); + + try { + jest.advanceTimersByTime(50); + + expect(result.current.progress).toEqual({ + isRunning: true, + loaded: 0, + }); + expect(result.current.response).toEqual({ ccsWarning: false }); + } finally { + unmount(); + } + }); + + it('should receive partial updates and finish running', async () => { + const { result, unmount, waitFor } = renderHook( + () => useFailedTransactionsCorrelations(), + { + wrapper, + } + ); + + try { + jest.advanceTimersByTime(50); + await waitFor(() => expect(result.current.progress.loaded).toBe(0)); + jest.advanceTimersByTime(100); + await waitFor(() => expect(result.current.progress.loaded).toBe(0)); + jest.advanceTimersByTime(100); + await waitFor(() => expect(result.current.progress.loaded).toBe(0.05)); + + expect(result.current.progress).toEqual({ + error: undefined, + isRunning: true, + loaded: 0.05, + }); + expect(result.current.response).toEqual({ + ccsWarning: false, + fieldStats: undefined, + errorHistogram: [ + { + doc_count: 1234, + key: 'the-key', + }, + ], + failedTransactionsCorrelations: undefined, + overallHistogram: [ + { + doc_count: 1234, + key: 'the-key', + }, + ], + percentileThresholdValue: 1.234, + }); + + jest.advanceTimersByTime(100); + await waitFor(() => expect(result.current.progress.loaded).toBe(0.1)); + + // field candidates are an implementation detail and + // will not be exposed, it will just set loaded to 0.1. + expect(result.current.progress).toEqual({ + error: undefined, + isRunning: true, + loaded: 0.1, + }); + + jest.advanceTimersByTime(100); + await waitFor(() => expect(result.current.progress.loaded).toBe(1)); + + expect(result.current.progress).toEqual({ + error: undefined, + isRunning: true, + loaded: 1, + }); + + expect(result.current.response).toEqual({ + ccsWarning: false, + fieldStats: undefined, + errorHistogram: [ + { + doc_count: 1234, + key: 'the-key', + }, + ], + failedTransactionsCorrelations: [ + { + fieldName: 'field-name-1', + fieldValue: 'field-value-1', + doc_count: 123, + bg_count: 1234, + score: 0.66, + pValue: 0.01, + normalizedScore: 0.85, + failurePercentage: 30, + successPercentage: 70, + histogram: [{ key: 'the-key', doc_count: 123 }], + }, + ], + overallHistogram: [ + { + doc_count: 1234, + key: 'the-key', + }, + ], + percentileThresholdValue: 1.234, + }); + + jest.advanceTimersByTime(100); + await waitFor(() => + expect(result.current.response.fieldStats).toBeDefined() + ); + + expect(result.current.progress).toEqual({ + error: undefined, + isRunning: false, + loaded: 1, + }); + + expect(result.current.response).toEqual({ + ccsWarning: false, + fieldStats: [ + { fieldName: 'field-name-1', count: 123 }, + { fieldName: 'field-name-2', count: 1111 }, + ], + errorHistogram: [ + { + doc_count: 1234, + key: 'the-key', + }, + ], + failedTransactionsCorrelations: [ + { + fieldName: 'field-name-1', + fieldValue: 'field-value-1', + doc_count: 123, + bg_count: 1234, + score: 0.66, + pValue: 0.01, + normalizedScore: 0.85, + failurePercentage: 30, + successPercentage: 70, + histogram: [{ key: 'the-key', doc_count: 123 }], + }, + ], + overallHistogram: [ + { + doc_count: 1234, + key: 'the-key', + }, + ], + percentileThresholdValue: 1.234, + }); + } finally { + unmount(); + } + }); + }); + describe('when throwing an error', () => { + it('should automatically start fetching results', async () => { + const { result, unmount } = renderHook( + () => useFailedTransactionsCorrelations(), + { + wrapper, + initialProps: { + error: true, + }, + } + ); + + try { + expect(result.current.progress).toEqual({ + isRunning: true, + loaded: 0, + }); + } finally { + unmount(); + } + }); + + it('should still be running after 50ms', async () => { + const { result, unmount } = renderHook( + () => useFailedTransactionsCorrelations(), + { + wrapper, + initialProps: { + error: true, + }, + } + ); + + try { + jest.advanceTimersByTime(50); + + expect(result.current.progress).toEqual({ + isRunning: true, + loaded: 0, + }); + expect(result.current.response).toEqual({ ccsWarning: false }); + } finally { + unmount(); + } + }); + + it('should stop and return an error after more than 100ms', async () => { + const { result, unmount, waitFor } = renderHook( + () => useFailedTransactionsCorrelations(), + { + wrapper, + initialProps: { + error: true, + }, + } + ); + + try { + jest.advanceTimersByTime(150); + await waitFor(() => + expect(result.current.progress.error).toBeDefined() + ); + + expect(result.current.progress).toEqual({ + error: 'Something went wrong', + isRunning: false, + loaded: 0, + }); + } finally { + unmount(); + } + }); + }); + + describe('when canceled', () => { + it('should stop running', async () => { + const { result, unmount, waitFor } = renderHook( + () => useFailedTransactionsCorrelations(), + { + wrapper, + } + ); + + try { + jest.advanceTimersByTime(50); + await waitFor(() => expect(result.current.progress.loaded).toBe(0)); + + expect(result.current.progress.isRunning).toBe(true); + + act(() => { + result.current.cancelFetch(); + }); + + await waitFor(() => + expect(result.current.progress.isRunning).toEqual(false) + ); + } finally { + unmount(); + } + }); + }); +}); diff --git a/x-pack/plugins/apm/public/components/app/correlations/use_failed_transactions_correlations.ts b/x-pack/plugins/apm/public/components/app/correlations/use_failed_transactions_correlations.ts new file mode 100644 index 0000000000000..163223e744a22 --- /dev/null +++ b/x-pack/plugins/apm/public/components/app/correlations/use_failed_transactions_correlations.ts @@ -0,0 +1,257 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { useCallback, useEffect, useMemo, useReducer, useRef } from 'react'; +import { chunk, debounce } from 'lodash'; + +import { IHttpFetchError, ResponseErrorBody } from 'src/core/public'; + +import { EVENT_OUTCOME } from '../../../../common/elasticsearch_fieldnames'; +import { EventOutcome } from '../../../../common/event_outcome'; +import { + DEBOUNCE_INTERVAL, + DEFAULT_PERCENTILE_THRESHOLD, +} from '../../../../common/correlations/constants'; +import type { + FailedTransactionsCorrelation, + FailedTransactionsCorrelationsResponse, +} from '../../../../common/correlations/failed_transactions_correlations/types'; + +import { callApmApi } from '../../../services/rest/createCallApmApi'; + +import { + getInitialResponse, + getFailedTransactionsCorrelationsSortedByScore, + getReducer, + CorrelationsProgress, +} from './utils/analysis_hook_utils'; +import { useFetchParams } from './use_fetch_params'; + +// Overall progress is a float from 0 to 1. +const LOADED_OVERALL_HISTOGRAM = 0.05; +const LOADED_FIELD_CANDIDATES = LOADED_OVERALL_HISTOGRAM + 0.05; +const LOADED_DONE = 1; +const PROGRESS_STEP_P_VALUES = 0.9; + +export function useFailedTransactionsCorrelations() { + const fetchParams = useFetchParams(); + + // This use of useReducer (the dispatch function won't get reinstantiated + // on every update) and debounce avoids flooding consuming components with updates. + // `setResponse.flush()` can be used to enforce an update. + const [response, setResponseUnDebounced] = useReducer( + getReducer(), + getInitialResponse() + ); + const setResponse = useMemo( + () => debounce(setResponseUnDebounced, DEBOUNCE_INTERVAL), + [] + ); + + const abortCtrl = useRef(new AbortController()); + + const startFetch = useCallback(async () => { + abortCtrl.current.abort(); + abortCtrl.current = new AbortController(); + + setResponse({ + ...getInitialResponse(), + isRunning: true, + // explicitly set these to undefined to override a possible previous state. + error: undefined, + failedTransactionsCorrelations: undefined, + percentileThresholdValue: undefined, + overallHistogram: undefined, + errorHistogram: undefined, + fieldStats: undefined, + }); + setResponse.flush(); + + try { + // `responseUpdate` will be enriched with additional data with subsequent + // calls to the overall histogram, field candidates, field value pairs, correlation results + // and histogram data for statistically significant results. + const responseUpdate: FailedTransactionsCorrelationsResponse = { + ccsWarning: false, + }; + + const [overallHistogramResponse, errorHistogramRespone] = + await Promise.all([ + // Initial call to fetch the overall distribution for the log-log plot. + callApmApi({ + endpoint: 'POST /internal/apm/latency/overall_distribution', + signal: abortCtrl.current.signal, + params: { + body: { + ...fetchParams, + percentileThreshold: DEFAULT_PERCENTILE_THRESHOLD, + }, + }, + }), + callApmApi({ + endpoint: 'POST /internal/apm/latency/overall_distribution', + signal: abortCtrl.current.signal, + params: { + body: { + ...fetchParams, + percentileThreshold: DEFAULT_PERCENTILE_THRESHOLD, + termFilters: [ + { + fieldName: EVENT_OUTCOME, + fieldValue: EventOutcome.failure, + }, + ], + }, + }, + }), + ]); + + const { overallHistogram, percentileThresholdValue } = + overallHistogramResponse; + const { overallHistogram: errorHistogram } = errorHistogramRespone; + + responseUpdate.errorHistogram = errorHistogram; + responseUpdate.overallHistogram = overallHistogram; + responseUpdate.percentileThresholdValue = percentileThresholdValue; + + if (abortCtrl.current.signal.aborted) { + return; + } + + setResponse({ + ...responseUpdate, + loaded: LOADED_OVERALL_HISTOGRAM, + }); + setResponse.flush(); + + const { fieldCandidates: candidates } = await callApmApi({ + endpoint: 'GET /internal/apm/correlations/field_candidates', + signal: abortCtrl.current.signal, + params: { + query: fetchParams, + }, + }); + + if (abortCtrl.current.signal.aborted) { + return; + } + + const fieldCandidates = candidates.filter((t) => !(t === EVENT_OUTCOME)); + + setResponse({ + loaded: LOADED_FIELD_CANDIDATES, + }); + setResponse.flush(); + + const failedTransactionsCorrelations: FailedTransactionsCorrelation[] = + []; + const fieldsToSample = new Set(); + const chunkSize = 10; + let chunkLoadCounter = 0; + + const fieldCandidatesChunks = chunk(fieldCandidates, chunkSize); + + for (const fieldCandidatesChunk of fieldCandidatesChunks) { + const pValues = await callApmApi({ + endpoint: 'POST /internal/apm/correlations/p_values', + signal: abortCtrl.current.signal, + params: { + body: { ...fetchParams, fieldCandidates: fieldCandidatesChunk }, + }, + }); + + if (pValues.failedTransactionsCorrelations.length > 0) { + pValues.failedTransactionsCorrelations.forEach((d) => { + fieldsToSample.add(d.fieldName); + }); + failedTransactionsCorrelations.push( + ...pValues.failedTransactionsCorrelations + ); + responseUpdate.failedTransactionsCorrelations = + getFailedTransactionsCorrelationsSortedByScore([ + ...failedTransactionsCorrelations, + ]); + } + + chunkLoadCounter++; + setResponse({ + ...responseUpdate, + loaded: + LOADED_FIELD_CANDIDATES + + (chunkLoadCounter / fieldCandidatesChunks.length) * + PROGRESS_STEP_P_VALUES, + }); + + if (abortCtrl.current.signal.aborted) { + return; + } + } + + setResponse.flush(); + + const { stats } = await callApmApi({ + endpoint: 'POST /internal/apm/correlations/field_stats', + signal: abortCtrl.current.signal, + params: { + body: { + ...fetchParams, + fieldsToSample: [...fieldsToSample], + }, + }, + }); + + responseUpdate.fieldStats = stats; + setResponse({ ...responseUpdate, loaded: LOADED_DONE, isRunning: false }); + setResponse.flush(); + } catch (e) { + if (!abortCtrl.current.signal.aborted) { + const err = e as Error | IHttpFetchError; + setResponse({ + error: + 'response' in err + ? err.body?.message ?? err.response?.statusText + : err.message, + isRunning: false, + }); + setResponse.flush(); + } + } + }, [fetchParams, setResponse]); + + const cancelFetch = useCallback(() => { + abortCtrl.current.abort(); + setResponse({ + isRunning: false, + }); + setResponse.flush(); + }, [setResponse]); + + // auto-update + useEffect(() => { + startFetch(); + return () => { + abortCtrl.current.abort(); + }; + }, [startFetch, cancelFetch]); + + const { error, loaded, isRunning, ...returnedResponse } = response; + const progress = useMemo( + () => ({ + error, + loaded, + isRunning, + }), + [error, loaded, isRunning] + ); + + return { + progress, + response: returnedResponse, + startFetch, + cancelFetch, + }; +} diff --git a/x-pack/plugins/apm/public/components/app/correlations/use_fetch_params.ts b/x-pack/plugins/apm/public/components/app/correlations/use_fetch_params.ts new file mode 100644 index 0000000000000..827604f776c5a --- /dev/null +++ b/x-pack/plugins/apm/public/components/app/correlations/use_fetch_params.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 { useMemo } from 'react'; + +import { useApmServiceContext } from '../../../context/apm_service/use_apm_service_context'; + +import { useApmParams } from '../../../hooks/use_apm_params'; +import { useTimeRange } from '../../../hooks/use_time_range'; + +export const useFetchParams = () => { + const { serviceName } = useApmServiceContext(); + + const { + query: { + kuery, + environment, + rangeFrom, + rangeTo, + transactionName, + transactionType, + }, + } = useApmParams('/services/{serviceName}/transactions/view'); + + const { start, end } = useTimeRange({ rangeFrom, rangeTo }); + + return useMemo( + () => ({ + serviceName, + transactionName, + transactionType, + kuery, + environment, + start, + end, + }), + [ + serviceName, + transactionName, + transactionType, + kuery, + environment, + start, + end, + ] + ); +}; diff --git a/x-pack/plugins/apm/public/components/app/correlations/use_latency_correlations.test.tsx b/x-pack/plugins/apm/public/components/app/correlations/use_latency_correlations.test.tsx new file mode 100644 index 0000000000000..90d976c389c58 --- /dev/null +++ b/x-pack/plugins/apm/public/components/app/correlations/use_latency_correlations.test.tsx @@ -0,0 +1,360 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license 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, { ReactNode } from 'react'; +import { merge } from 'lodash'; +import { createMemoryHistory } from 'history'; +import { renderHook, act } from '@testing-library/react-hooks'; + +import { ApmPluginContextValue } from '../../../context/apm_plugin/apm_plugin_context'; +import { + mockApmPluginContextValue, + MockApmPluginContextWrapper, +} from '../../../context/apm_plugin/mock_apm_plugin_context'; +import { delay } from '../../../utils/testHelpers'; + +import { fromQuery } from '../../shared/Links/url_helpers'; + +import { useLatencyCorrelations } from './use_latency_correlations'; + +function wrapper({ + children, + error = false, +}: { + children?: ReactNode; + error: boolean; +}) { + const httpMethodMock = jest.fn().mockImplementation(async (endpoint) => { + await delay(100); + if (error) { + throw new Error('Something went wrong'); + } + switch (endpoint) { + case '/internal/apm/latency/overall_distribution': + return { + overallHistogram: [{ key: 'the-key', doc_count: 1234 }], + percentileThresholdValue: 1.234, + }; + case '/internal/apm/correlations/field_candidates': + return { fieldCandidates: ['field-1', 'field2'] }; + case '/internal/apm/correlations/field_value_pairs': + return { + fieldValuePairs: [ + { fieldName: 'field-name-1', fieldValue: 'field-value-1' }, + ], + }; + case '/internal/apm/correlations/significant_correlations': + return { + latencyCorrelations: [ + { + fieldName: 'field-name-1', + fieldValue: 'field-value-1', + correlation: 0.5, + histogram: [{ key: 'the-key', doc_count: 123 }], + ksTest: 0.001, + }, + ], + }; + case '/internal/apm/correlations/field_stats': + return { + stats: [ + { fieldName: 'field-name-1', count: 123 }, + { fieldName: 'field-name-2', count: 1111 }, + ], + }; + default: + return {}; + } + }); + + const history = createMemoryHistory(); + jest.spyOn(history, 'push'); + jest.spyOn(history, 'replace'); + + history.replace({ + pathname: '/services/the-service-name/transactions/view', + search: fromQuery({ + transactionName: 'the-transaction-name', + rangeFrom: 'now-15m', + rangeTo: 'now', + }), + }); + + const mockPluginContext = merge({}, mockApmPluginContextValue, { + core: { http: { get: httpMethodMock, post: httpMethodMock } }, + }) as unknown as ApmPluginContextValue; + + return ( + + {children} + + ); +} + +describe('useLatencyCorrelations', () => { + beforeEach(async () => { + jest.useFakeTimers(); + }); + afterEach(() => { + jest.useRealTimers(); + }); + + describe('when successfully loading results', () => { + it('should automatically start fetching results', async () => { + const { result, unmount } = renderHook(() => useLatencyCorrelations(), { + wrapper, + }); + + try { + expect(result.current.progress).toEqual({ + isRunning: true, + loaded: 0, + }); + expect(result.current.response).toEqual({ ccsWarning: false }); + expect(typeof result.current.startFetch).toEqual('function'); + expect(typeof result.current.cancelFetch).toEqual('function'); + } finally { + unmount(); + } + }); + + it('should not have received any results after 50ms', async () => { + const { result, unmount } = renderHook(() => useLatencyCorrelations(), { + wrapper, + }); + + try { + jest.advanceTimersByTime(50); + + expect(result.current.progress).toEqual({ + isRunning: true, + loaded: 0, + }); + expect(result.current.response).toEqual({ ccsWarning: false }); + } finally { + unmount(); + } + }); + + it('should receive partial updates and finish running', async () => { + const { result, unmount, waitFor } = renderHook( + () => useLatencyCorrelations(), + { + wrapper, + } + ); + + try { + jest.advanceTimersByTime(150); + await waitFor(() => expect(result.current.progress.loaded).toBe(0.05)); + + expect(result.current.progress).toEqual({ + error: undefined, + isRunning: true, + loaded: 0.05, + }); + expect(result.current.response).toEqual({ + ccsWarning: false, + fieldStats: undefined, + latencyCorrelations: undefined, + overallHistogram: [ + { + doc_count: 1234, + key: 'the-key', + }, + ], + percentileThresholdValue: 1.234, + }); + + jest.advanceTimersByTime(100); + await waitFor(() => expect(result.current.progress.loaded).toBe(0.1)); + + // field candidates are an implementation detail and + // will not be exposed, it will just set loaded to 0.1. + expect(result.current.progress).toEqual({ + error: undefined, + isRunning: true, + loaded: 0.1, + }); + + jest.advanceTimersByTime(100); + await waitFor(() => expect(result.current.progress.loaded).toBe(0.4)); + + // field value pairs are an implementation detail and + // will not be exposed, it will just set loaded to 0.4. + expect(result.current.progress).toEqual({ + error: undefined, + isRunning: true, + loaded: 0.4, + }); + + jest.advanceTimersByTime(100); + await waitFor(() => expect(result.current.progress.loaded).toBe(1)); + + expect(result.current.progress).toEqual({ + error: undefined, + isRunning: true, + loaded: 1, + }); + + expect(result.current.response).toEqual({ + ccsWarning: false, + fieldStats: undefined, + latencyCorrelations: [ + { + fieldName: 'field-name-1', + fieldValue: 'field-value-1', + correlation: 0.5, + histogram: [{ key: 'the-key', doc_count: 123 }], + ksTest: 0.001, + }, + ], + overallHistogram: [ + { + doc_count: 1234, + key: 'the-key', + }, + ], + percentileThresholdValue: 1.234, + }); + + jest.advanceTimersByTime(100); + await waitFor(() => + expect(result.current.response.fieldStats).toBeDefined() + ); + + expect(result.current.progress).toEqual({ + error: undefined, + isRunning: false, + loaded: 1, + }); + + expect(result.current.response).toEqual({ + ccsWarning: false, + fieldStats: [ + { fieldName: 'field-name-1', count: 123 }, + { fieldName: 'field-name-2', count: 1111 }, + ], + latencyCorrelations: [ + { + fieldName: 'field-name-1', + fieldValue: 'field-value-1', + correlation: 0.5, + histogram: [{ key: 'the-key', doc_count: 123 }], + ksTest: 0.001, + }, + ], + overallHistogram: [ + { + doc_count: 1234, + key: 'the-key', + }, + ], + percentileThresholdValue: 1.234, + }); + } finally { + unmount(); + } + }); + }); + + describe('when throwing an error', () => { + it('should automatically start fetching results', async () => { + const { result, unmount } = renderHook(() => useLatencyCorrelations(), { + wrapper, + initialProps: { + error: true, + }, + }); + + try { + expect(result.current.progress).toEqual({ + isRunning: true, + loaded: 0, + }); + } finally { + unmount(); + } + }); + + it('should still be running after 50ms', async () => { + const { result, unmount } = renderHook(() => useLatencyCorrelations(), { + wrapper, + initialProps: { + error: true, + }, + }); + + try { + jest.advanceTimersByTime(50); + + expect(result.current.progress).toEqual({ + isRunning: true, + loaded: 0, + }); + expect(result.current.response).toEqual({ ccsWarning: false }); + } finally { + unmount(); + } + }); + + it('should stop and return an error after more than 100ms', async () => { + const { result, unmount, waitFor } = renderHook( + () => useLatencyCorrelations(), + { + wrapper, + initialProps: { + error: true, + }, + } + ); + + try { + jest.advanceTimersByTime(150); + await waitFor(() => + expect(result.current.progress.error).toBeDefined() + ); + + expect(result.current.progress).toEqual({ + error: 'Something went wrong', + isRunning: false, + loaded: 0, + }); + } finally { + unmount(); + } + }); + }); + + describe('when canceled', () => { + it('should stop running', async () => { + const { result, unmount, waitFor } = renderHook( + () => useLatencyCorrelations(), + { + wrapper, + } + ); + + try { + jest.advanceTimersByTime(150); + await waitFor(() => expect(result.current.progress.loaded).toBe(0.05)); + + expect(result.current.progress.isRunning).toBe(true); + + act(() => { + result.current.cancelFetch(); + }); + + await waitFor(() => + expect(result.current.progress.isRunning).toEqual(false) + ); + } finally { + unmount(); + } + }); + }); +}); diff --git a/x-pack/plugins/apm/public/components/app/correlations/use_latency_correlations.ts b/x-pack/plugins/apm/public/components/app/correlations/use_latency_correlations.ts new file mode 100644 index 0000000000000..358d436f8f0a5 --- /dev/null +++ b/x-pack/plugins/apm/public/components/app/correlations/use_latency_correlations.ts @@ -0,0 +1,275 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { useCallback, useEffect, useMemo, useReducer, useRef } from 'react'; +import { chunk, debounce } from 'lodash'; + +import { IHttpFetchError, ResponseErrorBody } from 'src/core/public'; + +import { + DEBOUNCE_INTERVAL, + DEFAULT_PERCENTILE_THRESHOLD, +} from '../../../../common/correlations/constants'; +import type { FieldValuePair } from '../../../../common/correlations/types'; +import { getPrioritizedFieldValuePairs } from '../../../../common/correlations/utils'; +import type { + LatencyCorrelation, + LatencyCorrelationsResponse, +} from '../../../../common/correlations/latency_correlations/types'; + +import { callApmApi } from '../../../services/rest/createCallApmApi'; + +import { + getInitialResponse, + getLatencyCorrelationsSortedByCorrelation, + getReducer, + CorrelationsProgress, +} from './utils/analysis_hook_utils'; +import { useFetchParams } from './use_fetch_params'; + +// Overall progress is a float from 0 to 1. +const LOADED_OVERALL_HISTOGRAM = 0.05; +const LOADED_FIELD_CANDIDATES = LOADED_OVERALL_HISTOGRAM + 0.05; +const LOADED_FIELD_VALUE_PAIRS = LOADED_FIELD_CANDIDATES + 0.3; +const LOADED_DONE = 1; +const PROGRESS_STEP_FIELD_VALUE_PAIRS = 0.3; +const PROGRESS_STEP_CORRELATIONS = 0.6; + +export function useLatencyCorrelations() { + const fetchParams = useFetchParams(); + + // This use of useReducer (the dispatch function won't get reinstantiated + // on every update) and debounce avoids flooding consuming components with updates. + // `setResponse.flush()` can be used to enforce an update. + const [response, setResponseUnDebounced] = useReducer( + getReducer(), + getInitialResponse() + ); + const setResponse = useMemo( + () => debounce(setResponseUnDebounced, DEBOUNCE_INTERVAL), + [] + ); + + const abortCtrl = useRef(new AbortController()); + + const startFetch = useCallback(async () => { + abortCtrl.current.abort(); + abortCtrl.current = new AbortController(); + + setResponse({ + ...getInitialResponse(), + isRunning: true, + // explicitly set these to undefined to override a possible previous state. + error: undefined, + latencyCorrelations: undefined, + percentileThresholdValue: undefined, + overallHistogram: undefined, + fieldStats: undefined, + }); + setResponse.flush(); + + try { + // `responseUpdate` will be enriched with additional data with subsequent + // calls to the overall histogram, field candidates, field value pairs, correlation results + // and histogram data for statistically significant results. + const responseUpdate: LatencyCorrelationsResponse = { + ccsWarning: false, + }; + + // Initial call to fetch the overall distribution for the log-log plot. + const { overallHistogram, percentileThresholdValue } = await callApmApi({ + endpoint: 'POST /internal/apm/latency/overall_distribution', + signal: abortCtrl.current.signal, + params: { + body: { + ...fetchParams, + percentileThreshold: DEFAULT_PERCENTILE_THRESHOLD, + }, + }, + }); + responseUpdate.overallHistogram = overallHistogram; + responseUpdate.percentileThresholdValue = percentileThresholdValue; + + if (abortCtrl.current.signal.aborted) { + return; + } + + setResponse({ + ...responseUpdate, + loaded: LOADED_OVERALL_HISTOGRAM, + }); + setResponse.flush(); + + const { fieldCandidates } = await callApmApi({ + endpoint: 'GET /internal/apm/correlations/field_candidates', + signal: abortCtrl.current.signal, + params: { + query: fetchParams, + }, + }); + + if (abortCtrl.current.signal.aborted) { + return; + } + + setResponse({ + loaded: LOADED_FIELD_CANDIDATES, + }); + setResponse.flush(); + + const chunkSize = 10; + let chunkLoadCounter = 0; + + const fieldValuePairs: FieldValuePair[] = []; + const fieldCandidateChunks = chunk(fieldCandidates, chunkSize); + + for (const fieldCandidateChunk of fieldCandidateChunks) { + const fieldValuePairChunkResponse = await callApmApi({ + endpoint: 'POST /internal/apm/correlations/field_value_pairs', + signal: abortCtrl.current.signal, + params: { + body: { + ...fetchParams, + fieldCandidates: fieldCandidateChunk, + }, + }, + }); + + if (fieldValuePairChunkResponse.fieldValuePairs.length > 0) { + fieldValuePairs.push(...fieldValuePairChunkResponse.fieldValuePairs); + } + + if (abortCtrl.current.signal.aborted) { + return; + } + + chunkLoadCounter++; + setResponse({ + loaded: + LOADED_FIELD_CANDIDATES + + (chunkLoadCounter / fieldCandidateChunks.length) * + PROGRESS_STEP_FIELD_VALUE_PAIRS, + }); + } + + if (abortCtrl.current.signal.aborted) { + return; + } + + setResponse.flush(); + + chunkLoadCounter = 0; + + const fieldsToSample = new Set(); + const latencyCorrelations: LatencyCorrelation[] = []; + const fieldValuePairChunks = chunk( + getPrioritizedFieldValuePairs(fieldValuePairs), + chunkSize + ); + + for (const fieldValuePairChunk of fieldValuePairChunks) { + const significantCorrelations = await callApmApi({ + endpoint: 'POST /internal/apm/correlations/significant_correlations', + signal: abortCtrl.current.signal, + params: { + body: { ...fetchParams, fieldValuePairs: fieldValuePairChunk }, + }, + }); + + if (significantCorrelations.latencyCorrelations.length > 0) { + significantCorrelations.latencyCorrelations.forEach((d) => { + fieldsToSample.add(d.fieldName); + }); + latencyCorrelations.push( + ...significantCorrelations.latencyCorrelations + ); + responseUpdate.latencyCorrelations = + getLatencyCorrelationsSortedByCorrelation([...latencyCorrelations]); + } + + chunkLoadCounter++; + setResponse({ + ...responseUpdate, + loaded: + LOADED_FIELD_VALUE_PAIRS + + (chunkLoadCounter / fieldValuePairChunks.length) * + PROGRESS_STEP_CORRELATIONS, + }); + + if (abortCtrl.current.signal.aborted) { + return; + } + } + + setResponse.flush(); + + const { stats } = await callApmApi({ + endpoint: 'POST /internal/apm/correlations/field_stats', + signal: abortCtrl.current.signal, + params: { + body: { + ...fetchParams, + fieldsToSample: [...fieldsToSample], + }, + }, + }); + + responseUpdate.fieldStats = stats; + setResponse({ + ...responseUpdate, + loaded: LOADED_DONE, + isRunning: false, + }); + setResponse.flush(); + } catch (e) { + if (!abortCtrl.current.signal.aborted) { + const err = e as Error | IHttpFetchError; + setResponse({ + error: + 'response' in err + ? err.body?.message ?? err.response?.statusText + : err.message, + isRunning: false, + }); + setResponse.flush(); + } + } + }, [fetchParams, setResponse]); + + const cancelFetch = useCallback(() => { + abortCtrl.current.abort(); + setResponse({ + isRunning: false, + }); + setResponse.flush(); + }, [setResponse]); + + // auto-update + useEffect(() => { + startFetch(); + return () => { + abortCtrl.current.abort(); + }; + }, [startFetch, cancelFetch]); + + const { error, loaded, isRunning, ...returnedResponse } = response; + const progress = useMemo( + () => ({ + error, + loaded: Math.round(loaded * 100) / 100, + isRunning, + }), + [error, loaded, isRunning] + ); + + return { + progress, + response: returnedResponse, + startFetch, + cancelFetch, + }; +} diff --git a/x-pack/plugins/apm/public/components/app/correlations/utils/analysis_hook_utils.ts b/x-pack/plugins/apm/public/components/app/correlations/utils/analysis_hook_utils.ts new file mode 100644 index 0000000000000..24cd76846fa9f --- /dev/null +++ b/x-pack/plugins/apm/public/components/app/correlations/utils/analysis_hook_utils.ts @@ -0,0 +1,40 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { FailedTransactionsCorrelation } from '../../../../../common/correlations/failed_transactions_correlations/types'; +import type { LatencyCorrelation } from '../../../../../common/correlations/latency_correlations/types'; + +export interface CorrelationsProgress { + error?: string; + isRunning: boolean; + loaded: number; +} + +export function getLatencyCorrelationsSortedByCorrelation( + latencyCorrelations: LatencyCorrelation[] +) { + return latencyCorrelations.sort((a, b) => b.correlation - a.correlation); +} + +export function getFailedTransactionsCorrelationsSortedByScore( + failedTransactionsCorrelations: FailedTransactionsCorrelation[] +) { + return failedTransactionsCorrelations.sort((a, b) => b.score - a.score); +} + +export const getInitialResponse = () => ({ + ccsWarning: false, + isRunning: false, + loaded: 0, +}); + +export const getReducer = + () => + (prev: T, update: Partial): T => ({ + ...prev, + ...update, + }); diff --git a/x-pack/plugins/apm/public/components/app/correlations/utils/get_failed_transactions_correlation_impact_label.test.ts b/x-pack/plugins/apm/public/components/app/correlations/utils/get_failed_transactions_correlation_impact_label.test.ts index e4c08b42b2420..d35833295703f 100644 --- a/x-pack/plugins/apm/public/components/app/correlations/utils/get_failed_transactions_correlation_impact_label.test.ts +++ b/x-pack/plugins/apm/public/components/app/correlations/utils/get_failed_transactions_correlation_impact_label.test.ts @@ -6,7 +6,7 @@ */ import { getFailedTransactionsCorrelationImpactLabel } from './get_failed_transactions_correlation_impact_label'; -import { FAILED_TRANSACTIONS_IMPACT_THRESHOLD } from '../../../../../common/search_strategies/failed_transactions_correlations/constants'; +import { FAILED_TRANSACTIONS_IMPACT_THRESHOLD } from '../../../../../common/correlations/failed_transactions_correlations/constants'; const EXPECTED_RESULT = { HIGH: { diff --git a/x-pack/plugins/apm/public/components/app/correlations/utils/get_failed_transactions_correlation_impact_label.ts b/x-pack/plugins/apm/public/components/app/correlations/utils/get_failed_transactions_correlation_impact_label.ts index cbfaee88ff6f4..d5d0fd4dcae51 100644 --- a/x-pack/plugins/apm/public/components/app/correlations/utils/get_failed_transactions_correlation_impact_label.ts +++ b/x-pack/plugins/apm/public/components/app/correlations/utils/get_failed_transactions_correlation_impact_label.ts @@ -8,8 +8,8 @@ import { FailedTransactionsCorrelation, FailedTransactionsCorrelationsImpactThreshold, -} from '../../../../../common/search_strategies/failed_transactions_correlations/types'; -import { FAILED_TRANSACTIONS_IMPACT_THRESHOLD } from '../../../../../common/search_strategies/failed_transactions_correlations/constants'; +} from '../../../../../common/correlations/failed_transactions_correlations/types'; +import { FAILED_TRANSACTIONS_IMPACT_THRESHOLD } from '../../../../../common/correlations/failed_transactions_correlations/constants'; export function getFailedTransactionsCorrelationImpactLabel( pValue: FailedTransactionsCorrelation['pValue'] diff --git a/x-pack/plugins/apm/public/components/app/correlations/utils/get_overall_histogram.test.ts b/x-pack/plugins/apm/public/components/app/correlations/utils/get_overall_histogram.test.ts index c323b69594013..b76777b660d8f 100644 --- a/x-pack/plugins/apm/public/components/app/correlations/utils/get_overall_histogram.test.ts +++ b/x-pack/plugins/apm/public/components/app/correlations/utils/get_overall_histogram.test.ts @@ -5,14 +5,14 @@ * 2.0. */ -import type { LatencyCorrelationsRawResponse } from '../../../../../common/search_strategies/latency_correlations/types'; +import type { LatencyCorrelationsResponse } from '../../../../../common/correlations/latency_correlations/types'; import { getOverallHistogram } from './get_overall_histogram'; describe('getOverallHistogram', () => { it('returns "loading" when undefined and running', () => { const { overallHistogram, hasData, status } = getOverallHistogram( - {} as LatencyCorrelationsRawResponse, + {} as LatencyCorrelationsResponse, true ); expect(overallHistogram).toStrictEqual(undefined); @@ -22,7 +22,7 @@ describe('getOverallHistogram', () => { it('returns "success" when undefined and not running', () => { const { overallHistogram, hasData, status } = getOverallHistogram( - {} as LatencyCorrelationsRawResponse, + {} as LatencyCorrelationsResponse, false ); expect(overallHistogram).toStrictEqual([]); @@ -34,7 +34,7 @@ describe('getOverallHistogram', () => { const { overallHistogram, hasData, status } = getOverallHistogram( { overallHistogram: [{ key: 1, doc_count: 1234 }], - } as LatencyCorrelationsRawResponse, + } as LatencyCorrelationsResponse, true ); expect(overallHistogram).toStrictEqual([{ key: 1, doc_count: 1234 }]); @@ -46,7 +46,7 @@ describe('getOverallHistogram', () => { const { overallHistogram, hasData, status } = getOverallHistogram( { overallHistogram: [{ key: 1, doc_count: 1234 }], - } as LatencyCorrelationsRawResponse, + } as LatencyCorrelationsResponse, false ); expect(overallHistogram).toStrictEqual([{ key: 1, doc_count: 1234 }]); diff --git a/x-pack/plugins/apm/public/components/app/correlations/utils/get_overall_histogram.ts b/x-pack/plugins/apm/public/components/app/correlations/utils/get_overall_histogram.ts index 3a90eb4b89123..3a6a2704b3984 100644 --- a/x-pack/plugins/apm/public/components/app/correlations/utils/get_overall_histogram.ts +++ b/x-pack/plugins/apm/public/components/app/correlations/utils/get_overall_histogram.ts @@ -5,7 +5,7 @@ * 2.0. */ -import type { LatencyCorrelationsRawResponse } from '../../../../../common/search_strategies/latency_correlations/types'; +import type { LatencyCorrelationsResponse } from '../../../../../common/correlations/latency_correlations/types'; import { FETCH_STATUS } from '../../../../hooks/use_fetcher'; @@ -13,7 +13,7 @@ import { FETCH_STATUS } from '../../../../hooks/use_fetcher'; // of fetching more data such as correlation results. That's why we have to determine // the `status` of the data for the latency chart separately. export function getOverallHistogram( - data: LatencyCorrelationsRawResponse, + data: LatencyCorrelationsResponse, isRunning: boolean ) { const overallHistogram = diff --git a/x-pack/plugins/apm/public/components/app/error_group_details/Distribution/index.tsx b/x-pack/plugins/apm/public/components/app/error_group_details/Distribution/index.tsx index 4c6aa78278093..8aa132bb85595 100644 --- a/x-pack/plugins/apm/public/components/app/error_group_details/Distribution/index.tsx +++ b/x-pack/plugins/apm/public/components/app/error_group_details/Distribution/index.tsx @@ -56,7 +56,7 @@ export function ErrorDistribution({ distribution, title, fetchStatus }: Props) { data: distribution.currentPeriod, color: theme.eui.euiColorVis1, title: i18n.translate('xpack.apm.errorGroup.chart.ocurrences', { - defaultMessage: 'Occurences', + defaultMessage: 'Occurrences', }), }, ...(comparisonEnabled diff --git a/x-pack/plugins/apm/public/components/app/error_group_details/detail_view/index.tsx b/x-pack/plugins/apm/public/components/app/error_group_details/detail_view/index.tsx index d4ffd8ece9d4d..5438fce7c4881 100644 --- a/x-pack/plugins/apm/public/components/app/error_group_details/detail_view/index.tsx +++ b/x-pack/plugins/apm/public/components/app/error_group_details/detail_view/index.tsx @@ -72,12 +72,14 @@ export function DetailView({ errorGroup, urlParams, kuery }: Props) { const history = useHistory(); const { transaction, error, occurrencesCount } = errorGroup; + const { detailTab, comparisonType, comparisonEnabled } = urlParams; + if (!error) { return null; } const tabs = getTabs(error); - const currentTab = getCurrentTab(tabs, urlParams.detailTab) as ErrorTab; + const currentTab = getCurrentTab(tabs, detailTab) as ErrorTab; const errorUrl = error.error.page?.url || error.url?.full; @@ -139,6 +141,8 @@ export function DetailView({ errorGroup, urlParams, kuery }: Props) { transactionName={transaction.transaction.name} transactionType={transaction.transaction.type} serviceName={transaction.service.name} + comparisonType={comparisonType} + comparisonEnabled={comparisonEnabled} > diff --git a/x-pack/plugins/apm/public/components/app/error_group_details/index.tsx b/x-pack/plugins/apm/public/components/app/error_group_details/index.tsx index f3bd8812dfd36..0765e0dd01061 100644 --- a/x-pack/plugins/apm/public/components/app/error_group_details/index.tsx +++ b/x-pack/plugins/apm/public/components/app/error_group_details/index.tsx @@ -26,6 +26,7 @@ import { useApmRouter } from '../../../hooks/use_apm_router'; import { useErrorGroupDistributionFetcher } from '../../../hooks/use_error_group_distribution_fetcher'; import { useFetcher } from '../../../hooks/use_fetcher'; import { useTimeRange } from '../../../hooks/use_time_range'; +import type { APIReturnType } from '../../../services/rest/createCallApmApi'; import { DetailView } from './detail_view'; import { ErrorDistribution } from './Distribution'; @@ -50,6 +51,15 @@ const Culprit = euiStyled.div` font-family: ${({ theme }) => theme.eui.euiCodeFontFamily}; `; +type ErrorDistributionAPIResponse = + APIReturnType<'GET /internal/apm/services/{serviceName}/errors/distribution'>; + +const emptyState: ErrorDistributionAPIResponse = { + currentPeriod: [], + previousPeriod: [], + bucketSize: 0, +}; + function getShortGroupId(errorGroupId?: string) { if (!errorGroupId) { return NOT_AVAILABLE_LABEL; @@ -210,7 +220,7 @@ export function ErrorGroupDetails() { )} = { + title: 'app/ServiceInventory', + component: ServiceInventory, + decorators: [ + (StoryComponent) => { + const coreMock = { + http: { + get: (endpoint: string) => { + switch (endpoint) { + case '/internal/apm/services': + return { items: [] }; + default: + return {}; + } + return {}; + }, + }, + notifications: { toasts: { add: () => {}, addWarning: () => {} } }, + uiSettings: { get: () => [] }, + } as unknown as CoreStart; + + const KibanaReactContext = createKibanaReactContext(coreMock); + + const anomlyDetectionJobsContextValue = { + anomalyDetectionJobsData: { jobs: [], hasLegacyJobs: false }, + anomalyDetectionJobsStatus: FETCH_STATUS.SUCCESS, + anomalyDetectionJobsRefetch: () => {}, + }; + + return ( + + + + + + + + + + + + ); + }, + ], +}; +export default stories; + +export const Example: Story<{}> = () => { + return ; +}; diff --git a/x-pack/plugins/apm/public/components/app/service_inventory/service_inventory.test.tsx b/x-pack/plugins/apm/public/components/app/service_inventory/service_inventory.test.tsx index 4a020f9b0db4e..36b1053248d25 100644 --- a/x-pack/plugins/apm/public/components/app/service_inventory/service_inventory.test.tsx +++ b/x-pack/plugins/apm/public/components/app/service_inventory/service_inventory.test.tsx @@ -5,249 +5,17 @@ * 2.0. */ -import { render, waitFor } from '@testing-library/react'; -import { CoreStart } from 'kibana/public'; -import { merge } from 'lodash'; -import React, { ReactNode } from 'react'; -import { MemoryRouter } from 'react-router-dom'; -import { EuiThemeProvider } from '../../../../../../../src/plugins/kibana_react/common'; -import { createKibanaReactContext } from '../../../../../../../src/plugins/kibana_react/public'; -import { ServiceHealthStatus } from '../../../../common/service_health_status'; -import { TimeRangeComparisonEnum } from '../../../../common/runtime_types/comparison_type_rt'; -import { ServiceInventory } from '.'; -import { ApmPluginContextValue } from '../../../context/apm_plugin/apm_plugin_context'; -import { - mockApmPluginContextValue, - MockApmPluginContextWrapper, -} from '../../../context/apm_plugin/mock_apm_plugin_context'; -import { FETCH_STATUS } from '../../../hooks/use_fetcher'; -import { clearCache } from '../../../services/rest/callApi'; -import * as useDynamicDataViewHooks from '../../../hooks/use_dynamic_data_view'; -import { SessionStorageMock } from '../../../services/__mocks__/SessionStorageMock'; -import { MockUrlParamsContextProvider } from '../../../context/url_params_context/mock_url_params_context_provider'; -import * as hook from '../../../context/anomaly_detection_jobs/use_anomaly_detection_jobs_context'; +import { composeStories } from '@storybook/testing-react'; +import { render, screen } from '@testing-library/react'; +import React from 'react'; +import * as stories from './service_inventory.stories'; -const KibanaReactContext = createKibanaReactContext({ - usageCollection: { reportUiCounter: () => {} }, -} as Partial); - -const addWarning = jest.fn(); -const httpGet = jest.fn(); - -function wrapper({ children }: { children?: ReactNode }) { - const mockPluginContext = merge({}, mockApmPluginContextValue, { - core: { - http: { - get: httpGet, - }, - notifications: { - toasts: { - addWarning, - }, - }, - }, - }) as unknown as ApmPluginContextValue; - - return ( - - - - - - {children} - - - - - - ); -} +const { Example } = composeStories(stories); describe('ServiceInventory', () => { - beforeEach(() => { - // @ts-expect-error - global.sessionStorage = new SessionStorageMock(); - clearCache(); - - jest.spyOn(hook, 'useAnomalyDetectionJobsContext').mockReturnValue({ - anomalyDetectionJobsData: { jobs: [], hasLegacyJobs: false }, - anomalyDetectionJobsStatus: FETCH_STATUS.SUCCESS, - anomalyDetectionJobsRefetch: () => {}, - }); - - jest - .spyOn(useDynamicDataViewHooks, 'useDynamicDataViewFetcher') - .mockReturnValue({ - dataView: undefined, - status: FETCH_STATUS.SUCCESS, - }); - }); - - afterEach(() => { - jest.resetAllMocks(); - }); - - it('should render services, when list is not empty', async () => { - // mock rest requests - httpGet - .mockResolvedValueOnce({ fallbackToTransactions: false }) - .mockResolvedValueOnce({ - hasLegacyData: false, - hasHistoricalData: true, - items: [ - { - serviceName: 'My Python Service', - agentName: 'python', - transactionsPerMinute: 100, - errorsPerMinute: 200, - avgResponseTime: 300, - environments: ['test', 'dev'], - healthStatus: ServiceHealthStatus.warning, - }, - { - serviceName: 'My Go Service', - agentName: 'go', - transactionsPerMinute: 400, - errorsPerMinute: 500, - avgResponseTime: 600, - environments: [], - severity: ServiceHealthStatus.healthy, - }, - ], - }); - - const { container, findByText } = render(, { wrapper }); - - // wait for requests to be made - await waitFor(() => expect(httpGet).toHaveBeenCalledTimes(2)); - await findByText('My Python Service'); - - expect(container.querySelectorAll('.euiTableRow')).toHaveLength(2); - }); - - it('should render empty message, when list is empty and historical data is found', async () => { - httpGet - .mockResolvedValueOnce({ fallbackToTransactions: false }) - .mockResolvedValueOnce({ - hasLegacyData: false, - hasHistoricalData: true, - items: [], - }); - - const { findByText } = render(, { wrapper }); - - // wait for requests to be made - await waitFor(() => expect(httpGet).toHaveBeenCalledTimes(2)); - const noServicesText = await findByText('No services found'); - - expect(noServicesText).not.toBeEmptyDOMElement(); - }); - - describe('when legacy data is found', () => { - it('renders an upgrade migration notification', async () => { - httpGet - .mockResolvedValueOnce({ fallbackToTransactions: false }) - .mockResolvedValueOnce({ - hasLegacyData: true, - hasHistoricalData: true, - items: [], - }); - - render(, { wrapper }); - - // wait for requests to be made - await waitFor(() => expect(httpGet).toHaveBeenCalledTimes(2)); - - expect(addWarning).toHaveBeenLastCalledWith( - expect.objectContaining({ - title: 'Legacy data was detected within the selected time range', - }) - ); - }); - }); - - describe('when legacy data is not found', () => { - it('does not render an upgrade migration notification', async () => { - httpGet - .mockResolvedValueOnce({ fallbackToTransactions: false }) - .mockResolvedValueOnce({ - hasLegacyData: false, - hasHistoricalData: true, - items: [], - }); - - render(, { wrapper }); - - // wait for requests to be made - await waitFor(() => expect(httpGet).toHaveBeenCalledTimes(2)); - - expect(addWarning).not.toHaveBeenCalled(); - }); - }); - - describe('when ML data is not found', () => { - it('does not render the health column', async () => { - httpGet - .mockResolvedValueOnce({ fallbackToTransactions: false }) - .mockResolvedValueOnce({ - hasLegacyData: false, - hasHistoricalData: true, - items: [ - { - serviceName: 'My Python Service', - agentName: 'python', - transactionsPerMinute: 100, - errorsPerMinute: 200, - avgResponseTime: 300, - environments: ['test', 'dev'], - }, - ], - }); - - const { queryByText } = render(, { wrapper }); - - // wait for requests to be made - await waitFor(() => expect(httpGet).toHaveBeenCalledTimes(2)); - - expect(queryByText('Health')).toBeNull(); - }); - }); - - describe('when ML data is found', () => { - it('renders the health column', async () => { - httpGet - .mockResolvedValueOnce({ fallbackToTransactions: false }) - .mockResolvedValueOnce({ - hasLegacyData: false, - hasHistoricalData: true, - items: [ - { - serviceName: 'My Python Service', - agentName: 'python', - transactionsPerMinute: 100, - errorsPerMinute: 200, - avgResponseTime: 300, - environments: ['test', 'dev'], - healthStatus: ServiceHealthStatus.warning, - }, - ], - }); - - const { queryAllByText } = render(, { wrapper }); - - // wait for requests to be made - await waitFor(() => expect(httpGet).toHaveBeenCalledTimes(2)); + it('renders', async () => { + render(); - expect(queryAllByText('Health').length).toBeGreaterThan(1); - }); + expect(await screen.findByRole('table')).toBeInTheDocument(); }); }); diff --git a/x-pack/plugins/apm/public/components/app/service_map/Controls.test.tsx b/x-pack/plugins/apm/public/components/app/service_map/Controls.test.tsx index 2ebd63badc41e..f2dd9cce8f27e 100644 --- a/x-pack/plugins/apm/public/components/app/service_map/Controls.test.tsx +++ b/x-pack/plugins/apm/public/components/app/service_map/Controls.test.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import lightTheme from '@elastic/eui/dist/eui_theme_light.json'; +import { euiLightVars as lightTheme } from '@kbn/ui-shared-deps-src/theme'; import { render } from '@testing-library/react'; import cytoscape from 'cytoscape'; import React, { ReactNode } from 'react'; diff --git a/x-pack/plugins/apm/public/components/app/service_overview/service_overview.stories.tsx b/x-pack/plugins/apm/public/components/app/service_overview/service_overview.stories.tsx new file mode 100644 index 0000000000000..b632d3a33dea8 --- /dev/null +++ b/x-pack/plugins/apm/public/components/app/service_overview/service_overview.stories.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 { Meta, Story } from '@storybook/react'; +import React from 'react'; +import { MemoryRouter } from 'react-router-dom'; +import type { CoreStart } from '../../../../../../../src/core/public'; +import { createKibanaReactContext } from '../../../../../../../src/plugins/kibana_react/public'; +import type { ApmPluginContextValue } from '../../../context/apm_plugin/apm_plugin_context'; +import { MockApmPluginContextWrapper } from '../../../context/apm_plugin/mock_apm_plugin_context'; +import { + APMServiceContext, + APMServiceContextValue, +} from '../../../context/apm_service/apm_service_context'; +import { ServiceOverview } from './'; + +const stories: Meta<{}> = { + title: 'app/ServiceOverview', + component: ServiceOverview, + decorators: [ + (StoryComponent) => { + const serviceName = 'testServiceName'; + const mockCore = { + http: { + basePath: { prepend: () => {} }, + get: (endpoint: string) => { + switch (endpoint) { + case `/api/apm/services/${serviceName}/annotation/search`: + return { annotations: [] }; + case '/internal/apm/fallback_to_transactions': + return { fallbackToTransactions: false }; + case `/internal/apm/services/${serviceName}/dependencies`: + return { serviceDependencies: [] }; + default: + return {}; + } + }, + }, + notifications: { toasts: { add: () => {} } }, + uiSettings: { get: () => 'Browser' }, + } as unknown as CoreStart; + const serviceContextValue = { + alerts: [], + serviceName, + } as unknown as APMServiceContextValue; + const KibanaReactContext = createKibanaReactContext(mockCore); + + return ( + + + + + + + + + + ); + }, + ], +}; +export default stories; + +export const Example: Story<{}> = () => { + return ; +}; diff --git a/x-pack/plugins/apm/public/components/app/service_overview/service_overview.test.tsx b/x-pack/plugins/apm/public/components/app/service_overview/service_overview.test.tsx index 7e9b4325591d9..fb60604aa53b2 100644 --- a/x-pack/plugins/apm/public/components/app/service_overview/service_overview.test.tsx +++ b/x-pack/plugins/apm/public/components/app/service_overview/service_overview.test.tsx @@ -5,178 +5,19 @@ * 2.0. */ -import React, { ReactNode } from 'react'; -import { MemoryRouter } from 'react-router-dom'; -import { CoreStart } from 'src/core/public'; -import { isEqual } from 'lodash'; -import { createKibanaReactContext } from '../../../../../../../src/plugins/kibana_react/public'; -import { ApmPluginContextValue } from '../../../context/apm_plugin/apm_plugin_context'; -import { - mockApmPluginContextValue, - MockApmPluginContextWrapper, -} from '../../../context/apm_plugin/mock_apm_plugin_context'; -import * as useDynamicDataViewHooks from '../../../hooks/use_dynamic_data_view'; -import { FETCH_STATUS } from '../../../hooks/use_fetcher'; -import * as useAnnotationsHooks from '../../../context/annotations/use_annotations_context'; -import * as useTransactionBreakdownHooks from '../../shared/charts/transaction_breakdown_chart/use_transaction_breakdown'; -import { renderWithTheme } from '../../../utils/testHelpers'; -import { ServiceOverview } from './'; -import { waitFor } from '@testing-library/dom'; -import * as useApmServiceContextHooks from '../../../context/apm_service/use_apm_service_context'; -import { LatencyAggregationType } from '../../../../common/latency_aggregation_types'; -import { - getCallApmApiSpy, - getCreateCallApmApiSpy, -} from '../../../services/rest/callApmApiSpy'; -import { fromQuery } from '../../shared/Links/url_helpers'; -import { MockUrlParamsContextProvider } from '../../../context/url_params_context/mock_url_params_context_provider'; -import { uiSettingsServiceMock } from '../../../../../../../src/core/public/mocks'; +import { composeStories } from '@storybook/testing-react'; +import { render, screen } from '@testing-library/react'; +import React from 'react'; +import * as stories from './service_overview.stories'; -const uiSettings = uiSettingsServiceMock.create().setup({} as any); - -const KibanaReactContext = createKibanaReactContext({ - notifications: { toasts: { add: () => {} } }, - uiSettings, - usageCollection: { reportUiCounter: () => {} }, -} as unknown as Partial); - -const mockParams = { - rangeFrom: 'now-15m', - rangeTo: 'now', - latencyAggregationType: LatencyAggregationType.avg, -}; - -const location = { - pathname: '/services/test%20service%20name/overview', - search: fromQuery(mockParams), -}; - -function Wrapper({ children }: { children?: ReactNode }) { - const value = { - ...mockApmPluginContextValue, - core: { - ...mockApmPluginContextValue.core, - http: { - basePath: { prepend: () => {} }, - get: () => {}, - }, - }, - } as unknown as ApmPluginContextValue; - - return ( - - - - - {children} - - - - - ); -} +const { Example } = composeStories(stories); describe('ServiceOverview', () => { it('renders', async () => { - jest - .spyOn(useApmServiceContextHooks, 'useApmServiceContext') - .mockReturnValue({ - serviceName: 'test service name', - agentName: 'java', - transactionType: 'request', - transactionTypes: ['request'], - alerts: [], - }); - jest - .spyOn(useAnnotationsHooks, 'useAnnotationsContext') - .mockReturnValue({ annotations: [] }); - jest - .spyOn(useDynamicDataViewHooks, 'useDynamicDataViewFetcher') - .mockReturnValue({ - dataView: undefined, - status: FETCH_STATUS.SUCCESS, - }); - - /* eslint-disable @typescript-eslint/naming-convention */ - const calls = { - 'GET /internal/apm/services/{serviceName}/error_groups/main_statistics': { - error_groups: [] as any[], - }, - 'GET /internal/apm/services/{serviceName}/transactions/groups/main_statistics': - { - transactionGroups: [] as any[], - totalTransactionGroups: 0, - isAggregationAccurate: true, - }, - 'GET /internal/apm/services/{serviceName}/dependencies': { - serviceDependencies: [], - }, - 'GET /internal/apm/services/{serviceName}/service_overview_instances/main_statistics': - [], - 'GET /internal/apm/services/{serviceName}/transactions/charts/latency': { - currentPeriod: { - overallAvgDuration: null, - latencyTimeseries: [], - }, - previousPeriod: { - overallAvgDuration: null, - latencyTimeseries: [], - }, - }, - 'GET /internal/apm/services/{serviceName}/throughput': { - currentPeriod: [], - previousPeriod: [], - }, - 'GET /internal/apm/services/{serviceName}/transactions/charts/error_rate': - { - currentPeriod: { - timeseries: [], - average: null, - }, - previousPeriod: { - timeseries: [], - average: null, - }, - }, - 'GET /api/apm/services/{serviceName}/annotation/search': { - annotations: [], - }, - 'GET /internal/apm/fallback_to_transactions': { - fallbackToTransactions: false, - }, - }; - /* eslint-enable @typescript-eslint/naming-convention */ - - const callApmApiSpy = getCallApmApiSpy().mockImplementation( - ({ endpoint }) => { - const response = calls[endpoint as keyof typeof calls]; - - return response - ? Promise.resolve(response) - : Promise.reject(`Response for ${endpoint} is not defined`); - } - ); - - getCreateCallApmApiSpy().mockImplementation(() => callApmApiSpy as any); - jest - .spyOn(useTransactionBreakdownHooks, 'useTransactionBreakdown') - .mockReturnValue({ - data: { timeseries: [] }, - error: undefined, - status: FETCH_STATUS.SUCCESS, - }); - - const { findAllByText } = renderWithTheme(, { - wrapper: Wrapper, - }); - - await waitFor(() => { - const endpoints = callApmApiSpy.mock.calls.map( - (call) => call[0].endpoint - ); - return isEqual(endpoints.sort(), Object.keys(calls).sort()); - }); + render(); - expect((await findAllByText('Latency')).length).toBeGreaterThan(0); + expect( + await screen.findByRole('heading', { name: /Latency/ }) + ).toBeInTheDocument(); }); }); diff --git a/x-pack/plugins/apm/public/components/app/service_overview/service_overview_throughput_chart.tsx b/x-pack/plugins/apm/public/components/app/service_overview/service_overview_throughput_chart.tsx index 058c7d97b43cc..7472eb780f119 100644 --- a/x-pack/plugins/apm/public/components/app/service_overview/service_overview_throughput_chart.tsx +++ b/x-pack/plugins/apm/public/components/app/service_overview/service_overview_throughput_chart.tsx @@ -30,7 +30,6 @@ import { const INITIAL_STATE = { currentPeriod: [], previousPeriod: [], - throughputUnit: 'minute' as const, }; export function ServiceOverviewThroughputChart({ diff --git a/x-pack/plugins/apm/public/components/app/transaction_details/distribution/index.tsx b/x-pack/plugins/apm/public/components/app/transaction_details/distribution/index.tsx index ad52adfa13a52..ee2f8fb50a0e5 100644 --- a/x-pack/plugins/apm/public/components/app/transaction_details/distribution/index.tsx +++ b/x-pack/plugins/apm/public/components/app/transaction_details/distribution/index.tsx @@ -23,7 +23,7 @@ import { FormattedMessage } from '@kbn/i18n/react'; import { useUiTracker } from '../../../../../../observability/public'; import { getDurationFormatter } from '../../../../../common/utils/formatters'; -import { DEFAULT_PERCENTILE_THRESHOLD } from '../../../../../common/search_strategies/constants'; +import { DEFAULT_PERCENTILE_THRESHOLD } from '../../../../../common/correlations/constants'; import { useLegacyUrlParams } from '../../../../context/url_params_context/use_url_params'; import { FETCH_STATUS } from '../../../../hooks/use_fetcher'; @@ -165,7 +165,7 @@ export function TransactionDistribution({ @@ -175,13 +175,13 @@ export function TransactionDistribution({ /> ), - allFailedTransactions: ( + failedTransactions: ( ), diff --git a/x-pack/plugins/apm/public/components/app/transaction_details/distribution/use_transaction_distribution_chart_data.ts b/x-pack/plugins/apm/public/components/app/transaction_details/distribution/use_transaction_distribution_chart_data.ts index 9fb945100414f..a02fc7fe6665f 100644 --- a/x-pack/plugins/apm/public/components/app/transaction_details/distribution/use_transaction_distribution_chart_data.ts +++ b/x-pack/plugins/apm/public/components/app/transaction_details/distribution/use_transaction_distribution_chart_data.ts @@ -5,77 +5,41 @@ * 2.0. */ -import { useEffect, useMemo } from 'react'; +import { useEffect } from 'react'; import { i18n } from '@kbn/i18n'; -import { DEFAULT_PERCENTILE_THRESHOLD } from '../../../../../common/search_strategies/constants'; -import { RawSearchStrategyClientParams } from '../../../../../common/search_strategies/types'; +import { DEFAULT_PERCENTILE_THRESHOLD } from '../../../../../common/correlations/constants'; import { EVENT_OUTCOME } from '../../../../../common/elasticsearch_fieldnames'; import { EventOutcome } from '../../../../../common/event_outcome'; import { useApmPluginContext } from '../../../../context/apm_plugin/use_apm_plugin_context'; -import { useApmServiceContext } from '../../../../context/apm_service/use_apm_service_context'; -import { useLegacyUrlParams } from '../../../../context/url_params_context/use_url_params'; -import { useApmParams } from '../../../../hooks/use_apm_params'; import { useFetcher, FETCH_STATUS } from '../../../../hooks/use_fetcher'; -import { useTimeRange } from '../../../../hooks/use_time_range'; import type { TransactionDistributionChartData } from '../../../shared/charts/transaction_distribution_chart'; import { isErrorMessage } from '../../correlations/utils/is_error_message'; - -function hasRequiredParams(params: RawSearchStrategyClientParams) { - const { serviceName, environment, start, end } = params; - return serviceName && environment && start && end; -} +import { useFetchParams } from '../../correlations/use_fetch_params'; export const useTransactionDistributionChartData = () => { - const { serviceName, transactionType } = useApmServiceContext(); + const params = useFetchParams(); const { core: { notifications }, } = useApmPluginContext(); - const { urlParams } = useLegacyUrlParams(); - const { transactionName } = urlParams; - - const { - query: { kuery, environment, rangeFrom, rangeTo }, - } = useApmParams('/services/{serviceName}/transactions/view'); - - const { start, end } = useTimeRange({ rangeFrom, rangeTo }); - - const params = useMemo( - () => ({ - serviceName, - transactionName, - transactionType, - kuery, - environment, - start, - end, - }), - [ - serviceName, - transactionName, - transactionType, - kuery, - environment, - start, - end, - ] - ); - const { - // TODO The default object has `log: []` to retain compatibility with the shared search strategies code. - // Remove once the other tabs are migrated away from search strategies. - data: overallLatencyData = { log: [] }, + data: overallLatencyData = {}, status: overallLatencyStatus, error: overallLatencyError, } = useFetcher( (callApmApi) => { - if (hasRequiredParams(params)) { + if ( + params.serviceName && + params.environment && + params.start && + params.end + ) { return callApmApi({ endpoint: 'POST /internal/apm/latency/overall_distribution', params: { @@ -114,12 +78,15 @@ export const useTransactionDistributionChartData = () => { Array.isArray(overallLatencyHistogram) && overallLatencyHistogram.length > 0; - // TODO The default object has `log: []` to retain compatibility with the shared search strategies code. - // Remove once the other tabs are migrated away from search strategies. - const { data: errorHistogramData = { log: [] }, error: errorHistogramError } = + const { data: errorHistogramData = {}, error: errorHistogramError } = useFetcher( (callApmApi) => { - if (hasRequiredParams(params)) { + if ( + params.serviceName && + params.environment && + params.start && + params.end + ) { return callApmApi({ endpoint: 'POST /internal/apm/latency/overall_distribution', params: { @@ -171,8 +138,8 @@ export const useTransactionDistributionChartData = () => { if (Array.isArray(errorHistogramData.overallHistogram)) { transactionDistributionChartData.push({ id: i18n.translate( - 'xpack.apm.transactionDistribution.chart.allFailedTransactionsLabel', - { defaultMessage: 'All failed transactions' } + 'xpack.apm.transactionDistribution.chart.failedTransactionsLabel', + { defaultMessage: 'Failed transactions' } ), histogram: errorHistogramData.overallHistogram, }); diff --git a/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/Waterfall/accordion_waterfall.tsx b/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/Waterfall/accordion_waterfall.tsx index 15883e7905142..5d089e53bd998 100644 --- a/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/Waterfall/accordion_waterfall.tsx +++ b/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/Waterfall/accordion_waterfall.tsx @@ -5,16 +5,22 @@ * 2.0. */ -import { EuiAccordion, EuiAccordionProps } from '@elastic/eui'; -import { isEmpty } from 'lodash'; +import { + EuiAccordion, + EuiAccordionProps, + EuiFlexGroup, + EuiFlexItem, + EuiIcon, + EuiText, +} from '@elastic/eui'; import React, { Dispatch, SetStateAction, useState } from 'react'; import { euiStyled } from '../../../../../../../../../../src/plugins/kibana_react/common'; import { Margins } from '../../../../../shared/charts/Timeline'; -import { WaterfallItem } from './waterfall_item'; import { IWaterfall, IWaterfallSpanOrTransaction, } from './waterfall_helpers/waterfall_helpers'; +import { WaterfallItem } from './waterfall_item'; interface AccordionWaterfallProps { isOpen: boolean; @@ -28,6 +34,8 @@ interface AccordionWaterfallProps { onClickWaterfallItem: (item: IWaterfallSpanOrTransaction) => void; } +const ACCORDION_HEIGHT = '48px'; + const StyledAccordion = euiStyled(EuiAccordion).withConfig({ shouldForwardProp: (prop) => !['childrenCount', 'marginLeftLevel', 'hasError'].includes(prop), @@ -38,54 +46,33 @@ const StyledAccordion = euiStyled(EuiAccordion).withConfig({ hasError: boolean; } >` - .euiAccordion { + .waterfall_accordion { border-top: 1px solid ${({ theme }) => theme.eui.euiColorLightShade}; } - .euiIEFlexWrapFix { - width: 100%; - height: 48px; - } + .euiAccordion__childWrapper { transition: none; } - .euiAccordion__padding--l { - padding-top: 0; - padding-bottom: 0; - } - - .euiAccordion__iconWrapper { - display: flex; - position: relative; - &:after { - content: ${(props) => `'${props.childrenCount}'`}; - position: absolute; - left: 20px; - top: -1px; - z-index: 1; - font-size: ${({ theme }) => theme.eui.euiFontSizeXS}; - } - } - ${(props) => { const borderLeft = props.hasError ? `2px solid ${props.theme.eui.euiColorDanger};` : `1px solid ${props.theme.eui.euiColorLightShade};`; return `.button_${props.id} { + width: 100%; + height: ${ACCORDION_HEIGHT}; margin-left: ${props.marginLeftLevel}px; border-left: ${borderLeft} &:hover { background-color: ${props.theme.eui.euiColorLightestShade}; } }`; - // }} -`; -const WaterfallItemContainer = euiStyled.div` - position: absolute; - width: 100%; - left: 0; + .accordion__buttonContent { + width: 100%; + height: 100%; + } `; export function AccordionWaterfall(props: AccordionWaterfallProps) { @@ -111,36 +98,51 @@ export function AccordionWaterfall(props: AccordionWaterfallProps) { // To indent the items creating the parent/child tree const marginLeftLevel = 8 * level; + function toggleAccordion() { + setIsOpen((isCurrentOpen) => !isCurrentOpen); + } + return ( - { - onClickWaterfallItem(item); - }} - /> - + + + + + + { + onClickWaterfallItem(item); + }} + /> + + } - arrowDisplay={isEmpty(children) ? 'none' : 'left'} + arrowDisplay="none" initialIsOpen={true} forceState={isOpen ? 'open' : 'closed'} - onToggle={() => { - setIsOpen((isCurrentOpen) => !isCurrentOpen); - }} + onToggle={toggleAccordion} > {children.map((child) => ( ); } + +function ToggleAccordionButton({ + show, + isOpen, + childrenAmount, + onClick, +}: { + show: boolean; + isOpen: boolean; + childrenAmount: number; + onClick: () => void; +}) { + if (!show) { + return null; + } + + return ( +
    + + + {/* eslint-disable-next-line jsx-a11y/click-events-have-key-events */} +
    { + e.stopPropagation(); + onClick(); + }} + > + +
    +
    + + {childrenAmount} + +
    +
    + ); +} diff --git a/x-pack/plugins/apm/public/components/fleet_integration/apm_agents/replace_template_strings.ts b/x-pack/plugins/apm/public/components/fleet_integration/apm_agents/replace_template_strings.ts index 2e0c0da8d0fb7..d36d76d466308 100644 --- a/x-pack/plugins/apm/public/components/fleet_integration/apm_agents/replace_template_strings.ts +++ b/x-pack/plugins/apm/public/components/fleet_integration/apm_agents/replace_template_strings.ts @@ -16,8 +16,6 @@ export function replaceTemplateStrings( ) { Mustache.parse(text, TEMPLATE_TAGS); return Mustache.render(text, { - curlyOpen: '{', - curlyClose: '}', config: { docs: { base_url: docLinks?.ELASTIC_WEBSITE_URL, diff --git a/x-pack/plugins/apm/public/components/routing/app_root.tsx b/x-pack/plugins/apm/public/components/routing/app_root.tsx index bc4119a3e835a..0e8e6732dc943 100644 --- a/x-pack/plugins/apm/public/components/routing/app_root.tsx +++ b/x-pack/plugins/apm/public/components/routing/app_root.tsx @@ -5,8 +5,7 @@ * 2.0. */ -import euiDarkVars from '@elastic/eui/dist/eui_theme_dark.json'; -import euiLightVars from '@elastic/eui/dist/eui_theme_light.json'; +import { euiLightVars, euiDarkVars } from '@kbn/ui-shared-deps-src/theme'; import { RouteRenderer, RouterProvider } from '@kbn/typed-react-router-config'; import React from 'react'; import { Route } from 'react-router-dom'; diff --git a/x-pack/plugins/apm/public/components/routing/home/index.tsx b/x-pack/plugins/apm/public/components/routing/home/index.tsx index 025fa8ddcc8a0..e70cb31eef88f 100644 --- a/x-pack/plugins/apm/public/components/routing/home/index.tsx +++ b/x-pack/plugins/apm/public/components/routing/home/index.tsx @@ -8,7 +8,7 @@ import { i18n } from '@kbn/i18n'; import { Outlet } from '@kbn/typed-react-router-config'; import * as t from 'io-ts'; import React from 'react'; -import { toBooleanRt } from '@kbn/io-ts-utils'; +import { toBooleanRt } from '@kbn/io-ts-utils/to_boolean_rt'; import { RedirectTo } from '../redirect_to'; import { comparisonTypeRt } from '../../../../common/runtime_types/comparison_type_rt'; import { ENVIRONMENT_ALL } from '../../../../common/environment_filter_values'; @@ -69,6 +69,8 @@ export const home = { t.partial({ refreshPaused: t.union([t.literal('true'), t.literal('false')]), refreshInterval: t.string, + comparisonEnabled: toBooleanRt, + comparisonType: comparisonTypeRt, }), ]), }), diff --git a/x-pack/plugins/apm/public/components/routing/service_detail/index.tsx b/x-pack/plugins/apm/public/components/routing/service_detail/index.tsx index 4afa10cbf9a5d..d8a996d2163bc 100644 --- a/x-pack/plugins/apm/public/components/routing/service_detail/index.tsx +++ b/x-pack/plugins/apm/public/components/routing/service_detail/index.tsx @@ -8,7 +8,7 @@ import * as t from 'io-ts'; import { i18n } from '@kbn/i18n'; import React from 'react'; import { Outlet } from '@kbn/typed-react-router-config'; -import { toBooleanRt } from '@kbn/io-ts-utils'; +import { toBooleanRt } from '@kbn/io-ts-utils/to_boolean_rt'; import { comparisonTypeRt } from '../../../../common/runtime_types/comparison_type_rt'; import { ENVIRONMENT_ALL } from '../../../../common/environment_filter_values'; import { environmentRt } from '../../../../common/environment_rt'; @@ -135,6 +135,8 @@ export const serviceDetail = { t.partial({ traceId: t.string, transactionId: t.string, + comparisonEnabled: toBooleanRt, + comparisonType: comparisonTypeRt, }), ]), }), diff --git a/x-pack/plugins/apm/public/components/routing/templates/apm_service_template/analyze_data_button.tsx b/x-pack/plugins/apm/public/components/routing/templates/apm_service_template/analyze_data_button.tsx index a4fc964a444c9..5fa37050e71a6 100644 --- a/x-pack/plugins/apm/public/components/routing/templates/apm_service_template/analyze_data_button.tsx +++ b/x-pack/plugins/apm/public/components/routing/templates/apm_service_template/analyze_data_button.tsx @@ -79,12 +79,12 @@ export function AnalyzeDataButton() { position="top" content={i18n.translate('xpack.apm.analyzeDataButton.tooltip', { defaultMessage: - 'EXPERIMENTAL - Analyze Data allows you to select and filter result data in any dimension, and look for the cause or impact of performance problems', + 'EXPERIMENTAL - Explore Data allows you to select and filter result data in any dimension, and look for the cause or impact of performance problems', })} > {i18n.translate('xpack.apm.analyzeDataButton.label', { - defaultMessage: 'Analyze data', + defaultMessage: 'Explore data', })} diff --git a/x-pack/plugins/apm/public/components/shared/EnvironmentFilter/index.tsx b/x-pack/plugins/apm/public/components/shared/EnvironmentFilter/index.tsx index d52e8a412e18f..d259d4340ce4c 100644 --- a/x-pack/plugins/apm/public/components/shared/EnvironmentFilter/index.tsx +++ b/x-pack/plugins/apm/public/components/shared/EnvironmentFilter/index.tsx @@ -137,6 +137,7 @@ export function EnvironmentFilter({ }} isLoading={status === 'loading'} style={{ minWidth }} + data-test-subj="environmentFilter" /> ); } diff --git a/x-pack/plugins/apm/public/components/shared/Links/apm/transaction_detail_link.tsx b/x-pack/plugins/apm/public/components/shared/Links/apm/transaction_detail_link.tsx index 8b885526fb67c..3de16cf4db029 100644 --- a/x-pack/plugins/apm/public/components/shared/Links/apm/transaction_detail_link.tsx +++ b/x-pack/plugins/apm/public/components/shared/Links/apm/transaction_detail_link.tsx @@ -14,6 +14,7 @@ import { useLegacyUrlParams } from '../../../../context/url_params_context/use_u import { pickKeys } from '../../../../../common/utils/pick_keys'; import { APMQueryParams } from '../url_helpers'; import { useApmPluginContext } from '../../../../context/apm_plugin/use_apm_plugin_context'; +import { TimeRangeComparisonType } from '../../../../../common/runtime_types/comparison_type_rt'; interface Props extends APMLinkExtendProps { serviceName: string; @@ -23,6 +24,8 @@ interface Props extends APMLinkExtendProps { transactionType: string; latencyAggregationType?: string; environment?: string; + comparisonEnabled?: boolean; + comparisonType?: TimeRangeComparisonType; } const persistedFilters: Array = [ @@ -38,6 +41,8 @@ export function TransactionDetailLink({ transactionType, latencyAggregationType, environment, + comparisonEnabled, + comparisonType, ...rest }: Props) { const { urlParams } = useLegacyUrlParams(); @@ -51,6 +56,8 @@ export function TransactionDetailLink({ transactionId, transactionName, transactionType, + comparisonEnabled, + comparisonType, ...pickKeys(urlParams as APMQueryParams, ...persistedFilters), ...pickBy({ latencyAggregationType, environment }, identity), }, diff --git a/x-pack/plugins/apm/public/components/shared/charts/transaction_distribution_chart/index.test.tsx b/x-pack/plugins/apm/public/components/shared/charts/transaction_distribution_chart/index.test.tsx index 8a57063ac4d45..b8d070c64ca9f 100644 --- a/x-pack/plugins/apm/public/components/shared/charts/transaction_distribution_chart/index.test.tsx +++ b/x-pack/plugins/apm/public/components/shared/charts/transaction_distribution_chart/index.test.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import type { HistogramItem } from '../../../../../common/search_strategies/types'; +import type { HistogramItem } from '../../../../../common/correlations/types'; import { replaceHistogramDotsWithBars } from './index'; diff --git a/x-pack/plugins/apm/public/components/shared/charts/transaction_distribution_chart/index.tsx b/x-pack/plugins/apm/public/components/shared/charts/transaction_distribution_chart/index.tsx index dcf52cebaeeda..80fbd864fd815 100644 --- a/x-pack/plugins/apm/public/components/shared/charts/transaction_distribution_chart/index.tsx +++ b/x-pack/plugins/apm/public/components/shared/charts/transaction_distribution_chart/index.tsx @@ -32,7 +32,7 @@ import { i18n } from '@kbn/i18n'; import { useChartTheme } from '../../../../../../observability/public'; import { getDurationFormatter } from '../../../../../common/utils/formatters'; -import type { HistogramItem } from '../../../../../common/search_strategies/types'; +import type { HistogramItem } from '../../../../../common/correlations/types'; import { FETCH_STATUS } from '../../../../hooks/use_fetcher'; import { useTheme } from '../../../../hooks/use_theme'; diff --git a/x-pack/plugins/apm/public/components/shared/time_comparison/get_comparison_enabled.test.ts b/x-pack/plugins/apm/public/components/shared/time_comparison/get_comparison_enabled.test.ts new file mode 100644 index 0000000000000..23e1c94729204 --- /dev/null +++ b/x-pack/plugins/apm/public/components/shared/time_comparison/get_comparison_enabled.test.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 { CoreStart } from 'kibana/public'; +import { getComparisonEnabled } from './get_comparison_enabled'; + +describe('getComparisonEnabled', () => { + function mockValues({ + uiSettings, + urlComparisonEnabled, + }: { + uiSettings: boolean; + urlComparisonEnabled?: boolean; + }) { + return { + core: { uiSettings: { get: () => uiSettings } } as unknown as CoreStart, + urlComparisonEnabled, + }; + } + + afterEach(() => { + jest.clearAllMocks(); + }); + + it('returns false when kibana config is disabled and url is empty', () => { + const { core, urlComparisonEnabled } = mockValues({ + uiSettings: false, + urlComparisonEnabled: undefined, + }); + expect(getComparisonEnabled({ core, urlComparisonEnabled })).toBeFalsy(); + }); + + it('returns true when kibana config is enabled and url is empty', () => { + const { core, urlComparisonEnabled } = mockValues({ + uiSettings: true, + urlComparisonEnabled: undefined, + }); + expect(getComparisonEnabled({ core, urlComparisonEnabled })).toBeTruthy(); + }); + + it('returns true when defined as true in the url', () => { + const { core, urlComparisonEnabled } = mockValues({ + uiSettings: false, + urlComparisonEnabled: true, + }); + expect(getComparisonEnabled({ core, urlComparisonEnabled })).toBeTruthy(); + }); + + it('returns false when defined as false in the url', () => { + const { core, urlComparisonEnabled } = mockValues({ + uiSettings: true, + urlComparisonEnabled: false, + }); + expect(getComparisonEnabled({ core, urlComparisonEnabled })).toBeFalsy(); + }); +}); diff --git a/x-pack/plugins/apm/public/components/shared/time_comparison/get_comparison_enabled.ts b/x-pack/plugins/apm/public/components/shared/time_comparison/get_comparison_enabled.ts new file mode 100644 index 0000000000000..5f2ca5dca4656 --- /dev/null +++ b/x-pack/plugins/apm/public/components/shared/time_comparison/get_comparison_enabled.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 { CoreStart } from 'kibana/public'; +import { enableComparisonByDefault } from '../../../../../observability/public'; + +export function getComparisonEnabled({ + core, + urlComparisonEnabled, +}: { + core: CoreStart; + urlComparisonEnabled?: boolean; +}) { + const isEnabledByDefault = core.uiSettings.get( + enableComparisonByDefault + ); + + return urlComparisonEnabled ?? isEnabledByDefault; +} diff --git a/x-pack/plugins/apm/public/components/shared/time_comparison/index.tsx b/x-pack/plugins/apm/public/components/shared/time_comparison/index.tsx index 9d077713ff12e..db085861ae095 100644 --- a/x-pack/plugins/apm/public/components/shared/time_comparison/index.tsx +++ b/x-pack/plugins/apm/public/components/shared/time_comparison/index.tsx @@ -14,10 +14,12 @@ import { euiStyled } from '../../../../../../../src/plugins/kibana_react/common' import { useUiTracker } from '../../../../../observability/public'; import { TimeRangeComparisonEnum } from '../../../../common/runtime_types/comparison_type_rt'; import { useLegacyUrlParams } from '../../../context/url_params_context/use_url_params'; +import { useApmPluginContext } from '../../../context/apm_plugin/use_apm_plugin_context'; import { useApmParams } from '../../../hooks/use_apm_params'; import { useBreakpoints } from '../../../hooks/use_breakpoints'; import { useTimeRange } from '../../../hooks/use_time_range'; import * as urlHelpers from '../../shared/Links/url_helpers'; +import { getComparisonEnabled } from './get_comparison_enabled'; import { getComparisonTypes } from './get_comparison_types'; import { getTimeRangeComparison } from './get_time_range_comparison'; @@ -113,6 +115,7 @@ export function getSelectOptions({ } export function TimeComparison() { + const { core } = useApmPluginContext(); const trackApmEvent = useUiTracker({ app: 'apm' }); const history = useHistory(); const { isSmall } = useBreakpoints(); @@ -138,7 +141,13 @@ export function TimeComparison() { if (comparisonEnabled === undefined || comparisonType === undefined) { urlHelpers.replace(history, { query: { - comparisonEnabled: comparisonEnabled === false ? 'false' : 'true', + comparisonEnabled: + getComparisonEnabled({ + core, + urlComparisonEnabled: comparisonEnabled, + }) === false + ? 'false' + : 'true', comparisonType: comparisonType ? comparisonType : comparisonTypes[0], }, }); diff --git a/x-pack/plugins/apm/public/components/shared/transactions_table/get_columns.tsx b/x-pack/plugins/apm/public/components/shared/transactions_table/get_columns.tsx index 18e9beb2c8795..c44fbb8b7f87a 100644 --- a/x-pack/plugins/apm/public/components/shared/transactions_table/get_columns.tsx +++ b/x-pack/plugins/apm/public/components/shared/transactions_table/get_columns.tsx @@ -15,6 +15,7 @@ import { i18n } from '@kbn/i18n'; import React from 'react'; import { ValuesType } from 'utility-types'; import { LatencyAggregationType } from '../../../../common/latency_aggregation_types'; +import { TimeRangeComparisonType } from '../../../../common/runtime_types/comparison_type_rt'; import { asMillisecondDuration, asPercent, @@ -42,12 +43,14 @@ export function getColumns({ transactionGroupDetailedStatistics, comparisonEnabled, shouldShowSparkPlots = true, + comparisonType, }: { serviceName: string; latencyAggregationType?: LatencyAggregationType; transactionGroupDetailedStatistics?: TransactionGroupDetailedStatistics; comparisonEnabled?: boolean; shouldShowSparkPlots?: boolean; + comparisonType?: TimeRangeComparisonType; }): Array> { return [ { @@ -67,6 +70,8 @@ export function getColumns({ transactionName={name} transactionType={type} latencyAggregationType={latencyAggregationType} + comparisonEnabled={comparisonEnabled} + comparisonType={comparisonType} > {name} diff --git a/x-pack/plugins/apm/public/components/shared/transactions_table/index.tsx b/x-pack/plugins/apm/public/components/shared/transactions_table/index.tsx index 6c934cc51e2f7..2d9f6584535fa 100644 --- a/x-pack/plugins/apm/public/components/shared/transactions_table/index.tsx +++ b/x-pack/plugins/apm/public/components/shared/transactions_table/index.tsx @@ -222,6 +222,7 @@ export function TransactionsTable({ transactionGroupDetailedStatistics, comparisonEnabled, shouldShowSparkPlots, + comparisonType, }); const isLoading = status === FETCH_STATUS.LOADING; diff --git a/x-pack/plugins/apm/public/context/apm_plugin/mock_apm_plugin_context.tsx b/x-pack/plugins/apm/public/context/apm_plugin/mock_apm_plugin_context.tsx index abdab939f4a0a..b519388a8bac7 100644 --- a/x-pack/plugins/apm/public/context/apm_plugin/mock_apm_plugin_context.tsx +++ b/x-pack/plugins/apm/public/context/apm_plugin/mock_apm_plugin_context.tsx @@ -6,11 +6,11 @@ */ import React, { ReactNode, useMemo } from 'react'; -import { Observable, of } from 'rxjs'; import { RouterProvider } from '@kbn/typed-react-router-config'; import { useHistory } from 'react-router-dom'; import { createMemoryHistory, History } from 'history'; import { merge } from 'lodash'; +import { coreMock } from '../../../../../../src/core/public/mocks'; import { UrlService } from '../../../../../../src/plugins/share/common/url_service'; import { createObservabilityRuleTypeRegistryMock } from '../../../../observability/public'; import { ApmPluginContext, ApmPluginContextValue } from './apm_plugin_context'; @@ -20,72 +20,43 @@ import { createCallApmApi } from '../../services/rest/createCallApmApi'; import { apmRouter } from '../../components/routing/apm_route_config'; import { MlLocatorDefinition } from '../../../../ml/public'; -const uiSettings: Record = { - [UI_SETTINGS.TIMEPICKER_QUICK_RANGES]: [ - { - from: 'now/d', - to: 'now/d', - display: 'Today', - }, - { - from: 'now/w', - to: 'now/w', - display: 'This week', - }, - ], - [UI_SETTINGS.TIMEPICKER_TIME_DEFAULTS]: { - from: 'now-15m', - to: 'now', - }, - [UI_SETTINGS.TIMEPICKER_REFRESH_INTERVAL_DEFAULTS]: { - pause: false, - value: 100000, - }, -}; +const coreStart = coreMock.createStart({ basePath: '/basepath' }); -const mockCore = { +const mockCore = merge({}, coreStart, { application: { capabilities: { apm: {}, ml: {}, }, - currentAppId$: new Observable(), - getUrlForApp: (appId: string) => '', - navigateToUrl: (url: string) => {}, - }, - chrome: { - docTitle: { change: () => {} }, - setBreadcrumbs: () => {}, - setHelpExtension: () => {}, - setBadge: () => {}, - }, - docLinks: { - DOC_LINK_VERSION: '0', - ELASTIC_WEBSITE_URL: 'https://www.elastic.co/', - links: { - apm: {}, - }, - }, - http: { - basePath: { - prepend: (path: string) => `/basepath${path}`, - get: () => `/basepath`, - }, - }, - i18n: { - Context: ({ children }: { children: ReactNode }) => children, - }, - notifications: { - toasts: { - addWarning: () => {}, - addDanger: () => {}, - }, }, uiSettings: { - get: (key: string) => uiSettings[key], - get$: (key: string) => of(mockCore.uiSettings.get(key)), + get: (key: string) => { + const uiSettings: Record = { + [UI_SETTINGS.TIMEPICKER_QUICK_RANGES]: [ + { + from: 'now/d', + to: 'now/d', + display: 'Today', + }, + { + from: 'now/w', + to: 'now/w', + display: 'This week', + }, + ], + [UI_SETTINGS.TIMEPICKER_TIME_DEFAULTS]: { + from: 'now-15m', + to: 'now', + }, + [UI_SETTINGS.TIMEPICKER_REFRESH_INTERVAL_DEFAULTS]: { + pause: false, + value: 100000, + }, + }; + return uiSettings[key]; + }, }, -}; +}); const mockConfig: ConfigSchema = { serviceMapEnabled: true, @@ -118,16 +89,22 @@ const mockPlugin = { }, }; -const mockAppMountParameters = { - setHeaderActionMenu: () => {}, +const mockCorePlugins = { + embeddable: {}, + inspector: {}, + maps: {}, + observability: {}, + data: {}, }; export const mockApmPluginContextValue = { - appMountParameters: mockAppMountParameters, + appMountParameters: coreMock.createAppMountParameters('/basepath'), config: mockConfig, core: mockCore, plugins: mockPlugin, observabilityRuleTypeRegistry: createObservabilityRuleTypeRegistryMock(), + corePlugins: mockCorePlugins, + deps: {}, }; export function MockApmPluginContextWrapper({ @@ -135,7 +112,7 @@ export function MockApmPluginContextWrapper({ value = {} as ApmPluginContextValue, history, }: { - children?: React.ReactNode; + children?: ReactNode; value?: ApmPluginContextValue; history?: History; }) { diff --git a/x-pack/plugins/apm/public/context/apm_service/apm_service_context.tsx b/x-pack/plugins/apm/public/context/apm_service/apm_service_context.tsx index 9d207eee2fbaa..c99ef519f9e69 100644 --- a/x-pack/plugins/apm/public/context/apm_service/apm_service_context.tsx +++ b/x-pack/plugins/apm/public/context/apm_service/apm_service_context.tsx @@ -23,14 +23,20 @@ export type APMServiceAlert = ValuesType< APIReturnType<'GET /internal/apm/services/{serviceName}/alerts'>['alerts'] >; -export const APMServiceContext = createContext<{ +export interface APMServiceContextValue { serviceName: string; agentName?: string; transactionType?: string; transactionTypes: string[]; alerts: APMServiceAlert[]; runtimeName?: string; -}>({ serviceName: '', transactionTypes: [], alerts: [] }); +} + +export const APMServiceContext = createContext({ + serviceName: '', + transactionTypes: [], + alerts: [], +}); export function ApmServiceContextProvider({ children, diff --git a/x-pack/plugins/apm/public/context/url_params_context/resolve_url_params.ts b/x-pack/plugins/apm/public/context/url_params_context/resolve_url_params.ts index 845fdb175bb65..c37d83983a00b 100644 --- a/x-pack/plugins/apm/public/context/url_params_context/resolve_url_params.ts +++ b/x-pack/plugins/apm/public/context/url_params_context/resolve_url_params.ts @@ -81,7 +81,7 @@ export function resolveUrlParams(location: Location, state: TimeUrlParams) { detailTab: toString(detailTab), flyoutDetailTab: toString(flyoutDetailTab), spanId: toNumber(spanId), - kuery: kuery && decodeURIComponent(kuery), + kuery, transactionName, transactionType, searchTerm: toString(searchTerm), diff --git a/x-pack/plugins/apm/public/hooks/use_search_strategy.ts b/x-pack/plugins/apm/public/hooks/use_search_strategy.ts deleted file mode 100644 index 95bc8cb7435a2..0000000000000 --- a/x-pack/plugins/apm/public/hooks/use_search_strategy.ts +++ /dev/null @@ -1,218 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { useCallback, useEffect, useReducer, useRef } from 'react'; -import type { Subscription } from 'rxjs'; - -import { - IKibanaSearchRequest, - IKibanaSearchResponse, - isCompleteResponse, - isErrorResponse, -} from '../../../../../src/plugins/data/public'; -import { useKibana } from '../../../../../src/plugins/kibana_react/public'; - -import type { RawSearchStrategyClientParams } from '../../common/search_strategies/types'; -import type { RawResponseBase } from '../../common/search_strategies/types'; -import type { - LatencyCorrelationsParams, - LatencyCorrelationsRawResponse, -} from '../../common/search_strategies/latency_correlations/types'; -import type { - FailedTransactionsCorrelationsParams, - FailedTransactionsCorrelationsRawResponse, -} from '../../common/search_strategies/failed_transactions_correlations/types'; -import { - ApmSearchStrategies, - APM_SEARCH_STRATEGIES, -} from '../../common/search_strategies/constants'; -import { useApmServiceContext } from '../context/apm_service/use_apm_service_context'; -import { useLegacyUrlParams } from '../context/url_params_context/use_url_params'; - -import { ApmPluginStartDeps } from '../plugin'; - -import { useApmParams } from './use_apm_params'; -import { useTimeRange } from './use_time_range'; - -interface SearchStrategyProgress { - error?: Error; - isRunning: boolean; - loaded: number; - total: number; -} - -const getInitialRawResponse = < - TRawResponse extends RawResponseBase ->(): TRawResponse => - ({ - ccsWarning: false, - took: 0, - } as TRawResponse); - -const getInitialProgress = (): SearchStrategyProgress => ({ - isRunning: false, - loaded: 0, - total: 100, -}); - -const getReducer = - () => - (prev: T, update: Partial): T => ({ - ...prev, - ...update, - }); - -interface SearchStrategyReturnBase { - progress: SearchStrategyProgress; - response: TRawResponse; - startFetch: () => void; - cancelFetch: () => void; -} - -// Function overload for Latency Correlations -export function useSearchStrategy( - searchStrategyName: typeof APM_SEARCH_STRATEGIES.APM_LATENCY_CORRELATIONS, - searchStrategyParams: LatencyCorrelationsParams -): SearchStrategyReturnBase; - -// Function overload for Failed Transactions Correlations -export function useSearchStrategy( - searchStrategyName: typeof APM_SEARCH_STRATEGIES.APM_FAILED_TRANSACTIONS_CORRELATIONS, - searchStrategyParams: FailedTransactionsCorrelationsParams -): SearchStrategyReturnBase< - FailedTransactionsCorrelationsRawResponse & RawResponseBase ->; - -export function useSearchStrategy< - TRawResponse extends RawResponseBase, - TParams = unknown ->( - searchStrategyName: ApmSearchStrategies, - searchStrategyParams?: TParams -): SearchStrategyReturnBase { - const { - services: { data }, - } = useKibana(); - - const { serviceName, transactionType } = useApmServiceContext(); - const { - query: { kuery, environment, rangeFrom, rangeTo }, - } = useApmParams('/services/{serviceName}/transactions/view'); - const { start, end } = useTimeRange({ rangeFrom, rangeTo }); - const { urlParams } = useLegacyUrlParams(); - const { transactionName } = urlParams; - - const [rawResponse, setRawResponse] = useReducer( - getReducer(), - getInitialRawResponse() - ); - - const [fetchState, setFetchState] = useReducer( - getReducer(), - getInitialProgress() - ); - - const abortCtrl = useRef(new AbortController()); - const searchSubscription$ = useRef(); - const searchStrategyParamsRef = useRef(searchStrategyParams); - - const startFetch = useCallback(() => { - searchSubscription$.current?.unsubscribe(); - abortCtrl.current.abort(); - abortCtrl.current = new AbortController(); - setFetchState({ - ...getInitialProgress(), - error: undefined, - }); - - const request = { - params: { - environment, - serviceName, - transactionName, - transactionType, - kuery, - start, - end, - ...(searchStrategyParamsRef.current - ? { ...searchStrategyParamsRef.current } - : {}), - }, - }; - - // Submit the search request using the `data.search` service. - searchSubscription$.current = data.search - .search< - IKibanaSearchRequest, - IKibanaSearchResponse - >(request, { - strategy: searchStrategyName, - abortSignal: abortCtrl.current.signal, - }) - .subscribe({ - next: (response: IKibanaSearchResponse) => { - setRawResponse(response.rawResponse); - setFetchState({ - isRunning: response.isRunning || false, - ...(response.loaded ? { loaded: response.loaded } : {}), - ...(response.total ? { total: response.total } : {}), - }); - - if (isCompleteResponse(response)) { - searchSubscription$.current?.unsubscribe(); - setFetchState({ - isRunning: false, - }); - } else if (isErrorResponse(response)) { - searchSubscription$.current?.unsubscribe(); - setFetchState({ - error: response as unknown as Error, - isRunning: false, - }); - } - }, - error: (error: Error) => { - setFetchState({ - error, - isRunning: false, - }); - }, - }); - }, [ - searchStrategyName, - data.search, - environment, - serviceName, - transactionName, - transactionType, - kuery, - start, - end, - ]); - - const cancelFetch = useCallback(() => { - searchSubscription$.current?.unsubscribe(); - searchSubscription$.current = undefined; - abortCtrl.current.abort(); - setFetchState({ - isRunning: false, - }); - }, []); - - // auto-update - useEffect(() => { - startFetch(); - return cancelFetch; - }, [startFetch, cancelFetch]); - - return { - progress: fetchState, - response: rawResponse, - startFetch, - cancelFetch, - }; -} diff --git a/x-pack/plugins/apm/public/tutorial/config_agent/commands/django.ts b/x-pack/plugins/apm/public/tutorial/config_agent/commands/django.ts index 97b5f3315bcdb..18fed9d329cd0 100644 --- a/x-pack/plugins/apm/public/tutorial/config_agent/commands/django.ts +++ b/x-pack/plugins/apm/public/tutorial/config_agent/commands/django.ts @@ -18,7 +18,7 @@ INSTALLED_APPS = ( # ... ) -ELASTIC_APM = {curlyOpen} +ELASTIC_APM = { # ${i18n.translate( 'xpack.apm.tutorial.djangoClient.configure.commands.setRequiredServiceNameComment', { @@ -58,7 +58,7 @@ ELASTIC_APM = {curlyOpen} } )} 'ENVIRONMENT': 'production', -{curlyClose} +} # ${i18n.translate( 'xpack.apm.tutorial.djangoClient.configure.commands.addTracingMiddlewareComment', diff --git a/x-pack/plugins/apm/public/tutorial/config_agent/commands/flask.ts b/x-pack/plugins/apm/public/tutorial/config_agent/commands/flask.ts index e4d7fd188e7c6..dbcd6f29225c1 100644 --- a/x-pack/plugins/apm/public/tutorial/config_agent/commands/flask.ts +++ b/x-pack/plugins/apm/public/tutorial/config_agent/commands/flask.ts @@ -25,7 +25,7 @@ apm = ElasticAPM(app) } )} from elasticapm.contrib.flask import ElasticAPM -app.config['ELASTIC_APM'] = {curlyOpen} +app.config['ELASTIC_APM'] = { # ${i18n.translate( 'xpack.apm.tutorial.flaskClient.configure.commands.setRequiredServiceNameComment', { @@ -65,6 +65,6 @@ app.config['ELASTIC_APM'] = {curlyOpen} } )} 'ENVIRONMENT': 'production', -{curlyClose} +} apm = ElasticAPM(app)`; diff --git a/x-pack/plugins/apm/public/tutorial/config_agent/commands/get_commands.test.ts b/x-pack/plugins/apm/public/tutorial/config_agent/commands/get_commands.test.ts index 5dc66e2230524..bb6593ae7acb8 100644 --- a/x-pack/plugins/apm/public/tutorial/config_agent/commands/get_commands.test.ts +++ b/x-pack/plugins/apm/public/tutorial/config_agent/commands/get_commands.test.ts @@ -178,7 +178,7 @@ describe('getCommands', () => { # ... ) - ELASTIC_APM = {curlyOpen} + ELASTIC_APM = { # Set the required service name. Allowed characters: # a-z, A-Z, 0-9, -, _, and space 'SERVICE_NAME': '', @@ -191,7 +191,7 @@ describe('getCommands', () => { # Set the service environment 'ENVIRONMENT': 'production', - {curlyClose} + } # To send performance metrics, add our tracing middleware: MIDDLEWARE = ( @@ -216,7 +216,7 @@ describe('getCommands', () => { # ... ) - ELASTIC_APM = {curlyOpen} + ELASTIC_APM = { # Set the required service name. Allowed characters: # a-z, A-Z, 0-9, -, _, and space 'SERVICE_NAME': '', @@ -229,7 +229,7 @@ describe('getCommands', () => { # Set the service environment 'ENVIRONMENT': 'production', - {curlyClose} + } # To send performance metrics, add our tracing middleware: MIDDLEWARE = ( @@ -254,7 +254,7 @@ describe('getCommands', () => { # or configure to use ELASTIC_APM in your application's settings from elasticapm.contrib.flask import ElasticAPM - app.config['ELASTIC_APM'] = {curlyOpen} + app.config['ELASTIC_APM'] = { # Set the required service name. Allowed characters: # a-z, A-Z, 0-9, -, _, and space 'SERVICE_NAME': '', @@ -267,7 +267,7 @@ describe('getCommands', () => { # Set the service environment 'ENVIRONMENT': 'production', - {curlyClose} + } apm = ElasticAPM(app)" `); @@ -289,7 +289,7 @@ describe('getCommands', () => { # or configure to use ELASTIC_APM in your application's settings from elasticapm.contrib.flask import ElasticAPM - app.config['ELASTIC_APM'] = {curlyOpen} + app.config['ELASTIC_APM'] = { # Set the required service name. Allowed characters: # a-z, A-Z, 0-9, -, _, and space 'SERVICE_NAME': '', @@ -302,7 +302,7 @@ describe('getCommands', () => { # Set the service environment 'ENVIRONMENT': 'production', - {curlyClose} + } apm = ElasticAPM(app)" `); diff --git a/x-pack/plugins/apm/public/utils/httpStatusCodeToColor.ts b/x-pack/plugins/apm/public/utils/httpStatusCodeToColor.ts index 345eb7aa3f635..1b44a90fe7bfc 100644 --- a/x-pack/plugins/apm/public/utils/httpStatusCodeToColor.ts +++ b/x-pack/plugins/apm/public/utils/httpStatusCodeToColor.ts @@ -5,7 +5,7 @@ * 2.0. */ -import theme from '@elastic/eui/dist/eui_theme_light.json'; +import { euiLightVars as theme } from '@kbn/ui-shared-deps-src/theme'; const { euiColorDarkShade, euiColorWarning } = theme; export const errorColor = '#c23c2b'; diff --git a/x-pack/plugins/apm/readme.md b/x-pack/plugins/apm/readme.md index 040e620ae91ca..df7b641fbb231 100644 --- a/x-pack/plugins/apm/readme.md +++ b/x-pack/plugins/apm/readme.md @@ -13,6 +13,9 @@ ## Tooling - [VSCode setup instructions](./dev_docs/vscode_setup.md) - [Github PR commands](./dev_docs/github_commands.md) +- [Synthtrace (data generation)](https://github.com/elastic/kibana/blob/main/packages/elastic-apm-synthtrace/README.md) +- [Query debugging in development and production](./dev_docs/query_debugging_in_development_and_production.md) ## Other resources - [Official APM UI settings docs](https://www.elastic.co/guide/en/kibana/current/apm-settings-in-kibana.html) +- [Reading Material](./dev_docs/learning_material.md) diff --git a/x-pack/plugins/apm/server/index.ts b/x-pack/plugins/apm/server/index.ts index bd30f9e212687..416a873bac0a9 100644 --- a/x-pack/plugins/apm/server/index.ts +++ b/x-pack/plugins/apm/server/index.ts @@ -116,7 +116,7 @@ export type { APMPluginSetup } from './types'; export type { APMServerRouteRepository, APIEndpoint, -} from './routes/get_global_apm_server_route_repository'; +} from './routes/apm_routes/get_global_apm_server_route_repository'; export type { APMRouteHandlerResources } from './routes/typings'; export type { ProcessorEvent } from '../common/processor_event'; diff --git a/x-pack/plugins/apm/server/lib/alerts/chart_preview/get_transaction_duration.ts b/x-pack/plugins/apm/server/lib/alerts/chart_preview/get_transaction_duration.ts index 8767b5a60d9b2..693502d7629e8 100644 --- a/x-pack/plugins/apm/server/lib/alerts/chart_preview/get_transaction_duration.ts +++ b/x-pack/plugins/apm/server/lib/alerts/chart_preview/get_transaction_duration.ts @@ -6,7 +6,7 @@ */ import { QueryDslQueryContainer } from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; -import { rangeQuery } from '../../../../../observability/server'; +import { rangeQuery, termQuery } from '../../../../../observability/server'; import { SERVICE_NAME, TRANSACTION_TYPE, @@ -46,10 +46,8 @@ export async function getTransactionDurationChartPreview({ const query = { bool: { filter: [ - ...(serviceName ? [{ term: { [SERVICE_NAME]: serviceName } }] : []), - ...(transactionType - ? [{ term: { [TRANSACTION_TYPE]: transactionType } }] - : []), + ...termQuery(SERVICE_NAME, serviceName), + ...termQuery(TRANSACTION_TYPE, transactionType), ...rangeQuery(start, end), ...environmentQuery(environment), ...getDocumentTypeFilterForTransactions(searchAggregatedTransactions), diff --git a/x-pack/plugins/apm/server/lib/alerts/chart_preview/get_transaction_error_count.ts b/x-pack/plugins/apm/server/lib/alerts/chart_preview/get_transaction_error_count.ts index 0cd1c1cddc651..0e1fa74199f60 100644 --- a/x-pack/plugins/apm/server/lib/alerts/chart_preview/get_transaction_error_count.ts +++ b/x-pack/plugins/apm/server/lib/alerts/chart_preview/get_transaction_error_count.ts @@ -8,7 +8,7 @@ import { SERVICE_NAME } from '../../../../common/elasticsearch_fieldnames'; import { ProcessorEvent } from '../../../../common/processor_event'; import { AlertParams } from '../../../routes/alerts/chart_preview'; -import { rangeQuery } from '../../../../../observability/server'; +import { rangeQuery, termQuery } from '../../../../../observability/server'; import { environmentQuery } from '../../../../common/utils/environment_query'; import { Setup } from '../../helpers/setup_request'; @@ -25,7 +25,7 @@ export async function getTransactionErrorCountChartPreview({ const query = { bool: { filter: [ - ...(serviceName ? [{ term: { [SERVICE_NAME]: serviceName } }] : []), + ...termQuery(SERVICE_NAME, serviceName), ...rangeQuery(start, end), ...environmentQuery(environment), ], diff --git a/x-pack/plugins/apm/server/lib/alerts/chart_preview/get_transaction_error_rate.ts b/x-pack/plugins/apm/server/lib/alerts/chart_preview/get_transaction_error_rate.ts index d3f03c597e8fb..e2bfaf29f83cb 100644 --- a/x-pack/plugins/apm/server/lib/alerts/chart_preview/get_transaction_error_rate.ts +++ b/x-pack/plugins/apm/server/lib/alerts/chart_preview/get_transaction_error_rate.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { rangeQuery } from '../../../../../observability/server'; +import { rangeQuery, termQuery } from '../../../../../observability/server'; import { SERVICE_NAME, TRANSACTION_TYPE, @@ -52,10 +52,8 @@ export async function getTransactionErrorRateChartPreview({ query: { bool: { filter: [ - ...(serviceName ? [{ term: { [SERVICE_NAME]: serviceName } }] : []), - ...(transactionType - ? [{ term: { [TRANSACTION_TYPE]: transactionType } }] - : []), + ...termQuery(SERVICE_NAME, serviceName), + ...termQuery(TRANSACTION_TYPE, transactionType), ...rangeQuery(start, end), ...environmentQuery(environment), ...getDocumentTypeFilterForTransactions( diff --git a/x-pack/plugins/apm/server/lib/alerts/register_error_count_alert_type.ts b/x-pack/plugins/apm/server/lib/alerts/register_error_count_alert_type.ts index 7fe2adcfe24d7..17beacae4b14d 100644 --- a/x-pack/plugins/apm/server/lib/alerts/register_error_count_alert_type.ts +++ b/x-pack/plugins/apm/server/lib/alerts/register_error_count_alert_type.ts @@ -41,6 +41,7 @@ import { getApmIndices } from '../settings/apm_indices/get_apm_indices'; import { apmActionVariables } from './action_variables'; import { alertingEsClient } from './alerting_es_client'; import { RegisterRuleDependencies } from './register_apm_alerts'; +import { termQuery } from '../../../../observability/server'; const ALERT_EVALUATION_THRESHOLD: typeof ALERT_EVALUATION_THRESHOLD_TYPED = ALERT_EVALUATION_THRESHOLD_NON_TYPED; @@ -113,9 +114,7 @@ export function registerErrorCountAlertType({ }, }, { term: { [PROCESSOR_EVENT]: ProcessorEvent.error } }, - ...(alertParams.serviceName - ? [{ term: { [SERVICE_NAME]: alertParams.serviceName } }] - : []), + ...termQuery(SERVICE_NAME, alertParams.serviceName), ...environmentQuery(alertParams.environment), ], }, diff --git a/x-pack/plugins/apm/server/lib/alerts/register_transaction_duration_anomaly_alert_type.ts b/x-pack/plugins/apm/server/lib/alerts/register_transaction_duration_anomaly_alert_type.ts index 2809d7feadb37..ec2fbb4028b74 100644 --- a/x-pack/plugins/apm/server/lib/alerts/register_transaction_duration_anomaly_alert_type.ts +++ b/x-pack/plugins/apm/server/lib/alerts/register_transaction_duration_anomaly_alert_type.ts @@ -46,6 +46,7 @@ import { getEnvironmentEsField, getEnvironmentLabel, } from '../../../common/environment_filter_values'; +import { termQuery } from '../../../../observability/server'; const ALERT_EVALUATION_THRESHOLD: typeof ALERT_EVALUATION_THRESHOLD_TYPED = ALERT_EVALUATION_THRESHOLD_NON_TYPED; @@ -157,24 +158,11 @@ export function registerTransactionDurationAnomalyAlertType({ }, }, }, - ...(alertParams.serviceName - ? [ - { - term: { - partition_field_value: alertParams.serviceName, - }, - }, - ] - : []), - ...(alertParams.transactionType - ? [ - { - term: { - by_field_value: alertParams.transactionType, - }, - }, - ] - : []), + ...termQuery( + 'partition_field_value', + alertParams.serviceName + ), + ...termQuery('by_field_value', alertParams.transactionType), ] as QueryDslQueryContainer[], }, }, diff --git a/x-pack/plugins/apm/server/lib/alerts/register_transaction_error_rate_alert_type.ts b/x-pack/plugins/apm/server/lib/alerts/register_transaction_error_rate_alert_type.ts index 5ba7ed5321d70..43dfbaf156f6c 100644 --- a/x-pack/plugins/apm/server/lib/alerts/register_transaction_error_rate_alert_type.ts +++ b/x-pack/plugins/apm/server/lib/alerts/register_transaction_error_rate_alert_type.ts @@ -48,6 +48,7 @@ import { RegisterRuleDependencies } from './register_apm_alerts'; import { SearchAggregatedTransactionSetting } from '../../../common/aggregated_transactions'; import { getDocumentTypeFilterForTransactions } from '../helpers/transactions'; import { asPercent } from '../../../../observability/common/utils/formatters'; +import { termQuery } from '../../../../observability/server'; const ALERT_EVALUATION_THRESHOLD: typeof ALERT_EVALUATION_THRESHOLD_TYPED = ALERT_EVALUATION_THRESHOLD_NON_TYPED; @@ -142,18 +143,8 @@ export function registerTransactionErrorRateAlertType({ ], }, }, - ...(alertParams.serviceName - ? [{ term: { [SERVICE_NAME]: alertParams.serviceName } }] - : []), - ...(alertParams.transactionType - ? [ - { - term: { - [TRANSACTION_TYPE]: alertParams.transactionType, - }, - }, - ] - : []), + ...termQuery(SERVICE_NAME, alertParams.serviceName), + ...termQuery(TRANSACTION_TYPE, alertParams.transactionType), ...environmentQuery(alertParams.environment), ], }, diff --git a/x-pack/plugins/apm/server/lib/anomaly_detection/create_anomaly_detection_jobs.ts b/x-pack/plugins/apm/server/lib/anomaly_detection/create_anomaly_detection_jobs.ts index 9d6abad0ff6a6..7277a12c2bf14 100644 --- a/x-pack/plugins/apm/server/lib/anomaly_detection/create_anomaly_detection_jobs.ts +++ b/x-pack/plugins/apm/server/lib/anomaly_detection/create_anomaly_detection_jobs.ts @@ -5,21 +5,21 @@ * 2.0. */ +import Boom from '@hapi/boom'; import { Logger } from 'kibana/server'; -import uuid from 'uuid/v4'; import { snakeCase } from 'lodash'; -import Boom from '@hapi/boom'; import moment from 'moment'; +import uuid from 'uuid/v4'; import { ML_ERRORS } from '../../../common/anomaly_detection'; -import { ProcessorEvent } from '../../../common/processor_event'; -import { environmentQuery } from '../../../common/utils/environment_query'; -import { Setup } from '../helpers/setup_request'; import { - TRANSACTION_DURATION, + METRICSET_NAME, PROCESSOR_EVENT, } from '../../../common/elasticsearch_fieldnames'; -import { APM_ML_JOB_GROUP, ML_MODULE_ID_APM_TRANSACTION } from './constants'; +import { ProcessorEvent } from '../../../common/processor_event'; +import { environmentQuery } from '../../../common/utils/environment_query'; import { withApmSpan } from '../../utils/with_apm_span'; +import { Setup } from '../helpers/setup_request'; +import { APM_ML_JOB_GROUP, ML_MODULE_ID_APM_TRANSACTION } from './constants'; import { getAnomalyDetectionJobs } from './get_anomaly_detection_jobs'; export async function createAnomalyDetectionJobs( @@ -92,8 +92,8 @@ async function createAnomalyDetectionJob({ query: { bool: { filter: [ - { term: { [PROCESSOR_EVENT]: ProcessorEvent.transaction } }, - { exists: { field: TRANSACTION_DURATION } }, + { term: { [PROCESSOR_EVENT]: ProcessorEvent.metric } }, + { term: { [METRICSET_NAME]: 'transaction' } }, ...environmentQuery(environment), ], }, @@ -105,7 +105,7 @@ async function createAnomalyDetectionJob({ job_tags: { environment, // identifies this as an APM ML job & facilitates future migrations - apm_ml_version: 2, + apm_ml_version: 3, }, }, }, diff --git a/x-pack/plugins/apm/server/lib/correlations/queries/field_stats/get_boolean_field_stats.ts b/x-pack/plugins/apm/server/lib/correlations/queries/field_stats/get_boolean_field_stats.ts new file mode 100644 index 0000000000000..c936e626a5599 --- /dev/null +++ b/x-pack/plugins/apm/server/lib/correlations/queries/field_stats/get_boolean_field_stats.ts @@ -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 { ElasticsearchClient } from 'kibana/server'; +import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; + +import { buildSamplerAggregation } from '../../utils/field_stats_utils'; +import { FieldValuePair } from '../../../../../common/correlations/types'; +import { + FieldStatsCommonRequestParams, + BooleanFieldStats, + Aggs, + TopValueBucket, +} from '../../../../../common/correlations/field_stats_types'; +import { getQueryWithParams } from '../get_query_with_params'; + +export const getBooleanFieldStatsRequest = ( + params: FieldStatsCommonRequestParams, + fieldName: string, + termFilters?: FieldValuePair[] +): estypes.SearchRequest => { + const query = getQueryWithParams({ params, termFilters }); + + const { index, samplerShardSize } = params; + + const size = 0; + const aggs: Aggs = { + sampled_value_count: { + filter: { exists: { field: fieldName } }, + }, + sampled_values: { + terms: { + field: fieldName, + size: 2, + }, + }, + }; + + const searchBody = { + query, + aggs: { + sample: buildSamplerAggregation(aggs, samplerShardSize), + }, + }; + + return { + index, + size, + body: searchBody, + }; +}; + +export const fetchBooleanFieldStats = async ( + esClient: ElasticsearchClient, + params: FieldStatsCommonRequestParams, + field: FieldValuePair, + termFilters?: FieldValuePair[] +): Promise => { + const request = getBooleanFieldStatsRequest( + params, + field.fieldName, + termFilters + ); + const { body } = await esClient.search(request); + const aggregations = body.aggregations as { + sample: { + sampled_value_count: estypes.AggregationsFiltersBucketItemKeys; + sampled_values: estypes.AggregationsTermsAggregate; + }; + }; + + const stats: BooleanFieldStats = { + fieldName: field.fieldName, + count: aggregations?.sample.sampled_value_count.doc_count ?? 0, + }; + + const valueBuckets: TopValueBucket[] = + aggregations?.sample.sampled_values?.buckets ?? []; + valueBuckets.forEach((bucket) => { + stats[`${bucket.key.toString()}Count`] = bucket.doc_count; + }); + return stats; +}; diff --git a/x-pack/plugins/apm/server/lib/search_strategies/queries/field_stats/get_field_stats.test.ts b/x-pack/plugins/apm/server/lib/correlations/queries/field_stats/get_field_stats.test.ts similarity index 100% rename from x-pack/plugins/apm/server/lib/search_strategies/queries/field_stats/get_field_stats.test.ts rename to x-pack/plugins/apm/server/lib/correlations/queries/field_stats/get_field_stats.test.ts diff --git a/x-pack/plugins/apm/server/lib/correlations/queries/field_stats/get_fields_stats.ts b/x-pack/plugins/apm/server/lib/correlations/queries/field_stats/get_fields_stats.ts new file mode 100644 index 0000000000000..8b41f7662679c --- /dev/null +++ b/x-pack/plugins/apm/server/lib/correlations/queries/field_stats/get_fields_stats.ts @@ -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 { ElasticsearchClient } from 'kibana/server'; +import { chunk } from 'lodash'; +import { ES_FIELD_TYPES } from '@kbn/field-types'; +import { + FieldValuePair, + CorrelationsParams, +} from '../../../../../common/correlations/types'; +import { + FieldStats, + FieldStatsCommonRequestParams, +} from '../../../../../common/correlations/field_stats_types'; +import { getRequestBase } from '../get_request_base'; +import { fetchKeywordFieldStats } from './get_keyword_field_stats'; +import { fetchNumericFieldStats } from './get_numeric_field_stats'; +import { fetchBooleanFieldStats } from './get_boolean_field_stats'; + +export const fetchFieldsStats = async ( + esClient: ElasticsearchClient, + params: CorrelationsParams, + fieldsToSample: string[], + termFilters?: FieldValuePair[] +): Promise<{ stats: FieldStats[]; errors: any[] }> => { + const stats: FieldStats[] = []; + const errors: any[] = []; + + if (fieldsToSample.length === 0) return { stats, errors }; + + const respMapping = await esClient.fieldCaps({ + ...getRequestBase(params), + fields: fieldsToSample, + }); + + const fieldStatsParams: FieldStatsCommonRequestParams = { + ...params, + samplerShardSize: 5000, + }; + const fieldStatsPromises = Object.entries(respMapping.body.fields) + .map(([key, value], idx) => { + const field: FieldValuePair = { fieldName: key, fieldValue: '' }; + const fieldTypes = Object.keys(value); + + for (const ft of fieldTypes) { + switch (ft) { + case ES_FIELD_TYPES.KEYWORD: + case ES_FIELD_TYPES.IP: + return fetchKeywordFieldStats( + esClient, + fieldStatsParams, + field, + termFilters + ); + break; + + case 'numeric': + case 'number': + case ES_FIELD_TYPES.FLOAT: + case ES_FIELD_TYPES.HALF_FLOAT: + case ES_FIELD_TYPES.SCALED_FLOAT: + case ES_FIELD_TYPES.DOUBLE: + case ES_FIELD_TYPES.INTEGER: + case ES_FIELD_TYPES.LONG: + case ES_FIELD_TYPES.SHORT: + case ES_FIELD_TYPES.UNSIGNED_LONG: + case ES_FIELD_TYPES.BYTE: + return fetchNumericFieldStats( + esClient, + fieldStatsParams, + field, + termFilters + ); + + break; + case ES_FIELD_TYPES.BOOLEAN: + return fetchBooleanFieldStats( + esClient, + fieldStatsParams, + field, + termFilters + ); + + default: + return; + } + } + }) + .filter((f) => f !== undefined) as Array>; + + const batches = chunk(fieldStatsPromises, 10); + for (let i = 0; i < batches.length; i++) { + try { + const results = await Promise.allSettled(batches[i]); + results.forEach((r) => { + if (r.status === 'fulfilled' && r.value !== undefined) { + stats.push(r.value); + } + }); + } catch (e) { + errors.push(e); + } + } + + return { stats, errors }; +}; diff --git a/x-pack/plugins/apm/server/lib/search_strategies/queries/field_stats/get_keyword_field_stats.ts b/x-pack/plugins/apm/server/lib/correlations/queries/field_stats/get_keyword_field_stats.ts similarity index 93% rename from x-pack/plugins/apm/server/lib/search_strategies/queries/field_stats/get_keyword_field_stats.ts rename to x-pack/plugins/apm/server/lib/correlations/queries/field_stats/get_keyword_field_stats.ts index a9c727457d0ae..c64bbc6678779 100644 --- a/x-pack/plugins/apm/server/lib/search_strategies/queries/field_stats/get_keyword_field_stats.ts +++ b/x-pack/plugins/apm/server/lib/correlations/queries/field_stats/get_keyword_field_stats.ts @@ -7,15 +7,15 @@ import { ElasticsearchClient } from 'kibana/server'; import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; -import { FieldValuePair } from '../../../../../common/search_strategies/types'; -import { getQueryWithParams } from '../get_query_with_params'; -import { buildSamplerAggregation } from '../../utils/field_stats_utils'; +import { FieldValuePair } from '../../../../../common/correlations/types'; import { FieldStatsCommonRequestParams, KeywordFieldStats, Aggs, TopValueBucket, -} from '../../../../../common/search_strategies/field_stats_types'; +} from '../../../../../common/correlations/field_stats_types'; +import { buildSamplerAggregation } from '../../utils/field_stats_utils'; +import { getQueryWithParams } from '../get_query_with_params'; export const getKeywordFieldStatsRequest = ( params: FieldStatsCommonRequestParams, diff --git a/x-pack/plugins/apm/server/lib/correlations/queries/field_stats/get_numeric_field_stats.ts b/x-pack/plugins/apm/server/lib/correlations/queries/field_stats/get_numeric_field_stats.ts new file mode 100644 index 0000000000000..21e6559fdda25 --- /dev/null +++ b/x-pack/plugins/apm/server/lib/correlations/queries/field_stats/get_numeric_field_stats.ts @@ -0,0 +1,129 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { ElasticsearchClient } from 'kibana/server'; +import { find, get } from 'lodash'; +import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; +import { + NumericFieldStats, + FieldStatsCommonRequestParams, + TopValueBucket, + Aggs, +} from '../../../../../common/correlations/field_stats_types'; +import { FieldValuePair } from '../../../../../common/correlations/types'; +import { getQueryWithParams } from '../get_query_with_params'; +import { buildSamplerAggregation } from '../../utils/field_stats_utils'; + +// Only need 50th percentile for the median +const PERCENTILES = [50]; + +export const getNumericFieldStatsRequest = ( + params: FieldStatsCommonRequestParams, + fieldName: string, + termFilters?: FieldValuePair[] +) => { + const query = getQueryWithParams({ params, termFilters }); + const size = 0; + + const { index, samplerShardSize } = params; + + const percents = PERCENTILES; + const aggs: Aggs = { + sampled_field_stats: { + filter: { exists: { field: fieldName } }, + aggs: { + actual_stats: { + stats: { field: fieldName }, + }, + }, + }, + sampled_percentiles: { + percentiles: { + field: fieldName, + percents, + keyed: false, + }, + }, + sampled_top: { + terms: { + field: fieldName, + size: 10, + order: { + _count: 'desc', + }, + }, + }, + }; + + const searchBody = { + query, + aggs: { + sample: buildSamplerAggregation(aggs, samplerShardSize), + }, + }; + + return { + index, + size, + body: searchBody, + }; +}; + +export const fetchNumericFieldStats = async ( + esClient: ElasticsearchClient, + params: FieldStatsCommonRequestParams, + field: FieldValuePair, + termFilters?: FieldValuePair[] +): Promise => { + const request: estypes.SearchRequest = getNumericFieldStatsRequest( + params, + field.fieldName, + termFilters + ); + const { body } = await esClient.search(request); + + const aggregations = body.aggregations as { + sample: { + sampled_top: estypes.AggregationsTermsAggregate; + sampled_percentiles: estypes.AggregationsHdrPercentilesAggregate; + sampled_field_stats: { + doc_count: number; + actual_stats: estypes.AggregationsStatsAggregate; + }; + }; + }; + const docCount = aggregations?.sample.sampled_field_stats?.doc_count ?? 0; + const fieldStatsResp = + aggregations?.sample.sampled_field_stats?.actual_stats ?? {}; + const topValues = aggregations?.sample.sampled_top?.buckets ?? []; + + const stats: NumericFieldStats = { + fieldName: field.fieldName, + count: docCount, + min: get(fieldStatsResp, 'min', 0), + max: get(fieldStatsResp, 'max', 0), + avg: get(fieldStatsResp, 'avg', 0), + topValues, + topValuesSampleSize: topValues.reduce( + (acc: number, curr: TopValueBucket) => acc + curr.doc_count, + aggregations.sample.sampled_top?.sum_other_doc_count ?? 0 + ), + }; + + if (stats.count !== undefined && stats.count > 0) { + const percentiles = aggregations?.sample.sampled_percentiles.values ?? []; + const medianPercentile: { value: number; key: number } | undefined = find( + percentiles, + { + key: 50, + } + ); + stats.median = medianPercentile !== undefined ? medianPercentile!.value : 0; + } + + return stats; +}; diff --git a/x-pack/plugins/apm/server/lib/search_strategies/queries/get_filters.ts b/x-pack/plugins/apm/server/lib/correlations/queries/get_filters.ts similarity index 91% rename from x-pack/plugins/apm/server/lib/search_strategies/queries/get_filters.ts rename to x-pack/plugins/apm/server/lib/correlations/queries/get_filters.ts index 4c91f2ca987b5..58ee5051d8863 100644 --- a/x-pack/plugins/apm/server/lib/search_strategies/queries/get_filters.ts +++ b/x-pack/plugins/apm/server/lib/correlations/queries/get_filters.ts @@ -15,7 +15,7 @@ import { PROCESSOR_EVENT, } from '../../../../common/elasticsearch_fieldnames'; import { ProcessorEvent } from '../../../../common/processor_event'; -import { SearchStrategyClientParams } from '../../../../common/search_strategies/types'; +import { CorrelationsClientParams } from '../../../../common/correlations/types'; export function getCorrelationsFilters({ environment, @@ -25,7 +25,7 @@ export function getCorrelationsFilters({ transactionName, start, end, -}: SearchStrategyClientParams) { +}: CorrelationsClientParams) { const correlationsFilters: ESFilter[] = [ { term: { [PROCESSOR_EVENT]: ProcessorEvent.transaction } }, ...rangeQuery(start, end), diff --git a/x-pack/plugins/apm/server/lib/search_strategies/queries/get_query_with_params.test.ts b/x-pack/plugins/apm/server/lib/correlations/queries/get_query_with_params.test.ts similarity index 100% rename from x-pack/plugins/apm/server/lib/search_strategies/queries/get_query_with_params.test.ts rename to x-pack/plugins/apm/server/lib/correlations/queries/get_query_with_params.test.ts diff --git a/x-pack/plugins/apm/server/lib/search_strategies/queries/get_query_with_params.ts b/x-pack/plugins/apm/server/lib/correlations/queries/get_query_with_params.ts similarity index 91% rename from x-pack/plugins/apm/server/lib/search_strategies/queries/get_query_with_params.ts rename to x-pack/plugins/apm/server/lib/correlations/queries/get_query_with_params.ts index 297fd68a7503f..6572d72f614c7 100644 --- a/x-pack/plugins/apm/server/lib/search_strategies/queries/get_query_with_params.ts +++ b/x-pack/plugins/apm/server/lib/correlations/queries/get_query_with_params.ts @@ -8,8 +8,8 @@ import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; import type { FieldValuePair, - SearchStrategyParams, -} from '../../../../common/search_strategies/types'; + CorrelationsParams, +} from '../../../../common/correlations/types'; import { getCorrelationsFilters } from './get_filters'; export const getTermsQuery = ({ fieldName, fieldValue }: FieldValuePair) => { @@ -17,7 +17,7 @@ export const getTermsQuery = ({ fieldName, fieldValue }: FieldValuePair) => { }; interface QueryParams { - params: SearchStrategyParams; + params: CorrelationsParams; termFilters?: FieldValuePair[]; } export const getQueryWithParams = ({ params, termFilters }: QueryParams) => { diff --git a/x-pack/plugins/apm/server/lib/search_strategies/queries/get_request_base.test.ts b/x-pack/plugins/apm/server/lib/correlations/queries/get_request_base.test.ts similarity index 100% rename from x-pack/plugins/apm/server/lib/search_strategies/queries/get_request_base.test.ts rename to x-pack/plugins/apm/server/lib/correlations/queries/get_request_base.test.ts diff --git a/x-pack/plugins/apm/server/lib/search_strategies/queries/get_request_base.ts b/x-pack/plugins/apm/server/lib/correlations/queries/get_request_base.ts similarity index 79% rename from x-pack/plugins/apm/server/lib/search_strategies/queries/get_request_base.ts rename to x-pack/plugins/apm/server/lib/correlations/queries/get_request_base.ts index fb1639b5d5f4a..5ab4e3b26122d 100644 --- a/x-pack/plugins/apm/server/lib/search_strategies/queries/get_request_base.ts +++ b/x-pack/plugins/apm/server/lib/correlations/queries/get_request_base.ts @@ -5,12 +5,12 @@ * 2.0. */ -import type { SearchStrategyParams } from '../../../../common/search_strategies/types'; +import type { CorrelationsParams } from '../../../../common/correlations/types'; export const getRequestBase = ({ index, includeFrozen, -}: SearchStrategyParams) => ({ +}: CorrelationsParams) => ({ index, // matches APM's event client settings ignore_throttled: includeFrozen === undefined ? true : !includeFrozen, diff --git a/x-pack/plugins/apm/server/lib/correlations/queries/index.ts b/x-pack/plugins/apm/server/lib/correlations/queries/index.ts new file mode 100644 index 0000000000000..548127eb7647d --- /dev/null +++ b/x-pack/plugins/apm/server/lib/correlations/queries/index.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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export { fetchFailedTransactionsCorrelationPValues } from './query_failure_correlation'; +export { fetchPValues } from './query_p_values'; +export { fetchSignificantCorrelations } from './query_significant_correlations'; +export { fetchTransactionDurationFieldCandidates } from './query_field_candidates'; +export { fetchTransactionDurationFieldValuePairs } from './query_field_value_pairs'; +export { fetchTransactionDurationFractions } from './query_fractions'; +export { fetchTransactionDurationPercentiles } from './query_percentiles'; +export { fetchTransactionDurationCorrelation } from './query_correlation'; +export { fetchTransactionDurationCorrelationWithHistogram } from './query_correlation_with_histogram'; +export { fetchTransactionDurationHistogramRangeSteps } from './query_histogram_range_steps'; +export { fetchTransactionDurationRanges } from './query_ranges'; diff --git a/x-pack/plugins/apm/server/lib/search_strategies/queries/query_correlation.test.ts b/x-pack/plugins/apm/server/lib/correlations/queries/query_correlation.test.ts similarity index 100% rename from x-pack/plugins/apm/server/lib/search_strategies/queries/query_correlation.test.ts rename to x-pack/plugins/apm/server/lib/correlations/queries/query_correlation.test.ts diff --git a/x-pack/plugins/apm/server/lib/search_strategies/queries/query_correlation.ts b/x-pack/plugins/apm/server/lib/correlations/queries/query_correlation.ts similarity index 95% rename from x-pack/plugins/apm/server/lib/search_strategies/queries/query_correlation.ts rename to x-pack/plugins/apm/server/lib/correlations/queries/query_correlation.ts index a150d23b27113..ed62b4dfa91b7 100644 --- a/x-pack/plugins/apm/server/lib/search_strategies/queries/query_correlation.ts +++ b/x-pack/plugins/apm/server/lib/correlations/queries/query_correlation.ts @@ -13,8 +13,8 @@ import { TRANSACTION_DURATION } from '../../../../common/elasticsearch_fieldname import type { FieldValuePair, ResponseHit, - SearchStrategyParams, -} from '../../../../common/search_strategies/types'; + CorrelationsParams, +} from '../../../../common/correlations/types'; import { getQueryWithParams } from './get_query_with_params'; import { getRequestBase } from './get_request_base'; @@ -33,7 +33,7 @@ export interface BucketCorrelation { } export const getTransactionDurationCorrelationRequest = ( - params: SearchStrategyParams, + params: CorrelationsParams, expectations: number[], ranges: estypes.AggregationsAggregationRange[], fractions: number[], @@ -87,7 +87,7 @@ export const getTransactionDurationCorrelationRequest = ( export const fetchTransactionDurationCorrelation = async ( esClient: ElasticsearchClient, - params: SearchStrategyParams, + params: CorrelationsParams, expectations: number[], ranges: estypes.AggregationsAggregationRange[], fractions: number[], diff --git a/x-pack/plugins/apm/server/lib/correlations/queries/query_correlation_with_histogram.test.ts b/x-pack/plugins/apm/server/lib/correlations/queries/query_correlation_with_histogram.test.ts new file mode 100644 index 0000000000000..2e1a635671794 --- /dev/null +++ b/x-pack/plugins/apm/server/lib/correlations/queries/query_correlation_with_histogram.test.ts @@ -0,0 +1,128 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; + +import type { ElasticsearchClient } from 'src/core/server'; +import { ENVIRONMENT_ALL } from '../../../../common/environment_filter_values'; + +import { splitAllSettledPromises } from '../utils'; + +import { fetchTransactionDurationCorrelationWithHistogram } from './query_correlation_with_histogram'; + +const params = { + index: 'apm-*', + start: 1577836800000, + end: 1609459200000, + includeFrozen: false, + environment: ENVIRONMENT_ALL.value, + kuery: '', +}; +const expectations = [1, 3, 5]; +const ranges = [{ to: 1 }, { from: 1, to: 3 }, { from: 3, to: 5 }, { from: 5 }]; +const fractions = [1, 2, 4, 5]; +const totalDocCount = 1234; +const histogramRangeSteps = [1, 2, 4, 5]; + +const fieldValuePairs = [ + { fieldName: 'the-field-name-1', fieldValue: 'the-field-value-1' }, + { fieldName: 'the-field-name-2', fieldValue: 'the-field-value-2' }, + { fieldName: 'the-field-name-2', fieldValue: 'the-field-value-3' }, +]; + +describe('query_correlation_with_histogram', () => { + describe('fetchTransactionDurationCorrelationWithHistogram', () => { + it(`doesn't break on failing ES queries and adds messages to the log`, async () => { + const esClientSearchMock = jest.fn( + ( + req: estypes.SearchRequest + ): { + body: estypes.SearchResponse; + } => { + return { + body: {} as unknown as estypes.SearchResponse, + }; + } + ); + + const esClientMock = { + search: esClientSearchMock, + } as unknown as ElasticsearchClient; + + const { fulfilled: items, rejected: errors } = splitAllSettledPromises( + await Promise.allSettled( + fieldValuePairs.map((fieldValuePair) => + fetchTransactionDurationCorrelationWithHistogram( + esClientMock, + params, + expectations, + ranges, + fractions, + histogramRangeSteps, + totalDocCount, + fieldValuePair + ) + ) + ) + ); + + expect(items.length).toEqual(0); + expect(esClientSearchMock).toHaveBeenCalledTimes(3); + expect(errors.map((e) => (e as Error).toString())).toEqual([ + 'Error: fetchTransactionDurationCorrelation failed, did not return aggregations.', + 'Error: fetchTransactionDurationCorrelation failed, did not return aggregations.', + 'Error: fetchTransactionDurationCorrelation failed, did not return aggregations.', + ]); + }); + + it('returns items with correlation and ks-test value', async () => { + const esClientSearchMock = jest.fn( + ( + req: estypes.SearchRequest + ): { + body: estypes.SearchResponse; + } => { + return { + body: { + aggregations: { + latency_ranges: { buckets: [] }, + transaction_duration_correlation: { value: 0.6 }, + ks_test: { less: 0.001 }, + logspace_ranges: { buckets: [] }, + }, + } as unknown as estypes.SearchResponse, + }; + } + ); + + const esClientMock = { + search: esClientSearchMock, + } as unknown as ElasticsearchClient; + + const { fulfilled: items, rejected: errors } = splitAllSettledPromises( + await Promise.allSettled( + fieldValuePairs.map((fieldValuePair) => + fetchTransactionDurationCorrelationWithHistogram( + esClientMock, + params, + expectations, + ranges, + fractions, + histogramRangeSteps, + totalDocCount, + fieldValuePair + ) + ) + ) + ); + + expect(items.length).toEqual(3); + expect(esClientSearchMock).toHaveBeenCalledTimes(6); + expect(errors.length).toEqual(0); + }); + }); +}); diff --git a/x-pack/plugins/apm/server/lib/correlations/queries/query_correlation_with_histogram.ts b/x-pack/plugins/apm/server/lib/correlations/queries/query_correlation_with_histogram.ts new file mode 100644 index 0000000000000..03b28b28d521a --- /dev/null +++ b/x-pack/plugins/apm/server/lib/correlations/queries/query_correlation_with_histogram.ts @@ -0,0 +1,65 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; + +import type { ElasticsearchClient } from 'src/core/server'; + +import type { + FieldValuePair, + CorrelationsParams, +} from '../../../../common/correlations/types'; + +import type { LatencyCorrelation } from '../../../../common/correlations/latency_correlations/types'; +import { + CORRELATION_THRESHOLD, + KS_TEST_THRESHOLD, +} from '../../../../common/correlations/constants'; + +import { fetchTransactionDurationCorrelation } from './query_correlation'; +import { fetchTransactionDurationRanges } from './query_ranges'; + +export async function fetchTransactionDurationCorrelationWithHistogram( + esClient: ElasticsearchClient, + params: CorrelationsParams, + expectations: number[], + ranges: estypes.AggregationsAggregationRange[], + fractions: number[], + histogramRangeSteps: number[], + totalDocCount: number, + fieldValuePair: FieldValuePair +): Promise { + const { correlation, ksTest } = await fetchTransactionDurationCorrelation( + esClient, + params, + expectations, + ranges, + fractions, + totalDocCount, + [fieldValuePair] + ); + + if ( + correlation !== null && + correlation > CORRELATION_THRESHOLD && + ksTest !== null && + ksTest < KS_TEST_THRESHOLD + ) { + const logHistogram = await fetchTransactionDurationRanges( + esClient, + params, + histogramRangeSteps, + [fieldValuePair] + ); + return { + ...fieldValuePair, + correlation, + ksTest, + histogram: logHistogram, + }; + } +} diff --git a/x-pack/plugins/apm/server/lib/search_strategies/queries/query_failure_correlation.ts b/x-pack/plugins/apm/server/lib/correlations/queries/query_failure_correlation.ts similarity index 92% rename from x-pack/plugins/apm/server/lib/search_strategies/queries/query_failure_correlation.ts rename to x-pack/plugins/apm/server/lib/correlations/queries/query_failure_correlation.ts index 10a098c4a3ffc..cd8d1aacde9ae 100644 --- a/x-pack/plugins/apm/server/lib/search_strategies/queries/query_failure_correlation.ts +++ b/x-pack/plugins/apm/server/lib/correlations/queries/query_failure_correlation.ts @@ -6,7 +6,8 @@ */ import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; import { ElasticsearchClient } from 'kibana/server'; -import { SearchStrategyParams } from '../../../../common/search_strategies/types'; +import { CorrelationsParams } from '../../../../common/correlations/types'; +import { FailedTransactionsCorrelation } from '../../../../common/correlations/failed_transactions_correlations/types'; import { EVENT_OUTCOME } from '../../../../common/elasticsearch_fieldnames'; import { EventOutcome } from '../../../../common/event_outcome'; import { fetchTransactionDurationRanges } from './query_ranges'; @@ -14,7 +15,7 @@ import { getQueryWithParams, getTermsQuery } from './get_query_with_params'; import { getRequestBase } from './get_request_base'; export const getFailureCorrelationRequest = ( - params: SearchStrategyParams, + params: CorrelationsParams, fieldName: string ): estypes.SearchRequest => { const query = getQueryWithParams({ @@ -65,7 +66,7 @@ export const getFailureCorrelationRequest = ( export const fetchFailedTransactionsCorrelationPValues = async ( esClient: ElasticsearchClient, - params: SearchStrategyParams, + params: CorrelationsParams, histogramRangeSteps: number[], fieldName: string ) => { @@ -88,7 +89,7 @@ export const fetchFailedTransactionsCorrelationPValues = async ( }>; // Using for of to sequentially augment the results with histogram data. - const result = []; + const result: FailedTransactionsCorrelation[] = []; for (const bucket of overallResult.buckets) { // Scale the score into a value from 0 - 1 // using a concave piecewise linear function in -log(p-value) diff --git a/x-pack/plugins/apm/server/lib/search_strategies/queries/query_field_candidates.test.ts b/x-pack/plugins/apm/server/lib/correlations/queries/query_field_candidates.test.ts similarity index 98% rename from x-pack/plugins/apm/server/lib/search_strategies/queries/query_field_candidates.test.ts rename to x-pack/plugins/apm/server/lib/correlations/queries/query_field_candidates.test.ts index 311016a1b0834..02af6637e5bb3 100644 --- a/x-pack/plugins/apm/server/lib/search_strategies/queries/query_field_candidates.test.ts +++ b/x-pack/plugins/apm/server/lib/correlations/queries/query_field_candidates.test.ts @@ -10,7 +10,7 @@ import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; import type { ElasticsearchClient } from 'src/core/server'; import { ENVIRONMENT_ALL } from '../../../../common/environment_filter_values'; -import { hasPrefixToInclude } from '../utils'; +import { hasPrefixToInclude } from '../../../../common/correlations/utils'; import { fetchTransactionDurationFieldCandidates, diff --git a/x-pack/plugins/apm/server/lib/search_strategies/queries/query_field_candidates.ts b/x-pack/plugins/apm/server/lib/correlations/queries/query_field_candidates.ts similarity index 91% rename from x-pack/plugins/apm/server/lib/search_strategies/queries/query_field_candidates.ts rename to x-pack/plugins/apm/server/lib/correlations/queries/query_field_candidates.ts index 612225a2348cb..801bb18e8957a 100644 --- a/x-pack/plugins/apm/server/lib/search_strategies/queries/query_field_candidates.ts +++ b/x-pack/plugins/apm/server/lib/correlations/queries/query_field_candidates.ts @@ -11,15 +11,14 @@ import { ES_FIELD_TYPES } from '@kbn/field-types'; import type { ElasticsearchClient } from 'src/core/server'; -import type { SearchStrategyParams } from '../../../../common/search_strategies/types'; - +import type { CorrelationsParams } from '../../../../common/correlations/types'; import { FIELD_PREFIX_TO_EXCLUDE_AS_CANDIDATE, FIELDS_TO_ADD_AS_CANDIDATE, FIELDS_TO_EXCLUDE_AS_CANDIDATE, POPULATED_DOC_COUNT_SAMPLE_SIZE, -} from '../constants'; -import { hasPrefixToInclude } from '../utils'; +} from '../../../../common/correlations/constants'; +import { hasPrefixToInclude } from '../../../../common/correlations/utils'; import { getQueryWithParams } from './get_query_with_params'; import { getRequestBase } from './get_request_base'; @@ -40,7 +39,7 @@ export const shouldBeExcluded = (fieldName: string) => { }; export const getRandomDocsRequest = ( - params: SearchStrategyParams + params: CorrelationsParams ): estypes.SearchRequest => ({ ...getRequestBase(params), body: { @@ -59,7 +58,7 @@ export const getRandomDocsRequest = ( export const fetchTransactionDurationFieldCandidates = async ( esClient: ElasticsearchClient, - params: SearchStrategyParams + params: CorrelationsParams ): Promise<{ fieldCandidates: string[] }> => { const { index } = params; // Get all supported fields diff --git a/x-pack/plugins/apm/server/lib/search_strategies/queries/query_field_value_pairs.test.ts b/x-pack/plugins/apm/server/lib/correlations/queries/query_field_value_pairs.test.ts similarity index 81% rename from x-pack/plugins/apm/server/lib/search_strategies/queries/query_field_value_pairs.test.ts rename to x-pack/plugins/apm/server/lib/correlations/queries/query_field_value_pairs.test.ts index bb3aa40b328af..80016930184b3 100644 --- a/x-pack/plugins/apm/server/lib/search_strategies/queries/query_field_value_pairs.test.ts +++ b/x-pack/plugins/apm/server/lib/correlations/queries/query_field_value_pairs.test.ts @@ -10,9 +10,6 @@ import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; import type { ElasticsearchClient } from 'src/core/server'; import { ENVIRONMENT_ALL } from '../../../../common/environment_filter_values'; -import { searchServiceLogProvider } from '../search_service_log'; -import { latencyCorrelationsSearchServiceStateProvider } from '../latency_correlations/latency_correlations_search_service_state'; - import { fetchTransactionDurationFieldValuePairs, getTermsAggRequest, @@ -66,21 +63,14 @@ describe('query_field_value_pairs', () => { search: esClientSearchMock, } as unknown as ElasticsearchClient; - const { addLogMessage, getLogMessages } = searchServiceLogProvider(); - const state = latencyCorrelationsSearchServiceStateProvider(); - const resp = await fetchTransactionDurationFieldValuePairs( esClientMock, params, - fieldCandidates, - state, - addLogMessage + fieldCandidates ); - const { progress } = state.getState(); - - expect(progress.loadedFieldValuePairs).toBe(1); - expect(resp).toEqual([ + expect(resp.errors).toEqual([]); + expect(resp.fieldValuePairs).toEqual([ { fieldName: 'myFieldCandidate1', fieldValue: 'myValue1' }, { fieldName: 'myFieldCandidate1', fieldValue: 'myValue2' }, { fieldName: 'myFieldCandidate2', fieldValue: 'myValue1' }, @@ -89,7 +79,6 @@ describe('query_field_value_pairs', () => { { fieldName: 'myFieldCandidate3', fieldValue: 'myValue2' }, ]); expect(esClientSearchMock).toHaveBeenCalledTimes(3); - expect(getLogMessages()).toEqual([]); }); }); }); diff --git a/x-pack/plugins/apm/server/lib/correlations/queries/query_field_value_pairs.ts b/x-pack/plugins/apm/server/lib/correlations/queries/query_field_value_pairs.ts new file mode 100644 index 0000000000000..16c4dacb5ef95 --- /dev/null +++ b/x-pack/plugins/apm/server/lib/correlations/queries/query_field_value_pairs.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 type { ElasticsearchClient } from 'src/core/server'; + +import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; + +import type { + FieldValuePair, + CorrelationsParams, +} from '../../../../common/correlations/types'; +import { TERMS_SIZE } from '../../../../common/correlations/constants'; + +import { splitAllSettledPromises } from '../utils'; + +import { getQueryWithParams } from './get_query_with_params'; +import { getRequestBase } from './get_request_base'; + +export const getTermsAggRequest = ( + params: CorrelationsParams, + fieldName: string +): estypes.SearchRequest => ({ + ...getRequestBase(params), + body: { + query: getQueryWithParams({ params }), + size: 0, + aggs: { + attribute_terms: { + terms: { + field: fieldName, + size: TERMS_SIZE, + }, + }, + }, + }, +}); + +const fetchTransactionDurationFieldTerms = async ( + esClient: ElasticsearchClient, + params: CorrelationsParams, + fieldName: string +): Promise => { + const resp = await esClient.search(getTermsAggRequest(params, fieldName)); + + if (resp.body.aggregations === undefined) { + throw new Error( + 'fetchTransactionDurationFieldTerms failed, did not return aggregations.' + ); + } + + const buckets = ( + resp.body.aggregations + .attribute_terms as estypes.AggregationsMultiBucketAggregate<{ + key: string; + key_as_string?: string; + }> + )?.buckets; + if (buckets?.length >= 1) { + return buckets.map((d) => ({ + fieldName, + // The terms aggregation returns boolean fields as { key: 0, key_as_string: "false" }, + // so we need to pick `key_as_string` if it's present, otherwise searches on boolean fields would fail later on. + fieldValue: d.key_as_string ?? d.key, + })); + } + + return []; +}; + +export const fetchTransactionDurationFieldValuePairs = async ( + esClient: ElasticsearchClient, + params: CorrelationsParams, + fieldCandidates: string[] +): Promise<{ fieldValuePairs: FieldValuePair[]; errors: any[] }> => { + const { fulfilled: responses, rejected: errors } = splitAllSettledPromises( + await Promise.allSettled( + fieldCandidates.map((fieldCandidate) => + fetchTransactionDurationFieldTerms(esClient, params, fieldCandidate) + ) + ) + ); + + return { fieldValuePairs: responses.flat(), errors }; +}; diff --git a/x-pack/plugins/apm/server/lib/search_strategies/queries/query_fractions.test.ts b/x-pack/plugins/apm/server/lib/correlations/queries/query_fractions.test.ts similarity index 98% rename from x-pack/plugins/apm/server/lib/search_strategies/queries/query_fractions.test.ts rename to x-pack/plugins/apm/server/lib/correlations/queries/query_fractions.test.ts index 5c18b21fc029c..12b054e18bab7 100644 --- a/x-pack/plugins/apm/server/lib/search_strategies/queries/query_fractions.test.ts +++ b/x-pack/plugins/apm/server/lib/correlations/queries/query_fractions.test.ts @@ -47,6 +47,7 @@ describe('query_fractions', () => { } => { return { body: { + hits: { total: { value: 3 } }, aggregations: { latency_ranges: { buckets: [{ doc_count: 1 }, { doc_count: 2 }], diff --git a/x-pack/plugins/apm/server/lib/search_strategies/queries/query_fractions.ts b/x-pack/plugins/apm/server/lib/correlations/queries/query_fractions.ts similarity index 87% rename from x-pack/plugins/apm/server/lib/search_strategies/queries/query_fractions.ts rename to x-pack/plugins/apm/server/lib/correlations/queries/query_fractions.ts index 555465466498a..fb9aa0f77b510 100644 --- a/x-pack/plugins/apm/server/lib/search_strategies/queries/query_fractions.ts +++ b/x-pack/plugins/apm/server/lib/correlations/queries/query_fractions.ts @@ -8,14 +8,14 @@ import { ElasticsearchClient } from 'kibana/server'; import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; -import { SearchStrategyParams } from '../../../../common/search_strategies/types'; +import { CorrelationsParams } from '../../../../common/correlations/types'; import { TRANSACTION_DURATION } from '../../../../common/elasticsearch_fieldnames'; import { getQueryWithParams } from './get_query_with_params'; import { getRequestBase } from './get_request_base'; export const getTransactionDurationRangesRequest = ( - params: SearchStrategyParams, + params: CorrelationsParams, ranges: estypes.AggregationsAggregationRange[] ): estypes.SearchRequest => ({ ...getRequestBase(params), @@ -38,12 +38,20 @@ export const getTransactionDurationRangesRequest = ( */ export const fetchTransactionDurationFractions = async ( esClient: ElasticsearchClient, - params: SearchStrategyParams, + params: CorrelationsParams, ranges: estypes.AggregationsAggregationRange[] ): Promise<{ fractions: number[]; totalDocCount: number }> => { const resp = await esClient.search( getTransactionDurationRangesRequest(params, ranges) ); + + if ((resp.body.hits.total as estypes.SearchTotalHits).value === 0) { + return { + fractions: [], + totalDocCount: 0, + }; + } + if (resp.body.aggregations === undefined) { throw new Error( 'fetchTransactionDurationFractions failed, did not return aggregations.' diff --git a/x-pack/plugins/apm/server/lib/search_strategies/queries/query_histogram.test.ts b/x-pack/plugins/apm/server/lib/correlations/queries/query_histogram.test.ts similarity index 100% rename from x-pack/plugins/apm/server/lib/search_strategies/queries/query_histogram.test.ts rename to x-pack/plugins/apm/server/lib/correlations/queries/query_histogram.test.ts diff --git a/x-pack/plugins/apm/server/lib/search_strategies/queries/query_histogram.ts b/x-pack/plugins/apm/server/lib/correlations/queries/query_histogram.ts similarity index 92% rename from x-pack/plugins/apm/server/lib/search_strategies/queries/query_histogram.ts rename to x-pack/plugins/apm/server/lib/correlations/queries/query_histogram.ts index 4e40834acccd1..0a96253803ea2 100644 --- a/x-pack/plugins/apm/server/lib/search_strategies/queries/query_histogram.ts +++ b/x-pack/plugins/apm/server/lib/correlations/queries/query_histogram.ts @@ -14,14 +14,14 @@ import type { FieldValuePair, HistogramItem, ResponseHit, - SearchStrategyParams, -} from '../../../../common/search_strategies/types'; + CorrelationsParams, +} from '../../../../common/correlations/types'; import { getQueryWithParams } from './get_query_with_params'; import { getRequestBase } from './get_request_base'; export const getTransactionDurationHistogramRequest = ( - params: SearchStrategyParams, + params: CorrelationsParams, interval: number, termFilters?: FieldValuePair[] ): estypes.SearchRequest => ({ @@ -39,7 +39,7 @@ export const getTransactionDurationHistogramRequest = ( export const fetchTransactionDurationHistogram = async ( esClient: ElasticsearchClient, - params: SearchStrategyParams, + params: CorrelationsParams, interval: number, termFilters?: FieldValuePair[] ): Promise => { diff --git a/x-pack/plugins/apm/server/lib/search_strategies/queries/query_histogram_range_steps.test.ts b/x-pack/plugins/apm/server/lib/correlations/queries/query_histogram_range_steps.test.ts similarity index 100% rename from x-pack/plugins/apm/server/lib/search_strategies/queries/query_histogram_range_steps.test.ts rename to x-pack/plugins/apm/server/lib/correlations/queries/query_histogram_range_steps.test.ts diff --git a/x-pack/plugins/apm/server/lib/search_strategies/queries/query_histogram_range_steps.ts b/x-pack/plugins/apm/server/lib/correlations/queries/query_histogram_range_steps.ts similarity index 93% rename from x-pack/plugins/apm/server/lib/search_strategies/queries/query_histogram_range_steps.ts rename to x-pack/plugins/apm/server/lib/correlations/queries/query_histogram_range_steps.ts index 176e7befda53b..aa63bcc770c21 100644 --- a/x-pack/plugins/apm/server/lib/search_strategies/queries/query_histogram_range_steps.ts +++ b/x-pack/plugins/apm/server/lib/correlations/queries/query_histogram_range_steps.ts @@ -12,7 +12,7 @@ import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; import type { ElasticsearchClient } from 'src/core/server'; import { TRANSACTION_DURATION } from '../../../../common/elasticsearch_fieldnames'; -import type { SearchStrategyParams } from '../../../../common/search_strategies/types'; +import type { CorrelationsParams } from '../../../../common/correlations/types'; import { getQueryWithParams } from './get_query_with_params'; import { getRequestBase } from './get_request_base'; @@ -31,7 +31,7 @@ export const getHistogramRangeSteps = ( }; export const getHistogramIntervalRequest = ( - params: SearchStrategyParams + params: CorrelationsParams ): estypes.SearchRequest => ({ ...getRequestBase(params), body: { @@ -46,7 +46,7 @@ export const getHistogramIntervalRequest = ( export const fetchTransactionDurationHistogramRangeSteps = async ( esClient: ElasticsearchClient, - params: SearchStrategyParams + params: CorrelationsParams ): Promise => { const steps = 100; diff --git a/x-pack/plugins/apm/server/lib/correlations/queries/query_p_values.ts b/x-pack/plugins/apm/server/lib/correlations/queries/query_p_values.ts new file mode 100644 index 0000000000000..7c471aebd0f7a --- /dev/null +++ b/x-pack/plugins/apm/server/lib/correlations/queries/query_p_values.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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { ElasticsearchClient } from 'src/core/server'; + +import type { CorrelationsParams } from '../../../../common/correlations/types'; +import type { FailedTransactionsCorrelation } from '../../../../common/correlations/failed_transactions_correlations/types'; +import { ERROR_CORRELATION_THRESHOLD } from '../../../../common/correlations/constants'; + +import { splitAllSettledPromises } from '../utils'; + +import { + fetchFailedTransactionsCorrelationPValues, + fetchTransactionDurationHistogramRangeSteps, +} from './index'; + +export const fetchPValues = async ( + esClient: ElasticsearchClient, + paramsWithIndex: CorrelationsParams, + fieldCandidates: string[] +) => { + const histogramRangeSteps = await fetchTransactionDurationHistogramRangeSteps( + esClient, + paramsWithIndex + ); + + const { fulfilled, rejected } = splitAllSettledPromises( + await Promise.allSettled( + fieldCandidates.map((fieldName) => + fetchFailedTransactionsCorrelationPValues( + esClient, + paramsWithIndex, + histogramRangeSteps, + fieldName + ) + ) + ) + ); + + const failedTransactionsCorrelations: FailedTransactionsCorrelation[] = + fulfilled + .flat() + .filter( + (record) => + record && + typeof record.pValue === 'number' && + record.pValue < ERROR_CORRELATION_THRESHOLD + ); + + const ccsWarning = + rejected.length > 0 && paramsWithIndex?.index.includes(':'); + + return { failedTransactionsCorrelations, ccsWarning }; +}; diff --git a/x-pack/plugins/apm/server/lib/search_strategies/queries/query_percentiles.test.ts b/x-pack/plugins/apm/server/lib/correlations/queries/query_percentiles.test.ts similarity index 100% rename from x-pack/plugins/apm/server/lib/search_strategies/queries/query_percentiles.test.ts rename to x-pack/plugins/apm/server/lib/correlations/queries/query_percentiles.test.ts diff --git a/x-pack/plugins/apm/server/lib/search_strategies/queries/query_percentiles.ts b/x-pack/plugins/apm/server/lib/correlations/queries/query_percentiles.ts similarity index 91% rename from x-pack/plugins/apm/server/lib/search_strategies/queries/query_percentiles.ts rename to x-pack/plugins/apm/server/lib/correlations/queries/query_percentiles.ts index 4e1a7b2015614..68efcadd1bd0b 100644 --- a/x-pack/plugins/apm/server/lib/search_strategies/queries/query_percentiles.ts +++ b/x-pack/plugins/apm/server/lib/correlations/queries/query_percentiles.ts @@ -10,18 +10,18 @@ import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; import type { ElasticsearchClient } from 'src/core/server'; import { TRANSACTION_DURATION } from '../../../../common/elasticsearch_fieldnames'; +import { SIGNIFICANT_VALUE_DIGITS } from '../../../../common/correlations/constants'; import type { FieldValuePair, ResponseHit, - SearchStrategyParams, -} from '../../../../common/search_strategies/types'; + CorrelationsParams, +} from '../../../../common/correlations/types'; import { getQueryWithParams } from './get_query_with_params'; import { getRequestBase } from './get_request_base'; -import { SIGNIFICANT_VALUE_DIGITS } from '../constants'; export const getTransactionDurationPercentilesRequest = ( - params: SearchStrategyParams, + params: CorrelationsParams, percents?: number[], termFilters?: FieldValuePair[] ): estypes.SearchRequest => { @@ -50,7 +50,7 @@ export const getTransactionDurationPercentilesRequest = ( export const fetchTransactionDurationPercentiles = async ( esClient: ElasticsearchClient, - params: SearchStrategyParams, + params: CorrelationsParams, percents?: number[], termFilters?: FieldValuePair[] ): Promise<{ totalDocs: number; percentiles: Record }> => { diff --git a/x-pack/plugins/apm/server/lib/search_strategies/queries/query_ranges.test.ts b/x-pack/plugins/apm/server/lib/correlations/queries/query_ranges.test.ts similarity index 100% rename from x-pack/plugins/apm/server/lib/search_strategies/queries/query_ranges.test.ts rename to x-pack/plugins/apm/server/lib/correlations/queries/query_ranges.test.ts diff --git a/x-pack/plugins/apm/server/lib/search_strategies/queries/query_ranges.ts b/x-pack/plugins/apm/server/lib/correlations/queries/query_ranges.ts similarity index 93% rename from x-pack/plugins/apm/server/lib/search_strategies/queries/query_ranges.ts rename to x-pack/plugins/apm/server/lib/correlations/queries/query_ranges.ts index 8b359c3665eaf..d35f438046276 100644 --- a/x-pack/plugins/apm/server/lib/search_strategies/queries/query_ranges.ts +++ b/x-pack/plugins/apm/server/lib/correlations/queries/query_ranges.ts @@ -13,14 +13,14 @@ import { TRANSACTION_DURATION } from '../../../../common/elasticsearch_fieldname import type { FieldValuePair, ResponseHit, - SearchStrategyParams, -} from '../../../../common/search_strategies/types'; + CorrelationsParams, +} from '../../../../common/correlations/types'; import { getQueryWithParams } from './get_query_with_params'; import { getRequestBase } from './get_request_base'; export const getTransactionDurationRangesRequest = ( - params: SearchStrategyParams, + params: CorrelationsParams, rangesSteps: number[], termFilters?: FieldValuePair[] ): estypes.SearchRequest => { @@ -57,7 +57,7 @@ export const getTransactionDurationRangesRequest = ( export const fetchTransactionDurationRanges = async ( esClient: ElasticsearchClient, - params: SearchStrategyParams, + params: CorrelationsParams, rangesSteps: number[], termFilters?: FieldValuePair[] ): Promise> => { diff --git a/x-pack/plugins/apm/server/lib/correlations/queries/query_significant_correlations.ts b/x-pack/plugins/apm/server/lib/correlations/queries/query_significant_correlations.ts new file mode 100644 index 0000000000000..ed5ad1c278143 --- /dev/null +++ b/x-pack/plugins/apm/server/lib/correlations/queries/query_significant_correlations.ts @@ -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 { range } from 'lodash'; + +import type { ElasticsearchClient } from 'src/core/server'; + +import type { + FieldValuePair, + CorrelationsParams, +} from '../../../../common/correlations/types'; +import { LatencyCorrelation } from '../../../../common/correlations/latency_correlations/types'; + +import { + computeExpectationsAndRanges, + splitAllSettledPromises, +} from '../utils'; + +import { + fetchTransactionDurationCorrelationWithHistogram, + fetchTransactionDurationFractions, + fetchTransactionDurationHistogramRangeSteps, + fetchTransactionDurationPercentiles, +} from './index'; + +export const fetchSignificantCorrelations = async ( + esClient: ElasticsearchClient, + paramsWithIndex: CorrelationsParams, + fieldValuePairs: FieldValuePair[] +) => { + // Create an array of ranges [2, 4, 6, ..., 98] + const percentileAggregationPercents = range(2, 100, 2); + const { percentiles: percentilesRecords } = + await fetchTransactionDurationPercentiles( + esClient, + paramsWithIndex, + percentileAggregationPercents + ); + + // We need to round the percentiles values + // because the queries we're using based on it + // later on wouldn't allow numbers with decimals. + const percentiles = Object.values(percentilesRecords).map(Math.round); + + const { expectations, ranges } = computeExpectationsAndRanges(percentiles); + + const { fractions, totalDocCount } = await fetchTransactionDurationFractions( + esClient, + paramsWithIndex, + ranges + ); + + const histogramRangeSteps = await fetchTransactionDurationHistogramRangeSteps( + esClient, + paramsWithIndex + ); + + const { fulfilled, rejected } = splitAllSettledPromises( + await Promise.allSettled( + fieldValuePairs.map((fieldValuePair) => + fetchTransactionDurationCorrelationWithHistogram( + esClient, + paramsWithIndex, + expectations, + ranges, + fractions, + histogramRangeSteps, + totalDocCount, + fieldValuePair + ) + ) + ) + ); + + const latencyCorrelations: LatencyCorrelation[] = fulfilled.filter( + (d): d is LatencyCorrelation => d !== undefined + ); + + const ccsWarning = + rejected.length > 0 && paramsWithIndex?.index.includes(':'); + + return { latencyCorrelations, ccsWarning, totalDocCount }; +}; diff --git a/x-pack/plugins/apm/server/lib/search_strategies/utils/compute_expectations_and_ranges.test.ts b/x-pack/plugins/apm/server/lib/correlations/utils/compute_expectations_and_ranges.test.ts similarity index 100% rename from x-pack/plugins/apm/server/lib/search_strategies/utils/compute_expectations_and_ranges.test.ts rename to x-pack/plugins/apm/server/lib/correlations/utils/compute_expectations_and_ranges.test.ts diff --git a/x-pack/plugins/apm/server/lib/search_strategies/utils/compute_expectations_and_ranges.ts b/x-pack/plugins/apm/server/lib/correlations/utils/compute_expectations_and_ranges.ts similarity index 79% rename from x-pack/plugins/apm/server/lib/search_strategies/utils/compute_expectations_and_ranges.ts rename to x-pack/plugins/apm/server/lib/correlations/utils/compute_expectations_and_ranges.ts index 1754a35280f86..1b92133c732cf 100644 --- a/x-pack/plugins/apm/server/lib/search_strategies/utils/compute_expectations_and_ranges.ts +++ b/x-pack/plugins/apm/server/lib/correlations/utils/compute_expectations_and_ranges.ts @@ -6,7 +6,8 @@ */ import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; -import { PERCENTILES_STEP } from '../constants'; + +import { PERCENTILES_STEP } from '../../../../common/correlations/constants'; export const computeExpectationsAndRanges = ( percentiles: number[], @@ -29,15 +30,17 @@ export const computeExpectationsAndRanges = ( } tempFractions.push(PERCENTILES_STEP / 100); - const ranges = tempPercentiles.reduce((p, to) => { - const from = p[p.length - 1]?.to; - if (from !== undefined) { - p.push({ from, to }); - } else { - p.push({ to }); - } - return p; - }, [] as Array<{ from?: number; to?: number }>); + const ranges = tempPercentiles + .map((tP) => Math.round(tP)) + .reduce((p, to) => { + const from = p[p.length - 1]?.to; + if (from !== undefined) { + p.push({ from, to }); + } else { + p.push({ to }); + } + return p; + }, [] as Array<{ from?: number; to?: number }>); if (ranges.length > 0) { ranges.push({ from: ranges[ranges.length - 1].to }); } diff --git a/x-pack/plugins/apm/server/lib/search_strategies/utils/field_stats_utils.ts b/x-pack/plugins/apm/server/lib/correlations/utils/field_stats_utils.ts similarity index 100% rename from x-pack/plugins/apm/server/lib/search_strategies/utils/field_stats_utils.ts rename to x-pack/plugins/apm/server/lib/correlations/utils/field_stats_utils.ts diff --git a/x-pack/plugins/apm/server/lib/correlations/utils/index.ts b/x-pack/plugins/apm/server/lib/correlations/utils/index.ts new file mode 100644 index 0000000000000..f7c5abef939b9 --- /dev/null +++ b/x-pack/plugins/apm/server/lib/correlations/utils/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 { computeExpectationsAndRanges } from './compute_expectations_and_ranges'; +export { splitAllSettledPromises } from './split_all_settled_promises'; diff --git a/x-pack/plugins/apm/server/lib/correlations/utils/split_all_settled_promises.ts b/x-pack/plugins/apm/server/lib/correlations/utils/split_all_settled_promises.ts new file mode 100644 index 0000000000000..4e060477f024f --- /dev/null +++ b/x-pack/plugins/apm/server/lib/correlations/utils/split_all_settled_promises.ts @@ -0,0 +1,29 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +interface HandledPromises { + fulfilled: T[]; + rejected: unknown[]; +} + +export const splitAllSettledPromises = ( + promises: Array> +): HandledPromises => + promises.reduce( + (result, current) => { + if (current.status === 'fulfilled') { + result.fulfilled.push(current.value as T); + } else if (current.status === 'rejected') { + result.rejected.push(current.reason); + } + return result; + }, + { + fulfilled: [], + rejected: [], + } as HandledPromises + ); diff --git a/x-pack/plugins/apm/server/lib/environments/get_all_environments.ts b/x-pack/plugins/apm/server/lib/environments/get_all_environments.ts index d6c53aeea078e..7bdb21b9fda78 100644 --- a/x-pack/plugins/apm/server/lib/environments/get_all_environments.ts +++ b/x-pack/plugins/apm/server/lib/environments/get_all_environments.ts @@ -5,6 +5,7 @@ * 2.0. */ +import { termQuery } from '../../../../observability/server'; import { ProcessorEvent } from '../../../common/processor_event'; import { Setup } from '../helpers/setup_request'; import { @@ -37,11 +38,6 @@ export async function getAllEnvironments({ const { apmEventClient } = setup; - // omit filter for service.name if "All" option is selected - const serviceNameFilter = serviceName - ? [{ term: { [SERVICE_NAME]: serviceName } }] - : []; - const params = { apm: { events: [ @@ -57,7 +53,7 @@ export async function getAllEnvironments({ size: 0, query: { bool: { - filter: [...serviceNameFilter], + filter: [...termQuery(SERVICE_NAME, serviceName)], }, }, aggs: { diff --git a/x-pack/plugins/apm/server/lib/environments/get_environments.ts b/x-pack/plugins/apm/server/lib/environments/get_environments.ts index 678cfd891ae57..cd5caab6d2587 100644 --- a/x-pack/plugins/apm/server/lib/environments/get_environments.ts +++ b/x-pack/plugins/apm/server/lib/environments/get_environments.ts @@ -11,7 +11,7 @@ import { } from '../../../common/elasticsearch_fieldnames'; import { ENVIRONMENT_NOT_DEFINED } from '../../../common/environment_filter_values'; import { ProcessorEvent } from '../../../common/processor_event'; -import { rangeQuery } from '../../../../observability/server'; +import { rangeQuery, termQuery } from '../../../../observability/server'; import { getProcessorEventForTransactions } from '../helpers/transactions'; import { Setup } from '../helpers/setup_request'; @@ -40,14 +40,6 @@ export async function getEnvironments({ const { apmEventClient } = setup; - const filter = rangeQuery(start, end); - - if (serviceName) { - filter.push({ - term: { [SERVICE_NAME]: serviceName }, - }); - } - const params = { apm: { events: [ @@ -60,7 +52,10 @@ export async function getEnvironments({ size: 0, query: { bool: { - filter, + filter: [ + ...rangeQuery(start, end), + ...termQuery(SERVICE_NAME, serviceName), + ], }, }, aggs: { diff --git a/x-pack/plugins/apm/server/lib/errors/distribution/get_buckets.ts b/x-pack/plugins/apm/server/lib/errors/distribution/get_buckets.ts index dce8a3f397eaa..625089e99d360 100644 --- a/x-pack/plugins/apm/server/lib/errors/distribution/get_buckets.ts +++ b/x-pack/plugins/apm/server/lib/errors/distribution/get_buckets.ts @@ -5,13 +5,16 @@ * 2.0. */ -import { ESFilter } from '../../../../../../../src/core/types/elasticsearch'; import { ERROR_GROUP_ID, SERVICE_NAME, } from '../../../../common/elasticsearch_fieldnames'; import { ProcessorEvent } from '../../../../common/processor_event'; -import { rangeQuery, kqlQuery } from '../../../../../observability/server'; +import { + rangeQuery, + kqlQuery, + termQuery, +} from '../../../../../observability/server'; import { environmentQuery } from '../../../../common/utils/environment_query'; import { Setup } from '../../helpers/setup_request'; @@ -35,16 +38,6 @@ export async function getBuckets({ end: number; }) { const { apmEventClient } = setup; - const filter: ESFilter[] = [ - { term: { [SERVICE_NAME]: serviceName } }, - ...rangeQuery(start, end), - ...environmentQuery(environment), - ...kqlQuery(kuery), - ]; - - if (groupId) { - filter.push({ term: { [ERROR_GROUP_ID]: groupId } }); - } const params = { apm: { @@ -54,7 +47,13 @@ export async function getBuckets({ size: 0, query: { bool: { - filter, + filter: [ + { term: { [SERVICE_NAME]: serviceName } }, + ...rangeQuery(start, end), + ...environmentQuery(environment), + ...kqlQuery(kuery), + ...termQuery(ERROR_GROUP_ID, groupId), + ], }, }, aggs: { diff --git a/x-pack/plugins/apm/server/lib/helpers/create_es_client/call_async_with_debug.ts b/x-pack/plugins/apm/server/lib/helpers/create_es_client/call_async_with_debug.ts index fb58357d68437..a9799c0cfb60c 100644 --- a/x-pack/plugins/apm/server/lib/helpers/create_es_client/call_async_with_debug.ts +++ b/x-pack/plugins/apm/server/lib/helpers/create_es_client/call_async_with_debug.ts @@ -11,7 +11,7 @@ import chalk from 'chalk'; import { KibanaRequest } from '../../../../../../../src/core/server'; import { RequestStatus } from '../../../../../../../src/plugins/inspector'; import { WrappedElasticsearchClientError } from '../../../../../observability/server'; -import { inspectableEsQueriesMap } from '../../../routes/register_routes'; +import { inspectableEsQueriesMap } from '../../../routes/apm_routes/register_apm_server_routes'; import { getInspectResponse } from '../../../../../observability/server'; function formatObj(obj: Record) { diff --git a/x-pack/plugins/apm/server/lib/latency/get_overall_latency_distribution.ts b/x-pack/plugins/apm/server/lib/latency/get_overall_latency_distribution.ts index ad1914d921211..0ef6712102a9b 100644 --- a/x-pack/plugins/apm/server/lib/latency/get_overall_latency_distribution.ts +++ b/x-pack/plugins/apm/server/lib/latency/get_overall_latency_distribution.ts @@ -14,8 +14,8 @@ import { withApmSpan } from '../../utils/with_apm_span'; import { getHistogramIntervalRequest, getHistogramRangeSteps, -} from '../search_strategies/queries/query_histogram_range_steps'; -import { getTransactionDurationRangesRequest } from '../search_strategies/queries/query_ranges'; +} from '../correlations/queries/query_histogram_range_steps'; +import { getTransactionDurationRangesRequest } from '../correlations/queries/query_ranges'; import { getPercentileThresholdValue } from './get_percentile_threshold_value'; import type { @@ -27,9 +27,7 @@ export async function getOverallLatencyDistribution( options: OverallLatencyDistributionOptions ) { return withApmSpan('get_overall_latency_distribution', async () => { - const overallLatencyDistribution: OverallLatencyDistributionResponse = { - log: [], - }; + const overallLatencyDistribution: OverallLatencyDistributionResponse = {}; const { setup, termFilters, ...rawParams } = options; const { apmEventClient } = setup; diff --git a/x-pack/plugins/apm/server/lib/latency/get_percentile_threshold_value.ts b/x-pack/plugins/apm/server/lib/latency/get_percentile_threshold_value.ts index 996e039841b88..fac22b13a93a8 100644 --- a/x-pack/plugins/apm/server/lib/latency/get_percentile_threshold_value.ts +++ b/x-pack/plugins/apm/server/lib/latency/get_percentile_threshold_value.ts @@ -9,7 +9,7 @@ import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; import { ProcessorEvent } from '../../../common/processor_event'; -import { getTransactionDurationPercentilesRequest } from '../search_strategies/queries/query_percentiles'; +import { getTransactionDurationPercentilesRequest } from '../correlations/queries/query_percentiles'; import type { OverallLatencyDistributionOptions } from './types'; diff --git a/x-pack/plugins/apm/server/lib/latency/types.ts b/x-pack/plugins/apm/server/lib/latency/types.ts index ed7408c297ad7..17c036f44f088 100644 --- a/x-pack/plugins/apm/server/lib/latency/types.ts +++ b/x-pack/plugins/apm/server/lib/latency/types.ts @@ -7,20 +7,19 @@ import type { FieldValuePair, - SearchStrategyClientParams, -} from '../../../common/search_strategies/types'; + CorrelationsClientParams, +} from '../../../common/correlations/types'; import { Setup } from '../helpers/setup_request'; export interface OverallLatencyDistributionOptions - extends SearchStrategyClientParams { + extends CorrelationsClientParams { percentileThreshold: number; termFilters?: FieldValuePair[]; setup: Setup; } export interface OverallLatencyDistributionResponse { - log: string[]; percentileThresholdValue?: number; overallHistogram?: Array<{ key: number; diff --git a/x-pack/plugins/apm/server/lib/metrics/by_agent/java/gc/fetch_and_transform_gc_metrics.ts b/x-pack/plugins/apm/server/lib/metrics/by_agent/java/gc/fetch_and_transform_gc_metrics.ts index fb66cb9649085..117b372d445d2 100644 --- a/x-pack/plugins/apm/server/lib/metrics/by_agent/java/gc/fetch_and_transform_gc_metrics.ts +++ b/x-pack/plugins/apm/server/lib/metrics/by_agent/java/gc/fetch_and_transform_gc_metrics.ts @@ -6,7 +6,7 @@ */ import { sum, round } from 'lodash'; -import theme from '@elastic/eui/dist/eui_theme_light.json'; +import { euiLightVars as theme } from '@kbn/ui-shared-deps-src/theme'; import { isFiniteNumber } from '../../../../../../common/utils/is_finite_number'; import { Setup } from '../../../../helpers/setup_request'; import { getMetricsDateHistogramParams } from '../../../../helpers/metrics'; diff --git a/x-pack/plugins/apm/server/lib/metrics/by_agent/java/gc/get_gc_rate_chart.ts b/x-pack/plugins/apm/server/lib/metrics/by_agent/java/gc/get_gc_rate_chart.ts index 07f02bb6f8fdc..22dcb3e0f08ff 100644 --- a/x-pack/plugins/apm/server/lib/metrics/by_agent/java/gc/get_gc_rate_chart.ts +++ b/x-pack/plugins/apm/server/lib/metrics/by_agent/java/gc/get_gc_rate_chart.ts @@ -5,7 +5,7 @@ * 2.0. */ -import theme from '@elastic/eui/dist/eui_theme_light.json'; +import { euiLightVars as theme } from '@kbn/ui-shared-deps-src/theme'; import { i18n } from '@kbn/i18n'; import { METRIC_JAVA_GC_COUNT } from '../../../../../../common/elasticsearch_fieldnames'; import { Setup } from '../../../../helpers/setup_request'; diff --git a/x-pack/plugins/apm/server/lib/metrics/by_agent/java/gc/get_gc_time_chart.ts b/x-pack/plugins/apm/server/lib/metrics/by_agent/java/gc/get_gc_time_chart.ts index 9f2fc2ba582f3..4b85ad94f6494 100644 --- a/x-pack/plugins/apm/server/lib/metrics/by_agent/java/gc/get_gc_time_chart.ts +++ b/x-pack/plugins/apm/server/lib/metrics/by_agent/java/gc/get_gc_time_chart.ts @@ -5,7 +5,7 @@ * 2.0. */ -import theme from '@elastic/eui/dist/eui_theme_light.json'; +import { euiLightVars as theme } from '@kbn/ui-shared-deps-src/theme'; import { i18n } from '@kbn/i18n'; import { METRIC_JAVA_GC_TIME } from '../../../../../../common/elasticsearch_fieldnames'; import { Setup } from '../../../../helpers/setup_request'; diff --git a/x-pack/plugins/apm/server/lib/metrics/by_agent/java/heap_memory/index.ts b/x-pack/plugins/apm/server/lib/metrics/by_agent/java/heap_memory/index.ts index 71f3973f51998..a872a3af76d7e 100644 --- a/x-pack/plugins/apm/server/lib/metrics/by_agent/java/heap_memory/index.ts +++ b/x-pack/plugins/apm/server/lib/metrics/by_agent/java/heap_memory/index.ts @@ -5,7 +5,7 @@ * 2.0. */ -import theme from '@elastic/eui/dist/eui_theme_light.json'; +import { euiLightVars as theme } from '@kbn/ui-shared-deps-src/theme'; import { i18n } from '@kbn/i18n'; import { METRIC_JAVA_HEAP_MEMORY_MAX, diff --git a/x-pack/plugins/apm/server/lib/metrics/by_agent/java/non_heap_memory/index.ts b/x-pack/plugins/apm/server/lib/metrics/by_agent/java/non_heap_memory/index.ts index 2ed70bf846dfa..9fa758cb4dbd8 100644 --- a/x-pack/plugins/apm/server/lib/metrics/by_agent/java/non_heap_memory/index.ts +++ b/x-pack/plugins/apm/server/lib/metrics/by_agent/java/non_heap_memory/index.ts @@ -5,7 +5,7 @@ * 2.0. */ -import theme from '@elastic/eui/dist/eui_theme_light.json'; +import { euiLightVars as theme } from '@kbn/ui-shared-deps-src/theme'; import { i18n } from '@kbn/i18n'; import { METRIC_JAVA_NON_HEAP_MEMORY_MAX, diff --git a/x-pack/plugins/apm/server/lib/metrics/by_agent/java/thread_count/index.ts b/x-pack/plugins/apm/server/lib/metrics/by_agent/java/thread_count/index.ts index e5e98fc418e5d..306666d27cd1c 100644 --- a/x-pack/plugins/apm/server/lib/metrics/by_agent/java/thread_count/index.ts +++ b/x-pack/plugins/apm/server/lib/metrics/by_agent/java/thread_count/index.ts @@ -5,7 +5,7 @@ * 2.0. */ -import theme from '@elastic/eui/dist/eui_theme_light.json'; +import { euiLightVars as theme } from '@kbn/ui-shared-deps-src/theme'; import { i18n } from '@kbn/i18n'; import { METRIC_JAVA_THREAD_COUNT, diff --git a/x-pack/plugins/apm/server/lib/metrics/by_agent/shared/cpu/index.ts b/x-pack/plugins/apm/server/lib/metrics/by_agent/shared/cpu/index.ts index e5042c8c80c70..0911081b20324 100644 --- a/x-pack/plugins/apm/server/lib/metrics/by_agent/shared/cpu/index.ts +++ b/x-pack/plugins/apm/server/lib/metrics/by_agent/shared/cpu/index.ts @@ -5,7 +5,7 @@ * 2.0. */ -import theme from '@elastic/eui/dist/eui_theme_light.json'; +import { euiLightVars as theme } from '@kbn/ui-shared-deps-src/theme'; import { i18n } from '@kbn/i18n'; import { METRIC_SYSTEM_CPU_PERCENT, diff --git a/x-pack/plugins/apm/server/lib/metrics/transform_metrics_chart.ts b/x-pack/plugins/apm/server/lib/metrics/transform_metrics_chart.ts index 7eaa90845d652..fea853af93b84 100644 --- a/x-pack/plugins/apm/server/lib/metrics/transform_metrics_chart.ts +++ b/x-pack/plugins/apm/server/lib/metrics/transform_metrics_chart.ts @@ -5,7 +5,7 @@ * 2.0. */ -import theme from '@elastic/eui/dist/eui_theme_light.json'; +import { euiLightVars as theme } from '@kbn/ui-shared-deps-src/theme'; import { ESSearchResponse } from '../../../../../../src/core/types/elasticsearch'; import { getVizColorForIndex } from '../../../common/viz_colors'; import { GenericMetricsRequest } from './fetch_and_transform_metrics'; @@ -26,26 +26,30 @@ export function transformDataToMetricsChart( title: chartBase.title, key: chartBase.key, yUnit: chartBase.yUnit, - series: Object.keys(chartBase.series).map((seriesKey, i) => { - const overallValue = aggregations?.[seriesKey]?.value; + series: + result.hits.total.value > 0 + ? Object.keys(chartBase.series).map((seriesKey, i) => { + const overallValue = aggregations?.[seriesKey]?.value; - return { - title: chartBase.series[seriesKey].title, - key: seriesKey, - type: chartBase.type, - color: - chartBase.series[seriesKey].color || getVizColorForIndex(i, theme), - overallValue, - data: - timeseriesData?.buckets.map((bucket) => { - const { value } = bucket[seriesKey]; - const y = value === null || isNaN(value) ? null : value; return { - x: bucket.key, - y, + title: chartBase.series[seriesKey].title, + key: seriesKey, + type: chartBase.type, + color: + chartBase.series[seriesKey].color || + getVizColorForIndex(i, theme), + overallValue, + data: + timeseriesData?.buckets.map((bucket) => { + const { value } = bucket[seriesKey]; + const y = value === null || isNaN(value) ? null : value; + return { + x: bucket.key, + y, + }; + }) || [], }; - }) || [], - }; - }), + }) + : [], }; } 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 index 829afa8330164..de4d6dec4e1fe 100644 --- 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 @@ -16,10 +16,7 @@ import { getDocumentTypeFilterForTransactions, getProcessorEventForTransactions, } from '../helpers/transactions'; -import { - calculateThroughputWithInterval, - calculateThroughputWithRange, -} from '../helpers/calculate_throughput'; +import { calculateThroughputWithRange } from '../helpers/calculate_throughput'; export async function getTransactionsPerMinute({ setup, @@ -70,6 +67,9 @@ export async function getTransactionsPerMinute({ fixed_interval: intervalString, min_doc_count: 0, }, + aggs: { + throughput: { rate: { unit: 'minute' as const } }, + }, }, }, }, @@ -98,10 +98,7 @@ export async function getTransactionsPerMinute({ timeseries: topTransactionTypeBucket?.timeseries.buckets.map((bucket) => ({ x: bucket.key, - y: calculateThroughputWithInterval({ - bucketSize, - value: bucket.doc_count, - }), + y: bucket.throughput.value, })) || [], }; } diff --git a/x-pack/plugins/apm/server/lib/search_strategies/constants.ts b/x-pack/plugins/apm/server/lib/search_strategies/constants.ts deleted file mode 100644 index 5af1b21630720..0000000000000 --- a/x-pack/plugins/apm/server/lib/search_strategies/constants.ts +++ /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; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -/** - * Fields to exclude as potential field candidates - */ -export const FIELDS_TO_EXCLUDE_AS_CANDIDATE = new Set([ - // Exclude for all usage Contexts - 'parent.id', - 'trace.id', - 'transaction.id', - '@timestamp', - 'timestamp.us', - 'agent.ephemeral_id', - 'ecs.version', - 'event.ingested', - 'http.response.finished', - 'parent.id', - 'trace.id', - 'transaction.duration.us', - 'transaction.id', - 'process.pid', - 'process.ppid', - 'processor.event', - 'processor.name', - 'transaction.sampled', - 'transaction.span_count.dropped', - // Exclude for correlation on a Single Service - 'agent.name', - 'http.request.method', - 'service.framework.name', - 'service.language.name', - 'service.name', - 'service.runtime.name', - 'transaction.name', - 'transaction.type', -]); - -export const FIELD_PREFIX_TO_EXCLUDE_AS_CANDIDATE = ['observer.']; - -/** - * Fields to include/prioritize as potential field candidates - */ -export const FIELDS_TO_ADD_AS_CANDIDATE = new Set([ - 'service.version', - 'service.node.name', - 'service.framework.version', - 'service.language.version', - 'service.runtime.version', - 'kubernetes.pod.name', - 'kubernetes.pod.uid', - 'container.id', - 'source.ip', - 'client.ip', - 'host.ip', - 'service.environment', - 'process.args', - 'http.response.status_code', -]); -export const FIELD_PREFIX_TO_ADD_AS_CANDIDATE = [ - 'cloud.', - 'labels.', - 'user_agent.', -]; - -/** - * Other constants - */ -export const POPULATED_DOC_COUNT_SAMPLE_SIZE = 1000; - -export const PERCENTILES_STEP = 2; -export const TERMS_SIZE = 20; -export const SIGNIFICANT_FRACTION = 3; -export const SIGNIFICANT_VALUE_DIGITS = 3; - -export const CORRELATION_THRESHOLD = 0.3; -export const KS_TEST_THRESHOLD = 0.1; - -export const ERROR_CORRELATION_THRESHOLD = 0.02; - -/** - * Field stats/top values sampling constants - */ - -export const SAMPLER_TOP_TERMS_THRESHOLD = 100000; -export const SAMPLER_TOP_TERMS_SHARD_SIZE = 5000; diff --git a/x-pack/plugins/apm/server/lib/search_strategies/failed_transactions_correlations/failed_transactions_correlations_search_service.ts b/x-pack/plugins/apm/server/lib/search_strategies/failed_transactions_correlations/failed_transactions_correlations_search_service.ts deleted file mode 100644 index efc28ce98e5e0..0000000000000 --- a/x-pack/plugins/apm/server/lib/search_strategies/failed_transactions_correlations/failed_transactions_correlations_search_service.ts +++ /dev/null @@ -1,259 +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 { chunk } from 'lodash'; - -import type { ElasticsearchClient } from 'src/core/server'; - -import { EVENT_OUTCOME } from '../../../../common/elasticsearch_fieldnames'; -import { EventOutcome } from '../../../../common/event_outcome'; -import type { - SearchStrategyClientParams, - SearchStrategyServerParams, - RawResponseBase, -} from '../../../../common/search_strategies/types'; -import type { - FailedTransactionsCorrelationsParams, - FailedTransactionsCorrelationsRawResponse, -} from '../../../../common/search_strategies/failed_transactions_correlations/types'; -import type { ApmIndicesConfig } from '../../settings/apm_indices/get_apm_indices'; -import { searchServiceLogProvider } from '../search_service_log'; -import { - fetchFailedTransactionsCorrelationPValues, - fetchTransactionDurationFieldCandidates, - fetchTransactionDurationPercentiles, - fetchTransactionDurationRanges, - fetchTransactionDurationHistogramRangeSteps, -} from '../queries'; -import type { SearchServiceProvider } from '../search_strategy_provider'; - -import { failedTransactionsCorrelationsSearchServiceStateProvider } from './failed_transactions_correlations_search_service_state'; - -import { ERROR_CORRELATION_THRESHOLD } from '../constants'; -import { fetchFieldsStats } from '../queries/field_stats/get_fields_stats'; - -type FailedTransactionsCorrelationsSearchServiceProvider = - SearchServiceProvider< - FailedTransactionsCorrelationsParams & SearchStrategyClientParams, - FailedTransactionsCorrelationsRawResponse & RawResponseBase - >; - -export const failedTransactionsCorrelationsSearchServiceProvider: FailedTransactionsCorrelationsSearchServiceProvider = - ( - esClient: ElasticsearchClient, - getApmIndices: () => Promise, - searchServiceParams: FailedTransactionsCorrelationsParams & - SearchStrategyClientParams, - includeFrozen: boolean - ) => { - const { addLogMessage, getLogMessages } = searchServiceLogProvider(); - - const state = failedTransactionsCorrelationsSearchServiceStateProvider(); - - async function fetchErrorCorrelations() { - try { - const indices = await getApmIndices(); - const params: FailedTransactionsCorrelationsParams & - SearchStrategyClientParams & - SearchStrategyServerParams = { - ...searchServiceParams, - index: indices.transaction, - includeFrozen, - }; - - // 95th percentile to be displayed as a marker in the log log chart - const { totalDocs, percentiles: percentilesResponseThresholds } = - await fetchTransactionDurationPercentiles( - esClient, - params, - params.percentileThreshold - ? [params.percentileThreshold] - : undefined - ); - const percentileThresholdValue = - percentilesResponseThresholds[`${params.percentileThreshold}.0`]; - state.setPercentileThresholdValue(percentileThresholdValue); - - addLogMessage( - `Fetched ${params.percentileThreshold}th percentile value of ${percentileThresholdValue} based on ${totalDocs} documents.` - ); - - // finish early if we weren't able to identify the percentileThresholdValue. - if (percentileThresholdValue === undefined) { - addLogMessage( - `Abort service since percentileThresholdValue could not be determined.` - ); - state.setProgress({ - loadedFieldCandidates: 1, - loadedErrorCorrelations: 1, - loadedOverallHistogram: 1, - loadedFailedTransactionsCorrelations: 1, - }); - state.setIsRunning(false); - return; - } - - const histogramRangeSteps = - await fetchTransactionDurationHistogramRangeSteps(esClient, params); - - const overallLogHistogramChartData = - await fetchTransactionDurationRanges( - esClient, - params, - histogramRangeSteps - ); - const errorLogHistogramChartData = await fetchTransactionDurationRanges( - esClient, - params, - histogramRangeSteps, - [{ fieldName: EVENT_OUTCOME, fieldValue: EventOutcome.failure }] - ); - - state.setProgress({ loadedOverallHistogram: 1 }); - state.setErrorHistogram(errorLogHistogramChartData); - state.setOverallHistogram(overallLogHistogramChartData); - - const { fieldCandidates: candidates } = - await fetchTransactionDurationFieldCandidates(esClient, params); - - const fieldCandidates = candidates.filter( - (t) => !(t === EVENT_OUTCOME) - ); - - addLogMessage(`Identified ${fieldCandidates.length} fieldCandidates.`); - - state.setProgress({ loadedFieldCandidates: 1 }); - - let fieldCandidatesFetchedCount = 0; - const fieldsToSample = new Set(); - if (params !== undefined && fieldCandidates.length > 0) { - const batches = chunk(fieldCandidates, 10); - for (let i = 0; i < batches.length; i++) { - try { - const results = await Promise.allSettled( - batches[i].map((fieldName) => - fetchFailedTransactionsCorrelationPValues( - esClient, - params, - histogramRangeSteps, - fieldName - ) - ) - ); - - results.forEach((result, idx) => { - if (result.status === 'fulfilled') { - const significantCorrelations = result.value.filter( - (record) => - record && - record.pValue !== undefined && - record.pValue < ERROR_CORRELATION_THRESHOLD - ); - - significantCorrelations.forEach((r) => { - fieldsToSample.add(r.fieldName); - }); - - state.addFailedTransactionsCorrelations( - significantCorrelations - ); - } else { - // If one of the fields in the batch had an error - addLogMessage( - `Error getting error correlation for field ${batches[i][idx]}: ${result.reason}.` - ); - } - }); - } catch (e) { - state.setError(e); - - if (params?.index.includes(':')) { - state.setCcsWarning(true); - } - } finally { - fieldCandidatesFetchedCount += batches[i].length; - state.setProgress({ - loadedFailedTransactionsCorrelations: - fieldCandidatesFetchedCount / fieldCandidates.length, - }); - } - } - - addLogMessage( - `Identified correlations for ${fieldCandidatesFetchedCount} fields out of ${fieldCandidates.length} candidates.` - ); - } - - addLogMessage( - `Identified ${fieldsToSample.size} fields to sample for field statistics.` - ); - - const { stats: fieldStats } = await fetchFieldsStats( - esClient, - params, - [...fieldsToSample], - [{ fieldName: EVENT_OUTCOME, fieldValue: EventOutcome.failure }] - ); - - addLogMessage( - `Retrieved field statistics for ${fieldStats.length} fields out of ${fieldsToSample.size} fields.` - ); - - state.addFieldStats(fieldStats); - } catch (e) { - state.setError(e); - } - - addLogMessage( - `Identified ${ - state.getState().failedTransactionsCorrelations.length - } significant correlations relating to failed transactions.` - ); - - state.setIsRunning(false); - } - - fetchErrorCorrelations(); - - return () => { - const { - ccsWarning, - error, - isRunning, - overallHistogram, - errorHistogram, - percentileThresholdValue, - progress, - fieldStats, - } = state.getState(); - - return { - cancel: () => { - addLogMessage(`Service cancelled.`); - state.setIsCancelled(true); - }, - error, - meta: { - loaded: Math.round(state.getOverallProgress() * 100), - total: 100, - isRunning, - isPartial: isRunning, - }, - rawResponse: { - ccsWarning, - log: getLogMessages(), - took: Date.now() - progress.started, - failedTransactionsCorrelations: - state.getFailedTransactionsCorrelationsSortedByScore(), - overallHistogram, - errorHistogram, - percentileThresholdValue, - fieldStats, - }, - }; - }; - }; diff --git a/x-pack/plugins/apm/server/lib/search_strategies/failed_transactions_correlations/failed_transactions_correlations_search_service_state.ts b/x-pack/plugins/apm/server/lib/search_strategies/failed_transactions_correlations/failed_transactions_correlations_search_service_state.ts deleted file mode 100644 index ed0fe5d6e178b..0000000000000 --- a/x-pack/plugins/apm/server/lib/search_strategies/failed_transactions_correlations/failed_transactions_correlations_search_service_state.ts +++ /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; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { FailedTransactionsCorrelation } from '../../../../common/search_strategies/failed_transactions_correlations/types'; - -import type { HistogramItem } from '../../../../common/search_strategies/types'; -import { FieldStats } from '../../../../common/search_strategies/field_stats_types'; - -interface Progress { - started: number; - loadedFieldCandidates: number; - loadedErrorCorrelations: number; - loadedOverallHistogram: number; - loadedFailedTransactionsCorrelations: number; -} - -export const failedTransactionsCorrelationsSearchServiceStateProvider = () => { - let ccsWarning = false; - function setCcsWarning(d: boolean) { - ccsWarning = d; - } - - let error: Error; - function setError(d: Error) { - error = d; - } - - let isCancelled = false; - function setIsCancelled(d: boolean) { - isCancelled = d; - } - - let isRunning = true; - function setIsRunning(d: boolean) { - isRunning = d; - } - - let errorHistogram: HistogramItem[] | undefined; - function setErrorHistogram(d: HistogramItem[]) { - errorHistogram = d; - } - - let overallHistogram: HistogramItem[] | undefined; - function setOverallHistogram(d: HistogramItem[]) { - overallHistogram = d; - } - - let percentileThresholdValue: number; - function setPercentileThresholdValue(d: number) { - percentileThresholdValue = d; - } - - let progress: Progress = { - started: Date.now(), - loadedFieldCandidates: 0, - loadedErrorCorrelations: 0, - loadedOverallHistogram: 0, - loadedFailedTransactionsCorrelations: 0, - }; - function getOverallProgress() { - return ( - progress.loadedFieldCandidates * 0.025 + - progress.loadedFailedTransactionsCorrelations * (1 - 0.025) - ); - } - function setProgress(d: Partial>) { - progress = { - ...progress, - ...d, - }; - } - - const fieldStats: FieldStats[] = []; - function addFieldStats(stats: FieldStats[]) { - fieldStats.push(...stats); - } - - const failedTransactionsCorrelations: FailedTransactionsCorrelation[] = []; - function addFailedTransactionsCorrelation(d: FailedTransactionsCorrelation) { - failedTransactionsCorrelations.push(d); - } - function addFailedTransactionsCorrelations( - d: FailedTransactionsCorrelation[] - ) { - failedTransactionsCorrelations.push(...d); - } - - function getFailedTransactionsCorrelationsSortedByScore() { - return failedTransactionsCorrelations.sort((a, b) => b.score - a.score); - } - - function getState() { - return { - ccsWarning, - error, - isCancelled, - isRunning, - overallHistogram, - errorHistogram, - percentileThresholdValue, - progress, - failedTransactionsCorrelations, - fieldStats, - }; - } - - return { - addFailedTransactionsCorrelation, - addFailedTransactionsCorrelations, - getOverallProgress, - getState, - getFailedTransactionsCorrelationsSortedByScore, - setCcsWarning, - setError, - setIsCancelled, - setIsRunning, - setOverallHistogram, - setErrorHistogram, - setPercentileThresholdValue, - setProgress, - addFieldStats, - }; -}; - -export type FailedTransactionsCorrelationsSearchServiceState = ReturnType< - typeof failedTransactionsCorrelationsSearchServiceStateProvider ->; diff --git a/x-pack/plugins/apm/server/lib/search_strategies/failed_transactions_correlations/index.ts b/x-pack/plugins/apm/server/lib/search_strategies/failed_transactions_correlations/index.ts deleted file mode 100644 index 4763cd994d309..0000000000000 --- a/x-pack/plugins/apm/server/lib/search_strategies/failed_transactions_correlations/index.ts +++ /dev/null @@ -1,8 +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 { failedTransactionsCorrelationsSearchServiceProvider } from './failed_transactions_correlations_search_service'; diff --git a/x-pack/plugins/apm/server/lib/search_strategies/index.ts b/x-pack/plugins/apm/server/lib/search_strategies/index.ts deleted file mode 100644 index b4668138eefab..0000000000000 --- a/x-pack/plugins/apm/server/lib/search_strategies/index.ts +++ /dev/null @@ -1,8 +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 { registerSearchStrategies } from './register_search_strategies'; diff --git a/x-pack/plugins/apm/server/lib/search_strategies/latency_correlations/index.ts b/x-pack/plugins/apm/server/lib/search_strategies/latency_correlations/index.ts deleted file mode 100644 index 040aa5a7e424e..0000000000000 --- a/x-pack/plugins/apm/server/lib/search_strategies/latency_correlations/index.ts +++ /dev/null @@ -1,8 +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 { latencyCorrelationsSearchServiceProvider } from './latency_correlations_search_service'; diff --git a/x-pack/plugins/apm/server/lib/search_strategies/latency_correlations/latency_correlations_search_service.ts b/x-pack/plugins/apm/server/lib/search_strategies/latency_correlations/latency_correlations_search_service.ts deleted file mode 100644 index 5fed2f4eb4dc4..0000000000000 --- a/x-pack/plugins/apm/server/lib/search_strategies/latency_correlations/latency_correlations_search_service.ts +++ /dev/null @@ -1,293 +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 { range } from 'lodash'; -import type { ElasticsearchClient } from 'src/core/server'; - -import type { - RawResponseBase, - SearchStrategyClientParams, - SearchStrategyServerParams, -} from '../../../../common/search_strategies/types'; -import type { - LatencyCorrelationsParams, - LatencyCorrelationsRawResponse, -} from '../../../../common/search_strategies/latency_correlations/types'; - -import type { ApmIndicesConfig } from '../../settings/apm_indices/get_apm_indices'; - -import { - fetchTransactionDurationFieldCandidates, - fetchTransactionDurationFieldValuePairs, - fetchTransactionDurationFractions, - fetchTransactionDurationPercentiles, - fetchTransactionDurationHistograms, - fetchTransactionDurationHistogramRangeSteps, - fetchTransactionDurationRanges, -} from '../queries'; -import { computeExpectationsAndRanges } from '../utils'; -import { searchServiceLogProvider } from '../search_service_log'; -import type { SearchServiceProvider } from '../search_strategy_provider'; - -import { latencyCorrelationsSearchServiceStateProvider } from './latency_correlations_search_service_state'; -import { fetchFieldsStats } from '../queries/field_stats/get_fields_stats'; - -type LatencyCorrelationsSearchServiceProvider = SearchServiceProvider< - LatencyCorrelationsParams & SearchStrategyClientParams, - LatencyCorrelationsRawResponse & RawResponseBase ->; - -export const latencyCorrelationsSearchServiceProvider: LatencyCorrelationsSearchServiceProvider = - ( - esClient: ElasticsearchClient, - getApmIndices: () => Promise, - searchServiceParams: LatencyCorrelationsParams & SearchStrategyClientParams, - includeFrozen: boolean - ) => { - const { addLogMessage, getLogMessages } = searchServiceLogProvider(); - - const state = latencyCorrelationsSearchServiceStateProvider(); - - async function fetchCorrelations() { - let params: - | (LatencyCorrelationsParams & - SearchStrategyClientParams & - SearchStrategyServerParams) - | undefined; - - try { - const indices = await getApmIndices(); - params = { - ...searchServiceParams, - index: indices.transaction, - includeFrozen, - }; - - // 95th percentile to be displayed as a marker in the log log chart - const { totalDocs, percentiles: percentilesResponseThresholds } = - await fetchTransactionDurationPercentiles( - esClient, - params, - params.percentileThreshold - ? [params.percentileThreshold] - : undefined - ); - const percentileThresholdValue = - percentilesResponseThresholds[`${params.percentileThreshold}.0`]; - state.setPercentileThresholdValue(percentileThresholdValue); - - addLogMessage( - `Fetched ${params.percentileThreshold}th percentile value of ${percentileThresholdValue} based on ${totalDocs} documents.` - ); - - // finish early if we weren't able to identify the percentileThresholdValue. - if (percentileThresholdValue === undefined) { - addLogMessage( - `Abort service since percentileThresholdValue could not be determined.` - ); - state.setProgress({ - loadedHistogramStepsize: 1, - loadedOverallHistogram: 1, - loadedFieldCandidates: 1, - loadedFieldValuePairs: 1, - loadedHistograms: 1, - }); - state.setIsRunning(false); - return; - } - - const histogramRangeSteps = - await fetchTransactionDurationHistogramRangeSteps(esClient, params); - state.setProgress({ loadedHistogramStepsize: 1 }); - - addLogMessage(`Loaded histogram range steps.`); - - if (state.getIsCancelled()) { - state.setIsRunning(false); - return; - } - - const overallLogHistogramChartData = - await fetchTransactionDurationRanges( - esClient, - params, - histogramRangeSteps - ); - state.setProgress({ loadedOverallHistogram: 1 }); - state.setOverallHistogram(overallLogHistogramChartData); - - addLogMessage(`Loaded overall histogram chart data.`); - - if (state.getIsCancelled()) { - state.setIsRunning(false); - return; - } - - // finish early if correlation analysis is not required. - if (params.analyzeCorrelations === false) { - addLogMessage( - `Finish service since correlation analysis wasn't requested.` - ); - state.setProgress({ - loadedHistogramStepsize: 1, - loadedOverallHistogram: 1, - loadedFieldCandidates: 1, - loadedFieldValuePairs: 1, - loadedHistograms: 1, - }); - state.setIsRunning(false); - return; - } - - // Create an array of ranges [2, 4, 6, ..., 98] - const percentileAggregationPercents = range(2, 100, 2); - const { percentiles: percentilesRecords } = - await fetchTransactionDurationPercentiles( - esClient, - params, - percentileAggregationPercents - ); - - // We need to round the percentiles values - // because the queries we're using based on it - // later on wouldn't allow numbers with decimals. - const percentiles = Object.values(percentilesRecords).map(Math.round); - - addLogMessage(`Loaded percentiles.`); - - if (state.getIsCancelled()) { - state.setIsRunning(false); - return; - } - - const { fieldCandidates } = - await fetchTransactionDurationFieldCandidates(esClient, params); - - addLogMessage(`Identified ${fieldCandidates.length} fieldCandidates.`); - - state.setProgress({ loadedFieldCandidates: 1 }); - - const fieldValuePairs = await fetchTransactionDurationFieldValuePairs( - esClient, - params, - fieldCandidates, - state, - addLogMessage - ); - - addLogMessage(`Identified ${fieldValuePairs.length} fieldValuePairs.`); - - if (state.getIsCancelled()) { - state.setIsRunning(false); - return; - } - - const { expectations, ranges } = - computeExpectationsAndRanges(percentiles); - - const { fractions, totalDocCount } = - await fetchTransactionDurationFractions(esClient, params, ranges); - - addLogMessage( - `Loaded fractions and totalDocCount of ${totalDocCount}.` - ); - - const fieldsToSample = new Set(); - let loadedHistograms = 0; - for await (const item of fetchTransactionDurationHistograms( - esClient, - addLogMessage, - params, - state, - expectations, - ranges, - fractions, - histogramRangeSteps, - totalDocCount, - fieldValuePairs - )) { - if (item !== undefined) { - state.addLatencyCorrelation(item); - fieldsToSample.add(item.fieldName); - } - loadedHistograms++; - state.setProgress({ - loadedHistograms: loadedHistograms / fieldValuePairs.length, - }); - } - - addLogMessage( - `Identified ${ - state.getState().latencyCorrelations.length - } significant correlations out of ${ - fieldValuePairs.length - } field/value pairs.` - ); - - addLogMessage( - `Identified ${fieldsToSample.size} fields to sample for field statistics.` - ); - - const { stats: fieldStats } = await fetchFieldsStats(esClient, params, [ - ...fieldsToSample, - ]); - - addLogMessage( - `Retrieved field statistics for ${fieldStats.length} fields out of ${fieldsToSample.size} fields.` - ); - state.addFieldStats(fieldStats); - } catch (e) { - state.setError(e); - } - - if (state.getState().error !== undefined && params?.index.includes(':')) { - state.setCcsWarning(true); - } - - state.setIsRunning(false); - } - - function cancel() { - addLogMessage(`Service cancelled.`); - state.setIsCancelled(true); - } - - fetchCorrelations(); - - return () => { - const { - ccsWarning, - error, - isRunning, - overallHistogram, - percentileThresholdValue, - progress, - fieldStats, - } = state.getState(); - - return { - cancel, - error, - meta: { - loaded: Math.round(state.getOverallProgress() * 100), - total: 100, - isRunning, - isPartial: isRunning, - }, - rawResponse: { - ccsWarning, - log: getLogMessages(), - took: Date.now() - progress.started, - latencyCorrelations: - state.getLatencyCorrelationsSortedByCorrelation(), - percentileThresholdValue, - overallHistogram, - fieldStats, - }, - }; - }; - }; diff --git a/x-pack/plugins/apm/server/lib/search_strategies/latency_correlations/latency_correlations_search_service_state.test.ts b/x-pack/plugins/apm/server/lib/search_strategies/latency_correlations/latency_correlations_search_service_state.test.ts deleted file mode 100644 index ce9014004f4b0..0000000000000 --- a/x-pack/plugins/apm/server/lib/search_strategies/latency_correlations/latency_correlations_search_service_state.test.ts +++ /dev/null @@ -1,62 +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 { latencyCorrelationsSearchServiceStateProvider } from './latency_correlations_search_service_state'; - -describe('search service', () => { - describe('latencyCorrelationsSearchServiceStateProvider', () => { - it('initializes with default state', () => { - const state = latencyCorrelationsSearchServiceStateProvider(); - const defaultState = state.getState(); - const defaultProgress = state.getOverallProgress(); - - expect(defaultState.ccsWarning).toBe(false); - expect(defaultState.error).toBe(undefined); - expect(defaultState.isCancelled).toBe(false); - expect(defaultState.isRunning).toBe(true); - expect(defaultState.overallHistogram).toBe(undefined); - expect(defaultState.progress.loadedFieldCandidates).toBe(0); - expect(defaultState.progress.loadedFieldValuePairs).toBe(0); - expect(defaultState.progress.loadedHistogramStepsize).toBe(0); - expect(defaultState.progress.loadedHistograms).toBe(0); - expect(defaultState.progress.loadedOverallHistogram).toBe(0); - expect(defaultState.progress.started > 0).toBe(true); - - expect(defaultProgress).toBe(0); - }); - - it('returns updated state', () => { - const state = latencyCorrelationsSearchServiceStateProvider(); - - state.setCcsWarning(true); - state.setError(new Error('the-error-message')); - state.setIsCancelled(true); - state.setIsRunning(false); - state.setOverallHistogram([{ key: 1392202800000, doc_count: 1234 }]); - state.setProgress({ loadedHistograms: 0.5 }); - - const updatedState = state.getState(); - const updatedProgress = state.getOverallProgress(); - - expect(updatedState.ccsWarning).toBe(true); - expect(updatedState.error?.message).toBe('the-error-message'); - expect(updatedState.isCancelled).toBe(true); - expect(updatedState.isRunning).toBe(false); - expect(updatedState.overallHistogram).toEqual([ - { key: 1392202800000, doc_count: 1234 }, - ]); - expect(updatedState.progress.loadedFieldCandidates).toBe(0); - expect(updatedState.progress.loadedFieldValuePairs).toBe(0); - expect(updatedState.progress.loadedHistogramStepsize).toBe(0); - expect(updatedState.progress.loadedHistograms).toBe(0.5); - expect(updatedState.progress.loadedOverallHistogram).toBe(0); - expect(updatedState.progress.started > 0).toBe(true); - - expect(updatedProgress).toBe(0.45); - }); - }); -}); diff --git a/x-pack/plugins/apm/server/lib/search_strategies/latency_correlations/latency_correlations_search_service_state.ts b/x-pack/plugins/apm/server/lib/search_strategies/latency_correlations/latency_correlations_search_service_state.ts deleted file mode 100644 index 186099e4c307a..0000000000000 --- a/x-pack/plugins/apm/server/lib/search_strategies/latency_correlations/latency_correlations_search_service_state.ts +++ /dev/null @@ -1,121 +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 type { HistogramItem } from '../../../../common/search_strategies/types'; -import type { - LatencyCorrelationSearchServiceProgress, - LatencyCorrelation, -} from '../../../../common/search_strategies/latency_correlations/types'; -import { FieldStats } from '../../../../common/search_strategies/field_stats_types'; - -export const latencyCorrelationsSearchServiceStateProvider = () => { - let ccsWarning = false; - function setCcsWarning(d: boolean) { - ccsWarning = d; - } - - let error: Error; - function setError(d: Error) { - error = d; - } - - let isCancelled = false; - function getIsCancelled() { - return isCancelled; - } - function setIsCancelled(d: boolean) { - isCancelled = d; - } - - let isRunning = true; - function setIsRunning(d: boolean) { - isRunning = d; - } - - let overallHistogram: HistogramItem[] | undefined; - function setOverallHistogram(d: HistogramItem[]) { - overallHistogram = d; - } - - let percentileThresholdValue: number; - function setPercentileThresholdValue(d: number) { - percentileThresholdValue = d; - } - - let progress: LatencyCorrelationSearchServiceProgress = { - started: Date.now(), - loadedHistogramStepsize: 0, - loadedOverallHistogram: 0, - loadedFieldCandidates: 0, - loadedFieldValuePairs: 0, - loadedHistograms: 0, - }; - function getOverallProgress() { - return ( - progress.loadedHistogramStepsize * 0.025 + - progress.loadedOverallHistogram * 0.025 + - progress.loadedFieldCandidates * 0.025 + - progress.loadedFieldValuePairs * 0.025 + - progress.loadedHistograms * 0.9 - ); - } - function setProgress( - d: Partial> - ) { - progress = { - ...progress, - ...d, - }; - } - - const latencyCorrelations: LatencyCorrelation[] = []; - function addLatencyCorrelation(d: LatencyCorrelation) { - latencyCorrelations.push(d); - } - - function getLatencyCorrelationsSortedByCorrelation() { - return latencyCorrelations.sort((a, b) => b.correlation - a.correlation); - } - const fieldStats: FieldStats[] = []; - function addFieldStats(stats: FieldStats[]) { - fieldStats.push(...stats); - } - - function getState() { - return { - ccsWarning, - error, - isCancelled, - isRunning, - overallHistogram, - percentileThresholdValue, - progress, - latencyCorrelations, - fieldStats, - }; - } - - return { - addLatencyCorrelation, - getIsCancelled, - getOverallProgress, - getState, - getLatencyCorrelationsSortedByCorrelation, - setCcsWarning, - setError, - setIsCancelled, - setIsRunning, - setOverallHistogram, - setPercentileThresholdValue, - setProgress, - addFieldStats, - }; -}; - -export type LatencyCorrelationsSearchServiceState = ReturnType< - typeof latencyCorrelationsSearchServiceStateProvider ->; diff --git a/x-pack/plugins/apm/server/lib/search_strategies/queries/field_stats/get_boolean_field_stats.ts b/x-pack/plugins/apm/server/lib/search_strategies/queries/field_stats/get_boolean_field_stats.ts deleted file mode 100644 index da5493376426c..0000000000000 --- a/x-pack/plugins/apm/server/lib/search_strategies/queries/field_stats/get_boolean_field_stats.ts +++ /dev/null @@ -1,87 +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 { ElasticsearchClient } from 'kibana/server'; -import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; - -import { buildSamplerAggregation } from '../../utils/field_stats_utils'; -import { FieldValuePair } from '../../../../../common/search_strategies/types'; -import { - FieldStatsCommonRequestParams, - BooleanFieldStats, - Aggs, - TopValueBucket, -} from '../../../../../common/search_strategies/field_stats_types'; -import { getQueryWithParams } from '../get_query_with_params'; - -export const getBooleanFieldStatsRequest = ( - params: FieldStatsCommonRequestParams, - fieldName: string, - termFilters?: FieldValuePair[] -): estypes.SearchRequest => { - const query = getQueryWithParams({ params, termFilters }); - - const { index, samplerShardSize } = params; - - const size = 0; - const aggs: Aggs = { - sampled_value_count: { - filter: { exists: { field: fieldName } }, - }, - sampled_values: { - terms: { - field: fieldName, - size: 2, - }, - }, - }; - - const searchBody = { - query, - aggs: { - sample: buildSamplerAggregation(aggs, samplerShardSize), - }, - }; - - return { - index, - size, - body: searchBody, - }; -}; - -export const fetchBooleanFieldStats = async ( - esClient: ElasticsearchClient, - params: FieldStatsCommonRequestParams, - field: FieldValuePair, - termFilters?: FieldValuePair[] -): Promise => { - const request = getBooleanFieldStatsRequest( - params, - field.fieldName, - termFilters - ); - const { body } = await esClient.search(request); - const aggregations = body.aggregations as { - sample: { - sampled_value_count: estypes.AggregationsFiltersBucketItemKeys; - sampled_values: estypes.AggregationsTermsAggregate; - }; - }; - - const stats: BooleanFieldStats = { - fieldName: field.fieldName, - count: aggregations?.sample.sampled_value_count.doc_count ?? 0, - }; - - const valueBuckets: TopValueBucket[] = - aggregations?.sample.sampled_values?.buckets ?? []; - valueBuckets.forEach((bucket) => { - stats[`${bucket.key.toString()}Count`] = bucket.doc_count; - }); - return stats; -}; diff --git a/x-pack/plugins/apm/server/lib/search_strategies/queries/field_stats/get_fields_stats.ts b/x-pack/plugins/apm/server/lib/search_strategies/queries/field_stats/get_fields_stats.ts deleted file mode 100644 index 2e1441ccbd6a1..0000000000000 --- a/x-pack/plugins/apm/server/lib/search_strategies/queries/field_stats/get_fields_stats.ts +++ /dev/null @@ -1,110 +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 { ElasticsearchClient } from 'kibana/server'; -import { chunk } from 'lodash'; -import { ES_FIELD_TYPES } from '@kbn/field-types'; -import { - FieldValuePair, - SearchStrategyParams, -} from '../../../../../common/search_strategies/types'; -import { getRequestBase } from '../get_request_base'; -import { fetchKeywordFieldStats } from './get_keyword_field_stats'; -import { fetchNumericFieldStats } from './get_numeric_field_stats'; -import { - FieldStats, - FieldStatsCommonRequestParams, -} from '../../../../../common/search_strategies/field_stats_types'; -import { fetchBooleanFieldStats } from './get_boolean_field_stats'; - -export const fetchFieldsStats = async ( - esClient: ElasticsearchClient, - params: SearchStrategyParams, - fieldsToSample: string[], - termFilters?: FieldValuePair[] -): Promise<{ stats: FieldStats[]; errors: any[] }> => { - const stats: FieldStats[] = []; - const errors: any[] = []; - - if (fieldsToSample.length === 0) return { stats, errors }; - - const respMapping = await esClient.fieldCaps({ - ...getRequestBase(params), - fields: fieldsToSample, - }); - - const fieldStatsParams: FieldStatsCommonRequestParams = { - ...params, - samplerShardSize: 5000, - }; - const fieldStatsPromises = Object.entries(respMapping.body.fields) - .map(([key, value], idx) => { - const field: FieldValuePair = { fieldName: key, fieldValue: '' }; - const fieldTypes = Object.keys(value); - - for (const ft of fieldTypes) { - switch (ft) { - case ES_FIELD_TYPES.KEYWORD: - case ES_FIELD_TYPES.IP: - return fetchKeywordFieldStats( - esClient, - fieldStatsParams, - field, - termFilters - ); - break; - - case 'numeric': - case 'number': - case ES_FIELD_TYPES.FLOAT: - case ES_FIELD_TYPES.HALF_FLOAT: - case ES_FIELD_TYPES.SCALED_FLOAT: - case ES_FIELD_TYPES.DOUBLE: - case ES_FIELD_TYPES.INTEGER: - case ES_FIELD_TYPES.LONG: - case ES_FIELD_TYPES.SHORT: - case ES_FIELD_TYPES.UNSIGNED_LONG: - case ES_FIELD_TYPES.BYTE: - return fetchNumericFieldStats( - esClient, - fieldStatsParams, - field, - termFilters - ); - - break; - case ES_FIELD_TYPES.BOOLEAN: - return fetchBooleanFieldStats( - esClient, - fieldStatsParams, - field, - termFilters - ); - - default: - return; - } - } - }) - .filter((f) => f !== undefined) as Array>; - - const batches = chunk(fieldStatsPromises, 10); - for (let i = 0; i < batches.length; i++) { - try { - const results = await Promise.allSettled(batches[i]); - results.forEach((r) => { - if (r.status === 'fulfilled' && r.value !== undefined) { - stats.push(r.value); - } - }); - } catch (e) { - errors.push(e); - } - } - - return { stats, errors }; -}; diff --git a/x-pack/plugins/apm/server/lib/search_strategies/queries/field_stats/get_numeric_field_stats.ts b/x-pack/plugins/apm/server/lib/search_strategies/queries/field_stats/get_numeric_field_stats.ts deleted file mode 100644 index c45d4356cfe23..0000000000000 --- a/x-pack/plugins/apm/server/lib/search_strategies/queries/field_stats/get_numeric_field_stats.ts +++ /dev/null @@ -1,129 +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 { ElasticsearchClient } from 'kibana/server'; -import { find, get } from 'lodash'; -import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; -import { - NumericFieldStats, - FieldStatsCommonRequestParams, - TopValueBucket, - Aggs, -} from '../../../../../common/search_strategies/field_stats_types'; -import { FieldValuePair } from '../../../../../common/search_strategies/types'; -import { getQueryWithParams } from '../get_query_with_params'; -import { buildSamplerAggregation } from '../../utils/field_stats_utils'; - -// Only need 50th percentile for the median -const PERCENTILES = [50]; - -export const getNumericFieldStatsRequest = ( - params: FieldStatsCommonRequestParams, - fieldName: string, - termFilters?: FieldValuePair[] -) => { - const query = getQueryWithParams({ params, termFilters }); - const size = 0; - - const { index, samplerShardSize } = params; - - const percents = PERCENTILES; - const aggs: Aggs = { - sampled_field_stats: { - filter: { exists: { field: fieldName } }, - aggs: { - actual_stats: { - stats: { field: fieldName }, - }, - }, - }, - sampled_percentiles: { - percentiles: { - field: fieldName, - percents, - keyed: false, - }, - }, - sampled_top: { - terms: { - field: fieldName, - size: 10, - order: { - _count: 'desc', - }, - }, - }, - }; - - const searchBody = { - query, - aggs: { - sample: buildSamplerAggregation(aggs, samplerShardSize), - }, - }; - - return { - index, - size, - body: searchBody, - }; -}; - -export const fetchNumericFieldStats = async ( - esClient: ElasticsearchClient, - params: FieldStatsCommonRequestParams, - field: FieldValuePair, - termFilters?: FieldValuePair[] -): Promise => { - const request: estypes.SearchRequest = getNumericFieldStatsRequest( - params, - field.fieldName, - termFilters - ); - const { body } = await esClient.search(request); - - const aggregations = body.aggregations as { - sample: { - sampled_top: estypes.AggregationsTermsAggregate; - sampled_percentiles: estypes.AggregationsHdrPercentilesAggregate; - sampled_field_stats: { - doc_count: number; - actual_stats: estypes.AggregationsStatsAggregate; - }; - }; - }; - const docCount = aggregations?.sample.sampled_field_stats?.doc_count ?? 0; - const fieldStatsResp = - aggregations?.sample.sampled_field_stats?.actual_stats ?? {}; - const topValues = aggregations?.sample.sampled_top?.buckets ?? []; - - const stats: NumericFieldStats = { - fieldName: field.fieldName, - count: docCount, - min: get(fieldStatsResp, 'min', 0), - max: get(fieldStatsResp, 'max', 0), - avg: get(fieldStatsResp, 'avg', 0), - topValues, - topValuesSampleSize: topValues.reduce( - (acc: number, curr: TopValueBucket) => acc + curr.doc_count, - aggregations.sample.sampled_top?.sum_other_doc_count ?? 0 - ), - }; - - if (stats.count !== undefined && stats.count > 0) { - const percentiles = aggregations?.sample.sampled_percentiles.values ?? []; - const medianPercentile: { value: number; key: number } | undefined = find( - percentiles, - { - key: 50, - } - ); - stats.median = medianPercentile !== undefined ? medianPercentile!.value : 0; - } - - return stats; -}; diff --git a/x-pack/plugins/apm/server/lib/search_strategies/queries/index.ts b/x-pack/plugins/apm/server/lib/search_strategies/queries/index.ts deleted file mode 100644 index e691b81e4adcf..0000000000000 --- a/x-pack/plugins/apm/server/lib/search_strategies/queries/index.ts +++ /dev/null @@ -1,16 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -export { fetchFailedTransactionsCorrelationPValues } from './query_failure_correlation'; -export { fetchTransactionDurationFieldCandidates } from './query_field_candidates'; -export { fetchTransactionDurationFieldValuePairs } from './query_field_value_pairs'; -export { fetchTransactionDurationFractions } from './query_fractions'; -export { fetchTransactionDurationPercentiles } from './query_percentiles'; -export { fetchTransactionDurationCorrelation } from './query_correlation'; -export { fetchTransactionDurationHistograms } from './query_histograms_generator'; -export { fetchTransactionDurationHistogramRangeSteps } from './query_histogram_range_steps'; -export { fetchTransactionDurationRanges } from './query_ranges'; diff --git a/x-pack/plugins/apm/server/lib/search_strategies/queries/query_field_value_pairs.ts b/x-pack/plugins/apm/server/lib/search_strategies/queries/query_field_value_pairs.ts deleted file mode 100644 index e57ef5ee341ee..0000000000000 --- a/x-pack/plugins/apm/server/lib/search_strategies/queries/query_field_value_pairs.ts +++ /dev/null @@ -1,124 +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 type { ElasticsearchClient } from 'src/core/server'; - -import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; - -import type { - FieldValuePair, - SearchStrategyParams, -} from '../../../../common/search_strategies/types'; - -import type { SearchServiceLog } from '../search_service_log'; -import type { LatencyCorrelationsSearchServiceState } from '../latency_correlations/latency_correlations_search_service_state'; -import { TERMS_SIZE } from '../constants'; - -import { getQueryWithParams } from './get_query_with_params'; -import { getRequestBase } from './get_request_base'; - -export const getTermsAggRequest = ( - params: SearchStrategyParams, - fieldName: string -): estypes.SearchRequest => ({ - ...getRequestBase(params), - body: { - query: getQueryWithParams({ params }), - size: 0, - aggs: { - attribute_terms: { - terms: { - field: fieldName, - size: TERMS_SIZE, - }, - }, - }, - }, -}); - -const fetchTransactionDurationFieldTerms = async ( - esClient: ElasticsearchClient, - params: SearchStrategyParams, - fieldName: string, - addLogMessage: SearchServiceLog['addLogMessage'] -): Promise => { - try { - const resp = await esClient.search(getTermsAggRequest(params, fieldName)); - - if (resp.body.aggregations === undefined) { - addLogMessage( - `Failed to fetch terms for field candidate ${fieldName} fieldValuePairs, no aggregations returned.`, - JSON.stringify(resp) - ); - return []; - } - const buckets = ( - resp.body.aggregations - .attribute_terms as estypes.AggregationsMultiBucketAggregate<{ - key: string; - key_as_string?: string; - }> - )?.buckets; - if (buckets?.length >= 1) { - return buckets.map((d) => ({ - fieldName, - // The terms aggregation returns boolean fields as { key: 0, key_as_string: "false" }, - // so we need to pick `key_as_string` if it's present, otherwise searches on boolean fields would fail later on. - fieldValue: d.key_as_string ?? d.key, - })); - } - } catch (e) { - addLogMessage( - `Failed to fetch terms for field candidate ${fieldName} fieldValuePairs.`, - JSON.stringify(e) - ); - } - - return []; -}; - -async function fetchInSequence( - fieldCandidates: string[], - fn: (fieldCandidate: string) => Promise -) { - const results = []; - - for (const fieldCandidate of fieldCandidates) { - results.push(...(await fn(fieldCandidate))); - } - - return results; -} - -export const fetchTransactionDurationFieldValuePairs = async ( - esClient: ElasticsearchClient, - params: SearchStrategyParams, - fieldCandidates: string[], - state: LatencyCorrelationsSearchServiceState, - addLogMessage: SearchServiceLog['addLogMessage'] -): Promise => { - let fieldValuePairsProgress = 1; - - return await fetchInSequence( - fieldCandidates, - async function (fieldCandidate: string) { - const fieldTerms = await fetchTransactionDurationFieldTerms( - esClient, - params, - fieldCandidate, - addLogMessage - ); - - state.setProgress({ - loadedFieldValuePairs: fieldValuePairsProgress / fieldCandidates.length, - }); - fieldValuePairsProgress++; - - return fieldTerms; - } - ); -}; diff --git a/x-pack/plugins/apm/server/lib/search_strategies/queries/query_histograms_generator.test.ts b/x-pack/plugins/apm/server/lib/search_strategies/queries/query_histograms_generator.test.ts deleted file mode 100644 index 27fd0dc31432d..0000000000000 --- a/x-pack/plugins/apm/server/lib/search_strategies/queries/query_histograms_generator.test.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; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; - -import type { ElasticsearchClient } from 'src/core/server'; -import { ENVIRONMENT_ALL } from '../../../../common/environment_filter_values'; - -import { searchServiceLogProvider } from '../search_service_log'; -import { latencyCorrelationsSearchServiceStateProvider } from '../latency_correlations/latency_correlations_search_service_state'; - -import { fetchTransactionDurationHistograms } from './query_histograms_generator'; - -const params = { - index: 'apm-*', - start: 1577836800000, - end: 1609459200000, - includeFrozen: false, - environment: ENVIRONMENT_ALL.value, - kuery: '', -}; -const expectations = [1, 3, 5]; -const ranges = [{ to: 1 }, { from: 1, to: 3 }, { from: 3, to: 5 }, { from: 5 }]; -const fractions = [1, 2, 4, 5]; -const totalDocCount = 1234; -const histogramRangeSteps = [1, 2, 4, 5]; - -const fieldValuePairs = [ - { fieldName: 'the-field-name-1', fieldValue: 'the-field-value-1' }, - { fieldName: 'the-field-name-2', fieldValue: 'the-field-value-2' }, - { fieldName: 'the-field-name-2', fieldValue: 'the-field-value-3' }, -]; - -describe('query_histograms_generator', () => { - describe('fetchTransactionDurationHistograms', () => { - it(`doesn't break on failing ES queries and adds messages to the log`, async () => { - const esClientSearchMock = jest.fn( - ( - req: estypes.SearchRequest - ): { - body: estypes.SearchResponse; - } => { - return { - body: {} as unknown as estypes.SearchResponse, - }; - } - ); - - const esClientMock = { - search: esClientSearchMock, - } as unknown as ElasticsearchClient; - - const state = latencyCorrelationsSearchServiceStateProvider(); - const { addLogMessage, getLogMessages } = searchServiceLogProvider(); - - let loadedHistograms = 0; - const items = []; - - for await (const item of fetchTransactionDurationHistograms( - esClientMock, - addLogMessage, - params, - state, - expectations, - ranges, - fractions, - histogramRangeSteps, - totalDocCount, - fieldValuePairs - )) { - if (item !== undefined) { - items.push(item); - } - loadedHistograms++; - } - - expect(items.length).toEqual(0); - expect(loadedHistograms).toEqual(3); - expect(esClientSearchMock).toHaveBeenCalledTimes(3); - expect(getLogMessages().map((d) => d.split(': ')[1])).toEqual([ - "Failed to fetch correlation/kstest for 'the-field-name-1/the-field-value-1'", - "Failed to fetch correlation/kstest for 'the-field-name-2/the-field-value-2'", - "Failed to fetch correlation/kstest for 'the-field-name-2/the-field-value-3'", - ]); - }); - - it('returns items with correlation and ks-test value', async () => { - const esClientSearchMock = jest.fn( - ( - req: estypes.SearchRequest - ): { - body: estypes.SearchResponse; - } => { - return { - body: { - aggregations: { - latency_ranges: { buckets: [] }, - transaction_duration_correlation: { value: 0.6 }, - ks_test: { less: 0.001 }, - logspace_ranges: { buckets: [] }, - }, - } as unknown as estypes.SearchResponse, - }; - } - ); - - const esClientMock = { - search: esClientSearchMock, - } as unknown as ElasticsearchClient; - - const state = latencyCorrelationsSearchServiceStateProvider(); - const { addLogMessage, getLogMessages } = searchServiceLogProvider(); - - let loadedHistograms = 0; - const items = []; - - for await (const item of fetchTransactionDurationHistograms( - esClientMock, - addLogMessage, - params, - state, - expectations, - ranges, - fractions, - histogramRangeSteps, - totalDocCount, - fieldValuePairs - )) { - if (item !== undefined) { - items.push(item); - } - loadedHistograms++; - } - - expect(items.length).toEqual(3); - expect(loadedHistograms).toEqual(3); - expect(esClientSearchMock).toHaveBeenCalledTimes(6); - expect(getLogMessages().length).toEqual(0); - }); - }); -}); diff --git a/x-pack/plugins/apm/server/lib/search_strategies/queries/query_histograms_generator.ts b/x-pack/plugins/apm/server/lib/search_strategies/queries/query_histograms_generator.ts deleted file mode 100644 index 500714ffdf0d5..0000000000000 --- a/x-pack/plugins/apm/server/lib/search_strategies/queries/query_histograms_generator.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 type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; - -import type { ElasticsearchClient } from 'src/core/server'; - -import type { - FieldValuePair, - SearchStrategyParams, -} from '../../../../common/search_strategies/types'; - -import type { SearchServiceLog } from '../search_service_log'; -import type { LatencyCorrelationsSearchServiceState } from '../latency_correlations/latency_correlations_search_service_state'; -import { CORRELATION_THRESHOLD, KS_TEST_THRESHOLD } from '../constants'; - -import { getPrioritizedFieldValuePairs } from './get_prioritized_field_value_pairs'; -import { fetchTransactionDurationCorrelation } from './query_correlation'; -import { fetchTransactionDurationRanges } from './query_ranges'; - -export async function* fetchTransactionDurationHistograms( - esClient: ElasticsearchClient, - addLogMessage: SearchServiceLog['addLogMessage'], - params: SearchStrategyParams, - state: LatencyCorrelationsSearchServiceState, - expectations: number[], - ranges: estypes.AggregationsAggregationRange[], - fractions: number[], - histogramRangeSteps: number[], - totalDocCount: number, - fieldValuePairs: FieldValuePair[] -) { - for (const item of getPrioritizedFieldValuePairs(fieldValuePairs)) { - if (params === undefined || item === undefined || state.getIsCancelled()) { - state.setIsRunning(false); - return; - } - - // If one of the fields have an error - // We don't want to stop the whole process - try { - const { correlation, ksTest } = await fetchTransactionDurationCorrelation( - esClient, - params, - expectations, - ranges, - fractions, - totalDocCount, - [item] - ); - - if (state.getIsCancelled()) { - state.setIsRunning(false); - return; - } - - if ( - correlation !== null && - correlation > CORRELATION_THRESHOLD && - ksTest !== null && - ksTest < KS_TEST_THRESHOLD - ) { - const logHistogram = await fetchTransactionDurationRanges( - esClient, - params, - histogramRangeSteps, - [item] - ); - yield { - ...item, - correlation, - ksTest, - histogram: logHistogram, - }; - } else { - yield undefined; - } - } catch (e) { - // don't fail the whole process for individual correlation queries, - // just add the error to the internal log and check if we'd want to set the - // cross-cluster search compatibility warning to true. - addLogMessage( - `Failed to fetch correlation/kstest for '${item.fieldName}/${item.fieldValue}'`, - JSON.stringify(e) - ); - if (params?.index.includes(':')) { - state.setCcsWarning(true); - } - yield undefined; - } - } -} diff --git a/x-pack/plugins/apm/server/lib/search_strategies/register_search_strategies.ts b/x-pack/plugins/apm/server/lib/search_strategies/register_search_strategies.ts deleted file mode 100644 index 713c5e390ca8b..0000000000000 --- a/x-pack/plugins/apm/server/lib/search_strategies/register_search_strategies.ts +++ /dev/null @@ -1,40 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { PluginSetup as DataPluginSetup } from 'src/plugins/data/server'; - -import { APM_SEARCH_STRATEGIES } from '../../../common/search_strategies/constants'; - -import type { ApmIndicesConfig } from '../settings/apm_indices/get_apm_indices'; - -import { failedTransactionsCorrelationsSearchServiceProvider } from './failed_transactions_correlations'; -import { latencyCorrelationsSearchServiceProvider } from './latency_correlations'; -import { searchStrategyProvider } from './search_strategy_provider'; - -export const registerSearchStrategies = ( - registerSearchStrategy: DataPluginSetup['search']['registerSearchStrategy'], - getApmIndices: () => Promise, - includeFrozen: boolean -) => { - registerSearchStrategy( - APM_SEARCH_STRATEGIES.APM_LATENCY_CORRELATIONS, - searchStrategyProvider( - latencyCorrelationsSearchServiceProvider, - getApmIndices, - includeFrozen - ) - ); - - registerSearchStrategy( - APM_SEARCH_STRATEGIES.APM_FAILED_TRANSACTIONS_CORRELATIONS, - searchStrategyProvider( - failedTransactionsCorrelationsSearchServiceProvider, - getApmIndices, - includeFrozen - ) - ); -}; diff --git a/x-pack/plugins/apm/server/lib/search_strategies/search_service_log.test.ts b/x-pack/plugins/apm/server/lib/search_strategies/search_service_log.test.ts deleted file mode 100644 index 5b887f15a584e..0000000000000 --- a/x-pack/plugins/apm/server/lib/search_strategies/search_service_log.test.ts +++ /dev/null @@ -1,47 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { - searchServiceLogProvider, - currentTimeAsString, -} from './search_service_log'; - -describe('search service', () => { - describe('currentTimeAsString', () => { - it('returns the current time as a string', () => { - const mockDate = new Date(1392202800000); - // @ts-ignore ignore the mockImplementation callback error - const spy = jest.spyOn(global, 'Date').mockReturnValue(mockDate); - - const timeString = currentTimeAsString(); - - expect(timeString).toEqual('2014-02-12T11:00:00.000Z'); - - spy.mockRestore(); - }); - }); - - describe('searchServiceLogProvider', () => { - it('adds and retrieves messages from the log', async () => { - const { addLogMessage, getLogMessages } = searchServiceLogProvider(); - - const mockDate = new Date(1392202800000); - // @ts-ignore ignore the mockImplementation callback error - const spy = jest.spyOn(global, 'Date').mockReturnValue(mockDate); - - addLogMessage('the first message'); - addLogMessage('the second message'); - - expect(getLogMessages()).toEqual([ - '2014-02-12T11:00:00.000Z: the first message', - '2014-02-12T11:00:00.000Z: the second message', - ]); - - spy.mockRestore(); - }); - }); -}); diff --git a/x-pack/plugins/apm/server/lib/search_strategies/search_service_log.ts b/x-pack/plugins/apm/server/lib/search_strategies/search_service_log.ts deleted file mode 100644 index 73a59021b01ed..0000000000000 --- a/x-pack/plugins/apm/server/lib/search_strategies/search_service_log.ts +++ /dev/null @@ -1,34 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -interface LogMessage { - timestamp: string; - message: string; - error?: string; -} - -export const currentTimeAsString = () => new Date().toISOString(); - -export const searchServiceLogProvider = () => { - const log: LogMessage[] = []; - - function addLogMessage(message: string, error?: string) { - log.push({ - timestamp: currentTimeAsString(), - message, - ...(error !== undefined ? { error } : {}), - }); - } - - function getLogMessages() { - return log.map((l) => `${l.timestamp}: ${l.message}`); - } - - return { addLogMessage, getLogMessages }; -}; - -export type SearchServiceLog = ReturnType; diff --git a/x-pack/plugins/apm/server/lib/search_strategies/search_strategy_provider.test.ts b/x-pack/plugins/apm/server/lib/search_strategies/search_strategy_provider.test.ts deleted file mode 100644 index ccccdeab5132d..0000000000000 --- a/x-pack/plugins/apm/server/lib/search_strategies/search_strategy_provider.test.ts +++ /dev/null @@ -1,302 +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 type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; - -import { SearchStrategyDependencies } from 'src/plugins/data/server'; - -import { IKibanaSearchRequest } from '../../../../../../src/plugins/data/common'; - -import { ENVIRONMENT_ALL } from '../../../common/environment_filter_values'; -import type { LatencyCorrelationsParams } from '../../../common/search_strategies/latency_correlations/types'; -import type { RawSearchStrategyClientParams } from '../../../common/search_strategies/types'; - -import type { ApmIndicesConfig } from '../settings/apm_indices/get_apm_indices'; - -import { latencyCorrelationsSearchServiceProvider } from './latency_correlations'; -import { searchStrategyProvider } from './search_strategy_provider'; - -// helper to trigger promises in the async search service -const flushPromises = () => new Promise(setImmediate); - -const clientFieldCapsMock = () => ({ body: { fields: [] } }); - -// minimal client mock to fulfill search requirements of the async search service to succeed -const clientSearchMock = ( - req: estypes.SearchRequest -): { body: estypes.SearchResponse } => { - let aggregations: - | { - transaction_duration_percentiles: estypes.AggregationsTDigestPercentilesAggregate; - } - | { - transaction_duration_min: estypes.AggregationsValueAggregate; - transaction_duration_max: estypes.AggregationsValueAggregate; - } - | { - logspace_ranges: estypes.AggregationsMultiBucketAggregate<{ - from: number; - doc_count: number; - }>; - } - | { - latency_ranges: estypes.AggregationsMultiBucketAggregate<{ - doc_count: number; - }>; - } - | undefined; - - if (req?.body?.aggs !== undefined) { - const aggs = req.body.aggs; - // fetchTransactionDurationPercentiles - if (aggs.transaction_duration_percentiles !== undefined) { - aggregations = { transaction_duration_percentiles: { values: {} } }; - } - - // fetchTransactionDurationCorrelation - if (aggs.logspace_ranges !== undefined) { - aggregations = { logspace_ranges: { buckets: [] } }; - } - - // fetchTransactionDurationFractions - if (aggs.latency_ranges !== undefined) { - aggregations = { latency_ranges: { buckets: [] } }; - } - } - - return { - body: { - _shards: { - failed: 0, - successful: 1, - total: 1, - }, - took: 162, - timed_out: false, - hits: { - hits: [], - total: { - value: 0, - relation: 'eq', - }, - }, - ...(aggregations !== undefined ? { aggregations } : {}), - }, - }; -}; - -const getApmIndicesMock = async () => - ({ transaction: 'apm-*' } as ApmIndicesConfig); - -describe('APM Correlations search strategy', () => { - describe('strategy interface', () => { - it('returns a custom search strategy with a `search` and `cancel` function', async () => { - const searchStrategy = await searchStrategyProvider( - latencyCorrelationsSearchServiceProvider, - getApmIndicesMock, - false - ); - expect(typeof searchStrategy.search).toBe('function'); - expect(typeof searchStrategy.cancel).toBe('function'); - }); - }); - - describe('search', () => { - let mockClientFieldCaps: jest.Mock; - let mockClientSearch: jest.Mock; - let mockGetApmIndicesMock: jest.Mock; - let mockDeps: SearchStrategyDependencies; - let params: Required< - IKibanaSearchRequest< - LatencyCorrelationsParams & RawSearchStrategyClientParams - > - >['params']; - - beforeEach(() => { - mockClientFieldCaps = jest.fn(clientFieldCapsMock); - mockClientSearch = jest.fn(clientSearchMock); - mockGetApmIndicesMock = jest.fn(getApmIndicesMock); - mockDeps = { - esClient: { - asCurrentUser: { - fieldCaps: mockClientFieldCaps, - search: mockClientSearch, - }, - }, - } as unknown as SearchStrategyDependencies; - params = { - start: '2020', - end: '2021', - environment: ENVIRONMENT_ALL.value, - kuery: '', - percentileThreshold: 95, - analyzeCorrelations: true, - }; - }); - - describe('async functionality', () => { - describe('when no params are provided', () => { - it('throws an error', async () => { - const searchStrategy = await searchStrategyProvider( - latencyCorrelationsSearchServiceProvider, - mockGetApmIndicesMock, - false - ); - - expect(mockGetApmIndicesMock).toHaveBeenCalledTimes(0); - - expect(() => searchStrategy.search({}, {}, mockDeps)).toThrow( - 'Invalid request parameters.' - ); - }); - }); - - describe('when no ID is provided', () => { - it('performs a client search with params', async () => { - const searchStrategy = await searchStrategyProvider( - latencyCorrelationsSearchServiceProvider, - mockGetApmIndicesMock, - false - ); - await searchStrategy.search({ params }, {}, mockDeps).toPromise(); - - expect(mockGetApmIndicesMock).toHaveBeenCalledTimes(1); - - const [[request]] = mockClientSearch.mock.calls; - - expect(request.index).toEqual('apm-*'); - expect(request.body).toEqual( - expect.objectContaining({ - aggs: { - transaction_duration_percentiles: { - percentiles: { - field: 'transaction.duration.us', - hdr: { number_of_significant_value_digits: 3 }, - percents: [95], - }, - }, - }, - query: { - bool: { - filter: [ - { term: { 'processor.event': 'transaction' } }, - { - range: { - '@timestamp': { - format: 'epoch_millis', - gte: 1577836800000, - lte: 1609459200000, - }, - }, - }, - ], - }, - }, - size: 0, - track_total_hits: true, - }) - ); - }); - }); - - describe('when an ID with params is provided', () => { - it('retrieves the current request', async () => { - const searchStrategy = await searchStrategyProvider( - latencyCorrelationsSearchServiceProvider, - mockGetApmIndicesMock, - false - ); - const response = await searchStrategy - .search({ params }, {}, mockDeps) - .toPromise(); - - const searchStrategyId = response.id; - - const response2 = await searchStrategy - .search({ id: searchStrategyId, params }, {}, mockDeps) - .toPromise(); - - expect(mockGetApmIndicesMock).toHaveBeenCalledTimes(1); - expect(response2).toEqual( - expect.objectContaining({ id: searchStrategyId }) - ); - }); - }); - - describe('if the client throws', () => { - it('does not emit an error', async () => { - mockClientSearch - .mockReset() - .mockRejectedValueOnce(new Error('client error')); - const searchStrategy = await searchStrategyProvider( - latencyCorrelationsSearchServiceProvider, - mockGetApmIndicesMock, - false - ); - const response = await searchStrategy - .search({ params }, {}, mockDeps) - .toPromise(); - - expect(mockGetApmIndicesMock).toHaveBeenCalledTimes(1); - - expect(response).toEqual( - expect.objectContaining({ isRunning: true }) - ); - }); - }); - - it('triggers the subscription only once', async () => { - expect.assertions(2); - const searchStrategy = await searchStrategyProvider( - latencyCorrelationsSearchServiceProvider, - mockGetApmIndicesMock, - false - ); - searchStrategy - .search({ params }, {}, mockDeps) - .subscribe((response) => { - expect(mockGetApmIndicesMock).toHaveBeenCalledTimes(1); - expect(response).toEqual( - expect.objectContaining({ loaded: 0, isRunning: true }) - ); - }); - }); - }); - - describe('response', () => { - it('sends an updated response on consecutive search calls', async () => { - const searchStrategy = await searchStrategyProvider( - latencyCorrelationsSearchServiceProvider, - mockGetApmIndicesMock, - false - ); - - const response1 = await searchStrategy - .search({ params }, {}, mockDeps) - .toPromise(); - - expect(mockGetApmIndicesMock).toHaveBeenCalledTimes(1); - expect(typeof response1.id).toEqual('string'); - expect(response1).toEqual( - expect.objectContaining({ loaded: 0, isRunning: true }) - ); - - await flushPromises(); - - const response2 = await searchStrategy - .search({ id: response1.id, params }, {}, mockDeps) - .toPromise(); - - expect(mockGetApmIndicesMock).toHaveBeenCalledTimes(1); - expect(response2.id).toEqual(response1.id); - expect(response2).toEqual( - expect.objectContaining({ loaded: 100, isRunning: false }) - ); - }); - }); - }); -}); diff --git a/x-pack/plugins/apm/server/lib/search_strategies/search_strategy_provider.ts b/x-pack/plugins/apm/server/lib/search_strategies/search_strategy_provider.ts deleted file mode 100644 index 8035e9e4d97ca..0000000000000 --- a/x-pack/plugins/apm/server/lib/search_strategies/search_strategy_provider.ts +++ /dev/null @@ -1,204 +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 uuid from 'uuid'; -import { of } from 'rxjs'; -import { getOrElse } from 'fp-ts/lib/Either'; -import { pipe } from 'fp-ts/lib/pipeable'; -import * as t from 'io-ts'; -import { failure } from 'io-ts/lib/PathReporter'; - -import type { ElasticsearchClient } from 'src/core/server'; - -import type { ISearchStrategy } from '../../../../../../src/plugins/data/server'; -import { - IKibanaSearchRequest, - IKibanaSearchResponse, -} from '../../../../../../src/plugins/data/common'; - -import type { - RawResponseBase, - RawSearchStrategyClientParams, - SearchStrategyClientParams, -} from '../../../common/search_strategies/types'; -import type { - LatencyCorrelationsParams, - LatencyCorrelationsRawResponse, -} from '../../../common/search_strategies/latency_correlations/types'; -import type { - FailedTransactionsCorrelationsParams, - FailedTransactionsCorrelationsRawResponse, -} from '../../../common/search_strategies/failed_transactions_correlations/types'; -import { rangeRt } from '../../routes/default_api_types'; -import type { ApmIndicesConfig } from '../settings/apm_indices/get_apm_indices'; - -interface SearchServiceState { - cancel: () => void; - error: Error; - meta: { - loaded: number; - total: number; - isRunning: boolean; - isPartial: boolean; - }; - rawResponse: TRawResponse; -} - -type GetSearchServiceState = - () => SearchServiceState; - -export type SearchServiceProvider< - TSearchStrategyClientParams extends SearchStrategyClientParams, - TRawResponse extends RawResponseBase -> = ( - esClient: ElasticsearchClient, - getApmIndices: () => Promise, - searchServiceParams: TSearchStrategyClientParams, - includeFrozen: boolean -) => GetSearchServiceState; - -// Failed Transactions Correlations function overload -export function searchStrategyProvider( - searchServiceProvider: SearchServiceProvider< - FailedTransactionsCorrelationsParams & SearchStrategyClientParams, - FailedTransactionsCorrelationsRawResponse & RawResponseBase - >, - getApmIndices: () => Promise, - includeFrozen: boolean -): ISearchStrategy< - IKibanaSearchRequest< - FailedTransactionsCorrelationsParams & RawSearchStrategyClientParams - >, - IKibanaSearchResponse< - FailedTransactionsCorrelationsRawResponse & RawResponseBase - > ->; - -// Latency Correlations function overload -export function searchStrategyProvider( - searchServiceProvider: SearchServiceProvider< - LatencyCorrelationsParams & SearchStrategyClientParams, - LatencyCorrelationsRawResponse & RawResponseBase - >, - getApmIndices: () => Promise, - includeFrozen: boolean -): ISearchStrategy< - IKibanaSearchRequest< - LatencyCorrelationsParams & RawSearchStrategyClientParams - >, - IKibanaSearchResponse ->; - -export function searchStrategyProvider( - searchServiceProvider: SearchServiceProvider< - TRequestParams & SearchStrategyClientParams, - TResponseParams & RawResponseBase - >, - getApmIndices: () => Promise, - includeFrozen: boolean -): ISearchStrategy< - IKibanaSearchRequest, - IKibanaSearchResponse -> { - const searchServiceMap = new Map< - string, - GetSearchServiceState - >(); - - return { - search: (request, options, deps) => { - if (request.params === undefined) { - throw new Error('Invalid request parameters.'); - } - - const { start: startString, end: endString } = request.params; - - // converts string based start/end to epochmillis - const decodedRange = pipe( - rangeRt.decode({ start: startString, end: endString }), - getOrElse((errors) => { - throw new Error(failure(errors).join('\n')); - }) - ); - - // The function to fetch the current state of the search service. - // This will be either an existing service for a follow up fetch or a new one for new requests. - let getSearchServiceState: GetSearchServiceState< - TResponseParams & RawResponseBase - >; - - // If the request includes an ID, we require that the search service already exists - // otherwise we throw an error. The client should never poll a service that's been cancelled or finished. - // This also avoids instantiating search services when the service gets called with random IDs. - if (typeof request.id === 'string') { - const existingGetSearchServiceState = searchServiceMap.get(request.id); - - if (typeof existingGetSearchServiceState === 'undefined') { - throw new Error( - `SearchService with ID '${request.id}' does not exist.` - ); - } - - getSearchServiceState = existingGetSearchServiceState; - } else { - const { - start, - end, - environment, - kuery, - serviceName, - transactionName, - transactionType, - ...requestParams - } = request.params; - - getSearchServiceState = searchServiceProvider( - deps.esClient.asCurrentUser, - getApmIndices, - { - environment, - kuery, - serviceName, - transactionName, - transactionType, - start: decodedRange.start, - end: decodedRange.end, - ...(requestParams as unknown as TRequestParams), - }, - includeFrozen - ); - } - - // Reuse the request's id or create a new one. - const id = request.id ?? uuid(); - - const { error, meta, rawResponse } = getSearchServiceState(); - - if (error instanceof Error) { - searchServiceMap.delete(id); - throw error; - } else if (meta.isRunning) { - searchServiceMap.set(id, getSearchServiceState); - } else { - searchServiceMap.delete(id); - } - - return of({ - id, - ...meta, - rawResponse, - }); - }, - cancel: async (id, options, deps) => { - const getSearchServiceState = searchServiceMap.get(id); - if (getSearchServiceState !== undefined) { - getSearchServiceState().cancel(); - searchServiceMap.delete(id); - } - }, - }; -} diff --git a/x-pack/plugins/apm/server/lib/search_strategies/utils/index.ts b/x-pack/plugins/apm/server/lib/search_strategies/utils/index.ts deleted file mode 100644 index 727bc6cd787a0..0000000000000 --- a/x-pack/plugins/apm/server/lib/search_strategies/utils/index.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; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -export { computeExpectationsAndRanges } from './compute_expectations_and_ranges'; -export { hasPrefixToInclude } from './has_prefix_to_include'; diff --git a/x-pack/plugins/apm/server/lib/service_map/get_service_anomalies.ts b/x-pack/plugins/apm/server/lib/service_map/get_service_anomalies.ts index 2ed1966dcacbd..2aa2f5c6eead5 100644 --- a/x-pack/plugins/apm/server/lib/service_map/get_service_anomalies.ts +++ b/x-pack/plugins/apm/server/lib/service_map/get_service_anomalies.ts @@ -22,6 +22,8 @@ import { rangeQuery } from '../../../../observability/server'; import { withApmSpan } from '../../utils/with_apm_span'; import { getMlJobsWithAPMGroup } from '../anomaly_detection/get_ml_jobs_with_apm_group'; import { Setup } from '../helpers/setup_request'; +import { apmMlAnomalyQuery } from '../../../common/anomaly_detection/apm_ml_anomaly_query'; +import { ApmMlDetectorIndex } from '../../../common/anomaly_detection/apm_ml_detectors'; export const DEFAULT_ANOMALIES: ServiceAnomaliesResponse = { mlJobIds: [], @@ -56,7 +58,7 @@ export async function getServiceAnomalies({ query: { bool: { filter: [ - { terms: { result_type: ['model_plot', 'record'] } }, + ...apmMlAnomalyQuery(ApmMlDetectorIndex.txLatency), ...rangeQuery( Math.min(end - 30 * 60 * 1000, start), end, diff --git a/x-pack/plugins/apm/server/lib/service_map/get_service_map.ts b/x-pack/plugins/apm/server/lib/service_map/get_service_map.ts index ae511d0fed8f8..aaf55413d9774 100644 --- a/x-pack/plugins/apm/server/lib/service_map/get_service_map.ts +++ b/x-pack/plugins/apm/server/lib/service_map/get_service_map.ts @@ -7,14 +7,14 @@ import { Logger } from 'kibana/server'; import { chunk } from 'lodash'; +import { ProcessorEvent } from '../../../common/processor_event'; +import { rangeQuery, termQuery } from '../../../../observability/server'; import { PromiseReturnType } from '../../../../observability/typings/common'; import { AGENT_NAME, SERVICE_ENVIRONMENT, SERVICE_NAME, } from '../../../common/elasticsearch_fieldnames'; -import { getServicesProjection } from '../../projections/services'; -import { mergeProjection } from '../../projections/util/merge_projection'; import { environmentQuery } from '../../../common/utils/environment_query'; import { withApmSpan } from '../../utils/with_apm_span'; import { Setup } from '../helpers/setup_request'; @@ -26,6 +26,7 @@ import { getServiceMapFromTraceIds } from './get_service_map_from_trace_ids'; import { getTraceSampleIds } from './get_trace_sample_ids'; import { transformServiceMapResponses } from './transform_service_map_responses'; import { ENVIRONMENT_ALL } from '../../../common/environment_filter_values'; +import { getProcessorEventForTransactions } from '../helpers/transactions'; export interface IEnvOptions { setup: Setup; @@ -94,40 +95,29 @@ async function getServicesData(options: IEnvOptions) { const { environment, setup, searchAggregatedTransactions, start, end } = options; - const projection = getServicesProjection({ - setup, - searchAggregatedTransactions, - kuery: '', - start, - end, - }); - - let filter = [ - ...projection.body.query.bool.filter, - ...environmentQuery(environment), - ]; - - if (options.serviceName) { - filter = filter.concat({ - term: { - [SERVICE_NAME]: options.serviceName, - }, - }); - } - - const params = mergeProjection(projection, { + const params = { + apm: { + events: [ + getProcessorEventForTransactions(searchAggregatedTransactions), + ProcessorEvent.metric as const, + ProcessorEvent.error as const, + ], + }, body: { size: 0, query: { bool: { - ...projection.body.query.bool, - filter, + filter: [ + ...rangeQuery(start, end), + ...environmentQuery(environment), + ...termQuery(SERVICE_NAME, options.serviceName), + ], }, }, aggs: { services: { terms: { - field: projection.body.aggs.services.terms.field, + field: SERVICE_NAME, size: 500, }, aggs: { @@ -140,7 +130,7 @@ async function getServicesData(options: IEnvOptions) { }, }, }, - }); + }; const { apmEventClient } = setup; diff --git a/x-pack/plugins/apm/server/lib/services/__snapshots__/queries.test.ts.snap b/x-pack/plugins/apm/server/lib/services/__snapshots__/queries.test.ts.snap index d6d6219440dad..95bd6106b9ff2 100644 --- a/x-pack/plugins/apm/server/lib/services/__snapshots__/queries.test.ts.snap +++ b/x-pack/plugins/apm/server/lib/services/__snapshots__/queries.test.ts.snap @@ -98,6 +98,9 @@ Object { }, }, "size": 1, + "sort": Object { + "_score": "desc", + }, }, "terminate_after": 1, } diff --git a/x-pack/plugins/apm/server/lib/services/get_service_agent.ts b/x-pack/plugins/apm/server/lib/services/get_service_agent.ts index 4c9ff9f124b10..dc3fee20fdf68 100644 --- a/x-pack/plugins/apm/server/lib/services/get_service_agent.ts +++ b/x-pack/plugins/apm/server/lib/services/get_service_agent.ts @@ -13,7 +13,6 @@ import { } from '../../../common/elasticsearch_fieldnames'; import { rangeQuery } from '../../../../observability/server'; import { Setup } from '../helpers/setup_request'; -import { getProcessorEventForTransactions } from '../helpers/transactions'; interface ServiceAgent { agent?: { @@ -29,13 +28,11 @@ interface ServiceAgent { export async function getServiceAgent({ serviceName, setup, - searchAggregatedTransactions, start, end, }: { serviceName: string; setup: Setup; - searchAggregatedTransactions: boolean; start: number; end: number; }) { @@ -46,7 +43,7 @@ export async function getServiceAgent({ apm: { events: [ ProcessorEvent.error, - getProcessorEventForTransactions(searchAggregatedTransactions), + ProcessorEvent.transaction, ProcessorEvent.metric, ], }, @@ -71,6 +68,9 @@ export async function getServiceAgent({ }, }, }, + sort: { + _score: 'desc' as const, + }, }, }; diff --git a/x-pack/plugins/apm/server/lib/services/get_service_infrastructure.ts b/x-pack/plugins/apm/server/lib/services/get_service_infrastructure.ts index 11669d5934303..09946187b90a2 100644 --- a/x-pack/plugins/apm/server/lib/services/get_service_infrastructure.ts +++ b/x-pack/plugins/apm/server/lib/services/get_service_infrastructure.ts @@ -6,7 +6,6 @@ */ import { Setup } from '../helpers/setup_request'; -import { ESFilter } from '../../../../../../src/core/types/elasticsearch'; import { rangeQuery, kqlQuery } from '../../../../observability/server'; import { environmentQuery } from '../../../common/utils/environment_query'; import { ProcessorEvent } from '../../../common/processor_event'; @@ -33,13 +32,6 @@ export const getServiceInfrastructure = async ({ }) => { const { apmEventClient } = setup; - const filter: ESFilter[] = [ - { term: { [SERVICE_NAME]: serviceName } }, - ...rangeQuery(start, end), - ...environmentQuery(environment), - ...kqlQuery(kuery), - ]; - const response = await apmEventClient.search('get_service_infrastructure', { apm: { events: [ProcessorEvent.metric], @@ -48,7 +40,12 @@ export const getServiceInfrastructure = async ({ size: 0, query: { bool: { - filter, + filter: [ + { term: { [SERVICE_NAME]: serviceName } }, + ...rangeQuery(start, end), + ...environmentQuery(environment), + ...kqlQuery(kuery), + ], }, }, aggs: { diff --git a/x-pack/plugins/apm/server/lib/services/get_services_detailed_statistics/get_service_transaction_detailed_statistics.ts b/x-pack/plugins/apm/server/lib/services/get_services_detailed_statistics/get_service_transaction_detailed_statistics.ts index 686555e7764ab..ea153a5ddcd4c 100644 --- a/x-pack/plugins/apm/server/lib/services/get_services_detailed_statistics/get_service_transaction_detailed_statistics.ts +++ b/x-pack/plugins/apm/server/lib/services/get_services_detailed_statistics/get_service_transaction_detailed_statistics.ts @@ -70,7 +70,7 @@ export async function getServiceTransactionDetailedStatistics({ }; const response = await apmEventClient.search( - 'get_service_transaction_stats', + 'get_service_transaction_detail_stats', { apm: { events: [ @@ -82,6 +82,7 @@ export async function getServiceTransactionDetailedStatistics({ query: { bool: { filter: [ + { terms: { [SERVICE_NAME]: serviceNames } }, ...getDocumentTypeFilterForTransactions( searchAggregatedTransactions ), @@ -95,8 +96,6 @@ export async function getServiceTransactionDetailedStatistics({ services: { terms: { field: SERVICE_NAME, - include: serviceNames, - size: serviceNames.length, }, aggs: { transactionType: { diff --git a/x-pack/plugins/apm/server/lib/services/get_throughput.ts b/x-pack/plugins/apm/server/lib/services/get_throughput.ts index e31e9dd3b8c9f..3161066ebadf9 100644 --- a/x-pack/plugins/apm/server/lib/services/get_throughput.ts +++ b/x-pack/plugins/apm/server/lib/services/get_throughput.ts @@ -5,20 +5,23 @@ * 2.0. */ -import { ESFilter } from '../../../../../../src/core/types/elasticsearch'; +import { AggregationsDateInterval } from '@elastic/elasticsearch/lib/api/types'; import { SERVICE_NAME, TRANSACTION_NAME, TRANSACTION_TYPE, } from '../../../common/elasticsearch_fieldnames'; -import { kqlQuery, rangeQuery } from '../../../../observability/server'; +import { + kqlQuery, + rangeQuery, + termQuery, +} from '../../../../observability/server'; import { environmentQuery } from '../../../common/utils/environment_query'; import { getDocumentTypeFilterForTransactions, getProcessorEventForTransactions, } from '../helpers/transactions'; import { Setup } from '../helpers/setup_request'; -import { calculateThroughputWithInterval } from '../helpers/calculate_throughput'; interface Options { environment: string; @@ -49,30 +52,27 @@ export async function getThroughput({ }: Options) { const { apmEventClient } = setup; - const filter: ESFilter[] = [ - { term: { [SERVICE_NAME]: serviceName } }, - { term: { [TRANSACTION_TYPE]: transactionType } }, - ...getDocumentTypeFilterForTransactions(searchAggregatedTransactions), - ...rangeQuery(start, end), - ...environmentQuery(environment), - ...kqlQuery(kuery), - ]; - - if (transactionName) { - filter.push({ - term: { - [TRANSACTION_NAME]: transactionName, - }, - }); - } - const params = { apm: { events: [getProcessorEventForTransactions(searchAggregatedTransactions)], }, body: { size: 0, - query: { bool: { filter } }, + query: { + bool: { + filter: [ + { term: { [SERVICE_NAME]: serviceName } }, + { term: { [TRANSACTION_TYPE]: transactionType } }, + ...getDocumentTypeFilterForTransactions( + searchAggregatedTransactions + ), + ...rangeQuery(start, end), + ...environmentQuery(environment), + ...kqlQuery(kuery), + ...termQuery(TRANSACTION_NAME, transactionName), + ], + }, + }, aggs: { timeseries: { date_histogram: { @@ -81,6 +81,11 @@ export async function getThroughput({ min_doc_count: 0, extended_bounds: { min: start, max: end }, }, + aggs: { + throughput: { + rate: { unit: 'minute' as AggregationsDateInterval }, + }, + }, }, }, }, @@ -95,10 +100,7 @@ export async function getThroughput({ response.aggregations?.timeseries.buckets.map((bucket) => { return { x: bucket.key, - y: calculateThroughputWithInterval({ - bucketSize, - value: bucket.doc_count, - }), + y: bucket.throughput.value, }; }) ?? [] ); diff --git a/x-pack/plugins/apm/server/lib/services/queries.test.ts b/x-pack/plugins/apm/server/lib/services/queries.test.ts index 4ed6f856d735b..30d89214959da 100644 --- a/x-pack/plugins/apm/server/lib/services/queries.test.ts +++ b/x-pack/plugins/apm/server/lib/services/queries.test.ts @@ -28,7 +28,6 @@ describe('services queries', () => { getServiceAgent({ serviceName: 'foo', setup, - searchAggregatedTransactions: false, start: 0, end: 50000, }) diff --git a/x-pack/plugins/apm/server/lib/transaction_groups/fetcher.ts b/x-pack/plugins/apm/server/lib/transaction_groups/fetcher.ts index 200d3d6ac7459..aea92d06b7589 100644 --- a/x-pack/plugins/apm/server/lib/transaction_groups/fetcher.ts +++ b/x-pack/plugins/apm/server/lib/transaction_groups/fetcher.ts @@ -10,7 +10,11 @@ import { sortBy } from 'lodash'; import moment from 'moment'; import { Unionize } from 'utility-types'; import { AggregationOptionsByType } from '../../../../../../src/core/types/elasticsearch'; -import { kqlQuery, rangeQuery } from '../../../../observability/server'; +import { + kqlQuery, + rangeQuery, + termQuery, +} from '../../../../observability/server'; import { PARENT_ID, SERVICE_NAME, @@ -69,10 +73,6 @@ function getRequest(topTraceOptions: TopTraceOptions) { end, } = topTraceOptions; - const transactionNameFilter = transactionName - ? [{ term: { [TRANSACTION_NAME]: transactionName } }] - : []; - return { apm: { events: [getProcessorEventForTransactions(searchAggregatedTransactions)], @@ -82,7 +82,7 @@ function getRequest(topTraceOptions: TopTraceOptions) { query: { bool: { filter: [ - ...transactionNameFilter, + ...termQuery(TRANSACTION_NAME, transactionName), ...getDocumentTypeFilterForTransactions( searchAggregatedTransactions ), diff --git a/x-pack/plugins/apm/server/lib/transaction_groups/get_error_rate.ts b/x-pack/plugins/apm/server/lib/transaction_groups/get_error_rate.ts index b7318e81a84a3..328d2da0f6df0 100644 --- a/x-pack/plugins/apm/server/lib/transaction_groups/get_error_rate.ts +++ b/x-pack/plugins/apm/server/lib/transaction_groups/get_error_rate.ts @@ -13,7 +13,11 @@ import { } from '../../../common/elasticsearch_fieldnames'; import { EventOutcome } from '../../../common/event_outcome'; import { offsetPreviousPeriodCoordinates } from '../../../common/utils/offset_previous_period_coordinate'; -import { kqlQuery, rangeQuery } from '../../../../observability/server'; +import { + kqlQuery, + rangeQuery, + termQuery, +} from '../../../../observability/server'; import { environmentQuery } from '../../../common/utils/environment_query'; import { Coordinate } from '../../../typings/timeseries'; import { @@ -54,13 +58,6 @@ export async function getErrorRate({ }> { const { apmEventClient } = setup; - const transactionNamefilter = transactionName - ? [{ term: { [TRANSACTION_NAME]: transactionName } }] - : []; - const transactionTypefilter = transactionType - ? [{ term: { [TRANSACTION_TYPE]: transactionType } }] - : []; - const filter = [ { term: { [SERVICE_NAME]: serviceName } }, { @@ -68,8 +65,8 @@ export async function getErrorRate({ [EVENT_OUTCOME]: [EventOutcome.failure, EventOutcome.success], }, }, - ...transactionNamefilter, - ...transactionTypefilter, + ...termQuery(TRANSACTION_NAME, transactionName), + ...termQuery(TRANSACTION_TYPE, transactionType), ...getDocumentTypeFilterForTransactions(searchAggregatedTransactions), ...rangeQuery(start, end), ...environmentQuery(environment), diff --git a/x-pack/plugins/apm/server/lib/transactions/get_anomaly_data/fetcher.ts b/x-pack/plugins/apm/server/lib/transactions/get_anomaly_data/fetcher.ts index 2fcbf5842d746..dd723f24abe1b 100644 --- a/x-pack/plugins/apm/server/lib/transactions/get_anomaly_data/fetcher.ts +++ b/x-pack/plugins/apm/server/lib/transactions/get_anomaly_data/fetcher.ts @@ -12,6 +12,8 @@ import { rangeQuery } from '../../../../../observability/server'; import { asMutableArray } from '../../../../common/utils/as_mutable_array'; import { withApmSpan } from '../../../utils/with_apm_span'; import { Setup } from '../../helpers/setup_request'; +import { apmMlAnomalyQuery } from '../../../../common/anomaly_detection/apm_ml_anomaly_query'; +import { ApmMlDetectorIndex } from '../../../../common/anomaly_detection/apm_ml_detectors'; export type ESResponse = Exclude< PromiseReturnType, @@ -40,7 +42,7 @@ export function anomalySeriesFetcher({ query: { bool: { filter: [ - { terms: { result_type: ['model_plot', 'record'] } }, + ...apmMlAnomalyQuery(ApmMlDetectorIndex.txLatency), { term: { partition_field_value: serviceName } }, { term: { by_field_value: transactionType } }, ...rangeQuery(start, end, 'timestamp'), diff --git a/x-pack/plugins/apm/server/lib/transactions/get_latency_charts/index.ts b/x-pack/plugins/apm/server/lib/transactions/get_latency_charts/index.ts index c4bae841764cf..4612d399b54a1 100644 --- a/x-pack/plugins/apm/server/lib/transactions/get_latency_charts/index.ts +++ b/x-pack/plugins/apm/server/lib/transactions/get_latency_charts/index.ts @@ -5,7 +5,6 @@ * 2.0. */ -import { ESFilter } from '../../../../../../../src/core/types/elasticsearch'; import { PromiseReturnType } from '../../../../../observability/typings/common'; import { SERVICE_NAME, @@ -14,7 +13,11 @@ import { } from '../../../../common/elasticsearch_fieldnames'; import { LatencyAggregationType } from '../../../../common/latency_aggregation_types'; import { offsetPreviousPeriodCoordinates } from '../../../../common/utils/offset_previous_period_coordinate'; -import { kqlQuery, rangeQuery } from '../../../../../observability/server'; +import { + kqlQuery, + rangeQuery, + termQuery, +} from '../../../../../observability/server'; import { environmentQuery } from '../../../../common/utils/environment_query'; import { getDocumentTypeFilterForTransactions, @@ -61,22 +64,6 @@ function searchLatency({ searchAggregatedTransactions, }); - const filter: ESFilter[] = [ - { term: { [SERVICE_NAME]: serviceName } }, - ...getDocumentTypeFilterForTransactions(searchAggregatedTransactions), - ...rangeQuery(start, end), - ...environmentQuery(environment), - ...kqlQuery(kuery), - ]; - - if (transactionName) { - filter.push({ term: { [TRANSACTION_NAME]: transactionName } }); - } - - if (transactionType) { - filter.push({ term: { [TRANSACTION_TYPE]: transactionType } }); - } - const transactionDurationField = getTransactionDurationFieldForTransactions( searchAggregatedTransactions ); @@ -87,7 +74,21 @@ function searchLatency({ }, body: { size: 0, - query: { bool: { filter } }, + query: { + bool: { + filter: [ + { term: { [SERVICE_NAME]: serviceName } }, + ...getDocumentTypeFilterForTransactions( + searchAggregatedTransactions + ), + ...rangeQuery(start, end), + ...environmentQuery(environment), + ...kqlQuery(kuery), + ...termQuery(TRANSACTION_NAME, transactionName), + ...termQuery(TRANSACTION_TYPE, transactionType), + ], + }, + }, aggs: { latencyTimeseries: { date_histogram: { diff --git a/x-pack/plugins/apm/server/lib/transactions/get_transaction/index.ts b/x-pack/plugins/apm/server/lib/transactions/get_transaction/index.ts index e5d8c930393e0..6d0bbcdb55ca4 100644 --- a/x-pack/plugins/apm/server/lib/transactions/get_transaction/index.ts +++ b/x-pack/plugins/apm/server/lib/transactions/get_transaction/index.ts @@ -9,7 +9,7 @@ import { TRACE_ID, TRANSACTION_ID, } from '../../../../common/elasticsearch_fieldnames'; -import { rangeQuery } from '../../../../../observability/server'; +import { rangeQuery, termQuery } from '../../../../../observability/server'; import { Setup } from '../../helpers/setup_request'; import { ProcessorEvent } from '../../../../common/processor_event'; import { asMutableArray } from '../../../../common/utils/as_mutable_array'; @@ -39,7 +39,7 @@ export async function getTransaction({ bool: { filter: asMutableArray([ { term: { [TRANSACTION_ID]: transactionId } }, - ...(traceId ? [{ term: { [TRACE_ID]: traceId } }] : []), + ...termQuery(TRACE_ID, traceId), ...(start && end ? rangeQuery(start, end) : []), ]), }, diff --git a/x-pack/plugins/apm/server/plugin.ts b/x-pack/plugins/apm/server/plugin.ts index 72a1bc483015e..b273fc867e5a8 100644 --- a/x-pack/plugins/apm/server/plugin.ts +++ b/x-pack/plugins/apm/server/plugin.ts @@ -15,7 +15,6 @@ import { PluginInitializerContext, } from 'src/core/server'; import { isEmpty, mapValues } from 'lodash'; -import { SavedObjectsClient } from '../../../../src/core/server'; import { mappingFromFieldMap } from '../../rule_registry/common/mapping_from_field_map'; import { Dataset } from '../../rule_registry/server'; import { APMConfig, APM_SERVER_FEATURE_ID } from '.'; @@ -26,7 +25,6 @@ import { registerFleetPolicyCallbacks } from './lib/fleet/register_fleet_policy_ import { createApmTelemetry } from './lib/apm_telemetry'; import { createApmEventClient } from './lib/helpers/create_es_client/create_apm_event_client'; import { getInternalSavedObjectsClient } from './lib/helpers/get_internal_saved_objects_client'; -import { registerSearchStrategies } from './lib/search_strategies'; import { createApmAgentConfigurationIndex } from './lib/settings/agent_configuration/create_agent_config_index'; import { getApmIndices } from './lib/settings/apm_indices/get_apm_indices'; import { createApmCustomLinkIndex } from './lib/settings/custom_link/create_custom_link_index'; @@ -40,8 +38,8 @@ import { APMPluginSetupDependencies, APMPluginStartDependencies, } from './types'; -import { registerRoutes } from './routes/register_routes'; -import { getGlobalApmServerRouteRepository } from './routes/get_global_apm_server_route_repository'; +import { registerRoutes } from './routes/apm_routes/register_apm_server_routes'; +import { getGlobalApmServerRouteRepository } from './routes/apm_routes/get_global_apm_server_route_repository'; import { PROCESSOR_EVENT, SERVICE_ENVIRONMENT, @@ -197,25 +195,6 @@ export class APMPlugin logger: this.logger, }); - // search strategies for async partial search results - core.getStartServices().then(([coreStart]) => { - (async () => { - const savedObjectsClient = new SavedObjectsClient( - coreStart.savedObjects.createInternalRepository() - ); - - const includeFrozen = await coreStart.uiSettings - .asScopedToClient(savedObjectsClient) - .get(UI_SETTINGS.SEARCH_INCLUDE_FROZEN); - - registerSearchStrategies( - plugins.data.search.registerSearchStrategy, - boundGetApmIndices, - includeFrozen - ); - })(); - }); - core.deprecations.registerDeprecations({ getDeprecations: getDeprecations({ cloudSetup: plugins.cloud, diff --git a/x-pack/plugins/apm/server/projections/services.ts b/x-pack/plugins/apm/server/projections/services.ts deleted file mode 100644 index 139c86acd5144..0000000000000 --- a/x-pack/plugins/apm/server/projections/services.ts +++ /dev/null @@ -1,51 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { Setup } from '../../server/lib/helpers/setup_request'; -import { SERVICE_NAME } from '../../common/elasticsearch_fieldnames'; -import { rangeQuery, kqlQuery } from '../../../observability/server'; -import { ProcessorEvent } from '../../common/processor_event'; -import { getProcessorEventForTransactions } from '../lib/helpers/transactions'; - -export function getServicesProjection({ - kuery, - setup, - searchAggregatedTransactions, - start, - end, -}: { - kuery: string; - setup: Setup; - searchAggregatedTransactions: boolean; - start: number; - end: number; -}) { - return { - apm: { - events: [ - getProcessorEventForTransactions(searchAggregatedTransactions), - ProcessorEvent.metric as const, - ProcessorEvent.error as const, - ], - }, - body: { - size: 0, - query: { - bool: { - filter: [...rangeQuery(start, end), ...kqlQuery(kuery)], - }, - }, - aggs: { - services: { - terms: { - field: SERVICE_NAME, - }, - }, - }, - }, - }; -} diff --git a/x-pack/plugins/apm/server/routes/alerts/chart_preview.ts b/x-pack/plugins/apm/server/routes/alerts/chart_preview.ts index 23a794bb7976a..cae35c7f06a85 100644 --- a/x-pack/plugins/apm/server/routes/alerts/chart_preview.ts +++ b/x-pack/plugins/apm/server/routes/alerts/chart_preview.ts @@ -10,8 +10,8 @@ import { getTransactionDurationChartPreview } from '../../lib/alerts/chart_previ import { getTransactionErrorCountChartPreview } from '../../lib/alerts/chart_preview/get_transaction_error_count'; import { getTransactionErrorRateChartPreview } from '../../lib/alerts/chart_preview/get_transaction_error_rate'; import { setupRequest } from '../../lib/helpers/setup_request'; -import { createApmServerRoute } from '../create_apm_server_route'; -import { createApmServerRouteRepository } from '../create_apm_server_route_repository'; +import { createApmServerRoute } from '../apm_routes/create_apm_server_route'; +import { createApmServerRouteRepository } from '../apm_routes/create_apm_server_route_repository'; import { environmentRt, rangeRt } from '../default_api_types'; const alertParamsRt = t.intersection([ diff --git a/x-pack/plugins/apm/server/routes/create_apm_server_route.ts b/x-pack/plugins/apm/server/routes/apm_routes/create_apm_server_route.ts similarity index 97% rename from x-pack/plugins/apm/server/routes/create_apm_server_route.ts rename to x-pack/plugins/apm/server/routes/apm_routes/create_apm_server_route.ts index 86330a87a8c55..b00b1ad6a1fa5 100644 --- a/x-pack/plugins/apm/server/routes/create_apm_server_route.ts +++ b/x-pack/plugins/apm/server/routes/apm_routes/create_apm_server_route.ts @@ -5,7 +5,7 @@ * 2.0. */ import { createServerRouteFactory } from '@kbn/server-route-repository'; -import { APMRouteCreateOptions, APMRouteHandlerResources } from './typings'; +import { APMRouteCreateOptions, APMRouteHandlerResources } from '../typings'; export const createApmServerRoute = createServerRouteFactory< APMRouteHandlerResources, diff --git a/x-pack/plugins/apm/server/routes/create_apm_server_route_repository.ts b/x-pack/plugins/apm/server/routes/apm_routes/create_apm_server_route_repository.ts similarity index 97% rename from x-pack/plugins/apm/server/routes/create_apm_server_route_repository.ts rename to x-pack/plugins/apm/server/routes/apm_routes/create_apm_server_route_repository.ts index b7cbe890c57db..43a5c2e33c9f8 100644 --- a/x-pack/plugins/apm/server/routes/create_apm_server_route_repository.ts +++ b/x-pack/plugins/apm/server/routes/apm_routes/create_apm_server_route_repository.ts @@ -5,7 +5,7 @@ * 2.0. */ import { createServerRouteRepository } from '@kbn/server-route-repository'; -import { APMRouteCreateOptions, APMRouteHandlerResources } from './typings'; +import { APMRouteCreateOptions, APMRouteHandlerResources } from '../typings'; export function createApmServerRouteRepository() { return createServerRouteRepository< diff --git a/x-pack/plugins/apm/server/routes/apm_routes/get_global_apm_server_route_repository.ts b/x-pack/plugins/apm/server/routes/apm_routes/get_global_apm_server_route_repository.ts new file mode 100644 index 0000000000000..fa8bc1e54ebfb --- /dev/null +++ b/x-pack/plugins/apm/server/routes/apm_routes/get_global_apm_server_route_repository.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 type { + ServerRouteRepository, + ReturnOf, + EndpointOf, +} from '@kbn/server-route-repository'; +import { PickByValue } from 'utility-types'; +import { correlationsRouteRepository } from '../correlations'; +import { alertsChartPreviewRouteRepository } from '../alerts/chart_preview'; +import { backendsRouteRepository } from '../backends/route'; +import { createApmServerRouteRepository } from '../apm_routes/create_apm_server_route_repository'; +import { environmentsRouteRepository } from '../environments'; +import { errorsRouteRepository } from '../errors'; +import { apmFleetRouteRepository } from '../fleet'; +import { dataViewRouteRepository } from '../data_view'; +import { latencyDistributionRouteRepository } from '../latency_distribution'; +import { metricsRouteRepository } from '../metrics'; +import { observabilityOverviewRouteRepository } from '../observability_overview'; +import { rumRouteRepository } from '../rum_client'; +import { fallbackToTransactionsRouteRepository } from '../fallback_to_transactions'; +import { serviceRouteRepository } from '../services'; +import { serviceMapRouteRepository } from '../service_map'; +import { serviceNodeRouteRepository } from '../service_nodes'; +import { agentConfigurationRouteRepository } from '../settings/agent_configuration'; +import { anomalyDetectionRouteRepository } from '../settings/anomaly_detection'; +import { apmIndicesRouteRepository } from '../settings/apm_indices'; +import { customLinkRouteRepository } from '../settings/custom_link'; +import { sourceMapsRouteRepository } from '../source_maps'; +import { traceRouteRepository } from '../traces'; +import { transactionRouteRepository } from '../transactions'; +import { APMRouteHandlerResources } from '../typings'; +import { historicalDataRouteRepository } from '../historical_data'; +import { eventMetadataRouteRepository } from '../event_metadata'; +import { suggestionsRouteRepository } from '../suggestions'; + +const getTypedGlobalApmServerRouteRepository = () => { + const repository = createApmServerRouteRepository() + .merge(dataViewRouteRepository) + .merge(environmentsRouteRepository) + .merge(errorsRouteRepository) + .merge(latencyDistributionRouteRepository) + .merge(metricsRouteRepository) + .merge(observabilityOverviewRouteRepository) + .merge(rumRouteRepository) + .merge(serviceMapRouteRepository) + .merge(serviceNodeRouteRepository) + .merge(serviceRouteRepository) + .merge(suggestionsRouteRepository) + .merge(traceRouteRepository) + .merge(transactionRouteRepository) + .merge(alertsChartPreviewRouteRepository) + .merge(agentConfigurationRouteRepository) + .merge(anomalyDetectionRouteRepository) + .merge(apmIndicesRouteRepository) + .merge(customLinkRouteRepository) + .merge(sourceMapsRouteRepository) + .merge(apmFleetRouteRepository) + .merge(backendsRouteRepository) + .merge(correlationsRouteRepository) + .merge(fallbackToTransactionsRouteRepository) + .merge(historicalDataRouteRepository) + .merge(eventMetadataRouteRepository); + + return repository; +}; + +const getGlobalApmServerRouteRepository = () => { + return getTypedGlobalApmServerRouteRepository() as ServerRouteRepository; +}; + +export type APMServerRouteRepository = ReturnType< + typeof getTypedGlobalApmServerRouteRepository +>; + +// Ensure no APIs return arrays (or, by proxy, the any type), +// to guarantee compatibility with _inspect. + +export type APIEndpoint = EndpointOf; + +type EndpointReturnTypes = { + [Endpoint in APIEndpoint]: ReturnOf; +}; + +type ArrayLikeReturnTypes = PickByValue; + +type ViolatingEndpoints = keyof ArrayLikeReturnTypes; + +function assertType() {} + +// if any endpoint has an array-like return type, the assertion below will fail +assertType(); + +export { getGlobalApmServerRouteRepository }; diff --git a/x-pack/plugins/apm/server/routes/apm_routes/register_apm_server_routes.test.ts b/x-pack/plugins/apm/server/routes/apm_routes/register_apm_server_routes.test.ts new file mode 100644 index 0000000000000..371652cdab957 --- /dev/null +++ b/x-pack/plugins/apm/server/routes/apm_routes/register_apm_server_routes.test.ts @@ -0,0 +1,516 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { jsonRt } from '@kbn/io-ts-utils/json_rt'; +import { createServerRouteRepository } from '@kbn/server-route-repository'; +import { ServerRoute } from '@kbn/server-route-repository'; +import * as t from 'io-ts'; +import { CoreSetup, Logger } from 'src/core/server'; +import { APMConfig } from '../..'; +import { APMRouteCreateOptions, APMRouteHandlerResources } from '../typings'; +import { registerRoutes } from './register_apm_server_routes'; + +type RegisterRouteDependencies = Parameters[0]; + +const getRegisterRouteDependencies = () => { + const get = jest.fn(); + const post = jest.fn(); + const put = jest.fn(); + const createRouter = jest.fn().mockReturnValue({ + get, + post, + put, + }); + + const coreSetup = { + http: { + createRouter, + }, + } as unknown as CoreSetup; + + const logger = { + error: jest.fn(), + } as unknown as Logger; + + return { + mocks: { + get, + post, + put, + createRouter, + coreSetup, + logger, + }, + dependencies: { + core: { + setup: coreSetup, + }, + logger, + config: {} as APMConfig, + plugins: {}, + } as unknown as RegisterRouteDependencies, + }; +}; + +const getRepository = () => + createServerRouteRepository< + APMRouteHandlerResources, + APMRouteCreateOptions + >(); + +const initApi = ( + routes: Array< + ServerRoute< + any, + t.Any, + APMRouteHandlerResources, + any, + APMRouteCreateOptions + > + > +) => { + const { mocks, dependencies } = getRegisterRouteDependencies(); + + let repository = getRepository(); + + routes.forEach((route) => { + repository = repository.add(route); + }); + + registerRoutes({ + ...dependencies, + repository, + }); + + const responseMock = { + ok: jest.fn(), + custom: jest.fn(), + }; + + const simulateRequest = (request: { + method: 'get' | 'post' | 'put'; + pathname: string; + params?: Record; + body?: unknown; + query?: Record; + }) => { + const [, registeredRouteHandler] = + mocks[request.method].mock.calls.find((call) => { + return call[0].path === request.pathname; + }) ?? []; + + const result = registeredRouteHandler( + {}, + { + params: {}, + query: {}, + body: null, + events: { + aborted$: { + toPromise: () => new Promise(() => {}), + }, + }, + ...request, + }, + responseMock + ); + + return result; + }; + + return { + simulateRequest, + mocks: { + ...mocks, + response: responseMock, + }, + }; +}; + +describe('createApi', () => { + it('registers a route with the server', () => { + const { + mocks: { createRouter, get, post, put }, + } = initApi([ + { + endpoint: 'GET /foo', + options: { tags: ['access:apm'] }, + handler: async () => ({}), + }, + { + endpoint: 'POST /bar', + params: t.type({ + body: t.string, + }), + options: { tags: ['access:apm'] }, + handler: async () => ({}), + }, + { + endpoint: 'PUT /baz', + options: { + tags: ['access:apm', 'access:apm_write'], + }, + handler: async () => ({}), + }, + { + endpoint: 'GET /qux', + options: { + tags: ['access:apm', 'access:apm_write'], + }, + handler: async () => ({}), + }, + ]); + + expect(createRouter).toHaveBeenCalledTimes(1); + + expect(get).toHaveBeenCalledTimes(2); + expect(post).toHaveBeenCalledTimes(1); + expect(put).toHaveBeenCalledTimes(1); + + expect(get.mock.calls[0][0]).toEqual({ + options: { + tags: ['access:apm'], + }, + path: '/foo', + validate: expect.anything(), + }); + + expect(get.mock.calls[1][0]).toEqual({ + options: { + tags: ['access:apm', 'access:apm_write'], + }, + path: '/qux', + validate: expect.anything(), + }); + + expect(post.mock.calls[0][0]).toEqual({ + options: { + tags: ['access:apm'], + }, + path: '/bar', + validate: expect.anything(), + }); + + expect(put.mock.calls[0][0]).toEqual({ + options: { + tags: ['access:apm', 'access:apm_write'], + }, + path: '/baz', + validate: expect.anything(), + }); + }); + + describe('when validating', () => { + describe('_inspect', () => { + it('allows _inspect=true', async () => { + const handlerMock = jest.fn().mockResolvedValue({}); + const { + simulateRequest, + mocks: { response }, + } = initApi([ + { + endpoint: 'GET /foo', + options: { + tags: [], + }, + handler: handlerMock, + }, + ]); + + await simulateRequest({ + method: 'get', + pathname: '/foo', + query: { _inspect: 'true' }, + }); + + // responds with ok + expect(response.custom).not.toHaveBeenCalled(); + + const params = handlerMock.mock.calls[0][0].params; + expect(params).toEqual({ query: { _inspect: true } }); + expect(handlerMock).toHaveBeenCalledTimes(1); + expect(response.ok).toHaveBeenCalledWith({ + body: { _inspect: [] }, + }); + }); + + it('rejects _inspect=1', async () => { + const handlerMock = jest.fn().mockResolvedValue({}); + + const { + simulateRequest, + mocks: { response }, + } = initApi([ + { + endpoint: 'GET /foo', + options: { + tags: [], + }, + handler: handlerMock, + }, + ]); + await simulateRequest({ + method: 'get', + pathname: '/foo', + query: { _inspect: 1 }, + }); + + // responds with error handler + expect(response.ok).not.toHaveBeenCalled(); + expect(response.custom).toHaveBeenCalledWith({ + body: { + attributes: { _inspect: [] }, + message: + 'Invalid value 1 supplied to : Partial<{| query: Partial<{| _inspect: pipe(JSON, boolean) |}> |}>/query: Partial<{| _inspect: pipe(JSON, boolean) |}>/_inspect: pipe(JSON, boolean)', + }, + statusCode: 400, + }); + }); + + it('allows omitting _inspect', async () => { + const handlerMock = jest.fn().mockResolvedValue({}); + + const { + simulateRequest, + mocks: { response }, + } = initApi([ + { endpoint: 'GET /foo', options: { tags: [] }, handler: handlerMock }, + ]); + await simulateRequest({ + method: 'get', + pathname: '/foo', + query: {}, + }); + + // responds with ok + expect(response.custom).not.toHaveBeenCalled(); + + const params = handlerMock.mock.calls[0][0].params; + expect(params).toEqual({ query: { _inspect: false } }); + expect(handlerMock).toHaveBeenCalledTimes(1); + + expect(response.ok).toHaveBeenCalledWith({ body: {} }); + }); + }); + + it('throws if unknown parameters are provided', async () => { + const { + simulateRequest, + mocks: { response }, + } = initApi([ + { + endpoint: 'GET /foo', + options: { tags: [] }, + handler: jest.fn().mockResolvedValue({}), + }, + ]); + + await simulateRequest({ + method: 'get', + pathname: '/foo', + query: { _inspect: 'true', extra: '' }, + }); + + expect(response.custom).toHaveBeenCalledTimes(1); + + await simulateRequest({ + method: 'get', + pathname: '/foo', + body: { foo: 'bar' }, + }); + + expect(response.custom).toHaveBeenCalledTimes(2); + + await simulateRequest({ + method: 'get', + pathname: '/foo', + params: { + foo: 'bar', + }, + }); + + expect(response.custom).toHaveBeenCalledTimes(3); + }); + + it('validates path parameters', async () => { + const handlerMock = jest.fn().mockResolvedValue({}); + const { + simulateRequest, + mocks: { response }, + } = initApi([ + { + endpoint: 'GET /foo', + options: { tags: [] }, + params: t.type({ + path: t.type({ + foo: t.string, + }), + }), + handler: handlerMock, + }, + ]); + + await simulateRequest({ + method: 'get', + pathname: '/foo', + params: { + foo: 'bar', + }, + }); + + expect(handlerMock).toHaveBeenCalledTimes(1); + + expect(response.ok).toHaveBeenCalledTimes(1); + expect(response.custom).not.toHaveBeenCalled(); + + const params = handlerMock.mock.calls[0][0].params; + + expect(params).toEqual({ + path: { + foo: 'bar', + }, + query: { + _inspect: false, + }, + }); + + await simulateRequest({ + method: 'get', + pathname: '/foo', + params: { + bar: 'foo', + }, + }); + + expect(response.custom).toHaveBeenCalledTimes(1); + + await simulateRequest({ + method: 'get', + pathname: '/foo', + params: { + foo: 9, + }, + }); + + expect(response.custom).toHaveBeenCalledTimes(2); + + await simulateRequest({ + method: 'get', + pathname: '/foo', + params: { + foo: 'bar', + extra: '', + }, + }); + + expect(response.custom).toHaveBeenCalledTimes(3); + }); + + it('validates body parameters', async () => { + const handlerMock = jest.fn().mockResolvedValue({}); + const { + simulateRequest, + mocks: { response }, + } = initApi([ + { + endpoint: 'GET /foo', + options: { + tags: [], + }, + params: t.type({ + body: t.string, + }), + handler: handlerMock, + }, + ]); + + await simulateRequest({ + method: 'get', + pathname: '/foo', + body: '', + }); + + expect(response.custom).not.toHaveBeenCalled(); + expect(handlerMock).toHaveBeenCalledTimes(1); + expect(response.ok).toHaveBeenCalledTimes(1); + + const params = handlerMock.mock.calls[0][0].params; + + expect(params).toEqual({ + body: '', + query: { + _inspect: false, + }, + }); + + await simulateRequest({ + method: 'get', + pathname: '/foo', + body: null, + }); + + expect(response.custom).toHaveBeenCalledTimes(1); + }); + + it('validates query parameters', async () => { + const handlerMock = jest.fn().mockResolvedValue({}); + const { + simulateRequest, + mocks: { response }, + } = initApi([ + { + endpoint: 'GET /foo', + options: { + tags: [], + }, + params: t.type({ + query: t.type({ + bar: t.string, + filterNames: jsonRt.pipe(t.array(t.string)), + }), + }), + handler: handlerMock, + }, + ]); + + await simulateRequest({ + method: 'get', + pathname: '/foo', + query: { + bar: '', + _inspect: 'true', + filterNames: JSON.stringify(['hostName', 'agentName']), + }, + }); + + expect(response.custom).not.toHaveBeenCalled(); + expect(handlerMock).toHaveBeenCalledTimes(1); + expect(response.ok).toHaveBeenCalledTimes(1); + + const params = handlerMock.mock.calls[0][0].params; + + expect(params).toEqual({ + query: { + bar: '', + _inspect: true, + filterNames: ['hostName', 'agentName'], + }, + }); + + await simulateRequest({ + method: 'get', + pathname: '/foo', + query: { + bar: '', + foo: '', + }, + }); + + expect(response.custom).toHaveBeenCalledTimes(1); + }); + }); +}); diff --git a/x-pack/plugins/apm/server/routes/apm_routes/register_apm_server_routes.ts b/x-pack/plugins/apm/server/routes/apm_routes/register_apm_server_routes.ts new file mode 100644 index 0000000000000..6ac4b3ac18e24 --- /dev/null +++ b/x-pack/plugins/apm/server/routes/apm_routes/register_apm_server_routes.ts @@ -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 Boom from '@hapi/boom'; +import * as t from 'io-ts'; +import { KibanaRequest, RouteRegistrar } from 'src/core/server'; +import { errors } from '@elastic/elasticsearch'; +import agent from 'elastic-apm-node'; +import { ServerRouteRepository } from '@kbn/server-route-repository'; +import { merge } from 'lodash'; +import { + decodeRequestParams, + parseEndpoint, + routeValidationObject, +} from '@kbn/server-route-repository'; +import { mergeRt } from '@kbn/io-ts-utils/merge_rt'; +import { jsonRt } from '@kbn/io-ts-utils/json_rt'; +import { pickKeys } from '../../../common/utils/pick_keys'; +import { APMRouteHandlerResources, TelemetryUsageCounter } from '../typings'; +import type { ApmPluginRequestHandlerContext } from '../typings'; +import { InspectResponse } from '../../../../observability/typings/common'; + +const inspectRt = t.exact( + t.partial({ + query: t.exact(t.partial({ _inspect: jsonRt.pipe(t.boolean) })), + }) +); + +const CLIENT_CLOSED_REQUEST = { + statusCode: 499, + body: { + message: 'Client closed request', + }, +}; + +export const inspectableEsQueriesMap = new WeakMap< + KibanaRequest, + InspectResponse +>(); + +export function registerRoutes({ + core, + repository, + plugins, + logger, + config, + ruleDataClient, + telemetryUsageCounter, +}: { + core: APMRouteHandlerResources['core']; + plugins: APMRouteHandlerResources['plugins']; + logger: APMRouteHandlerResources['logger']; + repository: ServerRouteRepository; + config: APMRouteHandlerResources['config']; + ruleDataClient: APMRouteHandlerResources['ruleDataClient']; + telemetryUsageCounter?: TelemetryUsageCounter; +}) { + const routes = repository.getRoutes(); + + const router = core.setup.http.createRouter(); + + routes.forEach((route) => { + const { params, endpoint, options, handler } = route; + + const { method, pathname } = parseEndpoint(endpoint); + + ( + router[method] as RouteRegistrar< + typeof method, + ApmPluginRequestHandlerContext + > + )( + { + path: pathname, + options, + validate: routeValidationObject, + }, + async (context, request, response) => { + if (agent.isStarted()) { + agent.addLabels({ + plugin: 'apm', + }); + } + + // init debug queries + inspectableEsQueriesMap.set(request, []); + + try { + const runtimeType = params ? mergeRt(params, inspectRt) : inspectRt; + + const validatedParams = decodeRequestParams( + pickKeys(request, 'params', 'body', 'query'), + runtimeType + ); + + const { aborted, data } = await Promise.race([ + handler({ + request, + context, + config, + logger, + core, + plugins, + telemetryUsageCounter, + params: merge( + { + query: { + _inspect: false, + }, + }, + validatedParams + ), + ruleDataClient, + }).then((value) => { + return { + aborted: false, + data: value as Record | undefined | null, + }; + }), + request.events.aborted$.toPromise().then(() => { + return { + aborted: true, + data: undefined, + }; + }), + ]); + + if (aborted) { + return response.custom(CLIENT_CLOSED_REQUEST); + } + + if (Array.isArray(data)) { + throw new Error('Return type cannot be an array'); + } + + const body = validatedParams.query?._inspect + ? { + ...data, + _inspect: inspectableEsQueriesMap.get(request), + } + : { ...data }; + + if (!options.disableTelemetry && telemetryUsageCounter) { + telemetryUsageCounter.incrementCounter({ + counterName: `${method.toUpperCase()} ${pathname}`, + counterType: 'success', + }); + } + + return response.ok({ body }); + } catch (error) { + logger.error(error); + + if (!options.disableTelemetry && telemetryUsageCounter) { + telemetryUsageCounter.incrementCounter({ + counterName: `${method.toUpperCase()} ${pathname}`, + counterType: 'error', + }); + } + const opts = { + statusCode: 500, + body: { + message: error.message, + attributes: { + _inspect: inspectableEsQueriesMap.get(request), + }, + }, + }; + + if (error instanceof errors.RequestAbortedError) { + return response.custom(merge(opts, CLIENT_CLOSED_REQUEST)); + } + + if (Boom.isBoom(error)) { + opts.statusCode = error.output.statusCode; + } + + return response.custom(opts); + } finally { + // cleanup + inspectableEsQueriesMap.delete(request); + } + } + ); + }); +} diff --git a/x-pack/plugins/apm/server/routes/backends.ts b/x-pack/plugins/apm/server/routes/backends.ts deleted file mode 100644 index 03466c7443665..0000000000000 --- a/x-pack/plugins/apm/server/routes/backends.ts +++ /dev/null @@ -1,300 +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 t from 'io-ts'; -import { toNumberRt } from '@kbn/io-ts-utils'; -import { setupRequest } from '../lib/helpers/setup_request'; -import { environmentRt, kueryRt, offsetRt, rangeRt } from './default_api_types'; -import { createApmServerRoute } from './create_apm_server_route'; -import { createApmServerRouteRepository } from './create_apm_server_route_repository'; -import { getMetadataForBackend } from '../lib/backends/get_metadata_for_backend'; -import { getLatencyChartsForBackend } from '../lib/backends/get_latency_charts_for_backend'; -import { getTopBackends } from '../lib/backends/get_top_backends'; -import { getUpstreamServicesForBackend } from '../lib/backends/get_upstream_services_for_backend'; -import { getThroughputChartsForBackend } from '../lib/backends/get_throughput_charts_for_backend'; -import { getErrorRateChartsForBackend } from '../lib/backends/get_error_rate_charts_for_backend'; - -const topBackendsRoute = createApmServerRoute({ - endpoint: 'GET /internal/apm/backends/top_backends', - params: t.intersection([ - t.type({ - query: t.intersection([ - rangeRt, - environmentRt, - kueryRt, - t.type({ numBuckets: toNumberRt }), - ]), - }), - t.partial({ - query: offsetRt, - }), - ]), - options: { - tags: ['access:apm'], - }, - handler: async (resources) => { - const setup = await setupRequest(resources); - const { environment, offset, numBuckets, kuery, start, end } = - resources.params.query; - - const opts = { setup, start, end, numBuckets, environment, kuery }; - - const [currentBackends, previousBackends] = await Promise.all([ - getTopBackends(opts), - offset ? getTopBackends({ ...opts, offset }) : Promise.resolve([]), - ]); - - return { - backends: currentBackends.map((backend) => { - const { stats, ...rest } = backend; - const prev = previousBackends.find( - (item) => item.location.id === backend.location.id - ); - return { - ...rest, - currentStats: stats, - previousStats: prev?.stats ?? null, - }; - }), - }; - }, -}); - -const upstreamServicesForBackendRoute = createApmServerRoute({ - endpoint: 'GET /internal/apm/backends/upstream_services', - params: t.intersection([ - t.type({ - query: t.intersection([ - t.type({ backendName: t.string }), - rangeRt, - t.type({ numBuckets: toNumberRt }), - ]), - }), - t.partial({ - query: t.intersection([environmentRt, offsetRt, kueryRt]), - }), - ]), - options: { - tags: ['access:apm'], - }, - handler: async (resources) => { - const setup = await setupRequest(resources); - const { - query: { - backendName, - environment, - offset, - numBuckets, - kuery, - start, - end, - }, - } = resources.params; - - const opts = { - backendName, - setup, - start, - end, - numBuckets, - environment, - kuery, - }; - - const [currentServices, previousServices] = await Promise.all([ - getUpstreamServicesForBackend(opts), - offset - ? getUpstreamServicesForBackend({ ...opts, offset }) - : Promise.resolve([]), - ]); - - return { - services: currentServices.map((service) => { - const { stats, ...rest } = service; - const prev = previousServices.find( - (item) => item.location.id === service.location.id - ); - return { - ...rest, - currentStats: stats, - previousStats: prev?.stats ?? null, - }; - }), - }; - }, -}); - -const backendMetadataRoute = createApmServerRoute({ - endpoint: 'GET /internal/apm/backends/metadata', - params: t.type({ - query: t.intersection([t.type({ backendName: t.string }), rangeRt]), - }), - options: { - tags: ['access:apm'], - }, - handler: async (resources) => { - const setup = await setupRequest(resources); - const { params } = resources; - - const { backendName, start, end } = params.query; - - const metadata = await getMetadataForBackend({ - backendName, - setup, - start, - end, - }); - - return { metadata }; - }, -}); - -const backendLatencyChartsRoute = createApmServerRoute({ - endpoint: 'GET /internal/apm/backends/charts/latency', - params: t.type({ - query: t.intersection([ - t.type({ backendName: t.string }), - rangeRt, - kueryRt, - environmentRt, - offsetRt, - ]), - }), - options: { - tags: ['access:apm'], - }, - handler: async (resources) => { - const setup = await setupRequest(resources); - const { params } = resources; - const { backendName, kuery, environment, offset, start, end } = - params.query; - - const [currentTimeseries, comparisonTimeseries] = await Promise.all([ - getLatencyChartsForBackend({ - backendName, - setup, - start, - end, - kuery, - environment, - }), - offset - ? getLatencyChartsForBackend({ - backendName, - setup, - start, - end, - kuery, - environment, - offset, - }) - : null, - ]); - - return { currentTimeseries, comparisonTimeseries }; - }, -}); - -const backendThroughputChartsRoute = createApmServerRoute({ - endpoint: 'GET /internal/apm/backends/charts/throughput', - params: t.type({ - query: t.intersection([ - t.type({ backendName: t.string }), - rangeRt, - kueryRt, - environmentRt, - offsetRt, - ]), - }), - options: { - tags: ['access:apm'], - }, - handler: async (resources) => { - const setup = await setupRequest(resources); - const { params } = resources; - const { backendName, kuery, environment, offset, start, end } = - params.query; - - const [currentTimeseries, comparisonTimeseries] = await Promise.all([ - getThroughputChartsForBackend({ - backendName, - setup, - start, - end, - kuery, - environment, - }), - offset - ? getThroughputChartsForBackend({ - backendName, - setup, - start, - end, - kuery, - environment, - offset, - }) - : null, - ]); - - return { currentTimeseries, comparisonTimeseries }; - }, -}); - -const backendFailedTransactionRateChartsRoute = createApmServerRoute({ - endpoint: 'GET /internal/apm/backends/charts/error_rate', - params: t.type({ - query: t.intersection([ - t.type({ backendName: t.string }), - rangeRt, - kueryRt, - environmentRt, - offsetRt, - ]), - }), - options: { - tags: ['access:apm'], - }, - handler: async (resources) => { - const setup = await setupRequest(resources); - const { params } = resources; - const { backendName, kuery, environment, offset, start, end } = - params.query; - - const [currentTimeseries, comparisonTimeseries] = await Promise.all([ - getErrorRateChartsForBackend({ - backendName, - setup, - start, - end, - kuery, - environment, - }), - offset - ? getErrorRateChartsForBackend({ - backendName, - setup, - start, - end, - kuery, - environment, - offset, - }) - : null, - ]); - - return { currentTimeseries, comparisonTimeseries }; - }, -}); - -export const backendsRouteRepository = createApmServerRouteRepository() - .add(topBackendsRoute) - .add(upstreamServicesForBackendRoute) - .add(backendMetadataRoute) - .add(backendLatencyChartsRoute) - .add(backendThroughputChartsRoute) - .add(backendFailedTransactionRateChartsRoute); diff --git a/x-pack/plugins/apm/server/lib/backends/get_error_rate_charts_for_backend.ts b/x-pack/plugins/apm/server/routes/backends/get_error_rate_charts_for_backend.ts similarity index 94% rename from x-pack/plugins/apm/server/lib/backends/get_error_rate_charts_for_backend.ts rename to x-pack/plugins/apm/server/routes/backends/get_error_rate_charts_for_backend.ts index aa20b4b586335..378db134e7cf7 100644 --- a/x-pack/plugins/apm/server/lib/backends/get_error_rate_charts_for_backend.ts +++ b/x-pack/plugins/apm/server/routes/backends/get_error_rate_charts_for_backend.ts @@ -13,8 +13,8 @@ import { import { environmentQuery } from '../../../common/utils/environment_query'; import { kqlQuery, rangeQuery } from '../../../../observability/server'; import { ProcessorEvent } from '../../../common/processor_event'; -import { Setup } from '../helpers/setup_request'; -import { getMetricsDateHistogramParams } from '../helpers/metrics'; +import { Setup } from '../../lib/helpers/setup_request'; +import { getMetricsDateHistogramParams } from '../../lib/helpers/metrics'; import { getOffsetInMs } from '../../../common/utils/get_offset_in_ms'; export async function getErrorRateChartsForBackend({ diff --git a/x-pack/plugins/apm/server/lib/backends/get_latency_charts_for_backend.ts b/x-pack/plugins/apm/server/routes/backends/get_latency_charts_for_backend.ts similarity index 94% rename from x-pack/plugins/apm/server/lib/backends/get_latency_charts_for_backend.ts rename to x-pack/plugins/apm/server/routes/backends/get_latency_charts_for_backend.ts index 9ef238fa13147..8f72d83fcc0b0 100644 --- a/x-pack/plugins/apm/server/lib/backends/get_latency_charts_for_backend.ts +++ b/x-pack/plugins/apm/server/routes/backends/get_latency_charts_for_backend.ts @@ -13,8 +13,8 @@ import { import { environmentQuery } from '../../../common/utils/environment_query'; import { kqlQuery, rangeQuery } from '../../../../observability/server'; import { ProcessorEvent } from '../../../common/processor_event'; -import { Setup } from '../helpers/setup_request'; -import { getMetricsDateHistogramParams } from '../helpers/metrics'; +import { Setup } from '../../lib/helpers/setup_request'; +import { getMetricsDateHistogramParams } from '../../lib/helpers/metrics'; import { getOffsetInMs } from '../../../common/utils/get_offset_in_ms'; export async function getLatencyChartsForBackend({ diff --git a/x-pack/plugins/apm/server/lib/backends/get_metadata_for_backend.ts b/x-pack/plugins/apm/server/routes/backends/get_metadata_for_backend.ts similarity index 96% rename from x-pack/plugins/apm/server/lib/backends/get_metadata_for_backend.ts rename to x-pack/plugins/apm/server/routes/backends/get_metadata_for_backend.ts index 912014602dd13..1f40b975a8f9e 100644 --- a/x-pack/plugins/apm/server/lib/backends/get_metadata_for_backend.ts +++ b/x-pack/plugins/apm/server/routes/backends/get_metadata_for_backend.ts @@ -9,7 +9,7 @@ import { maybe } from '../../../common/utils/maybe'; import { ProcessorEvent } from '../../../common/processor_event'; import { SPAN_DESTINATION_SERVICE_RESOURCE } from '../../../common/elasticsearch_fieldnames'; import { rangeQuery } from '../../../../observability/server'; -import { Setup } from '../helpers/setup_request'; +import { Setup } from '../../lib/helpers/setup_request'; export async function getMetadataForBackend({ setup, diff --git a/x-pack/plugins/apm/server/lib/backends/get_throughput_charts_for_backend.ts b/x-pack/plugins/apm/server/routes/backends/get_throughput_charts_for_backend.ts similarity index 83% rename from x-pack/plugins/apm/server/lib/backends/get_throughput_charts_for_backend.ts rename to x-pack/plugins/apm/server/routes/backends/get_throughput_charts_for_backend.ts index 1fbdd1c680c58..64fdc3eb264f8 100644 --- a/x-pack/plugins/apm/server/lib/backends/get_throughput_charts_for_backend.ts +++ b/x-pack/plugins/apm/server/routes/backends/get_throughput_charts_for_backend.ts @@ -12,10 +12,9 @@ import { import { environmentQuery } from '../../../common/utils/environment_query'; import { kqlQuery, rangeQuery } from '../../../../observability/server'; import { ProcessorEvent } from '../../../common/processor_event'; -import { Setup } from '../helpers/setup_request'; +import { Setup } from '../../lib/helpers/setup_request'; import { getOffsetInMs } from '../../../common/utils/get_offset_in_ms'; -import { getBucketSize } from '../helpers/get_bucket_size'; -import { calculateThroughputWithInterval } from '../helpers/calculate_throughput'; +import { getBucketSize } from '../../lib/helpers/get_bucket_size'; export async function getThroughputChartsForBackend({ backendName, @@ -42,7 +41,7 @@ export async function getThroughputChartsForBackend({ offset, }); - const { intervalString, bucketSize } = getBucketSize({ + const { intervalString } = getBucketSize({ start: startWithOffset, end: endWithOffset, minBucketSize: 60, @@ -73,9 +72,10 @@ export async function getThroughputChartsForBackend({ extended_bounds: { min: startWithOffset, max: endWithOffset }, }, aggs: { - spanDestinationLatencySum: { - sum: { + throughput: { + rate: { field: SPAN_DESTINATION_SERVICE_RESPONSE_TIME_COUNT, + unit: 'minute', }, }, }, @@ -88,10 +88,7 @@ export async function getThroughputChartsForBackend({ response.aggregations?.timeseries.buckets.map((bucket) => { return { x: bucket.key + offsetInMs, - y: calculateThroughputWithInterval({ - bucketSize, - value: bucket.spanDestinationLatencySum.value || 0, - }), + y: bucket.throughput.value, }; }) ?? [] ); diff --git a/x-pack/plugins/apm/server/lib/backends/get_top_backends.ts b/x-pack/plugins/apm/server/routes/backends/get_top_backends.ts similarity index 78% rename from x-pack/plugins/apm/server/lib/backends/get_top_backends.ts rename to x-pack/plugins/apm/server/routes/backends/get_top_backends.ts index 15fb58345e5c0..7251718396660 100644 --- a/x-pack/plugins/apm/server/lib/backends/get_top_backends.ts +++ b/x-pack/plugins/apm/server/routes/backends/get_top_backends.ts @@ -8,9 +8,9 @@ import { kqlQuery } from '../../../../observability/server'; import { NodeType } from '../../../common/connections'; import { environmentQuery } from '../../../common/utils/environment_query'; -import { getConnectionStats } from '../connections/get_connection_stats'; -import { getConnectionStatsItemsWithRelativeImpact } from '../connections/get_connection_stats/get_connection_stats_items_with_relative_impact'; -import { Setup } from '../helpers/setup_request'; +import { getConnectionStats } from '../../lib/connections/get_connection_stats'; +import { getConnectionStatsItemsWithRelativeImpact } from '../../lib/connections/get_connection_stats/get_connection_stats_items_with_relative_impact'; +import { Setup } from '../../lib/helpers/setup_request'; export async function getTopBackends({ setup, diff --git a/x-pack/plugins/apm/server/lib/backends/get_upstream_services_for_backend.ts b/x-pack/plugins/apm/server/routes/backends/get_upstream_services_for_backend.ts similarity index 79% rename from x-pack/plugins/apm/server/lib/backends/get_upstream_services_for_backend.ts rename to x-pack/plugins/apm/server/routes/backends/get_upstream_services_for_backend.ts index adc461f882216..31204c960c87d 100644 --- a/x-pack/plugins/apm/server/lib/backends/get_upstream_services_for_backend.ts +++ b/x-pack/plugins/apm/server/routes/backends/get_upstream_services_for_backend.ts @@ -8,9 +8,9 @@ import { kqlQuery } from '../../../../observability/server'; import { SPAN_DESTINATION_SERVICE_RESOURCE } from '../../../common/elasticsearch_fieldnames'; import { environmentQuery } from '../../../common/utils/environment_query'; -import { getConnectionStats } from '../connections/get_connection_stats'; -import { getConnectionStatsItemsWithRelativeImpact } from '../connections/get_connection_stats/get_connection_stats_items_with_relative_impact'; -import { Setup } from '../helpers/setup_request'; +import { getConnectionStats } from '../../lib/connections/get_connection_stats'; +import { getConnectionStatsItemsWithRelativeImpact } from '../../lib/connections/get_connection_stats/get_connection_stats_items_with_relative_impact'; +import { Setup } from '../../lib/helpers/setup_request'; export async function getUpstreamServicesForBackend({ setup, diff --git a/x-pack/plugins/apm/server/routes/backends/route.ts b/x-pack/plugins/apm/server/routes/backends/route.ts new file mode 100644 index 0000000000000..58160477994bd --- /dev/null +++ b/x-pack/plugins/apm/server/routes/backends/route.ts @@ -0,0 +1,305 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license 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 t from 'io-ts'; +import { toNumberRt } from '@kbn/io-ts-utils/to_number_rt'; +import { setupRequest } from '../../lib/helpers/setup_request'; +import { + environmentRt, + kueryRt, + offsetRt, + rangeRt, +} from '../default_api_types'; +import { createApmServerRoute } from '../apm_routes/create_apm_server_route'; +import { createApmServerRouteRepository } from '../apm_routes/create_apm_server_route_repository'; +import { getMetadataForBackend } from './get_metadata_for_backend'; +import { getLatencyChartsForBackend } from './get_latency_charts_for_backend'; +import { getTopBackends } from './get_top_backends'; +import { getUpstreamServicesForBackend } from './get_upstream_services_for_backend'; +import { getThroughputChartsForBackend } from './get_throughput_charts_for_backend'; +import { getErrorRateChartsForBackend } from './get_error_rate_charts_for_backend'; + +const topBackendsRoute = createApmServerRoute({ + endpoint: 'GET /internal/apm/backends/top_backends', + params: t.intersection([ + t.type({ + query: t.intersection([ + rangeRt, + environmentRt, + kueryRt, + t.type({ numBuckets: toNumberRt }), + ]), + }), + t.partial({ + query: offsetRt, + }), + ]), + options: { + tags: ['access:apm'], + }, + handler: async (resources) => { + const setup = await setupRequest(resources); + const { environment, offset, numBuckets, kuery, start, end } = + resources.params.query; + + const opts = { setup, start, end, numBuckets, environment, kuery }; + + const [currentBackends, previousBackends] = await Promise.all([ + getTopBackends(opts), + offset ? getTopBackends({ ...opts, offset }) : Promise.resolve([]), + ]); + + return { + backends: currentBackends.map((backend) => { + const { stats, ...rest } = backend; + const prev = previousBackends.find( + (item) => item.location.id === backend.location.id + ); + return { + ...rest, + currentStats: stats, + previousStats: prev?.stats ?? null, + }; + }), + }; + }, +}); + +const upstreamServicesForBackendRoute = createApmServerRoute({ + endpoint: 'GET /internal/apm/backends/upstream_services', + params: t.intersection([ + t.type({ + query: t.intersection([ + t.type({ backendName: t.string }), + rangeRt, + t.type({ numBuckets: toNumberRt }), + ]), + }), + t.partial({ + query: t.intersection([environmentRt, offsetRt, kueryRt]), + }), + ]), + options: { + tags: ['access:apm'], + }, + handler: async (resources) => { + const setup = await setupRequest(resources); + const { + query: { + backendName, + environment, + offset, + numBuckets, + kuery, + start, + end, + }, + } = resources.params; + + const opts = { + backendName, + setup, + start, + end, + numBuckets, + environment, + kuery, + }; + + const [currentServices, previousServices] = await Promise.all([ + getUpstreamServicesForBackend(opts), + offset + ? getUpstreamServicesForBackend({ ...opts, offset }) + : Promise.resolve([]), + ]); + + return { + services: currentServices.map((service) => { + const { stats, ...rest } = service; + const prev = previousServices.find( + (item) => item.location.id === service.location.id + ); + return { + ...rest, + currentStats: stats, + previousStats: prev?.stats ?? null, + }; + }), + }; + }, +}); + +const backendMetadataRoute = createApmServerRoute({ + endpoint: 'GET /internal/apm/backends/metadata', + params: t.type({ + query: t.intersection([t.type({ backendName: t.string }), rangeRt]), + }), + options: { + tags: ['access:apm'], + }, + handler: async (resources) => { + const setup = await setupRequest(resources); + const { params } = resources; + + const { backendName, start, end } = params.query; + + const metadata = await getMetadataForBackend({ + backendName, + setup, + start, + end, + }); + + return { metadata }; + }, +}); + +const backendLatencyChartsRoute = createApmServerRoute({ + endpoint: 'GET /internal/apm/backends/charts/latency', + params: t.type({ + query: t.intersection([ + t.type({ backendName: t.string }), + rangeRt, + kueryRt, + environmentRt, + offsetRt, + ]), + }), + options: { + tags: ['access:apm'], + }, + handler: async (resources) => { + const setup = await setupRequest(resources); + const { params } = resources; + const { backendName, kuery, environment, offset, start, end } = + params.query; + + const [currentTimeseries, comparisonTimeseries] = await Promise.all([ + getLatencyChartsForBackend({ + backendName, + setup, + start, + end, + kuery, + environment, + }), + offset + ? getLatencyChartsForBackend({ + backendName, + setup, + start, + end, + kuery, + environment, + offset, + }) + : null, + ]); + + return { currentTimeseries, comparisonTimeseries }; + }, +}); + +const backendThroughputChartsRoute = createApmServerRoute({ + endpoint: 'GET /internal/apm/backends/charts/throughput', + params: t.type({ + query: t.intersection([ + t.type({ backendName: t.string }), + rangeRt, + kueryRt, + environmentRt, + offsetRt, + ]), + }), + options: { + tags: ['access:apm'], + }, + handler: async (resources) => { + const setup = await setupRequest(resources); + const { params } = resources; + const { backendName, kuery, environment, offset, start, end } = + params.query; + + const [currentTimeseries, comparisonTimeseries] = await Promise.all([ + getThroughputChartsForBackend({ + backendName, + setup, + start, + end, + kuery, + environment, + }), + offset + ? getThroughputChartsForBackend({ + backendName, + setup, + start, + end, + kuery, + environment, + offset, + }) + : null, + ]); + + return { currentTimeseries, comparisonTimeseries }; + }, +}); + +const backendFailedTransactionRateChartsRoute = createApmServerRoute({ + endpoint: 'GET /internal/apm/backends/charts/error_rate', + params: t.type({ + query: t.intersection([ + t.type({ backendName: t.string }), + rangeRt, + kueryRt, + environmentRt, + offsetRt, + ]), + }), + options: { + tags: ['access:apm'], + }, + handler: async (resources) => { + const setup = await setupRequest(resources); + const { params } = resources; + const { backendName, kuery, environment, offset, start, end } = + params.query; + + const [currentTimeseries, comparisonTimeseries] = await Promise.all([ + getErrorRateChartsForBackend({ + backendName, + setup, + start, + end, + kuery, + environment, + }), + offset + ? getErrorRateChartsForBackend({ + backendName, + setup, + start, + end, + kuery, + environment, + offset, + }) + : null, + ]); + + return { currentTimeseries, comparisonTimeseries }; + }, +}); + +export const backendsRouteRepository = createApmServerRouteRepository() + .add(topBackendsRoute) + .add(upstreamServicesForBackendRoute) + .add(backendMetadataRoute) + .add(backendLatencyChartsRoute) + .add(backendThroughputChartsRoute) + .add(backendFailedTransactionRateChartsRoute); diff --git a/x-pack/plugins/apm/server/routes/correlations.ts b/x-pack/plugins/apm/server/routes/correlations.ts new file mode 100644 index 0000000000000..f6ca064b4385f --- /dev/null +++ b/x-pack/plugins/apm/server/routes/correlations.ts @@ -0,0 +1,256 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license 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 t from 'io-ts'; +import Boom from '@hapi/boom'; + +import { i18n } from '@kbn/i18n'; +import { toNumberRt } from '@kbn/io-ts-utils/to_number_rt'; + +import { isActivePlatinumLicense } from '../../common/license_check'; + +import { setupRequest } from '../lib/helpers/setup_request'; +import { + fetchPValues, + fetchSignificantCorrelations, + fetchTransactionDurationFieldCandidates, + fetchTransactionDurationFieldValuePairs, +} from '../lib/correlations/queries'; +import { fetchFieldsStats } from '../lib/correlations/queries/field_stats/get_fields_stats'; + +import { withApmSpan } from '../utils/with_apm_span'; + +import { createApmServerRoute } from './apm_routes/create_apm_server_route'; +import { createApmServerRouteRepository } from './apm_routes/create_apm_server_route_repository'; +import { environmentRt, kueryRt, rangeRt } from './default_api_types'; + +const INVALID_LICENSE = i18n.translate('xpack.apm.correlations.license.text', { + defaultMessage: + 'To use the correlations API, you must be subscribed to an Elastic Platinum license.', +}); + +const fieldCandidatesRoute = createApmServerRoute({ + endpoint: 'GET /internal/apm/correlations/field_candidates', + 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 (resources) => { + const { context } = resources; + if (!isActivePlatinumLicense(context.licensing.license)) { + throw Boom.forbidden(INVALID_LICENSE); + } + + const { indices } = await setupRequest(resources); + const esClient = resources.context.core.elasticsearch.client.asCurrentUser; + + return withApmSpan( + 'get_correlations_field_candidates', + async () => + await fetchTransactionDurationFieldCandidates(esClient, { + ...resources.params.query, + index: indices.transaction, + }) + ); + }, +}); + +const fieldStatsRoute = createApmServerRoute({ + endpoint: 'POST /internal/apm/correlations/field_stats', + params: t.type({ + body: t.intersection([ + t.partial({ + serviceName: t.string, + transactionName: t.string, + transactionType: t.string, + }), + environmentRt, + kueryRt, + rangeRt, + t.type({ + fieldsToSample: t.array(t.string), + }), + ]), + }), + options: { tags: ['access:apm'] }, + handler: async (resources) => { + const { context } = resources; + if (!isActivePlatinumLicense(context.licensing.license)) { + throw Boom.forbidden(INVALID_LICENSE); + } + + const { indices } = await setupRequest(resources); + const esClient = resources.context.core.elasticsearch.client.asCurrentUser; + + const { fieldsToSample, ...params } = resources.params.body; + + return withApmSpan( + 'get_correlations_field_stats', + async () => + await fetchFieldsStats( + esClient, + { + ...params, + index: indices.transaction, + }, + fieldsToSample + ) + ); + }, +}); + +const fieldValuePairsRoute = createApmServerRoute({ + endpoint: 'POST /internal/apm/correlations/field_value_pairs', + params: t.type({ + body: t.intersection([ + t.partial({ + serviceName: t.string, + transactionName: t.string, + transactionType: t.string, + }), + environmentRt, + kueryRt, + rangeRt, + t.type({ + fieldCandidates: t.array(t.string), + }), + ]), + }), + options: { tags: ['access:apm'] }, + handler: async (resources) => { + const { context } = resources; + if (!isActivePlatinumLicense(context.licensing.license)) { + throw Boom.forbidden(INVALID_LICENSE); + } + + const { indices } = await setupRequest(resources); + const esClient = resources.context.core.elasticsearch.client.asCurrentUser; + + const { fieldCandidates, ...params } = resources.params.body; + + return withApmSpan( + 'get_correlations_field_value_pairs', + async () => + await fetchTransactionDurationFieldValuePairs( + esClient, + { + ...params, + index: indices.transaction, + }, + fieldCandidates + ) + ); + }, +}); + +const significantCorrelationsRoute = createApmServerRoute({ + endpoint: 'POST /internal/apm/correlations/significant_correlations', + params: t.type({ + body: t.intersection([ + t.partial({ + serviceName: t.string, + transactionName: t.string, + transactionType: t.string, + }), + environmentRt, + kueryRt, + rangeRt, + t.type({ + fieldValuePairs: t.array( + t.type({ + fieldName: t.string, + fieldValue: t.union([t.string, toNumberRt]), + }) + ), + }), + ]), + }), + options: { tags: ['access:apm'] }, + handler: async (resources) => { + const { context } = resources; + if (!isActivePlatinumLicense(context.licensing.license)) { + throw Boom.forbidden(INVALID_LICENSE); + } + + const { indices } = await setupRequest(resources); + const esClient = resources.context.core.elasticsearch.client.asCurrentUser; + + const { fieldValuePairs, ...params } = resources.params.body; + + const paramsWithIndex = { + ...params, + index: indices.transaction, + }; + + return withApmSpan( + 'get_significant_correlations', + async () => + await fetchSignificantCorrelations( + esClient, + paramsWithIndex, + fieldValuePairs + ) + ); + }, +}); + +const pValuesRoute = createApmServerRoute({ + endpoint: 'POST /internal/apm/correlations/p_values', + params: t.type({ + body: t.intersection([ + t.partial({ + serviceName: t.string, + transactionName: t.string, + transactionType: t.string, + }), + environmentRt, + kueryRt, + rangeRt, + t.type({ + fieldCandidates: t.array(t.string), + }), + ]), + }), + options: { tags: ['access:apm'] }, + handler: async (resources) => { + const { context } = resources; + if (!isActivePlatinumLicense(context.licensing.license)) { + throw Boom.forbidden(INVALID_LICENSE); + } + + const { indices } = await setupRequest(resources); + const esClient = resources.context.core.elasticsearch.client.asCurrentUser; + + const { fieldCandidates, ...params } = resources.params.body; + + const paramsWithIndex = { + ...params, + index: indices.transaction, + }; + + return withApmSpan( + 'get_p_values', + async () => await fetchPValues(esClient, paramsWithIndex, fieldCandidates) + ); + }, +}); + +export const correlationsRouteRepository = createApmServerRouteRepository() + .add(pValuesRoute) + .add(fieldCandidatesRoute) + .add(fieldStatsRoute) + .add(fieldValuePairsRoute) + .add(significantCorrelationsRoute); diff --git a/x-pack/plugins/apm/server/routes/data_view.ts b/x-pack/plugins/apm/server/routes/data_view.ts index 5b06b51078ec7..3590ef9db9bd0 100644 --- a/x-pack/plugins/apm/server/routes/data_view.ts +++ b/x-pack/plugins/apm/server/routes/data_view.ts @@ -6,10 +6,10 @@ */ import { createStaticDataView } from '../lib/data_view/create_static_data_view'; -import { createApmServerRouteRepository } from './create_apm_server_route_repository'; +import { createApmServerRouteRepository } from './apm_routes/create_apm_server_route_repository'; import { setupRequest } from '../lib/helpers/setup_request'; import { getDynamicDataView } from '../lib/data_view/get_dynamic_data_view'; -import { createApmServerRoute } from './create_apm_server_route'; +import { createApmServerRoute } from './apm_routes/create_apm_server_route'; const staticDataViewRoute = createApmServerRoute({ endpoint: 'POST /internal/apm/data_view/static', diff --git a/x-pack/plugins/apm/server/routes/default_api_types.ts b/x-pack/plugins/apm/server/routes/default_api_types.ts index 5622b12e1b099..b31de8e53dad2 100644 --- a/x-pack/plugins/apm/server/routes/default_api_types.ts +++ b/x-pack/plugins/apm/server/routes/default_api_types.ts @@ -6,7 +6,7 @@ */ import * as t from 'io-ts'; -import { isoToEpochRt } from '@kbn/io-ts-utils'; +import { isoToEpochRt } from '@kbn/io-ts-utils/iso_to_epoch_rt'; export { environmentRt } from '../../common/environment_rt'; diff --git a/x-pack/plugins/apm/server/routes/environments.ts b/x-pack/plugins/apm/server/routes/environments.ts index e54ad79f177c4..38328a63a411e 100644 --- a/x-pack/plugins/apm/server/routes/environments.ts +++ b/x-pack/plugins/apm/server/routes/environments.ts @@ -11,8 +11,8 @@ import { getSearchAggregatedTransactions } from '../lib/helpers/transactions'; import { setupRequest } from '../lib/helpers/setup_request'; import { getEnvironments } from '../lib/environments/get_environments'; import { rangeRt } from './default_api_types'; -import { createApmServerRoute } from './create_apm_server_route'; -import { createApmServerRouteRepository } from './create_apm_server_route_repository'; +import { createApmServerRoute } from './apm_routes/create_apm_server_route'; +import { createApmServerRouteRepository } from './apm_routes/create_apm_server_route_repository'; const environmentsRoute = createApmServerRoute({ endpoint: 'GET /internal/apm/environments', diff --git a/x-pack/plugins/apm/server/routes/errors.ts b/x-pack/plugins/apm/server/routes/errors.ts index 3a6e07acd14bc..02df03f108083 100644 --- a/x-pack/plugins/apm/server/routes/errors.ts +++ b/x-pack/plugins/apm/server/routes/errors.ts @@ -6,7 +6,7 @@ */ import * as t from 'io-ts'; -import { createApmServerRoute } from './create_apm_server_route'; +import { createApmServerRoute } from './apm_routes/create_apm_server_route'; import { getErrorDistribution } from '../lib/errors/distribution/get_distribution'; import { getErrorGroupSample } from '../lib/errors/get_error_group_sample'; import { getErrorGroups } from '../lib/errors/get_error_groups'; @@ -17,7 +17,7 @@ import { rangeRt, comparisonRangeRt, } from './default_api_types'; -import { createApmServerRouteRepository } from './create_apm_server_route_repository'; +import { createApmServerRouteRepository } from './apm_routes/create_apm_server_route_repository'; const errorsRoute = createApmServerRoute({ endpoint: 'GET /internal/apm/services/{serviceName}/errors', diff --git a/x-pack/plugins/apm/server/routes/event_metadata.ts b/x-pack/plugins/apm/server/routes/event_metadata.ts index 00241d2ef1c68..3a40e445007ee 100644 --- a/x-pack/plugins/apm/server/routes/event_metadata.ts +++ b/x-pack/plugins/apm/server/routes/event_metadata.ts @@ -6,8 +6,8 @@ */ import * as t from 'io-ts'; -import { createApmServerRouteRepository } from './create_apm_server_route_repository'; -import { createApmServerRoute } from './create_apm_server_route'; +import { createApmServerRouteRepository } from './apm_routes/create_apm_server_route_repository'; +import { createApmServerRoute } from './apm_routes/create_apm_server_route'; import { getEventMetadata } from '../lib/event_metadata/get_event_metadata'; import { processorEventRt } from '../../common/processor_event'; import { setupRequest } from '../lib/helpers/setup_request'; diff --git a/x-pack/plugins/apm/server/routes/fallback_to_transactions.ts b/x-pack/plugins/apm/server/routes/fallback_to_transactions.ts index 99c6a290e34b1..53e3ebae0d4ff 100644 --- a/x-pack/plugins/apm/server/routes/fallback_to_transactions.ts +++ b/x-pack/plugins/apm/server/routes/fallback_to_transactions.ts @@ -8,8 +8,8 @@ import * as t from 'io-ts'; import { getIsUsingTransactionEvents } from '../lib/helpers/transactions/get_is_using_transaction_events'; import { setupRequest } from '../lib/helpers/setup_request'; -import { createApmServerRoute } from './create_apm_server_route'; -import { createApmServerRouteRepository } from './create_apm_server_route_repository'; +import { createApmServerRoute } from './apm_routes/create_apm_server_route'; +import { createApmServerRouteRepository } from './apm_routes/create_apm_server_route_repository'; import { kueryRt, rangeRt } from './default_api_types'; const fallbackToTransactionsRoute = createApmServerRoute({ diff --git a/x-pack/plugins/apm/server/routes/fleet.ts b/x-pack/plugins/apm/server/routes/fleet.ts index e18aefcd6e0d8..a6e0cb09d894a 100644 --- a/x-pack/plugins/apm/server/routes/fleet.ts +++ b/x-pack/plugins/apm/server/routes/fleet.ts @@ -24,8 +24,8 @@ import { getUnsupportedApmServerSchema } from '../lib/fleet/get_unsupported_apm_ import { isSuperuser } from '../lib/fleet/is_superuser'; import { getInternalSavedObjectsClient } from '../lib/helpers/get_internal_saved_objects_client'; import { setupRequest } from '../lib/helpers/setup_request'; -import { createApmServerRoute } from './create_apm_server_route'; -import { createApmServerRouteRepository } from './create_apm_server_route_repository'; +import { createApmServerRoute } from './apm_routes/create_apm_server_route'; +import { createApmServerRouteRepository } from './apm_routes/create_apm_server_route_repository'; const hasFleetDataRoute = createApmServerRoute({ endpoint: 'GET /internal/apm/fleet/has_data', diff --git a/x-pack/plugins/apm/server/routes/get_global_apm_server_route_repository.ts b/x-pack/plugins/apm/server/routes/get_global_apm_server_route_repository.ts deleted file mode 100644 index b4b370589e4bc..0000000000000 --- a/x-pack/plugins/apm/server/routes/get_global_apm_server_route_repository.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 type { - ServerRouteRepository, - ReturnOf, - EndpointOf, -} from '@kbn/server-route-repository'; -import { PickByValue } from 'utility-types'; -import { alertsChartPreviewRouteRepository } from './alerts/chart_preview'; -import { backendsRouteRepository } from './backends'; -import { createApmServerRouteRepository } from './create_apm_server_route_repository'; -import { environmentsRouteRepository } from './environments'; -import { errorsRouteRepository } from './errors'; -import { apmFleetRouteRepository } from './fleet'; -import { dataViewRouteRepository } from './data_view'; -import { latencyDistributionRouteRepository } from './latency_distribution'; -import { metricsRouteRepository } from './metrics'; -import { observabilityOverviewRouteRepository } from './observability_overview'; -import { rumRouteRepository } from './rum_client'; -import { fallbackToTransactionsRouteRepository } from './fallback_to_transactions'; -import { serviceRouteRepository } from './services'; -import { serviceMapRouteRepository } from './service_map'; -import { serviceNodeRouteRepository } from './service_nodes'; -import { agentConfigurationRouteRepository } from './settings/agent_configuration'; -import { anomalyDetectionRouteRepository } from './settings/anomaly_detection'; -import { apmIndicesRouteRepository } from './settings/apm_indices'; -import { customLinkRouteRepository } from './settings/custom_link'; -import { sourceMapsRouteRepository } from './source_maps'; -import { traceRouteRepository } from './traces'; -import { transactionRouteRepository } from './transactions'; -import { APMRouteHandlerResources } from './typings'; -import { historicalDataRouteRepository } from './historical_data'; -import { eventMetadataRouteRepository } from './event_metadata'; -import { suggestionsRouteRepository } from './suggestions'; - -const getTypedGlobalApmServerRouteRepository = () => { - const repository = createApmServerRouteRepository() - .merge(dataViewRouteRepository) - .merge(environmentsRouteRepository) - .merge(errorsRouteRepository) - .merge(latencyDistributionRouteRepository) - .merge(metricsRouteRepository) - .merge(observabilityOverviewRouteRepository) - .merge(rumRouteRepository) - .merge(serviceMapRouteRepository) - .merge(serviceNodeRouteRepository) - .merge(serviceRouteRepository) - .merge(suggestionsRouteRepository) - .merge(traceRouteRepository) - .merge(transactionRouteRepository) - .merge(alertsChartPreviewRouteRepository) - .merge(agentConfigurationRouteRepository) - .merge(anomalyDetectionRouteRepository) - .merge(apmIndicesRouteRepository) - .merge(customLinkRouteRepository) - .merge(sourceMapsRouteRepository) - .merge(apmFleetRouteRepository) - .merge(backendsRouteRepository) - .merge(fallbackToTransactionsRouteRepository) - .merge(historicalDataRouteRepository) - .merge(eventMetadataRouteRepository); - - return repository; -}; - -const getGlobalApmServerRouteRepository = () => { - return getTypedGlobalApmServerRouteRepository() as ServerRouteRepository; -}; - -export type APMServerRouteRepository = ReturnType< - typeof getTypedGlobalApmServerRouteRepository ->; - -// Ensure no APIs return arrays (or, by proxy, the any type), -// to guarantee compatibility with _inspect. - -export type APIEndpoint = EndpointOf; - -type EndpointReturnTypes = { - [Endpoint in APIEndpoint]: ReturnOf; -}; - -type ArrayLikeReturnTypes = PickByValue; - -type ViolatingEndpoints = keyof ArrayLikeReturnTypes; - -function assertType() {} - -// if any endpoint has an array-like return type, the assertion below will fail -assertType(); - -export { getGlobalApmServerRouteRepository }; diff --git a/x-pack/plugins/apm/server/routes/historical_data/index.ts b/x-pack/plugins/apm/server/routes/historical_data/index.ts index fb67dc4f5b649..f488669fffa11 100644 --- a/x-pack/plugins/apm/server/routes/historical_data/index.ts +++ b/x-pack/plugins/apm/server/routes/historical_data/index.ts @@ -6,8 +6,8 @@ */ import { setupRequest } from '../../lib/helpers/setup_request'; -import { createApmServerRoute } from '../create_apm_server_route'; -import { createApmServerRouteRepository } from '../create_apm_server_route_repository'; +import { createApmServerRoute } from '../apm_routes/create_apm_server_route'; +import { createApmServerRouteRepository } from '../apm_routes/create_apm_server_route_repository'; import { hasHistoricalAgentData } from './has_historical_agent_data'; const hasDataRoute = createApmServerRoute({ diff --git a/x-pack/plugins/apm/server/routes/latency_distribution.ts b/x-pack/plugins/apm/server/routes/latency_distribution.ts index 128192d0464c7..826898784835e 100644 --- a/x-pack/plugins/apm/server/routes/latency_distribution.ts +++ b/x-pack/plugins/apm/server/routes/latency_distribution.ts @@ -6,11 +6,11 @@ */ import * as t from 'io-ts'; -import { toNumberRt } from '@kbn/io-ts-utils'; +import { toNumberRt } from '@kbn/io-ts-utils/to_number_rt'; import { getOverallLatencyDistribution } from '../lib/latency/get_overall_latency_distribution'; import { setupRequest } from '../lib/helpers/setup_request'; -import { createApmServerRoute } from './create_apm_server_route'; -import { createApmServerRouteRepository } from './create_apm_server_route_repository'; +import { createApmServerRoute } from './apm_routes/create_apm_server_route'; +import { createApmServerRouteRepository } from './apm_routes/create_apm_server_route_repository'; import { environmentRt, kueryRt, rangeRt } from './default_api_types'; const latencyOverallDistributionRoute = createApmServerRoute({ diff --git a/x-pack/plugins/apm/server/routes/metrics.ts b/x-pack/plugins/apm/server/routes/metrics.ts index 8b6b16a26f1d8..1817c3e1546bd 100644 --- a/x-pack/plugins/apm/server/routes/metrics.ts +++ b/x-pack/plugins/apm/server/routes/metrics.ts @@ -8,8 +8,8 @@ import * as t from 'io-ts'; import { setupRequest } from '../lib/helpers/setup_request'; import { getMetricsChartDataByAgent } from '../lib/metrics/get_metrics_chart_data_by_agent'; -import { createApmServerRoute } from './create_apm_server_route'; -import { createApmServerRouteRepository } from './create_apm_server_route_repository'; +import { createApmServerRoute } from './apm_routes/create_apm_server_route'; +import { createApmServerRouteRepository } from './apm_routes/create_apm_server_route_repository'; import { environmentRt, kueryRt, rangeRt } from './default_api_types'; const metricsChartsRoute = createApmServerRoute({ diff --git a/x-pack/plugins/apm/server/routes/observability_overview.ts b/x-pack/plugins/apm/server/routes/observability_overview.ts index 2aff798f9ad0b..2df3212d8da70 100644 --- a/x-pack/plugins/apm/server/routes/observability_overview.ts +++ b/x-pack/plugins/apm/server/routes/observability_overview.ts @@ -6,7 +6,7 @@ */ import * as t from 'io-ts'; -import { toNumberRt } from '@kbn/io-ts-utils'; +import { toNumberRt } from '@kbn/io-ts-utils/to_number_rt'; import { setupRequest } from '../lib/helpers/setup_request'; import { getServiceCount } from '../lib/observability_overview/get_service_count'; import { getTransactionsPerMinute } from '../lib/observability_overview/get_transactions_per_minute'; @@ -14,8 +14,8 @@ import { getHasData } from '../lib/observability_overview/has_data'; import { rangeRt } from './default_api_types'; import { getSearchAggregatedTransactions } from '../lib/helpers/transactions'; import { withApmSpan } from '../utils/with_apm_span'; -import { createApmServerRouteRepository } from './create_apm_server_route_repository'; -import { createApmServerRoute } from './create_apm_server_route'; +import { createApmServerRouteRepository } from './apm_routes/create_apm_server_route_repository'; +import { createApmServerRoute } from './apm_routes/create_apm_server_route'; const observabilityOverviewHasDataRoute = createApmServerRoute({ endpoint: 'GET /internal/apm/observability_overview/has_data', diff --git a/x-pack/plugins/apm/server/routes/register_routes/index.test.ts b/x-pack/plugins/apm/server/routes/register_routes/index.test.ts deleted file mode 100644 index 6cee6d8cad920..0000000000000 --- a/x-pack/plugins/apm/server/routes/register_routes/index.test.ts +++ /dev/null @@ -1,516 +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 { jsonRt } from '@kbn/io-ts-utils'; -import { createServerRouteRepository } from '@kbn/server-route-repository'; -import { ServerRoute } from '@kbn/server-route-repository'; -import * as t from 'io-ts'; -import { CoreSetup, Logger } from 'src/core/server'; -import { APMConfig } from '../..'; -import { APMRouteCreateOptions, APMRouteHandlerResources } from '../typings'; -import { registerRoutes } from './index'; - -type RegisterRouteDependencies = Parameters[0]; - -const getRegisterRouteDependencies = () => { - const get = jest.fn(); - const post = jest.fn(); - const put = jest.fn(); - const createRouter = jest.fn().mockReturnValue({ - get, - post, - put, - }); - - const coreSetup = { - http: { - createRouter, - }, - } as unknown as CoreSetup; - - const logger = { - error: jest.fn(), - } as unknown as Logger; - - return { - mocks: { - get, - post, - put, - createRouter, - coreSetup, - logger, - }, - dependencies: { - core: { - setup: coreSetup, - }, - logger, - config: {} as APMConfig, - plugins: {}, - } as unknown as RegisterRouteDependencies, - }; -}; - -const getRepository = () => - createServerRouteRepository< - APMRouteHandlerResources, - APMRouteCreateOptions - >(); - -const initApi = ( - routes: Array< - ServerRoute< - any, - t.Any, - APMRouteHandlerResources, - any, - APMRouteCreateOptions - > - > -) => { - const { mocks, dependencies } = getRegisterRouteDependencies(); - - let repository = getRepository(); - - routes.forEach((route) => { - repository = repository.add(route); - }); - - registerRoutes({ - ...dependencies, - repository, - }); - - const responseMock = { - ok: jest.fn(), - custom: jest.fn(), - }; - - const simulateRequest = (request: { - method: 'get' | 'post' | 'put'; - pathname: string; - params?: Record; - body?: unknown; - query?: Record; - }) => { - const [, registeredRouteHandler] = - mocks[request.method].mock.calls.find((call) => { - return call[0].path === request.pathname; - }) ?? []; - - const result = registeredRouteHandler( - {}, - { - params: {}, - query: {}, - body: null, - events: { - aborted$: { - toPromise: () => new Promise(() => {}), - }, - }, - ...request, - }, - responseMock - ); - - return result; - }; - - return { - simulateRequest, - mocks: { - ...mocks, - response: responseMock, - }, - }; -}; - -describe('createApi', () => { - it('registers a route with the server', () => { - const { - mocks: { createRouter, get, post, put }, - } = initApi([ - { - endpoint: 'GET /foo', - options: { tags: ['access:apm'] }, - handler: async () => ({}), - }, - { - endpoint: 'POST /bar', - params: t.type({ - body: t.string, - }), - options: { tags: ['access:apm'] }, - handler: async () => ({}), - }, - { - endpoint: 'PUT /baz', - options: { - tags: ['access:apm', 'access:apm_write'], - }, - handler: async () => ({}), - }, - { - endpoint: 'GET /qux', - options: { - tags: ['access:apm', 'access:apm_write'], - }, - handler: async () => ({}), - }, - ]); - - expect(createRouter).toHaveBeenCalledTimes(1); - - expect(get).toHaveBeenCalledTimes(2); - expect(post).toHaveBeenCalledTimes(1); - expect(put).toHaveBeenCalledTimes(1); - - expect(get.mock.calls[0][0]).toEqual({ - options: { - tags: ['access:apm'], - }, - path: '/foo', - validate: expect.anything(), - }); - - expect(get.mock.calls[1][0]).toEqual({ - options: { - tags: ['access:apm', 'access:apm_write'], - }, - path: '/qux', - validate: expect.anything(), - }); - - expect(post.mock.calls[0][0]).toEqual({ - options: { - tags: ['access:apm'], - }, - path: '/bar', - validate: expect.anything(), - }); - - expect(put.mock.calls[0][0]).toEqual({ - options: { - tags: ['access:apm', 'access:apm_write'], - }, - path: '/baz', - validate: expect.anything(), - }); - }); - - describe('when validating', () => { - describe('_inspect', () => { - it('allows _inspect=true', async () => { - const handlerMock = jest.fn().mockResolvedValue({}); - const { - simulateRequest, - mocks: { response }, - } = initApi([ - { - endpoint: 'GET /foo', - options: { - tags: [], - }, - handler: handlerMock, - }, - ]); - - await simulateRequest({ - method: 'get', - pathname: '/foo', - query: { _inspect: 'true' }, - }); - - // responds with ok - expect(response.custom).not.toHaveBeenCalled(); - - const params = handlerMock.mock.calls[0][0].params; - expect(params).toEqual({ query: { _inspect: true } }); - expect(handlerMock).toHaveBeenCalledTimes(1); - expect(response.ok).toHaveBeenCalledWith({ - body: { _inspect: [] }, - }); - }); - - it('rejects _inspect=1', async () => { - const handlerMock = jest.fn().mockResolvedValue({}); - - const { - simulateRequest, - mocks: { response }, - } = initApi([ - { - endpoint: 'GET /foo', - options: { - tags: [], - }, - handler: handlerMock, - }, - ]); - await simulateRequest({ - method: 'get', - pathname: '/foo', - query: { _inspect: 1 }, - }); - - // responds with error handler - expect(response.ok).not.toHaveBeenCalled(); - expect(response.custom).toHaveBeenCalledWith({ - body: { - attributes: { _inspect: [] }, - message: - 'Invalid value 1 supplied to : Partial<{| query: Partial<{| _inspect: pipe(JSON, boolean) |}> |}>/query: Partial<{| _inspect: pipe(JSON, boolean) |}>/_inspect: pipe(JSON, boolean)', - }, - statusCode: 400, - }); - }); - - it('allows omitting _inspect', async () => { - const handlerMock = jest.fn().mockResolvedValue({}); - - const { - simulateRequest, - mocks: { response }, - } = initApi([ - { endpoint: 'GET /foo', options: { tags: [] }, handler: handlerMock }, - ]); - await simulateRequest({ - method: 'get', - pathname: '/foo', - query: {}, - }); - - // responds with ok - expect(response.custom).not.toHaveBeenCalled(); - - const params = handlerMock.mock.calls[0][0].params; - expect(params).toEqual({ query: { _inspect: false } }); - expect(handlerMock).toHaveBeenCalledTimes(1); - - expect(response.ok).toHaveBeenCalledWith({ body: {} }); - }); - }); - - it('throws if unknown parameters are provided', async () => { - const { - simulateRequest, - mocks: { response }, - } = initApi([ - { - endpoint: 'GET /foo', - options: { tags: [] }, - handler: jest.fn().mockResolvedValue({}), - }, - ]); - - await simulateRequest({ - method: 'get', - pathname: '/foo', - query: { _inspect: 'true', extra: '' }, - }); - - expect(response.custom).toHaveBeenCalledTimes(1); - - await simulateRequest({ - method: 'get', - pathname: '/foo', - body: { foo: 'bar' }, - }); - - expect(response.custom).toHaveBeenCalledTimes(2); - - await simulateRequest({ - method: 'get', - pathname: '/foo', - params: { - foo: 'bar', - }, - }); - - expect(response.custom).toHaveBeenCalledTimes(3); - }); - - it('validates path parameters', async () => { - const handlerMock = jest.fn().mockResolvedValue({}); - const { - simulateRequest, - mocks: { response }, - } = initApi([ - { - endpoint: 'GET /foo', - options: { tags: [] }, - params: t.type({ - path: t.type({ - foo: t.string, - }), - }), - handler: handlerMock, - }, - ]); - - await simulateRequest({ - method: 'get', - pathname: '/foo', - params: { - foo: 'bar', - }, - }); - - expect(handlerMock).toHaveBeenCalledTimes(1); - - expect(response.ok).toHaveBeenCalledTimes(1); - expect(response.custom).not.toHaveBeenCalled(); - - const params = handlerMock.mock.calls[0][0].params; - - expect(params).toEqual({ - path: { - foo: 'bar', - }, - query: { - _inspect: false, - }, - }); - - await simulateRequest({ - method: 'get', - pathname: '/foo', - params: { - bar: 'foo', - }, - }); - - expect(response.custom).toHaveBeenCalledTimes(1); - - await simulateRequest({ - method: 'get', - pathname: '/foo', - params: { - foo: 9, - }, - }); - - expect(response.custom).toHaveBeenCalledTimes(2); - - await simulateRequest({ - method: 'get', - pathname: '/foo', - params: { - foo: 'bar', - extra: '', - }, - }); - - expect(response.custom).toHaveBeenCalledTimes(3); - }); - - it('validates body parameters', async () => { - const handlerMock = jest.fn().mockResolvedValue({}); - const { - simulateRequest, - mocks: { response }, - } = initApi([ - { - endpoint: 'GET /foo', - options: { - tags: [], - }, - params: t.type({ - body: t.string, - }), - handler: handlerMock, - }, - ]); - - await simulateRequest({ - method: 'get', - pathname: '/foo', - body: '', - }); - - expect(response.custom).not.toHaveBeenCalled(); - expect(handlerMock).toHaveBeenCalledTimes(1); - expect(response.ok).toHaveBeenCalledTimes(1); - - const params = handlerMock.mock.calls[0][0].params; - - expect(params).toEqual({ - body: '', - query: { - _inspect: false, - }, - }); - - await simulateRequest({ - method: 'get', - pathname: '/foo', - body: null, - }); - - expect(response.custom).toHaveBeenCalledTimes(1); - }); - - it('validates query parameters', async () => { - const handlerMock = jest.fn().mockResolvedValue({}); - const { - simulateRequest, - mocks: { response }, - } = initApi([ - { - endpoint: 'GET /foo', - options: { - tags: [], - }, - params: t.type({ - query: t.type({ - bar: t.string, - filterNames: jsonRt.pipe(t.array(t.string)), - }), - }), - handler: handlerMock, - }, - ]); - - await simulateRequest({ - method: 'get', - pathname: '/foo', - query: { - bar: '', - _inspect: 'true', - filterNames: JSON.stringify(['hostName', 'agentName']), - }, - }); - - expect(response.custom).not.toHaveBeenCalled(); - expect(handlerMock).toHaveBeenCalledTimes(1); - expect(response.ok).toHaveBeenCalledTimes(1); - - const params = handlerMock.mock.calls[0][0].params; - - expect(params).toEqual({ - query: { - bar: '', - _inspect: true, - filterNames: ['hostName', 'agentName'], - }, - }); - - await simulateRequest({ - method: 'get', - pathname: '/foo', - query: { - bar: '', - foo: '', - }, - }); - - expect(response.custom).toHaveBeenCalledTimes(1); - }); - }); -}); diff --git a/x-pack/plugins/apm/server/routes/register_routes/index.ts b/x-pack/plugins/apm/server/routes/register_routes/index.ts deleted file mode 100644 index 576c23dc0882f..0000000000000 --- a/x-pack/plugins/apm/server/routes/register_routes/index.ts +++ /dev/null @@ -1,189 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import Boom from '@hapi/boom'; -import * as t from 'io-ts'; -import { KibanaRequest, RouteRegistrar } from 'src/core/server'; -import { errors } from '@elastic/elasticsearch'; -import agent from 'elastic-apm-node'; -import { ServerRouteRepository } from '@kbn/server-route-repository'; -import { merge } from 'lodash'; -import { - decodeRequestParams, - parseEndpoint, - routeValidationObject, -} from '@kbn/server-route-repository'; -import { mergeRt, jsonRt } from '@kbn/io-ts-utils'; -import { pickKeys } from '../../../common/utils/pick_keys'; -import { APMRouteHandlerResources, TelemetryUsageCounter } from '../typings'; -import type { ApmPluginRequestHandlerContext } from '../typings'; -import { InspectResponse } from '../../../../observability/typings/common'; - -const inspectRt = t.exact( - t.partial({ - query: t.exact(t.partial({ _inspect: jsonRt.pipe(t.boolean) })), - }) -); - -const CLIENT_CLOSED_REQUEST = { - statusCode: 499, - body: { - message: 'Client closed request', - }, -}; - -export const inspectableEsQueriesMap = new WeakMap< - KibanaRequest, - InspectResponse ->(); - -export function registerRoutes({ - core, - repository, - plugins, - logger, - config, - ruleDataClient, - telemetryUsageCounter, -}: { - core: APMRouteHandlerResources['core']; - plugins: APMRouteHandlerResources['plugins']; - logger: APMRouteHandlerResources['logger']; - repository: ServerRouteRepository; - config: APMRouteHandlerResources['config']; - ruleDataClient: APMRouteHandlerResources['ruleDataClient']; - telemetryUsageCounter?: TelemetryUsageCounter; -}) { - const routes = repository.getRoutes(); - - const router = core.setup.http.createRouter(); - - routes.forEach((route) => { - const { params, endpoint, options, handler } = route; - - const { method, pathname } = parseEndpoint(endpoint); - - ( - router[method] as RouteRegistrar< - typeof method, - ApmPluginRequestHandlerContext - > - )( - { - path: pathname, - options, - validate: routeValidationObject, - }, - async (context, request, response) => { - if (agent.isStarted()) { - agent.addLabels({ - plugin: 'apm', - }); - } - - // init debug queries - inspectableEsQueriesMap.set(request, []); - - try { - const runtimeType = params ? mergeRt(params, inspectRt) : inspectRt; - - const validatedParams = decodeRequestParams( - pickKeys(request, 'params', 'body', 'query'), - runtimeType - ); - - const { aborted, data } = await Promise.race([ - handler({ - request, - context, - config, - logger, - core, - plugins, - telemetryUsageCounter, - params: merge( - { - query: { - _inspect: false, - }, - }, - validatedParams - ), - ruleDataClient, - }).then((value) => { - return { - aborted: false, - data: value as Record | undefined | null, - }; - }), - request.events.aborted$.toPromise().then(() => { - return { - aborted: true, - data: undefined, - }; - }), - ]); - - if (aborted) { - return response.custom(CLIENT_CLOSED_REQUEST); - } - - if (Array.isArray(data)) { - throw new Error('Return type cannot be an array'); - } - - const body = validatedParams.query?._inspect - ? { - ...data, - _inspect: inspectableEsQueriesMap.get(request), - } - : { ...data }; - - if (!options.disableTelemetry && telemetryUsageCounter) { - telemetryUsageCounter.incrementCounter({ - counterName: `${method.toUpperCase()} ${pathname}`, - counterType: 'success', - }); - } - - return response.ok({ body }); - } catch (error) { - logger.error(error); - - if (!options.disableTelemetry && telemetryUsageCounter) { - telemetryUsageCounter.incrementCounter({ - counterName: `${method.toUpperCase()} ${pathname}`, - counterType: 'error', - }); - } - const opts = { - statusCode: 500, - body: { - message: error.message, - attributes: { - _inspect: inspectableEsQueriesMap.get(request), - }, - }, - }; - - if (error instanceof errors.RequestAbortedError) { - return response.custom(merge(opts, CLIENT_CLOSED_REQUEST)); - } - - if (Boom.isBoom(error)) { - opts.statusCode = error.output.statusCode; - } - - return response.custom(opts); - } finally { - // cleanup - inspectableEsQueriesMap.delete(request); - } - } - ); - }); -} diff --git a/x-pack/plugins/apm/server/routes/rum_client.ts b/x-pack/plugins/apm/server/routes/rum_client.ts index d1b7e9233e9c8..e84a281a7ce1b 100644 --- a/x-pack/plugins/apm/server/routes/rum_client.ts +++ b/x-pack/plugins/apm/server/routes/rum_client.ts @@ -6,7 +6,7 @@ */ import * as t from 'io-ts'; import { Logger } from 'kibana/server'; -import { isoToEpochRt } from '@kbn/io-ts-utils'; +import { isoToEpochRt } from '@kbn/io-ts-utils/iso_to_epoch_rt'; import { setupRequest, Setup } from '../lib/helpers/setup_request'; import { getClientMetrics } from '../lib/rum_client/get_client_metrics'; import { getJSErrors } from '../lib/rum_client/get_js_errors'; @@ -19,8 +19,8 @@ import { getUrlSearch } from '../lib/rum_client/get_url_search'; import { getVisitorBreakdown } from '../lib/rum_client/get_visitor_breakdown'; import { getWebCoreVitals } from '../lib/rum_client/get_web_core_vitals'; import { hasRumData } from '../lib/rum_client/has_rum_data'; -import { createApmServerRoute } from './create_apm_server_route'; -import { createApmServerRouteRepository } from './create_apm_server_route_repository'; +import { createApmServerRoute } from './apm_routes/create_apm_server_route'; +import { createApmServerRouteRepository } from './apm_routes/create_apm_server_route_repository'; import { rangeRt } from './default_api_types'; import { UxUIFilters } from '../../typings/ui_filters'; import { APMRouteHandlerResources } from '../routes/typings'; @@ -65,7 +65,7 @@ const uxQueryRt = t.intersection([ ]); const rumClientMetricsRoute = createApmServerRoute({ - endpoint: 'GET /api/apm/rum/client-metrics', + endpoint: 'GET /internal/apm/ux/client-metrics', params: t.type({ query: uxQueryRt, }), @@ -88,7 +88,7 @@ const rumClientMetricsRoute = createApmServerRoute({ }); const rumPageLoadDistributionRoute = createApmServerRoute({ - endpoint: 'GET /api/apm/rum-client/page-load-distribution', + endpoint: 'GET /internal/apm/ux/page-load-distribution', params: t.type({ query: t.intersection([uxQueryRt, percentileRangeRt]), }), @@ -114,7 +114,7 @@ const rumPageLoadDistributionRoute = createApmServerRoute({ }); const rumPageLoadDistBreakdownRoute = createApmServerRoute({ - endpoint: 'GET /api/apm/rum-client/page-load-distribution/breakdown', + endpoint: 'GET /internal/apm/ux/page-load-distribution/breakdown', params: t.type({ query: t.intersection([ uxQueryRt, @@ -145,7 +145,7 @@ const rumPageLoadDistBreakdownRoute = createApmServerRoute({ }); const rumPageViewsTrendRoute = createApmServerRoute({ - endpoint: 'GET /api/apm/rum-client/page-view-trends', + endpoint: 'GET /internal/apm/ux/page-view-trends', params: t.type({ query: t.intersection([uxQueryRt, t.partial({ breakdowns: t.string })]), }), @@ -168,7 +168,7 @@ const rumPageViewsTrendRoute = createApmServerRoute({ }); const rumServicesRoute = createApmServerRoute({ - endpoint: 'GET /api/apm/rum-client/services', + endpoint: 'GET /internal/apm/ux/services', params: t.type({ query: t.intersection([uiFiltersRt, rangeRt]), }), @@ -184,7 +184,7 @@ const rumServicesRoute = createApmServerRoute({ }); const rumVisitorsBreakdownRoute = createApmServerRoute({ - endpoint: 'GET /api/apm/rum-client/visitor-breakdown', + endpoint: 'GET /internal/apm/ux/visitor-breakdown', params: t.type({ query: uxQueryRt, }), @@ -206,7 +206,7 @@ const rumVisitorsBreakdownRoute = createApmServerRoute({ }); const rumWebCoreVitals = createApmServerRoute({ - endpoint: 'GET /api/apm/rum-client/web-core-vitals', + endpoint: 'GET /internal/apm/ux/web-core-vitals', params: t.type({ query: uxQueryRt, }), @@ -229,7 +229,7 @@ const rumWebCoreVitals = createApmServerRoute({ }); const rumLongTaskMetrics = createApmServerRoute({ - endpoint: 'GET /api/apm/rum-client/long-task-metrics', + endpoint: 'GET /internal/apm/ux/long-task-metrics', params: t.type({ query: uxQueryRt, }), @@ -252,7 +252,7 @@ const rumLongTaskMetrics = createApmServerRoute({ }); const rumUrlSearch = createApmServerRoute({ - endpoint: 'GET /api/apm/rum-client/url-search', + endpoint: 'GET /internal/apm/ux/url-search', params: t.type({ query: uxQueryRt, }), @@ -275,7 +275,7 @@ const rumUrlSearch = createApmServerRoute({ }); const rumJSErrors = createApmServerRoute({ - endpoint: 'GET /api/apm/rum-client/js-errors', + endpoint: 'GET /internal/apm/ux/js-errors', params: t.type({ query: t.intersection([ uiFiltersRt, diff --git a/x-pack/plugins/apm/server/routes/service_map.ts b/x-pack/plugins/apm/server/routes/service_map.ts index 3711ee20d814b..e75b4ec832d82 100644 --- a/x-pack/plugins/apm/server/routes/service_map.ts +++ b/x-pack/plugins/apm/server/routes/service_map.ts @@ -15,8 +15,8 @@ import { setupRequest } from '../lib/helpers/setup_request'; import { getServiceMap } from '../lib/service_map/get_service_map'; import { getServiceMapBackendNodeInfo } from '../lib/service_map/get_service_map_backend_node_info'; import { getServiceMapServiceNodeInfo } from '../lib/service_map/get_service_map_service_node_info'; -import { createApmServerRoute } from './create_apm_server_route'; -import { createApmServerRouteRepository } from './create_apm_server_route_repository'; +import { createApmServerRoute } from './apm_routes/create_apm_server_route'; +import { createApmServerRouteRepository } from './apm_routes/create_apm_server_route_repository'; import { environmentRt, rangeRt } from './default_api_types'; const serviceMapRoute = createApmServerRoute({ diff --git a/x-pack/plugins/apm/server/routes/service_nodes.ts b/x-pack/plugins/apm/server/routes/service_nodes.ts index 2081b794f8ab1..61d58bfa3cf38 100644 --- a/x-pack/plugins/apm/server/routes/service_nodes.ts +++ b/x-pack/plugins/apm/server/routes/service_nodes.ts @@ -6,8 +6,8 @@ */ import * as t from 'io-ts'; -import { createApmServerRouteRepository } from './create_apm_server_route_repository'; -import { createApmServerRoute } from './create_apm_server_route'; +import { createApmServerRouteRepository } from './apm_routes/create_apm_server_route_repository'; +import { createApmServerRoute } from './apm_routes/create_apm_server_route'; import { setupRequest } from '../lib/helpers/setup_request'; import { getServiceNodes } from '../lib/service_nodes'; import { rangeRt, kueryRt } from './default_api_types'; diff --git a/x-pack/plugins/apm/server/routes/services.ts b/x-pack/plugins/apm/server/routes/services.ts index 257aec216eb06..cb557f56d8165 100644 --- a/x-pack/plugins/apm/server/routes/services.ts +++ b/x-pack/plugins/apm/server/routes/services.ts @@ -6,7 +6,9 @@ */ import Boom from '@hapi/boom'; -import { jsonRt, isoToEpochRt, toNumberRt } from '@kbn/io-ts-utils'; +import { jsonRt } from '@kbn/io-ts-utils/json_rt'; +import { isoToEpochRt } from '@kbn/io-ts-utils/iso_to_epoch_rt'; +import { toNumberRt } from '@kbn/io-ts-utils/to_number_rt'; import * as t from 'io-ts'; import { uniq } from 'lodash'; import { latencyAggregationTypeRt } from '../../common/latency_aggregation_types'; @@ -32,8 +34,8 @@ import { getServiceProfilingStatistics } from '../lib/services/profiling/get_ser import { getServiceProfilingTimeline } from '../lib/services/profiling/get_service_profiling_timeline'; import { getServiceInfrastructure } from '../lib/services/get_service_infrastructure'; import { withApmSpan } from '../utils/with_apm_span'; -import { createApmServerRoute } from './create_apm_server_route'; -import { createApmServerRouteRepository } from './create_apm_server_route_repository'; +import { createApmServerRoute } from './apm_routes/create_apm_server_route'; +import { createApmServerRouteRepository } from './apm_routes/create_apm_server_route_repository'; import { comparisonRangeRt, environmentRt, @@ -191,18 +193,9 @@ const serviceAgentRoute = createApmServerRoute({ const { serviceName } = params.path; const { start, end } = params.query; - const searchAggregatedTransactions = await getSearchAggregatedTransactions({ - apmEventClient: setup.apmEventClient, - config: setup.config, - start, - end, - kuery: '', - }); - return getServiceAgent({ serviceName, setup, - searchAggregatedTransactions, start, end, }); diff --git a/x-pack/plugins/apm/server/routes/settings/agent_configuration.ts b/x-pack/plugins/apm/server/routes/settings/agent_configuration.ts index 0488d0ebd01bd..563fa40c6c0d9 100644 --- a/x-pack/plugins/apm/server/routes/settings/agent_configuration.ts +++ b/x-pack/plugins/apm/server/routes/settings/agent_configuration.ts @@ -7,7 +7,7 @@ import * as t from 'io-ts'; import Boom from '@hapi/boom'; -import { toBooleanRt } from '@kbn/io-ts-utils'; +import { toBooleanRt } from '@kbn/io-ts-utils/to_boolean_rt'; import { maxSuggestions } from '../../../../observability/common'; import { setupRequest } from '../../lib/helpers/setup_request'; import { getServiceNames } from '../../lib/settings/agent_configuration/get_service_names'; @@ -17,7 +17,7 @@ import { findExactConfiguration } from '../../lib/settings/agent_configuration/f import { listConfigurations } from '../../lib/settings/agent_configuration/list_configurations'; import { getEnvironments } from '../../lib/settings/agent_configuration/get_environments'; import { deleteConfiguration } from '../../lib/settings/agent_configuration/delete_configuration'; -import { createApmServerRoute } from '../create_apm_server_route'; +import { createApmServerRoute } from '../apm_routes/create_apm_server_route'; import { getAgentNameByService } from '../../lib/settings/agent_configuration/get_agent_name_by_service'; import { markAppliedByAgent } from '../../lib/settings/agent_configuration/mark_applied_by_agent'; import { @@ -25,7 +25,7 @@ import { agentConfigurationIntakeRt, } from '../../../common/agent_configuration/runtime_types/agent_configuration_intake_rt'; import { getSearchAggregatedTransactions } from '../../lib/helpers/transactions'; -import { createApmServerRouteRepository } from '../create_apm_server_route_repository'; +import { createApmServerRouteRepository } from '../apm_routes/create_apm_server_route_repository'; import { syncAgentConfigsToApmPackagePolicies } from '../../lib/fleet/sync_agent_configs_to_apm_package_policies'; // get list of configurations diff --git a/x-pack/plugins/apm/server/routes/settings/anomaly_detection.ts b/x-pack/plugins/apm/server/routes/settings/anomaly_detection.ts index f614f35810c57..e8b2ef5e119cd 100644 --- a/x-pack/plugins/apm/server/routes/settings/anomaly_detection.ts +++ b/x-pack/plugins/apm/server/routes/settings/anomaly_detection.ts @@ -10,7 +10,7 @@ import Boom from '@hapi/boom'; import { maxSuggestions } from '../../../../observability/common'; import { isActivePlatinumLicense } from '../../../common/license_check'; import { ML_ERRORS } from '../../../common/anomaly_detection'; -import { createApmServerRoute } from '../create_apm_server_route'; +import { createApmServerRoute } from '../apm_routes/create_apm_server_route'; import { getAnomalyDetectionJobs } from '../../lib/anomaly_detection/get_anomaly_detection_jobs'; import { createAnomalyDetectionJobs } from '../../lib/anomaly_detection/create_anomaly_detection_jobs'; import { setupRequest } from '../../lib/helpers/setup_request'; @@ -19,7 +19,7 @@ import { hasLegacyJobs } from '../../lib/anomaly_detection/has_legacy_jobs'; import { getSearchAggregatedTransactions } from '../../lib/helpers/transactions'; import { notifyFeatureUsage } from '../../feature'; import { withApmSpan } from '../../utils/with_apm_span'; -import { createApmServerRouteRepository } from '../create_apm_server_route_repository'; +import { createApmServerRouteRepository } from '../apm_routes/create_apm_server_route_repository'; // get ML anomaly detection jobs for each environment const anomalyDetectionJobsRoute = createApmServerRoute({ diff --git a/x-pack/plugins/apm/server/routes/settings/apm_indices.ts b/x-pack/plugins/apm/server/routes/settings/apm_indices.ts index 156f4d1af0bb2..ed99f0c8862f0 100644 --- a/x-pack/plugins/apm/server/routes/settings/apm_indices.ts +++ b/x-pack/plugins/apm/server/routes/settings/apm_indices.ts @@ -6,8 +6,8 @@ */ import * as t from 'io-ts'; -import { createApmServerRouteRepository } from '../create_apm_server_route_repository'; -import { createApmServerRoute } from '../create_apm_server_route'; +import { createApmServerRouteRepository } from '../apm_routes/create_apm_server_route_repository'; +import { createApmServerRoute } from '../apm_routes/create_apm_server_route'; import { getApmIndices, getApmIndexSettings, diff --git a/x-pack/plugins/apm/server/routes/settings/custom_link.ts b/x-pack/plugins/apm/server/routes/settings/custom_link.ts index af880898176bb..044b56c3c273d 100644 --- a/x-pack/plugins/apm/server/routes/settings/custom_link.ts +++ b/x-pack/plugins/apm/server/routes/settings/custom_link.ts @@ -21,8 +21,8 @@ import { import { deleteCustomLink } from '../../lib/settings/custom_link/delete_custom_link'; import { getTransaction } from '../../lib/settings/custom_link/get_transaction'; import { listCustomLinks } from '../../lib/settings/custom_link/list_custom_links'; -import { createApmServerRoute } from '../create_apm_server_route'; -import { createApmServerRouteRepository } from '../create_apm_server_route_repository'; +import { createApmServerRoute } from '../apm_routes/create_apm_server_route'; +import { createApmServerRouteRepository } from '../apm_routes/create_apm_server_route_repository'; const customLinkTransactionRoute = createApmServerRoute({ endpoint: 'GET /internal/apm/settings/custom_links/transaction', diff --git a/x-pack/plugins/apm/server/routes/source_maps.ts b/x-pack/plugins/apm/server/routes/source_maps.ts index e0f872239b623..602a3a725eac4 100644 --- a/x-pack/plugins/apm/server/routes/source_maps.ts +++ b/x-pack/plugins/apm/server/routes/source_maps.ts @@ -7,7 +7,7 @@ import Boom from '@hapi/boom'; import * as t from 'io-ts'; import { SavedObjectsClientContract } from 'kibana/server'; -import { jsonRt } from '@kbn/io-ts-utils'; +import { jsonRt } from '@kbn/io-ts-utils/json_rt'; import { createApmArtifact, deleteApmArtifact, @@ -16,8 +16,8 @@ import { getCleanedBundleFilePath, } from '../lib/fleet/source_maps'; import { getInternalSavedObjectsClient } from '../lib/helpers/get_internal_saved_objects_client'; -import { createApmServerRoute } from './create_apm_server_route'; -import { createApmServerRouteRepository } from './create_apm_server_route_repository'; +import { createApmServerRoute } from './apm_routes/create_apm_server_route'; +import { createApmServerRouteRepository } from './apm_routes/create_apm_server_route_repository'; import { stringFromBufferRt } from '../utils/string_from_buffer_rt'; export const sourceMapRt = t.intersection([ diff --git a/x-pack/plugins/apm/server/routes/suggestions.ts b/x-pack/plugins/apm/server/routes/suggestions.ts index 4834d894f364a..9b8952d09d162 100644 --- a/x-pack/plugins/apm/server/routes/suggestions.ts +++ b/x-pack/plugins/apm/server/routes/suggestions.ts @@ -10,8 +10,8 @@ import { maxSuggestions } from '../../../observability/common'; import { getSuggestions } from '../lib/suggestions/get_suggestions'; import { getSearchAggregatedTransactions } from '../lib/helpers/transactions'; import { setupRequest } from '../lib/helpers/setup_request'; -import { createApmServerRoute } from './create_apm_server_route'; -import { createApmServerRouteRepository } from './create_apm_server_route_repository'; +import { createApmServerRoute } from './apm_routes/create_apm_server_route'; +import { createApmServerRouteRepository } from './apm_routes/create_apm_server_route_repository'; const suggestionsRoute = createApmServerRoute({ endpoint: 'GET /internal/apm/suggestions', diff --git a/x-pack/plugins/apm/server/routes/traces.ts b/x-pack/plugins/apm/server/routes/traces.ts index cc800c348b165..5fdac470a81ed 100644 --- a/x-pack/plugins/apm/server/routes/traces.ts +++ b/x-pack/plugins/apm/server/routes/traces.ts @@ -9,11 +9,11 @@ import * as t from 'io-ts'; import { setupRequest } from '../lib/helpers/setup_request'; import { getTraceItems } from '../lib/traces/get_trace_items'; import { getTopTransactionGroupList } from '../lib/transaction_groups'; -import { createApmServerRoute } from './create_apm_server_route'; +import { createApmServerRoute } from './apm_routes/create_apm_server_route'; import { environmentRt, kueryRt, rangeRt } from './default_api_types'; import { getSearchAggregatedTransactions } from '../lib/helpers/transactions'; import { getRootTransactionByTraceId } from '../lib/transactions/get_transaction_by_trace'; -import { createApmServerRouteRepository } from './create_apm_server_route_repository'; +import { createApmServerRouteRepository } from './apm_routes/create_apm_server_route_repository'; import { getTransaction } from '../lib/transactions/get_transaction'; const tracesRoute = createApmServerRoute({ diff --git a/x-pack/plugins/apm/server/routes/transactions.ts b/x-pack/plugins/apm/server/routes/transactions.ts index 56b7ead2254d3..c0d83bac6e8e4 100644 --- a/x-pack/plugins/apm/server/routes/transactions.ts +++ b/x-pack/plugins/apm/server/routes/transactions.ts @@ -5,7 +5,8 @@ * 2.0. */ -import { jsonRt, toNumberRt } from '@kbn/io-ts-utils'; +import { jsonRt } from '@kbn/io-ts-utils/json_rt'; +import { toNumberRt } from '@kbn/io-ts-utils/to_number_rt'; import * as t from 'io-ts'; import { LatencyAggregationType, @@ -20,8 +21,8 @@ import { getTransactionTraceSamples } from '../lib/transactions/trace_samples'; import { getAnomalySeries } from '../lib/transactions/get_anomaly_data'; import { getLatencyPeriods } from '../lib/transactions/get_latency_charts'; import { getErrorRatePeriods } from '../lib/transaction_groups/get_error_rate'; -import { createApmServerRoute } from './create_apm_server_route'; -import { createApmServerRouteRepository } from './create_apm_server_route_repository'; +import { createApmServerRoute } from './apm_routes/create_apm_server_route'; +import { createApmServerRouteRepository } from './apm_routes/create_apm_server_route_repository'; import { comparisonRangeRt, environmentRt, diff --git a/x-pack/plugins/apm/server/tutorial/index.ts b/x-pack/plugins/apm/server/tutorial/index.ts index 66ff8f5b2c92c..f04b794091ff2 100644 --- a/x-pack/plugins/apm/server/tutorial/index.ts +++ b/x-pack/plugins/apm/server/tutorial/index.ts @@ -103,7 +103,6 @@ It allows you to monitor the performance of thousands of applications in real ti } ), euiIconType: 'apmApp', - eprPackageOverlap: 'apm', integrationBrowserCategories: ['web'], artifacts, customStatusCheckName: 'apm_fleet_server_status_check', diff --git a/x-pack/plugins/canvas/canvas_plugin_src/elements/index.ts b/x-pack/plugins/canvas/canvas_plugin_src/elements/index.ts index 667854bf3e7e2..b73957b500196 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/elements/index.ts +++ b/x-pack/plugins/canvas/canvas_plugin_src/elements/index.ts @@ -32,6 +32,7 @@ import { verticalBarChart } from './vert_bar_chart'; import { verticalProgressBar } from './vertical_progress_bar'; import { verticalProgressPill } from './vertical_progress_pill'; import { tagCloud } from './tag_cloud'; +import { metricVis } from './metric_vis'; import { SetupInitializer } from '../plugin'; import { ElementFactory } from '../../types'; @@ -73,3 +74,9 @@ export const initializeElements: SetupInitializer = (core, plu ]; return applyElementStrings(specs); }; + +// For testing purpose. Will be removed after exposing `metricVis` element. +export const initializeElementsSpec: SetupInitializer = (core, plugins) => { + const specs = initializeElements(core, plugins); + return [...applyElementStrings([metricVis]), ...specs]; +}; diff --git a/x-pack/plugins/canvas/canvas_plugin_src/elements/metric_vis/index.ts b/x-pack/plugins/canvas/canvas_plugin_src/elements/metric_vis/index.ts new file mode 100644 index 0000000000000..3f01a8ccb3e73 --- /dev/null +++ b/x-pack/plugins/canvas/canvas_plugin_src/elements/metric_vis/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 { ElementFactory } from '../../../types'; + +export const metricVis: ElementFactory = () => ({ + name: 'metricVis', + displayName: '(New) Metric Vis', + type: 'chart', + help: 'Metric visualization', + icon: 'visMetric', + expression: `filters + | demodata + | head 1 + | metricVis metric={visdimension "percent_uptime"} colorMode="Labels" + | render`, +}); diff --git a/x-pack/plugins/canvas/canvas_plugin_src/expression_types/embeddable.ts b/x-pack/plugins/canvas/canvas_plugin_src/expression_types/embeddable.ts index 66cb95a4a210a..1f447c7ed834e 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/expression_types/embeddable.ts +++ b/x-pack/plugins/canvas/canvas_plugin_src/expression_types/embeddable.ts @@ -6,7 +6,7 @@ */ import { ExpressionTypeDefinition } from '../../../../../src/plugins/expressions'; -import { EmbeddableInput } from '../../../../../src/plugins/embeddable/common/'; +import { EmbeddableInput } from '../../types'; import { EmbeddableTypes } from './embeddable_types'; export const EmbeddableExpressionType = 'embeddable'; diff --git a/x-pack/plugins/canvas/canvas_plugin_src/functions/browser/index.ts b/x-pack/plugins/canvas/canvas_plugin_src/functions/browser/index.ts index 2cfdebafb70df..d6d7a0f867849 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/functions/browser/index.ts +++ b/x-pack/plugins/canvas/canvas_plugin_src/functions/browser/index.ts @@ -6,7 +6,6 @@ */ import { functions as commonFunctions } from '../common'; -import { functions as externalFunctions } from '../external'; import { location } from './location'; import { markdown } from './markdown'; import { urlparam } from './urlparam'; @@ -14,13 +13,4 @@ import { escount } from './escount'; import { esdocs } from './esdocs'; import { essql } from './essql'; -export const functions = [ - location, - markdown, - urlparam, - escount, - esdocs, - essql, - ...commonFunctions, - ...externalFunctions, -]; +export const functions = [location, markdown, urlparam, escount, esdocs, essql, ...commonFunctions]; diff --git a/x-pack/plugins/canvas/canvas_plugin_src/functions/external/embeddable.test.ts b/x-pack/plugins/canvas/canvas_plugin_src/functions/external/embeddable.test.ts new file mode 100644 index 0000000000000..001fb0e3f62e3 --- /dev/null +++ b/x-pack/plugins/canvas/canvas_plugin_src/functions/external/embeddable.test.ts @@ -0,0 +1,60 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { embeddableFunctionFactory } from './embeddable'; +import { getQueryFilters } from '../../../common/lib/build_embeddable_filters'; +import { ExpressionValueFilter } from '../../../types'; +import { encode } from '../../../common/lib/embeddable_dataurl'; +import { InitializeArguments } from '.'; + +const filterContext: ExpressionValueFilter = { + type: 'filter', + and: [ + { + type: 'filter', + and: [], + value: 'filter-value', + column: 'filter-column', + filterType: 'exactly', + }, + { + type: 'filter', + and: [], + column: 'time-column', + filterType: 'time', + from: '2019-06-04T04:00:00.000Z', + to: '2019-06-05T04:00:00.000Z', + }, + ], +}; + +describe('embeddable', () => { + const fn = embeddableFunctionFactory({} as InitializeArguments)().fn; + const config = { + id: 'some-id', + timerange: { from: '15m', to: 'now' }, + title: 'test embeddable', + }; + + const args = { + config: encode(config), + type: 'visualization', + }; + + it('accepts null context', () => { + const expression = fn(null, args, {} as any); + + expect(expression.input.filters).toEqual([]); + }); + + it('accepts filter context', () => { + const expression = fn(filterContext, args, {} as any); + const embeddableFilters = getQueryFilters(filterContext.and); + + expect(expression.input.filters).toEqual(embeddableFilters); + }); +}); diff --git a/x-pack/plugins/canvas/canvas_plugin_src/functions/external/embeddable.ts b/x-pack/plugins/canvas/canvas_plugin_src/functions/external/embeddable.ts new file mode 100644 index 0000000000000..7ef8f0a09eb90 --- /dev/null +++ b/x-pack/plugins/canvas/canvas_plugin_src/functions/external/embeddable.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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { ExpressionFunctionDefinition } from 'src/plugins/expressions/common'; +import { ExpressionValueFilter, EmbeddableInput } from '../../../types'; +import { EmbeddableExpressionType, EmbeddableExpression } from '../../expression_types'; +import { getFunctionHelp } from '../../../i18n'; +import { SavedObjectReference } from '../../../../../../src/core/types'; +import { getQueryFilters } from '../../../common/lib/build_embeddable_filters'; +import { decode, encode } from '../../../common/lib/embeddable_dataurl'; +import { InitializeArguments } from '.'; + +export interface Arguments { + config: string; + type: string; +} + +const defaultTimeRange = { + from: 'now-15m', + to: 'now', +}; + +const baseEmbeddableInput = { + timeRange: defaultTimeRange, + disableTriggers: true, + renderMode: 'noInteractivity', +}; + +type Return = EmbeddableExpression; + +type EmbeddableFunction = ExpressionFunctionDefinition< + 'embeddable', + ExpressionValueFilter | null, + Arguments, + Return +>; + +export function embeddableFunctionFactory({ + embeddablePersistableStateService, +}: InitializeArguments): () => EmbeddableFunction { + return function embeddable(): EmbeddableFunction { + const { help, args: argHelp } = getFunctionHelp().embeddable; + + return { + name: 'embeddable', + help, + args: { + config: { + aliases: ['_'], + types: ['string'], + required: true, + help: argHelp.config, + }, + type: { + types: ['string'], + required: true, + help: argHelp.type, + }, + }, + context: { + types: ['filter'], + }, + type: EmbeddableExpressionType, + fn: (input, args) => { + const filters = input ? input.and : []; + + const embeddableInput = decode(args.config) as EmbeddableInput; + + return { + type: EmbeddableExpressionType, + input: { + ...baseEmbeddableInput, + ...embeddableInput, + filters: getQueryFilters(filters), + }, + generatedAt: Date.now(), + embeddableType: args.type, + }; + }, + + extract(state) { + const input = decode(state.config[0] as string); + + // extracts references for by-reference embeddables + if (input.savedObjectId) { + const refName = 'embeddable.savedObjectId'; + + const references: SavedObjectReference[] = [ + { + name: refName, + type: state.type[0] as string, + id: input.savedObjectId as string, + }, + ]; + + return { + state, + references, + }; + } + + // extracts references for by-value embeddables + const { state: extractedState, references: extractedReferences } = + embeddablePersistableStateService.extract({ + ...input, + type: state.type[0], + }); + + const { type, ...extractedInput } = extractedState; + + return { + state: { ...state, config: [encode(extractedInput)], type: [type] }, + references: extractedReferences, + }; + }, + + inject(state, references) { + const input = decode(state.config[0] as string); + const savedObjectReference = references.find( + (ref) => ref.name === 'embeddable.savedObjectId' + ); + + // injects saved object id for by-references embeddable + if (savedObjectReference) { + input.savedObjectId = savedObjectReference.id; + state.config[0] = encode(input); + state.type[0] = savedObjectReference.type; + } else { + // injects references for by-value embeddables + const { type, ...injectedInput } = embeddablePersistableStateService.inject( + { ...input, type: state.type[0] }, + references + ); + state.config[0] = encode(injectedInput); + state.type[0] = type; + } + return state; + }, + }; + }; +} diff --git a/x-pack/plugins/canvas/canvas_plugin_src/functions/external/index.ts b/x-pack/plugins/canvas/canvas_plugin_src/functions/external/index.ts index 407a0e2ebfe05..1d69e181b5fd9 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/functions/external/index.ts +++ b/x-pack/plugins/canvas/canvas_plugin_src/functions/external/index.ts @@ -5,9 +5,26 @@ * 2.0. */ +import { EmbeddableStart } from 'src/plugins/embeddable/public'; +import { embeddableFunctionFactory } from './embeddable'; import { savedLens } from './saved_lens'; import { savedMap } from './saved_map'; import { savedSearch } from './saved_search'; import { savedVisualization } from './saved_visualization'; -export const functions = [savedLens, savedMap, savedVisualization, savedSearch]; +export interface InitializeArguments { + embeddablePersistableStateService: { + extract: EmbeddableStart['extract']; + inject: EmbeddableStart['inject']; + }; +} + +export function initFunctions(initialize: InitializeArguments) { + return [ + embeddableFunctionFactory(initialize), + savedLens, + savedMap, + savedSearch, + savedVisualization, + ]; +} diff --git a/x-pack/plugins/canvas/canvas_plugin_src/functions/external/saved_lens.ts b/x-pack/plugins/canvas/canvas_plugin_src/functions/external/saved_lens.ts index 082a69a874cae..67947691f7757 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/functions/external/saved_lens.ts +++ b/x-pack/plugins/canvas/canvas_plugin_src/functions/external/saved_lens.ts @@ -9,9 +9,8 @@ import { ExpressionFunctionDefinition } from 'src/plugins/expressions/common'; import { PaletteOutput } from 'src/plugins/charts/common'; import { Filter as DataFilter } from '@kbn/es-query'; import { TimeRange } from 'src/plugins/data/common'; -import { EmbeddableInput } from 'src/plugins/embeddable/common'; import { getQueryFilters } from '../../../common/lib/build_embeddable_filters'; -import { ExpressionValueFilter, TimeRange as TimeRangeArg } from '../../../types'; +import { ExpressionValueFilter, EmbeddableInput, TimeRange as TimeRangeArg } from '../../../types'; import { EmbeddableTypes, EmbeddableExpressionType, @@ -27,7 +26,7 @@ interface Arguments { } export type SavedLensInput = EmbeddableInput & { - id: string; + savedObjectId: string; timeRange?: TimeRange; filters: DataFilter[]; palette?: PaletteOutput; @@ -73,18 +72,19 @@ export function savedLens(): ExpressionFunctionDefinition< }, }, type: EmbeddableExpressionType, - fn: (input, args) => { + fn: (input, { id, timerange, title, palette }) => { const filters = input ? input.and : []; return { type: EmbeddableExpressionType, input: { - id: args.id, + id, + savedObjectId: id, filters: getQueryFilters(filters), - timeRange: args.timerange || defaultTimeRange, - title: args.title === null ? undefined : args.title, + timeRange: timerange || defaultTimeRange, + title: title === null ? undefined : title, disableTriggers: true, - palette: args.palette, + palette, }, embeddableType: EmbeddableTypes.lens, generatedAt: Date.now(), diff --git a/x-pack/plugins/canvas/canvas_plugin_src/functions/external/saved_map.ts b/x-pack/plugins/canvas/canvas_plugin_src/functions/external/saved_map.ts index 538ed3f919823..a7471c755155c 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/functions/external/saved_map.ts +++ b/x-pack/plugins/canvas/canvas_plugin_src/functions/external/saved_map.ts @@ -30,7 +30,7 @@ const defaultTimeRange = { to: 'now', }; -type Output = EmbeddableExpression; +type Output = EmbeddableExpression; export function savedMap(): ExpressionFunctionDefinition< 'savedMap', @@ -85,8 +85,9 @@ export function savedMap(): ExpressionFunctionDefinition< return { type: EmbeddableExpressionType, input: { - attributes: { title: '' }, id: args.id, + attributes: { title: '' }, + savedObjectId: args.id, filters: getQueryFilters(filters), timeRange: args.timerange || defaultTimeRange, refreshConfig: { diff --git a/x-pack/plugins/canvas/canvas_plugin_src/functions/external/saved_visualization.ts b/x-pack/plugins/canvas/canvas_plugin_src/functions/external/saved_visualization.ts index 5c0442b43250c..31e3fb2a8c564 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/functions/external/saved_visualization.ts +++ b/x-pack/plugins/canvas/canvas_plugin_src/functions/external/saved_visualization.ts @@ -25,7 +25,7 @@ interface Arguments { title: string | null; } -type Output = EmbeddableExpression; +type Output = EmbeddableExpression; const defaultTimeRange = { from: 'now-15m', @@ -94,6 +94,7 @@ export function savedVisualization(): ExpressionFunctionDefinition< type: EmbeddableExpressionType, input: { id, + savedObjectId: id, disableTriggers: true, timeRange: timerange || defaultTimeRange, filters: getQueryFilters(filters), diff --git a/x-pack/plugins/canvas/canvas_plugin_src/plugin.ts b/x-pack/plugins/canvas/canvas_plugin_src/plugin.ts index 91c573fc4148b..591795637aebe 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/plugin.ts +++ b/x-pack/plugins/canvas/canvas_plugin_src/plugin.ts @@ -7,12 +7,14 @@ import { CoreSetup, CoreStart, Plugin } from 'src/core/public'; import { ChartsPluginStart } from 'src/plugins/charts/public'; +import { PresentationUtilPluginStart } from 'src/plugins/presentation_util/public'; import { CanvasSetup } from '../public'; import { EmbeddableStart } from '../../../../src/plugins/embeddable/public'; import { UiActionsStart } from '../../../../src/plugins/ui_actions/public'; import { Start as InspectorStart } from '../../../../src/plugins/inspector/public'; import { functions } from './functions/browser'; +import { initFunctions } from './functions/external'; import { typeFunctions } from './expression_types'; import { renderFunctions, renderFunctionFactories } from './renderers'; @@ -25,6 +27,7 @@ export interface StartDeps { uiActions: UiActionsStart; inspector: InspectorStart; charts: ChartsPluginStart; + presentationUtil: PresentationUtilPluginStart; } export type SetupInitializer = (core: CoreSetup, plugins: SetupDeps) => T; @@ -39,6 +42,13 @@ export class CanvasSrcPlugin implements Plugin plugins.canvas.addRenderers(renderFunctions); core.getStartServices().then(([coreStart, depsStart]) => { + const externalFunctions = initFunctions({ + embeddablePersistableStateService: { + extract: depsStart.embeddable.extract, + inject: depsStart.embeddable.inject, + }, + }); + plugins.canvas.addFunctions(externalFunctions); plugins.canvas.addRenderers( renderFunctionFactories.map((factory: any) => factory(coreStart, depsStart)) ); diff --git a/x-pack/plugins/canvas/canvas_plugin_src/renderers/embeddable/embeddable.tsx b/x-pack/plugins/canvas/canvas_plugin_src/renderers/embeddable/embeddable.tsx index 73e839433c25e..953746c280840 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/renderers/embeddable/embeddable.tsx +++ b/x-pack/plugins/canvas/canvas_plugin_src/renderers/embeddable/embeddable.tsx @@ -13,16 +13,17 @@ import { IEmbeddable, EmbeddableFactory, EmbeddableFactoryNotFoundError, + isErrorEmbeddable, } from '../../../../../../src/plugins/embeddable/public'; import { EmbeddableExpression } from '../../expression_types/embeddable'; import { RendererStrings } from '../../../i18n'; import { embeddableInputToExpression } from './embeddable_input_to_expression'; -import { EmbeddableInput } from '../../expression_types'; -import { RendererFactory } from '../../../types'; +import { RendererFactory, EmbeddableInput } from '../../../types'; import { CANVAS_EMBEDDABLE_CLASSNAME } from '../../../common/lib'; const { embeddable: strings } = RendererStrings; +// registry of references to embeddables on the workpad const embeddablesRegistry: { [key: string]: IEmbeddable | Promise; } = {}; @@ -30,11 +31,11 @@ const embeddablesRegistry: { const renderEmbeddableFactory = (core: CoreStart, plugins: StartDeps) => { const I18nContext = core.i18n.Context; - return (embeddableObject: IEmbeddable, domNode: HTMLElement) => { + return (embeddableObject: IEmbeddable) => { return (
    @@ -56,6 +57,9 @@ export const embeddableRendererFactory = ( reuseDomNode: true, render: async (domNode, { input, embeddableType }, handlers) => { const uniqueId = handlers.getElementId(); + const isByValueEnabled = plugins.presentationUtil.labsService.isProjectEnabled( + 'labs:canvas:byValueEmbeddable' + ); if (!embeddablesRegistry[uniqueId]) { const factory = Array.from(plugins.embeddable.getEmbeddableFactories()).find( @@ -67,15 +71,27 @@ export const embeddableRendererFactory = ( throw new EmbeddableFactoryNotFoundError(embeddableType); } - const embeddablePromise = factory - .createFromSavedObject(input.id, input) - .then((embeddable) => { - embeddablesRegistry[uniqueId] = embeddable; - return embeddable; - }); - embeddablesRegistry[uniqueId] = embeddablePromise; - - const embeddableObject = await (async () => embeddablePromise)(); + const embeddableInput = { ...input, id: uniqueId }; + + const embeddablePromise = input.savedObjectId + ? factory + .createFromSavedObject(input.savedObjectId, embeddableInput) + .then((embeddable) => { + // stores embeddable in registrey + embeddablesRegistry[uniqueId] = embeddable; + return embeddable; + }) + : factory.create(embeddableInput).then((embeddable) => { + if (!embeddable || isErrorEmbeddable(embeddable)) { + return; + } + // stores embeddable in registry + embeddablesRegistry[uniqueId] = embeddable as IEmbeddable; + return embeddable; + }); + embeddablesRegistry[uniqueId] = embeddablePromise as Promise; + + const embeddableObject = (await (async () => embeddablePromise)()) as IEmbeddable; const palettes = await plugins.charts.palettes.getPalettes(); @@ -86,7 +102,8 @@ export const embeddableRendererFactory = ( const updatedExpression = embeddableInputToExpression( updatedInput, embeddableType, - palettes + palettes, + isByValueEnabled ); if (updatedExpression) { @@ -94,15 +111,7 @@ export const embeddableRendererFactory = ( } }); - ReactDOM.render(renderEmbeddable(embeddableObject, domNode), domNode, () => - handlers.done() - ); - - handlers.onResize(() => { - ReactDOM.render(renderEmbeddable(embeddableObject, domNode), domNode, () => - handlers.done() - ); - }); + ReactDOM.render(renderEmbeddable(embeddableObject), domNode, () => handlers.done()); handlers.onDestroy(() => { subscription.unsubscribe(); @@ -115,6 +124,7 @@ export const embeddableRendererFactory = ( } else { const embeddable = embeddablesRegistry[uniqueId]; + // updating embeddable input with changes made to expression or filters if ('updateInput' in embeddable) { embeddable.updateInput(input); embeddable.reload(); diff --git a/x-pack/plugins/canvas/canvas_plugin_src/renderers/embeddable/embeddable_input_to_expression.ts b/x-pack/plugins/canvas/canvas_plugin_src/renderers/embeddable/embeddable_input_to_expression.ts index 41cefad6a470f..80830eac24021 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/renderers/embeddable/embeddable_input_to_expression.ts +++ b/x-pack/plugins/canvas/canvas_plugin_src/renderers/embeddable/embeddable_input_to_expression.ts @@ -10,6 +10,7 @@ import { EmbeddableTypes, EmbeddableInput } from '../../expression_types'; import { toExpression as mapToExpression } from './input_type_to_expression/map'; import { toExpression as visualizationToExpression } from './input_type_to_expression/visualization'; import { toExpression as lensToExpression } from './input_type_to_expression/lens'; +import { toExpression as genericToExpression } from './input_type_to_expression/embeddable'; export const inputToExpressionTypeMap = { [EmbeddableTypes.map]: mapToExpression, @@ -23,8 +24,13 @@ export const inputToExpressionTypeMap = { export function embeddableInputToExpression( input: EmbeddableInput, embeddableType: string, - palettes: PaletteRegistry + palettes: PaletteRegistry, + useGenericEmbeddable?: boolean ): string | undefined { + if (useGenericEmbeddable) { + return genericToExpression(input, embeddableType); + } + if (inputToExpressionTypeMap[embeddableType]) { return inputToExpressionTypeMap[embeddableType](input as any, palettes); } diff --git a/x-pack/plugins/canvas/canvas_plugin_src/renderers/embeddable/input_type_to_expression/embeddable.test.ts b/x-pack/plugins/canvas/canvas_plugin_src/renderers/embeddable/input_type_to_expression/embeddable.test.ts new file mode 100644 index 0000000000000..4b78acec8750a --- /dev/null +++ b/x-pack/plugins/canvas/canvas_plugin_src/renderers/embeddable/input_type_to_expression/embeddable.test.ts @@ -0,0 +1,128 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { toExpression } from './embeddable'; +import { EmbeddableInput } from '../../../../types'; +import { decode } from '../../../../common/lib/embeddable_dataurl'; +import { fromExpression } from '@kbn/interpreter/common'; + +describe('toExpression', () => { + describe('by-reference embeddable input', () => { + const baseEmbeddableInput = { + id: 'elementId', + savedObjectId: 'embeddableId', + filters: [], + }; + + it('converts to an embeddable expression', () => { + const input: EmbeddableInput = baseEmbeddableInput; + + const expression = toExpression(input, 'visualization'); + const ast = fromExpression(expression); + + expect(ast.type).toBe('expression'); + expect(ast.chain[0].function).toBe('embeddable'); + expect(ast.chain[0].arguments.type[0]).toBe('visualization'); + + const config = decode(ast.chain[0].arguments.config[0] as string); + + expect(config.savedObjectId).toStrictEqual(input.savedObjectId); + }); + + it('includes optional input values', () => { + const input: EmbeddableInput = { + ...baseEmbeddableInput, + title: 'title', + timeRange: { + from: 'now-1h', + to: 'now', + }, + }; + + const expression = toExpression(input, 'visualization'); + const ast = fromExpression(expression); + + const config = decode(ast.chain[0].arguments.config[0] as string); + + expect(config).toHaveProperty('title', input.title); + expect(config).toHaveProperty('timeRange'); + expect(config.timeRange).toHaveProperty('from', input.timeRange?.from); + expect(config.timeRange).toHaveProperty('to', input.timeRange?.to); + }); + + it('includes empty panel title', () => { + const input: EmbeddableInput = { + ...baseEmbeddableInput, + title: '', + }; + + const expression = toExpression(input, 'visualization'); + const ast = fromExpression(expression); + + const config = decode(ast.chain[0].arguments.config[0] as string); + + expect(config).toHaveProperty('title', input.title); + }); + }); + + describe('by-value embeddable input', () => { + const baseEmbeddableInput = { + id: 'elementId', + disableTriggers: true, + filters: [], + }; + it('converts to an embeddable expression', () => { + const input: EmbeddableInput = baseEmbeddableInput; + + const expression = toExpression(input, 'visualization'); + const ast = fromExpression(expression); + + expect(ast.type).toBe('expression'); + expect(ast.chain[0].function).toBe('embeddable'); + expect(ast.chain[0].arguments.type[0]).toBe('visualization'); + + const config = decode(ast.chain[0].arguments.config[0] as string); + expect(config.filters).toStrictEqual(input.filters); + expect(config.disableTriggers).toStrictEqual(input.disableTriggers); + }); + + it('includes optional input values', () => { + const input: EmbeddableInput = { + ...baseEmbeddableInput, + title: 'title', + timeRange: { + from: 'now-1h', + to: 'now', + }, + }; + + const expression = toExpression(input, 'visualization'); + const ast = fromExpression(expression); + + const config = decode(ast.chain[0].arguments.config[0] as string); + + expect(config).toHaveProperty('title', input.title); + expect(config).toHaveProperty('timeRange'); + expect(config.timeRange).toHaveProperty('from', input.timeRange?.from); + expect(config.timeRange).toHaveProperty('to', input.timeRange?.to); + }); + + it('includes empty panel title', () => { + const input: EmbeddableInput = { + ...baseEmbeddableInput, + title: '', + }; + + const expression = toExpression(input, 'visualization'); + const ast = fromExpression(expression); + + const config = decode(ast.chain[0].arguments.config[0] as string); + + expect(config).toHaveProperty('title', input.title); + }); + }); +}); diff --git a/x-pack/plugins/canvas/canvas_plugin_src/renderers/embeddable/input_type_to_expression/embeddable.ts b/x-pack/plugins/canvas/canvas_plugin_src/renderers/embeddable/input_type_to_expression/embeddable.ts new file mode 100644 index 0000000000000..94d86f6640be1 --- /dev/null +++ b/x-pack/plugins/canvas/canvas_plugin_src/renderers/embeddable/input_type_to_expression/embeddable.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 { encode } from '../../../../common/lib/embeddable_dataurl'; +import { EmbeddableInput } from '../../../expression_types'; + +export function toExpression(input: EmbeddableInput, embeddableType: string): string { + return `embeddable config="${encode(input)}" type="${embeddableType}"`; +} diff --git a/x-pack/plugins/canvas/canvas_plugin_src/renderers/embeddable/input_type_to_expression/lens.test.ts b/x-pack/plugins/canvas/canvas_plugin_src/renderers/embeddable/input_type_to_expression/lens.test.ts index 24da7238bcee9..224cdfba389d7 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/renderers/embeddable/input_type_to_expression/lens.test.ts +++ b/x-pack/plugins/canvas/canvas_plugin_src/renderers/embeddable/input_type_to_expression/lens.test.ts @@ -11,7 +11,8 @@ import { fromExpression, Ast } from '@kbn/interpreter/common'; import { chartPluginMock } from 'src/plugins/charts/public/mocks'; const baseEmbeddableInput = { - id: 'embeddableId', + id: 'elementId', + savedObjectId: 'embeddableId', filters: [], }; @@ -27,7 +28,7 @@ describe('toExpression', () => { expect(ast.type).toBe('expression'); expect(ast.chain[0].function).toBe('savedLens'); - expect(ast.chain[0].arguments.id).toStrictEqual([input.id]); + expect(ast.chain[0].arguments.id).toStrictEqual([input.savedObjectId]); expect(ast.chain[0].arguments).not.toHaveProperty('title'); expect(ast.chain[0].arguments).not.toHaveProperty('timerange'); diff --git a/x-pack/plugins/canvas/canvas_plugin_src/renderers/embeddable/input_type_to_expression/lens.ts b/x-pack/plugins/canvas/canvas_plugin_src/renderers/embeddable/input_type_to_expression/lens.ts index 35e106f234fa4..5a13b73b3fe74 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/renderers/embeddable/input_type_to_expression/lens.ts +++ b/x-pack/plugins/canvas/canvas_plugin_src/renderers/embeddable/input_type_to_expression/lens.ts @@ -14,7 +14,7 @@ export function toExpression(input: SavedLensInput, palettes: PaletteRegistry): expressionParts.push('savedLens'); - expressionParts.push(`id="${input.id}"`); + expressionParts.push(`id="${input.savedObjectId}"`); if (input.title !== undefined) { expressionParts.push(`title="${input.title}"`); diff --git a/x-pack/plugins/canvas/canvas_plugin_src/renderers/embeddable/input_type_to_expression/map.test.ts b/x-pack/plugins/canvas/canvas_plugin_src/renderers/embeddable/input_type_to_expression/map.test.ts index 804d0d849cc7f..af7b40a9b283d 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/renderers/embeddable/input_type_to_expression/map.test.ts +++ b/x-pack/plugins/canvas/canvas_plugin_src/renderers/embeddable/input_type_to_expression/map.test.ts @@ -6,12 +6,12 @@ */ import { toExpression } from './map'; -import { MapEmbeddableInput } from '../../../../../../plugins/maps/public/embeddable'; import { fromExpression, Ast } from '@kbn/interpreter/common'; const baseSavedMapInput = { + id: 'elementId', attributes: { title: '' }, - id: 'embeddableId', + savedObjectId: 'embeddableId', filters: [], isLayerTOCOpen: false, refreshConfig: { @@ -23,7 +23,7 @@ const baseSavedMapInput = { describe('toExpression', () => { it('converts to a savedMap expression', () => { - const input: MapEmbeddableInput = { + const input = { ...baseSavedMapInput, }; @@ -33,7 +33,7 @@ describe('toExpression', () => { expect(ast.type).toBe('expression'); expect(ast.chain[0].function).toBe('savedMap'); - expect(ast.chain[0].arguments.id).toStrictEqual([input.id]); + expect(ast.chain[0].arguments.id).toStrictEqual([input.savedObjectId]); expect(ast.chain[0].arguments).not.toHaveProperty('title'); expect(ast.chain[0].arguments).not.toHaveProperty('center'); @@ -41,7 +41,7 @@ describe('toExpression', () => { }); it('includes optional input values', () => { - const input: MapEmbeddableInput = { + const input = { ...baseSavedMapInput, mapCenter: { lat: 1, @@ -73,7 +73,7 @@ describe('toExpression', () => { }); it('includes empty panel title', () => { - const input: MapEmbeddableInput = { + const input = { ...baseSavedMapInput, title: '', }; diff --git a/x-pack/plugins/canvas/canvas_plugin_src/renderers/embeddable/input_type_to_expression/map.ts b/x-pack/plugins/canvas/canvas_plugin_src/renderers/embeddable/input_type_to_expression/map.ts index 3fd6a68a327c6..03746f38b4696 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/renderers/embeddable/input_type_to_expression/map.ts +++ b/x-pack/plugins/canvas/canvas_plugin_src/renderers/embeddable/input_type_to_expression/map.ts @@ -5,13 +5,14 @@ * 2.0. */ -import { MapEmbeddableInput } from '../../../../../../plugins/maps/public/embeddable'; +import { MapEmbeddableInput } from '../../../../../../plugins/maps/public'; -export function toExpression(input: MapEmbeddableInput): string { +export function toExpression(input: MapEmbeddableInput & { savedObjectId: string }): string { const expressionParts = [] as string[]; expressionParts.push('savedMap'); - expressionParts.push(`id="${input.id}"`); + + expressionParts.push(`id="${input.savedObjectId}"`); if (input.title !== undefined) { expressionParts.push(`title="${input.title}"`); diff --git a/x-pack/plugins/canvas/canvas_plugin_src/renderers/embeddable/input_type_to_expression/visualization.test.ts b/x-pack/plugins/canvas/canvas_plugin_src/renderers/embeddable/input_type_to_expression/visualization.test.ts index c5106b9a102b4..4c61a130f3c95 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/renderers/embeddable/input_type_to_expression/visualization.test.ts +++ b/x-pack/plugins/canvas/canvas_plugin_src/renderers/embeddable/input_type_to_expression/visualization.test.ts @@ -9,7 +9,8 @@ import { toExpression } from './visualization'; import { fromExpression, Ast } from '@kbn/interpreter/common'; const baseInput = { - id: 'embeddableId', + id: 'elementId', + savedObjectId: 'embeddableId', }; describe('toExpression', () => { @@ -24,7 +25,7 @@ describe('toExpression', () => { expect(ast.type).toBe('expression'); expect(ast.chain[0].function).toBe('savedVisualization'); - expect(ast.chain[0].arguments.id).toStrictEqual([input.id]); + expect(ast.chain[0].arguments.id).toStrictEqual([input.savedObjectId]); }); it('includes timerange if given', () => { diff --git a/x-pack/plugins/canvas/canvas_plugin_src/renderers/embeddable/input_type_to_expression/visualization.ts b/x-pack/plugins/canvas/canvas_plugin_src/renderers/embeddable/input_type_to_expression/visualization.ts index bcb73b2081fee..364d7cd0755db 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/renderers/embeddable/input_type_to_expression/visualization.ts +++ b/x-pack/plugins/canvas/canvas_plugin_src/renderers/embeddable/input_type_to_expression/visualization.ts @@ -7,11 +7,11 @@ import { VisualizeInput } from 'src/plugins/visualizations/public'; -export function toExpression(input: VisualizeInput): string { +export function toExpression(input: VisualizeInput & { savedObjectId: string }): string { const expressionParts = [] as string[]; expressionParts.push('savedVisualization'); - expressionParts.push(`id="${input.id}"`); + expressionParts.push(`id="${input.savedObjectId}"`); if (input.title !== undefined) { expressionParts.push(`title="${input.title}"`); diff --git a/x-pack/plugins/canvas/canvas_plugin_src/renderers/filters/dropdown_filter/component/__stories__/__snapshots__/dropdown_filter.stories.storyshot b/x-pack/plugins/canvas/canvas_plugin_src/renderers/filters/dropdown_filter/component/__stories__/__snapshots__/dropdown_filter.stories.storyshot index a14ad820586eb..52694d3b04089 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/renderers/filters/dropdown_filter/component/__stories__/__snapshots__/dropdown_filter.stories.storyshot +++ b/x-pack/plugins/canvas/canvas_plugin_src/renderers/filters/dropdown_filter/component/__stories__/__snapshots__/dropdown_filter.stories.storyshot @@ -4,23 +4,42 @@ exports[`Storyshots renderers/DropdownFilter default 1`] = `
    - - + +
    + + +
    +
    +
    `; @@ -28,41 +47,60 @@ exports[`Storyshots renderers/DropdownFilter with choices 1`] = `
    - - + +
    + + +
    +
    +
    `; @@ -70,41 +108,60 @@ exports[`Storyshots renderers/DropdownFilter with choices and new value 1`] = `
    - - + +
    + + +
    +
    +
    `; @@ -112,41 +169,60 @@ exports[`Storyshots renderers/DropdownFilter with choices and value 1`] = `
    - - + +
    + + +
    +
    +
    `; @@ -154,22 +230,41 @@ exports[`Storyshots renderers/DropdownFilter with new value 1`] = `
    - - + +
    + + +
    +
    +
    `; diff --git a/x-pack/plugins/canvas/canvas_plugin_src/renderers/filters/dropdown_filter/component/dropdown_filter.scss b/x-pack/plugins/canvas/canvas_plugin_src/renderers/filters/dropdown_filter/component/dropdown_filter.scss index 425caee256d36..aa7c176f9d389 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/renderers/filters/dropdown_filter/component/dropdown_filter.scss +++ b/x-pack/plugins/canvas/canvas_plugin_src/renderers/filters/dropdown_filter/component/dropdown_filter.scss @@ -1,31 +1,3 @@ .canvasDropdownFilter { - width: 100%; - font-size: inherit; position: relative; - - .canvasDropdownFilter__select { - background-color: $euiColorEmptyShade; - width: 100%; - padding: $euiSizeXS $euiSize; - border: $euiBorderThin; - border-radius: $euiBorderRadius; - appearance: none; - font-size: inherit; - color: $euiTextColor; - - &:after { - display: none; - } - - &:focus { - box-shadow: none; - } - } - - .canvasDropdownFilter__icon { - position: absolute; - right: $euiSizeS; - top: $euiSizeS; - pointer-events: none; - } } diff --git a/x-pack/plugins/canvas/canvas_plugin_src/renderers/filters/dropdown_filter/component/dropdown_filter.tsx b/x-pack/plugins/canvas/canvas_plugin_src/renderers/filters/dropdown_filter/component/dropdown_filter.tsx index 5b020a2e194dc..ec9db940c00a1 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/renderers/filters/dropdown_filter/component/dropdown_filter.tsx +++ b/x-pack/plugins/canvas/canvas_plugin_src/renderers/filters/dropdown_filter/component/dropdown_filter.tsx @@ -7,7 +7,7 @@ import React, { ChangeEvent, FocusEvent, FunctionComponent, useEffect, useState } from 'react'; import PropTypes from 'prop-types'; -import { EuiIcon } from '@elastic/eui'; +import { EuiSelect, EuiSelectOption } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; const strings = { @@ -57,30 +57,29 @@ export const DropdownFilter: FunctionComponent = ({ } }; - const dropdownOptions = options.map((option) => { + const dropdownOptions: EuiSelectOption[] = options.map((option) => { const { text } = option; const optionValue = option.value; const selected = optionValue === value; - return ( - - ); + return { + text, + selected, + value: optionValue, + }; }); - /* eslint-disable jsx-a11y/no-onchange */ return (
    - - + options={dropdownOptions} + fullWidth + compressed + />
    ); }; diff --git a/x-pack/plugins/canvas/canvas_plugin_src/renderers/filters/dropdown_filter/index.tsx b/x-pack/plugins/canvas/canvas_plugin_src/renderers/filters/dropdown_filter/index.tsx index d956c6291c609..001f2dc7652d2 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/renderers/filters/dropdown_filter/index.tsx +++ b/x-pack/plugins/canvas/canvas_plugin_src/renderers/filters/dropdown_filter/index.tsx @@ -47,7 +47,10 @@ export const dropdownFilter: RendererFactory = () => ({ render(domNode, config, handlers) { let filterExpression = handlers.getFilter(); - if (filterExpression === undefined || !filterExpression.includes('exactly')) { + if ( + filterExpression !== '' && + (filterExpression === undefined || !filterExpression.includes('exactly')) + ) { filterExpression = ''; handlers.setFilter(filterExpression); } else if (filterExpression !== '') { diff --git a/x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/__stories__/palette.stories.tsx b/x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/__stories__/palette.stories.tsx index ed9a47ad97484..27bec26750874 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/__stories__/palette.stories.tsx +++ b/x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/__stories__/palette.stories.tsx @@ -29,6 +29,7 @@ storiesOf('arguments/Palette', module).add('default', () => ( }} onValueChange={action('onValueChange')} renderError={action('renderError')} + typeInstance={{}} />
    )); diff --git a/x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/index.ts b/x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/index.ts index dd013116bb808..c6a220062227e 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/index.ts +++ b/x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/index.ts @@ -16,7 +16,7 @@ import { imageUpload } from './image_upload'; // @ts-expect-error untyped local import { number } from './number'; import { numberFormatInitializer } from './number_format'; -import { palette } from './palette'; +import { palette, stopsPalette } from './palette'; // @ts-expect-error untyped local import { percentage } from './percentage'; // @ts-expect-error untyped local @@ -42,6 +42,7 @@ export const args = [ imageUpload, number, palette, + stopsPalette, percentage, range, select, diff --git a/x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/palette.tsx b/x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/palette.tsx deleted file mode 100644 index d01424af3a584..0000000000000 --- a/x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/palette.tsx +++ /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; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React, { FC } from 'react'; -import PropTypes from 'prop-types'; -import { get } from 'lodash'; -import { getType } from '@kbn/interpreter/common'; -import { ExpressionAstFunction, ExpressionAstExpression } from 'src/plugins/expressions'; -import { PalettePicker } from '../../../public/components/palette_picker'; -import { templateFromReactComponent } from '../../../public/lib/template_from_react_component'; -import { ArgumentStrings } from '../../../i18n'; -import { identifyPalette, ColorPalette } from '../../../common/lib'; - -const { Palette: strings } = ArgumentStrings; - -interface Props { - onValueChange: (value: ExpressionAstExpression) => void; - argValue: ExpressionAstExpression; - renderError: () => void; - argId?: string; -} - -export const PaletteArgInput: FC = ({ onValueChange, argId, argValue, renderError }) => { - // TODO: This is weird, its basically a reimplementation of what the interpretter would return. - // Probably a better way todo this, and maybe a better way to handle template type objects in general? - const astToPalette = ({ chain }: { chain: ExpressionAstFunction[] }): ColorPalette | null => { - if (chain.length !== 1 || chain[0].function !== 'palette') { - renderError(); - return null; - } - - try { - const colors = chain[0].arguments._.map((astObj) => { - if (getType(astObj) !== 'string') { - renderError(); - } - return astObj; - }) as string[]; - - const gradient = get(chain[0].arguments.gradient, '[0]') as boolean; - const palette = identifyPalette({ colors, gradient }); - - if (palette) { - return palette; - } - - return { - id: 'custom', - label: strings.getCustomPaletteLabel(), - colors, - gradient, - } as any as ColorPalette; - } catch (e) { - renderError(); - } - return null; - }; - - const handleChange = (palette: ColorPalette): void => { - const astObj: ExpressionAstExpression = { - type: 'expression', - chain: [ - { - type: 'function', - function: 'palette', - arguments: { - _: palette.colors, - gradient: [palette.gradient], - }, - }, - ], - }; - - onValueChange(astObj); - }; - - const palette = astToPalette(argValue); - - if (!palette) { - renderError(); - return null; - } - - return ; -}; - -PaletteArgInput.propTypes = { - argId: PropTypes.string, - onValueChange: PropTypes.func.isRequired, - argValue: PropTypes.any.isRequired, - renderError: PropTypes.func, -}; - -export const palette = () => ({ - name: 'palette', - displayName: strings.getDisplayName(), - help: strings.getHelp(), - default: - '{palette #882E72 #B178A6 #D6C1DE #1965B0 #5289C7 #7BAFDE #4EB265 #90C987 #CAE0AB #F7EE55 #F6C141 #F1932D #E8601C #DC050C}', - simpleTemplate: templateFromReactComponent(PaletteArgInput), -}); diff --git a/x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/palette/index.tsx b/x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/palette/index.tsx new file mode 100644 index 0000000000000..2eb756d34fff3 --- /dev/null +++ b/x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/palette/index.tsx @@ -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 { PaletteArgInput, SimplePaletteArgInput, palette, stopsPalette } from './palette'; diff --git a/x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/palette/palette.tsx b/x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/palette/palette.tsx new file mode 100644 index 0000000000000..eddefb8dadd2c --- /dev/null +++ b/x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/palette/palette.tsx @@ -0,0 +1,121 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { FC } from 'react'; +import PropTypes from 'prop-types'; +import { ExpressionAstExpression } from 'src/plugins/expressions'; +import { templateFromReactComponent } from '../../../../public/lib/template_from_react_component'; +import { ArgumentStrings } from '../../../../i18n'; +import { ColorPalette } from '../../../../common/lib'; +import { astToPalette } from './utils'; +import { ColorPaletteName, getPaletteType } from './palette_types'; +import { CustomColorPalette } from '../../../../public/components/palette_picker'; + +const { Palette: strings, StopsPalette: stopsPaletteStrings } = ArgumentStrings; + +interface Props { + onValueChange: (value: ExpressionAstExpression) => void; + argValue: ExpressionAstExpression; + renderError: () => void; + argId?: string; + typeInstance: { + options?: { + type?: ColorPaletteName; + }; + }; +} + +export const PaletteArgInput: FC = ({ + onValueChange, + argId, + argValue, + renderError, + typeInstance, +}) => { + const handleChange = (palette: ColorPalette | CustomColorPalette): void => { + let colorStopsPaletteConfig = {}; + if (palette.stops?.length) { + colorStopsPaletteConfig = { + stop: palette.stops, + ...(palette.range ? { range: [palette.range] } : {}), + ...(palette.continuity ? { continuity: [palette.continuity] } : {}), + }; + } + + const astObj: ExpressionAstExpression = { + type: 'expression', + chain: [ + { + type: 'function', + function: 'palette', + arguments: { + _: palette.colors, + gradient: [palette.gradient], + ...colorStopsPaletteConfig, + }, + }, + ], + }; + + onValueChange(astObj); + }; + + const palette = astToPalette(argValue, renderError); + if (!palette) { + renderError(); + return null; + } + + const PalettePicker = getPaletteType(typeInstance.options?.type); + return ; +}; + +export const SimplePaletteArgInput: FC = (props) => { + const { typeInstance } = props; + const { type, ...restOptions } = typeInstance.options ?? {}; + return ( + + ); +}; + +export const StopsPaletteArgInput: FC = (props) => ( + +); + +PaletteArgInput.propTypes = { + argId: PropTypes.string, + onValueChange: PropTypes.func.isRequired, + argValue: PropTypes.any.isRequired, + renderError: PropTypes.func, +}; + +const defaultPaletteOptions = { + default: + '{palette #882E72 #B178A6 #D6C1DE #1965B0 #5289C7 #7BAFDE #4EB265 #90C987 #CAE0AB #F7EE55 #F6C141 #F1932D #E8601C #DC050C}', +}; + +export const palette = () => ({ + name: 'palette', + displayName: strings.getDisplayName(), + help: strings.getHelp(), + simpleTemplate: templateFromReactComponent(SimplePaletteArgInput), + ...defaultPaletteOptions, +}); + +export const stopsPalette = () => ({ + name: 'stops_palette', + help: stopsPaletteStrings.getHelp(), + displayName: stopsPaletteStrings.getDisplayName(), + template: templateFromReactComponent(StopsPaletteArgInput), + ...defaultPaletteOptions, +}); diff --git a/x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/palette/palette_types.ts b/x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/palette/palette_types.ts new file mode 100644 index 0000000000000..8a0ec11af3448 --- /dev/null +++ b/x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/palette/palette_types.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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { PalettePicker, StopsPalettePicker } from '../../../../public/components/palette_picker'; + +const DEFAULT_PALETTE = 'default'; +const STOPS_PALETTE = 'stops'; + +export type ColorPaletteName = typeof DEFAULT_PALETTE | typeof STOPS_PALETTE; + +const paletteTypes = { + [DEFAULT_PALETTE]: PalettePicker, + [STOPS_PALETTE]: StopsPalettePicker, +}; + +export const getPaletteType = (type: ColorPaletteName = DEFAULT_PALETTE) => + paletteTypes[type] ?? paletteTypes[DEFAULT_PALETTE]; diff --git a/x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/palette/utils.ts b/x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/palette/utils.ts new file mode 100644 index 0000000000000..5734bf7ce4f66 --- /dev/null +++ b/x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/palette/utils.ts @@ -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 { getType } from '@kbn/interpreter/common'; +import { ExpressionAstArgument, ExpressionAstFunction } from 'src/plugins/expressions'; +import { identifyPalette, ColorPalette, identifyPartialPalette } from '../../../../common/lib'; +import { ArgumentStrings } from '../../../../i18n'; + +const { Palette: strings } = ArgumentStrings; + +export const CUSTOM_PALETTE = 'custom'; + +interface PaletteParams { + colors: string[]; + gradient: boolean; + stops?: number[]; +} + +export const createCustomPalette = ( + paletteParams: PaletteParams +): ColorPalette => ({ + id: CUSTOM_PALETTE, + label: strings.getCustomPaletteLabel(), + ...paletteParams, +}); + +type UnboxArray = T extends Array ? U : T; + +function reduceElementsWithType( + arr: any[], + arg: ExpressionAstArgument, + type: string, + onError: () => void +) { + if (getType(arg) !== type) { + onError(); + } + return [...arr, arg as UnboxArray]; +} + +// TODO: This is weird, its basically a reimplementation of what the interpretter would return. +// Probably a better way todo this, and maybe a better way to handle template type objects in general? +export const astToPalette = ( + { chain }: { chain: ExpressionAstFunction[] }, + onError: () => void +): ColorPalette | ColorPalette | null => { + if (chain.length !== 1 || chain[0].function !== 'palette') { + onError(); + return null; + } + + const { _, stop: argStops, gradient: argGradient, ...restArgs } = chain[0].arguments ?? {}; + + try { + const colors = + _?.reduce((args, arg) => { + return reduceElementsWithType(args, arg, 'string', onError); + }, []) ?? []; + + const stops = + argStops?.reduce((args, arg) => { + return reduceElementsWithType(args, arg, 'number', onError); + }, []) ?? []; + + const gradient = !!argGradient?.[0]; + const palette = (stops.length ? identifyPartialPalette : identifyPalette)({ colors, gradient }); + const restPreparedArgs = Object.keys(restArgs).reduce< + Record + >((acc, argName) => { + acc[argName] = restArgs[argName]?.length > 1 ? restArgs[argName] : restArgs[argName]?.[0]; + return acc; + }, {}); + + if (palette) { + return { + ...palette, + ...restPreparedArgs, + stops, + }; + } + + return createCustomPalette({ colors, gradient, stops, ...restPreparedArgs }); + } catch (e) { + onError(); + } + return null; +}; diff --git a/x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/vis_dimension.tsx b/x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/vis_dimension.tsx index df75704ababb5..312457b658ad9 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/vis_dimension.tsx +++ b/x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/vis_dimension.tsx @@ -31,9 +31,6 @@ const VisDimensionArgInput: React.FC = ({ onValueChange, argId, columns, -}: { - // @todo define types - [key: string]: any; }) => { const [value, setValue] = useState(argValue); const confirm = typeInstance?.options?.confirm; diff --git a/x-pack/plugins/canvas/canvas_plugin_src/uis/datasources/esdocs.js b/x-pack/plugins/canvas/canvas_plugin_src/uis/datasources/esdocs.js index 817851b53c186..150b7c0616887 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/uis/datasources/esdocs.js +++ b/x-pack/plugins/canvas/canvas_plugin_src/uis/datasources/esdocs.js @@ -5,7 +5,7 @@ * 2.0. */ -import React from 'react'; +import React, { useEffect, useCallback } from 'react'; import PropTypes from 'prop-types'; import { EuiFormRow, @@ -27,13 +27,16 @@ import { DataSourceStrings, LUCENE_QUERY_URL } from '../../../i18n'; const { ESDocs: strings } = DataSourceStrings; const EsdocsDatasource = ({ args, updateArgs, defaultIndex }) => { - const setArg = (name, value) => { - updateArgs && - updateArgs({ - ...args, - ...setSimpleArg(name, value), - }); - }; + const setArg = useCallback( + (name, value) => { + updateArgs && + updateArgs({ + ...args, + ...setSimpleArg(name, value), + }); + }, + [args, updateArgs] + ); // TODO: This is a terrible way of doing defaults. We need to find a way to read the defaults for the function // and set them for the data source UI. @@ -73,6 +76,12 @@ const EsdocsDatasource = ({ args, updateArgs, defaultIndex }) => { const index = getIndex(); + useEffect(() => { + if (getSimpleArg('index', args)[0] !== index) { + setArg('index', index); + } + }, [args, index, setArg]); + const sortOptions = [ { value: 'asc', text: strings.getAscendingOption() }, { value: 'desc', text: strings.getDescendingOption() }, diff --git a/x-pack/plugins/canvas/canvas_plugin_src/uis/models/index.js b/x-pack/plugins/canvas/canvas_plugin_src/uis/models/index.js index 0762f70b19858..8fadc9e2e6c8a 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/uis/models/index.js +++ b/x-pack/plugins/canvas/canvas_plugin_src/uis/models/index.js @@ -8,5 +8,6 @@ import { pointseries } from './point_series'; import { math } from './math'; import { tagcloud } from './tagcloud'; +import { metricVis } from './metric_vis'; -export const modelSpecs = [pointseries, math, tagcloud]; +export const modelSpecs = [pointseries, math, tagcloud, metricVis]; diff --git a/x-pack/plugins/canvas/canvas_plugin_src/uis/models/metric_vis.ts b/x-pack/plugins/canvas/canvas_plugin_src/uis/models/metric_vis.ts new file mode 100644 index 0000000000000..9796c4553978e --- /dev/null +++ b/x-pack/plugins/canvas/canvas_plugin_src/uis/models/metric_vis.ts @@ -0,0 +1,79 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { get } from 'lodash'; + +import { ViewStrings } from '../../../i18n'; +import { getState, getValue } from '../../../public/lib/resolved_arg'; + +const { MetricVis: strings } = ViewStrings; + +export const metricVis = () => ({ + name: 'metricVis', + displayName: strings.getDisplayName(), + args: [ + { + name: 'metric', + displayName: strings.getMetricColumnDisplayName(), + help: strings.getMetricColumnHelp(), + argType: 'vis_dimension', + multi: true, + default: `{visdimension}`, + }, + { + name: 'bucket', + displayName: strings.getBucketColumnDisplayName(), + help: strings.getBucketColumnHelp(), + argType: 'vis_dimension', + default: `{visdimension}`, + }, + { + name: 'palette', + argType: 'stops_palette', + }, + { + name: 'font', + displayName: strings.getFontColumnDisplayName(), + help: strings.getFontColumnHelp(), + argType: 'font', + default: `{font size=60 align="center"}`, + }, + { + name: 'colorMode', + displayName: strings.getColorModeColumnDisplayName(), + help: strings.getColorModeColumnHelp(), + argType: 'select', + default: 'Labels', + options: { + choices: [ + { value: 'None', name: strings.getColorModeNoneOption() }, + { value: 'Labels', name: strings.getColorModeLabelOption() }, + { value: 'Background', name: strings.getColorModeBackgroundOption() }, + ], + }, + }, + { + name: 'showLabels', + displayName: strings.getShowLabelsColumnDisplayName(), + help: strings.getShowLabelsColumnHelp(), + argType: 'toggle', + default: true, + }, + { + name: 'percentageMode', + displayName: strings.getPercentageModeColumnDisplayName(), + help: strings.getPercentageModeColumnHelp(), + argType: 'toggle', + }, + ], + resolve({ context }: any) { + if (getState(context) !== 'ready') { + return { columns: [] }; + } + return { columns: get(getValue(context), 'columns', []) }; + }, +}); diff --git a/x-pack/plugins/canvas/common/lib/embeddable_dataurl.ts b/x-pack/plugins/canvas/common/lib/embeddable_dataurl.ts new file mode 100644 index 0000000000000..e76dedfe63b14 --- /dev/null +++ b/x-pack/plugins/canvas/common/lib/embeddable_dataurl.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 { EmbeddableInput } from '../../types'; + +export const encode = (input: Partial) => + Buffer.from(JSON.stringify(input)).toString('base64'); +export const decode = (serializedInput: string) => + JSON.parse(Buffer.from(serializedInput, 'base64').toString()); diff --git a/x-pack/plugins/canvas/common/lib/palettes.ts b/x-pack/plugins/canvas/common/lib/palettes.ts index e9c2f23b62dfb..2e7eac1e1a84a 100644 --- a/x-pack/plugins/canvas/common/lib/palettes.ts +++ b/x-pack/plugins/canvas/common/lib/palettes.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { isEqual } from 'lodash'; +import { difference, isEqual } from 'lodash'; import { LibStrings } from '../../i18n'; const { Palettes: strings } = LibStrings; @@ -19,11 +19,14 @@ export type PaletteID = typeof palettes[number]['id']; * An interface representing a color palette in Canvas, with a textual label and a set of * hex values. */ -export interface ColorPalette { - id: PaletteID; +export interface ColorPalette { + id: PaletteID | AdditionalPaletteID; label: string; colors: string[]; gradient: boolean; + stops?: number[]; + range?: 'number' | 'percent'; + continuity?: 'above' | 'below' | 'all' | 'none'; } // This function allows one to create a strongly-typed palette for inclusion in @@ -52,6 +55,15 @@ export const identifyPalette = ( }); }; +export const identifyPartialPalette = ( + input: Pick +): ColorPalette | undefined => { + return palettes.find((palette) => { + const { colors, gradient } = palette; + return gradient === input.gradient && difference(input.colors, colors).length === 0; + }); +}; + export const paulTor14 = createPalette({ id: 'paul_tor_14', label: 'Paul Tor 14', diff --git a/x-pack/plugins/canvas/i18n/elements/element_strings.test.ts b/x-pack/plugins/canvas/i18n/elements/element_strings.test.ts index 8070e86b7d7fd..6589bf36cbec5 100644 --- a/x-pack/plugins/canvas/i18n/elements/element_strings.test.ts +++ b/x-pack/plugins/canvas/i18n/elements/element_strings.test.ts @@ -6,10 +6,10 @@ */ import { getElementStrings } from './element_strings'; -import { initializeElements } from '../../canvas_plugin_src/elements'; +import { initializeElementsSpec } from '../../canvas_plugin_src/elements'; import { coreMock } from '../../../../../src/core/public/mocks'; -const elementSpecs = initializeElements(coreMock.createSetup() as any, {} as any); +const elementSpecs = initializeElementsSpec(coreMock.createSetup() as any, {} as any); describe('ElementStrings', () => { const elementStrings = getElementStrings(); diff --git a/x-pack/plugins/canvas/i18n/elements/element_strings.ts b/x-pack/plugins/canvas/i18n/elements/element_strings.ts index e1540572f4af6..c97dd1b434d51 100644 --- a/x-pack/plugins/canvas/i18n/elements/element_strings.ts +++ b/x-pack/plugins/canvas/i18n/elements/element_strings.ts @@ -230,4 +230,12 @@ export const getElementStrings = (): ElementStringDict => ({ defaultMessage: 'Tagcloud visualization', }), }, + metricVis: { + displayName: i18n.translate('xpack.canvas.elements.metricVisDisplayName', { + defaultMessage: '(New) Metric Vis', + }), + help: i18n.translate('xpack.canvas.elements.metricVisHelpText', { + defaultMessage: 'Metric visualization', + }), + }, }); diff --git a/x-pack/plugins/canvas/i18n/functions/dict/embeddable.ts b/x-pack/plugins/canvas/i18n/functions/dict/embeddable.ts new file mode 100644 index 0000000000000..279f58799e8c0 --- /dev/null +++ b/x-pack/plugins/canvas/i18n/functions/dict/embeddable.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 { i18n } from '@kbn/i18n'; +import { embeddableFunctionFactory } from '../../../canvas_plugin_src/functions/external/embeddable'; +import { FunctionHelp } from '../function_help'; +import { FunctionFactory } from '../../../types'; + +export const help: FunctionHelp>> = { + help: i18n.translate('xpack.canvas.functions.embeddableHelpText', { + defaultMessage: `Returns an embeddable with the provided configuration`, + }), + args: { + config: i18n.translate('xpack.canvas.functions.embeddable.args.idHelpText', { + defaultMessage: `The base64 encoded embeddable input object`, + }), + type: i18n.translate('xpack.canvas.functions.embeddable.args.typeHelpText', { + defaultMessage: `The embeddable type`, + }), + }, +}; diff --git a/x-pack/plugins/canvas/i18n/functions/function_help.ts b/x-pack/plugins/canvas/i18n/functions/function_help.ts index 5eae785fefa2e..520d32af1c272 100644 --- a/x-pack/plugins/canvas/i18n/functions/function_help.ts +++ b/x-pack/plugins/canvas/i18n/functions/function_help.ts @@ -27,6 +27,7 @@ import { help as demodata } from './dict/demodata'; import { help as doFn } from './dict/do'; import { help as dropdownControl } from './dict/dropdown_control'; import { help as eq } from './dict/eq'; +import { help as embeddable } from './dict/embeddable'; import { help as escount } from './dict/escount'; import { help as esdocs } from './dict/esdocs'; import { help as essql } from './dict/essql'; @@ -182,6 +183,7 @@ export const getFunctionHelp = (): FunctionHelpDict => ({ do: doFn, dropdownControl, eq, + embeddable, escount, esdocs, essql, diff --git a/x-pack/plugins/canvas/i18n/ui.ts b/x-pack/plugins/canvas/i18n/ui.ts index 30a09d51ffab4..4856de96885e7 100644 --- a/x-pack/plugins/canvas/i18n/ui.ts +++ b/x-pack/plugins/canvas/i18n/ui.ts @@ -328,6 +328,16 @@ export const ArgumentStrings = { defaultMessage: 'Select column', }), }, + StopsPalette: { + getDisplayName: () => + i18n.translate('xpack.canvas.uis.arguments.stopsPaletteTitle', { + defaultMessage: 'Palette picker with bounds', + }), + getHelp: () => + i18n.translate('xpack.canvas.uis.arguments.stopsPaletteLabel', { + defaultMessage: 'Provides colors for the values, based on the bounds', + }), + }, }; export const DataSourceStrings = { @@ -1273,4 +1283,70 @@ export const ViewStrings = { defaultMessage: 'Bucket dimension configuration', }), }, + MetricVis: { + getDisplayName: () => + i18n.translate('xpack.canvas.uis.views.metricVisTitle', { + defaultMessage: 'Metric Vis', + }), + getMetricColumnDisplayName: () => + i18n.translate('xpack.canvas.uis.views.metricVis.args.metricDisplayName', { + defaultMessage: 'Metric', + }), + getMetricColumnHelp: () => + i18n.translate('xpack.canvas.uis.views.metricVis.args.metricHelp', { + defaultMessage: 'Metric dimension configuration', + }), + getBucketColumnDisplayName: () => + i18n.translate('xpack.canvas.uis.views.metricVis.args.bucketDisplayName', { + defaultMessage: 'Bucket', + }), + getBucketColumnHelp: () => + i18n.translate('xpack.canvas.uis.views.metricVis.args.bucketHelp', { + defaultMessage: 'Bucket dimension configuration', + }), + getFontColumnDisplayName: () => + i18n.translate('xpack.canvas.uis.views.metricVis.args.fontDisplayName', { + defaultMessage: 'Font', + }), + getFontColumnHelp: () => + i18n.translate('xpack.canvas.uis.views.metricVis.args.fontHelp', { + defaultMessage: 'Metric font configuration', + }), + getPercentageModeColumnDisplayName: () => + i18n.translate('xpack.canvas.uis.views.metricVis.args.percentageModeDisplayName', { + defaultMessage: 'Enable percentage mode', + }), + getPercentageModeColumnHelp: () => + i18n.translate('xpack.canvas.uis.views.metricVis.args.percentageModeHelp', { + defaultMessage: 'Shows metric in percentage mode.', + }), + getShowLabelsColumnDisplayName: () => + i18n.translate('xpack.canvas.uis.views.metricVis.args.showLabelsDisplayName', { + defaultMessage: 'Show metric labels', + }), + getShowLabelsColumnHelp: () => + i18n.translate('xpack.canvas.uis.views.metricVis.args.showLabelsHelp', { + defaultMessage: 'Shows labels under the metric values.', + }), + getColorModeColumnDisplayName: () => + i18n.translate('xpack.canvas.uis.views.metricVis.args.colorModeDisplayName', { + defaultMessage: 'Metric color mode', + }), + getColorModeColumnHelp: () => + i18n.translate('xpack.canvas.uis.views.metricVis.args.colorModeHelp', { + defaultMessage: 'Which part of metric to fill with color.', + }), + getColorModeNoneOption: () => + i18n.translate('xpack.canvas.uis.views.metricVis.args.colorMode.noneOption', { + defaultMessage: 'None', + }), + getColorModeLabelOption: () => + i18n.translate('xpack.canvas.uis.views.metricVis.args.colorMode.labelsOption', { + defaultMessage: 'Labels', + }), + getColorModeBackgroundOption: () => + i18n.translate('xpack.canvas.uis.views.metricVis.args.colorMode.backgroundOption', { + defaultMessage: 'Background', + }), + }, }; diff --git a/x-pack/plugins/canvas/kibana.json b/x-pack/plugins/canvas/kibana.json index 9c4d1b2179d82..2fd312502a3c7 100644 --- a/x-pack/plugins/canvas/kibana.json +++ b/x-pack/plugins/canvas/kibana.json @@ -25,6 +25,7 @@ "features", "inspector", "presentationUtil", + "visualizations", "uiActions", "share" ], diff --git a/x-pack/plugins/canvas/public/components/arg_form/arg_form.js b/x-pack/plugins/canvas/public/components/arg_form/arg_form.js index 1e79b8152c9d1..0bbac0e4dd25d 100644 --- a/x-pack/plugins/canvas/public/components/arg_form/arg_form.js +++ b/x-pack/plugins/canvas/public/components/arg_form/arg_form.js @@ -5,7 +5,7 @@ * 2.0. */ -import React, { useRef, useEffect } from 'react'; +import React, { useRef, useEffect, useState } from 'react'; import PropTypes from 'prop-types'; import { ErrorBoundary } from '../enhance/error_boundary'; import { ArgSimpleForm } from './arg_simple_form'; @@ -39,11 +39,9 @@ export const ArgForm = (props) => { onValueRemove, workpad, assets, - renderError, - setRenderError, resolvedArgValue, } = props; - + const [renderError, setRenderError] = useState(false); const isMounted = useRef(); useEffect(() => { @@ -62,21 +60,15 @@ export const ArgForm = (props) => { {({ error, resetErrorState }) => { const { template, simpleTemplate } = argTypeInstance.argType; const hasError = Boolean(error) || renderError; - const argumentProps = { ...templateProps, resolvedArgValue, defaultValue: argTypeInstance.default, renderError: () => { - // TODO: don't do this - // It's an ugly hack to avoid React's render cycle and ensure the error happens on the next tick - // This is important; Otherwise we end up updating state in the middle of a render cycle - Promise.resolve().then(() => { - // Provide templates with a renderError method, and wrap the error in a known error type - // to stop Kibana's window.error from being called - isMounted.current && setRenderError(true); - }); + // Provide templates with a renderError method, and wrap the error in a known error type + // to stop Kibana's window.error from being called + isMounted.current && setRenderError(true); }, error: hasError, setLabel: (label) => isMounted.current && setLabel(label), @@ -154,7 +146,5 @@ ArgForm.propTypes = { expand: PropTypes.bool, setExpand: PropTypes.func, onValueRemove: PropTypes.func, - renderError: PropTypes.bool.isRequired, - setRenderError: PropTypes.func.isRequired, resolvedArgValue: PropTypes.any, }; diff --git a/x-pack/plugins/canvas/public/components/arg_form/arg_template_form.tsx b/x-pack/plugins/canvas/public/components/arg_form/arg_template_form.tsx index aec68993df98c..172b3f1a590e6 100644 --- a/x-pack/plugins/canvas/public/components/arg_form/arg_template_form.tsx +++ b/x-pack/plugins/canvas/public/components/arg_form/arg_template_form.tsx @@ -7,14 +7,17 @@ import React, { useState, useEffect, useCallback, useRef } from 'react'; import usePrevious from 'react-use/lib/usePrevious'; +import useEffectOnce from 'react-use/lib/useEffectOnce'; import { RenderToDom } from '../render_to_dom'; import { ExpressionFormHandlers } from '../../../common/lib/expression_form_handlers'; +import { UpdatePropsRef } from '../../../types/arguments'; interface ArgTemplateFormProps { template?: ( domNode: HTMLElement, config: ArgTemplateFormProps['argumentProps'], - handlers: ArgTemplateFormProps['handlers'] + handlers: ArgTemplateFormProps['handlers'], + onMount?: (ref: UpdatePropsRef | null) => void ) => void; argumentProps: { valueMissing?: boolean; @@ -42,11 +45,27 @@ export const ArgTemplateForm: React.FunctionComponent = ({ errorTemplate, }) => { const [updatedHandlers, setHandlers] = useState(mergeWithFormHandlers(handlers)); - const previousError = usePrevious(error); + const [mounted, setMounted] = useState(false); + const prevError = usePrevious(error); + const prevMounted = usePrevious(mounted); + const mountedArgumentRef = useRef>(); + const domNodeRef = useRef(); + + useEffectOnce(() => () => { + mountedArgumentRef.current = undefined; + }); + const renderTemplate = useCallback( - (domNode) => template && template(domNode, argumentProps, updatedHandlers), - [template, argumentProps, updatedHandlers] + (domNode) => + template && + template(domNode, argumentProps, updatedHandlers, (ref) => { + if (!mountedArgumentRef.current && ref) { + mountedArgumentRef.current = ref; + setMounted(true); + } + }), + [argumentProps, template, updatedHandlers] ); const renderErrorTemplate = useCallback( @@ -59,22 +78,30 @@ export const ArgTemplateForm: React.FunctionComponent = ({ }, [handlers]); useEffect(() => { - if (previousError !== error) { + if (!prevError && error) { updatedHandlers.destroy(); } - }, [previousError, error, updatedHandlers]); + }, [prevError, error, updatedHandlers]); useEffect(() => { - if (!error) { + if ((!error && prevError && mounted) || (mounted && !prevMounted && !error)) { renderTemplate(domNodeRef.current); } - }, [error, renderTemplate, domNodeRef]); + }, [error, mounted, prevError, prevMounted, renderTemplate]); + + useEffect(() => { + if (mountedArgumentRef.current) { + mountedArgumentRef.current?.updateProps(argumentProps); + } + }, [argumentProps]); if (error) { + mountedArgumentRef.current = undefined; return renderErrorTemplate(); } if (!template) { + mountedArgumentRef.current = undefined; return null; } @@ -82,7 +109,7 @@ export const ArgTemplateForm: React.FunctionComponent = ({ { domNodeRef.current = domNode; - renderTemplate(domNode); + setMounted(true); }} /> ); diff --git a/x-pack/plugins/canvas/public/components/arg_form/index.js b/x-pack/plugins/canvas/public/components/arg_form/index.js index 5dbc6c33db706..4ba510c120552 100644 --- a/x-pack/plugins/canvas/public/components/arg_form/index.js +++ b/x-pack/plugins/canvas/public/components/arg_form/index.js @@ -19,12 +19,10 @@ export const ArgForm = (props) => { const { argTypeInstance, label: labelFromProps, templateProps } = props; const [label, setLabel] = useState(getLabel(labelFromProps, argTypeInstance)); const [resolvedArgValue, setResolvedArgValue] = useState(null); - const [renderError, setRenderError] = useState(false); const workpad = useSelector(getWorkpadInfo); const assets = useSelector(getAssets); useEffect(() => { - setRenderError(false); setResolvedArgValue(); }, [templateProps?.argValue]); @@ -37,8 +35,6 @@ export const ArgForm = (props) => { setLabel={setLabel} resolvedArgValue={resolvedArgValue} setResolvedArgValue={setResolvedArgValue} - renderError={renderError} - setRenderError={setRenderError} /> ); }; diff --git a/x-pack/plugins/canvas/public/components/embeddable_flyout/flyout.component.tsx b/x-pack/plugins/canvas/public/components/embeddable_flyout/flyout.component.tsx index bf731876bf8c8..57f52fcf21f0f 100644 --- a/x-pack/plugins/canvas/public/components/embeddable_flyout/flyout.component.tsx +++ b/x-pack/plugins/canvas/public/components/embeddable_flyout/flyout.component.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import React, { FC } from 'react'; +import React, { FC, useCallback } from 'react'; import { EuiFlyout, EuiFlyoutHeader, EuiFlyoutBody, EuiTitle } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; @@ -27,38 +27,44 @@ const strings = { }; export interface Props { onClose: () => void; - onSelect: (id: string, embeddableType: string) => void; + onSelect: (id: string, embeddableType: string, isByValueEnabled?: boolean) => void; availableEmbeddables: string[]; + isByValueEnabled?: boolean; } -export const AddEmbeddableFlyout: FC = ({ onSelect, availableEmbeddables, onClose }) => { +export const AddEmbeddableFlyout: FC = ({ + onSelect, + availableEmbeddables, + onClose, + isByValueEnabled, +}) => { const embeddablesService = useEmbeddablesService(); const platformService = usePlatformService(); const { getEmbeddableFactories } = embeddablesService; const { getSavedObjects, getUISettings } = platformService; - const onAddPanel = (id: string, savedObjectType: string, name: string) => { - const embeddableFactories = getEmbeddableFactories(); + const onAddPanel = useCallback( + (id: string, savedObjectType: string) => { + const embeddableFactories = getEmbeddableFactories(); + // Find the embeddable type from the saved object type + const found = Array.from(embeddableFactories).find((embeddableFactory) => { + return Boolean( + embeddableFactory.savedObjectMetaData && + embeddableFactory.savedObjectMetaData.type === savedObjectType + ); + }); - // Find the embeddable type from the saved object type - const found = Array.from(embeddableFactories).find((embeddableFactory) => { - return Boolean( - embeddableFactory.savedObjectMetaData && - embeddableFactory.savedObjectMetaData.type === savedObjectType - ); - }); - - const foundEmbeddableType = found ? found.type : 'unknown'; + const foundEmbeddableType = found ? found.type : 'unknown'; - onSelect(id, foundEmbeddableType); - }; + onSelect(id, foundEmbeddableType, isByValueEnabled); + }, + [isByValueEnabled, getEmbeddableFactories, onSelect] + ); const embeddableFactories = getEmbeddableFactories(); const availableSavedObjects = Array.from(embeddableFactories) - .filter((factory) => { - return availableEmbeddables.includes(factory.type); - }) + .filter((factory) => isByValueEnabled || availableEmbeddables.includes(factory.type)) .map((factory) => factory.savedObjectMetaData) .filter>(function ( maybeSavedObjectMetaData diff --git a/x-pack/plugins/canvas/public/components/embeddable_flyout/flyout.tsx b/x-pack/plugins/canvas/public/components/embeddable_flyout/flyout.tsx index 770a4cac606b0..4dc8d963932d8 100644 --- a/x-pack/plugins/canvas/public/components/embeddable_flyout/flyout.tsx +++ b/x-pack/plugins/canvas/public/components/embeddable_flyout/flyout.tsx @@ -8,12 +8,14 @@ import React, { useMemo, useEffect, useCallback } from 'react'; import { createPortal } from 'react-dom'; import { useSelector, useDispatch } from 'react-redux'; +import { encode } from '../../../common/lib/embeddable_dataurl'; import { AddEmbeddableFlyout as Component, Props as ComponentProps } from './flyout.component'; // @ts-expect-error untyped local import { addElement } from '../../state/actions/elements'; import { getSelectedPage } from '../../state/selectors/workpad'; import { EmbeddableTypes } from '../../../canvas_plugin_src/expression_types/embeddable'; import { State } from '../../../types'; +import { useLabsService } from '../../services'; const allowedEmbeddables = { [EmbeddableTypes.map]: (id: string) => { @@ -65,6 +67,9 @@ export const AddEmbeddablePanel: React.FunctionComponent = ({ availableEmbeddables, ...restProps }) => { + const labsService = useLabsService(); + const isByValueEnabled = labsService.isProjectEnabled('labs:canvas:byValueEmbeddable'); + const dispatch = useDispatch(); const pageId = useSelector((state) => getSelectedPage(state)); @@ -74,18 +79,27 @@ export const AddEmbeddablePanel: React.FunctionComponent = ({ ); const onSelect = useCallback( - (id: string, type: string) => { + (id: string, type: string): void => { const partialElement = { expression: `markdown "Could not find embeddable for type ${type}" | render`, }; - if (allowedEmbeddables[type]) { + + // If by-value is enabled, we'll handle both by-reference and by-value embeddables + // with the new generic `embeddable` function. + // Otherwise we fallback to the embeddable type specific expressions. + if (isByValueEnabled) { + const config = encode({ savedObjectId: id }); + partialElement.expression = `embeddable config="${config}" + type="${type}" +| render`; + } else if (allowedEmbeddables[type]) { partialElement.expression = allowedEmbeddables[type](id); } addEmbeddable(pageId, partialElement); restProps.onClose(); }, - [addEmbeddable, pageId, restProps] + [addEmbeddable, pageId, restProps, isByValueEnabled] ); return ( @@ -93,6 +107,7 @@ export const AddEmbeddablePanel: React.FunctionComponent = ({ {...restProps} availableEmbeddables={availableEmbeddables || []} onSelect={onSelect} + isByValueEnabled={isByValueEnabled} /> ); }; diff --git a/x-pack/plugins/canvas/public/components/function_form/function_form.tsx b/x-pack/plugins/canvas/public/components/function_form/function_form.tsx index abe31f0105108..491bb6becf988 100644 --- a/x-pack/plugins/canvas/public/components/function_form/function_form.tsx +++ b/x-pack/plugins/canvas/public/components/function_form/function_form.tsx @@ -6,7 +6,7 @@ */ import React from 'react'; -import { FunctionFormComponent } from './function_form_component'; +import { FunctionFormComponent as Component } from './function_form_component'; import { FunctionUnknown } from './function_unknown'; import { FunctionFormContextPending } from './function_form_context_pending'; import { FunctionFormContextError } from './function_form_context_error'; @@ -56,5 +56,5 @@ export const FunctionForm: React.FunctionComponent = (props) ); } - return ; + return ; }; diff --git a/x-pack/plugins/canvas/public/components/function_form/index.tsx b/x-pack/plugins/canvas/public/components/function_form/index.tsx index aff019d1cb69c..47c6925f6fbbb 100644 --- a/x-pack/plugins/canvas/public/components/function_form/index.tsx +++ b/x-pack/plugins/canvas/public/components/function_form/index.tsx @@ -6,8 +6,9 @@ */ import React, { useCallback } from 'react'; -import { useDispatch, useSelector } from 'react-redux'; +import { shallowEqual, useDispatch, useSelector } from 'react-redux'; import { Ast } from '@kbn/interpreter/common'; +import deepEqual from 'react-fast-compare'; import { ExpressionAstExpression, ExpressionValue, @@ -49,15 +50,21 @@ interface FunctionFormProps { export const FunctionForm: React.FunctionComponent = (props) => { const { expressionIndex, argType, nextArgType } = props; const dispatch = useDispatch(); - const context = useSelector((state) => - getContextForIndex(state, expressionIndex) + const context = useSelector( + (state) => getContextForIndex(state, expressionIndex), + deepEqual ); - const element = useSelector((state) => - getSelectedElement(state) + const element = useSelector( + (state) => getSelectedElement(state), + deepEqual ); - const pageId = useSelector((state) => getSelectedPage(state)); - const assets = useSelector((state) => getAssets(state)); - const filterGroups = useSelector((state) => getGlobalFilterGroups(state)); + const pageId = useSelector((state) => getSelectedPage(state), shallowEqual); + const assets = useSelector((state) => getAssets(state), shallowEqual); + const filterGroups = useSelector( + (state) => getGlobalFilterGroups(state), + shallowEqual + ); + const addArgument = useCallback( (argName: string, argValue: string | Ast | null) => () => { dispatch( @@ -131,7 +138,6 @@ export const FunctionForm: React.FunctionComponent = (props) }, [assets, onAssetAddDispatch] ); - return ( { + const embeddablesService = useEmbeddablesService(); + const labsService = useLabsService(); + const dispatch = useDispatch(); + const isByValueEnabled = labsService.isProjectEnabled('labs:canvas:byValueEmbeddable'); + const stateTransferService = embeddablesService.getStateTransfer(); + + // fetch incoming embeddable from state transfer service. + const incomingEmbeddable = stateTransferService.getIncomingEmbeddablePackage(CANVAS_APP, true); + + useEffect(() => { + if (isByValueEnabled && incomingEmbeddable) { + const { embeddableId, input: incomingInput, type } = incomingEmbeddable; + + // retrieve existing element + const originalElement = selectedPage.elements.find( + ({ id }: CanvasElement) => id === embeddableId + ); + + if (originalElement) { + const originalAst = fromExpression(originalElement!.expression); + + const functionIndex = originalAst.chain.findIndex( + ({ function: fn }) => fn === 'embeddable' + ); + + const originalInput = decode( + originalAst.chain[functionIndex].arguments.config[0] as string + ); + + // clear out resolved arg for old embeddable + const argumentPath = [embeddableId, 'expressionRenderable']; + dispatch(clearValue({ path: argumentPath })); + + const updatedInput = { ...originalInput, ...incomingInput }; + + const expression = `embeddable config="${encode(updatedInput)}" + type="${type}" +| render`; + + dispatch( + updateEmbeddableExpression({ + elementId: originalElement.id, + embeddableExpression: expression, + }) + ); + + // update resolved args + dispatch(fetchEmbeddableRenderable(originalElement.id)); + + // select new embeddable element + dispatch(selectToplevelNodes([embeddableId])); + } else { + const expression = `embeddable config="${encode(incomingInput)}" + type="${type}" +| render`; + dispatch(addElement(selectedPage.id, { expression })); + } + } + }, [dispatch, selectedPage, incomingEmbeddable, isByValueEnabled]); +}; diff --git a/x-pack/plugins/canvas/public/components/palette_picker/index.ts b/x-pack/plugins/canvas/public/components/palette_picker/index.ts index ac9085abf0a5a..02406a4de2048 100644 --- a/x-pack/plugins/canvas/public/components/palette_picker/index.ts +++ b/x-pack/plugins/canvas/public/components/palette_picker/index.ts @@ -6,3 +6,5 @@ */ export { PalettePicker } from './palette_picker'; +export { StopsPalettePicker } from './stops_palette_picker'; +export type { PalettePickerProps, CustomColorPalette, StopsPalettePickerProps } from './types'; diff --git a/x-pack/plugins/canvas/public/components/palette_picker/palette_picker.tsx b/x-pack/plugins/canvas/public/components/palette_picker/palette_picker.tsx deleted file mode 100644 index c962dbae46b07..0000000000000 --- a/x-pack/plugins/canvas/public/components/palette_picker/palette_picker.tsx +++ /dev/null @@ -1,117 +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, { FC } from 'react'; -import PropTypes from 'prop-types'; -import { isEqual } from 'lodash'; -import { EuiColorPalettePicker, EuiColorPalettePickerPaletteProps } from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; - -import { palettes, ColorPalette } from '../../../common/lib/palettes'; - -const strings = { - getEmptyPaletteLabel: () => - i18n.translate('xpack.canvas.palettePicker.emptyPaletteLabel', { - defaultMessage: 'None', - }), - getNoPaletteFoundErrorTitle: () => - i18n.translate('xpack.canvas.palettePicker.noPaletteFoundErrorTitle', { - defaultMessage: 'Color palette not found', - }), -}; - -interface RequiredProps { - id?: string; - onChange?: (palette: ColorPalette) => void; - palette: ColorPalette; - clearable?: false; -} - -interface ClearableProps { - id?: string; - onChange?: (palette: ColorPalette | null) => void; - palette: ColorPalette | null; - clearable: true; -} - -type Props = RequiredProps | ClearableProps; - -const findPalette = (colorPalette: ColorPalette | null, colorPalettes: ColorPalette[] = []) => { - const palette = colorPalettes.filter((cp) => cp.id === colorPalette?.id)[0] ?? null; - if (palette === null) { - return colorPalettes.filter((cp) => isEqual(cp.colors, colorPalette?.colors))[0] ?? null; - } - - return palette; -}; - -export const PalettePicker: FC = (props) => { - const colorPalettes: EuiColorPalettePickerPaletteProps[] = palettes.map((item) => ({ - value: item.id, - title: item.label, - type: item.gradient ? 'gradient' : 'fixed', - palette: item.colors, - })); - - if (props.clearable) { - const { palette, onChange = () => {} } = props; - - colorPalettes.unshift({ - value: 'clear', - title: strings.getEmptyPaletteLabel(), - type: 'text', - }); - - const onPickerChange = (value: string) => { - const canvasPalette = palettes.find((item) => item.id === value); - onChange(canvasPalette || null); - }; - - const foundPalette = findPalette(palette, palettes); - - return ( - - ); - } - - const { palette, onChange = () => {} } = props; - - const onPickerChange = (value: string) => { - const canvasPalette = palettes.find((item) => item.id === value); - - if (!canvasPalette) { - throw new Error(strings.getNoPaletteFoundErrorTitle()); - } - - onChange(canvasPalette); - }; - - const foundPalette = findPalette(palette, palettes); - - return ( - - ); -}; - -PalettePicker.propTypes = { - id: PropTypes.string, - palette: PropTypes.object, - onChange: PropTypes.func, - clearable: PropTypes.bool, -}; diff --git a/x-pack/plugins/canvas/public/components/palette_picker/__stories__/__snapshots__/palette_picker.stories.storyshot b/x-pack/plugins/canvas/public/components/palette_picker/palette_picker/__stories__/__snapshots__/palette_picker.stories.storyshot similarity index 100% rename from x-pack/plugins/canvas/public/components/palette_picker/__stories__/__snapshots__/palette_picker.stories.storyshot rename to x-pack/plugins/canvas/public/components/palette_picker/palette_picker/__stories__/__snapshots__/palette_picker.stories.storyshot diff --git a/x-pack/plugins/canvas/public/components/palette_picker/__stories__/palette_picker.stories.tsx b/x-pack/plugins/canvas/public/components/palette_picker/palette_picker/__stories__/palette_picker.stories.tsx similarity index 76% rename from x-pack/plugins/canvas/public/components/palette_picker/__stories__/palette_picker.stories.tsx rename to x-pack/plugins/canvas/public/components/palette_picker/palette_picker/__stories__/palette_picker.stories.tsx index d097556285a6e..3c1b5f3537ac0 100644 --- a/x-pack/plugins/canvas/public/components/palette_picker/__stories__/palette_picker.stories.tsx +++ b/x-pack/plugins/canvas/public/components/palette_picker/palette_picker/__stories__/palette_picker.stories.tsx @@ -8,12 +8,13 @@ import React, { FC, useState } from 'react'; import { action } from '@storybook/addon-actions'; import { storiesOf } from '@storybook/react'; -import { PalettePicker } from '../palette_picker'; +import { PalettePicker } from '../../palette_picker'; -import { paulTor14, ColorPalette } from '../../../../common/lib/palettes'; +import { paulTor14, ColorPalette } from '../../../../../common/lib/palettes'; +import { CustomColorPalette } from '../../types'; const Interactive: FC = () => { - const [palette, setPalette] = useState(paulTor14); + const [palette, setPalette] = useState(paulTor14); return ; }; diff --git a/x-pack/plugins/canvas/public/components/palette_picker/palette_picker/clearable_palette_picker.tsx b/x-pack/plugins/canvas/public/components/palette_picker/palette_picker/clearable_palette_picker.tsx new file mode 100644 index 0000000000000..1dd4d7355050d --- /dev/null +++ b/x-pack/plugins/canvas/public/components/palette_picker/palette_picker/clearable_palette_picker.tsx @@ -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 { EuiColorPalettePicker } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import React, { FC } from 'react'; +import { ClearableComponentProps } from '../types'; +import { findPalette, prepareColorPalette } from '../utils'; + +const strings = { + getEmptyPaletteLabel: () => + i18n.translate('xpack.canvas.palettePicker.emptyPaletteLabel', { + defaultMessage: 'None', + }), +}; + +export const ClearablePalettePicker: FC = (props) => { + const { palette, palettes, onChange = () => {} } = props; + const colorPalettes = palettes.map(prepareColorPalette); + + const onPickerChange = (value: string) => { + const canvasPalette = palettes.find((item) => item.id === value); + onChange(canvasPalette || null); + }; + + const foundPalette = findPalette(palette ?? null, palettes); + + return ( + + ); +}; diff --git a/x-pack/plugins/canvas/public/components/palette_picker/palette_picker/default_palette_picker.tsx b/x-pack/plugins/canvas/public/components/palette_picker/palette_picker/default_palette_picker.tsx new file mode 100644 index 0000000000000..c63964075e5b4 --- /dev/null +++ b/x-pack/plugins/canvas/public/components/palette_picker/palette_picker/default_palette_picker.tsx @@ -0,0 +1,45 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { EuiColorPalettePicker } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import React, { FC } from 'react'; +import { RequiredComponentProps } from '../types'; +import { findPalette, prepareColorPalette } from '../utils'; + +const strings = { + getNoPaletteFoundErrorTitle: () => + i18n.translate('xpack.canvas.palettePicker.noPaletteFoundErrorTitle', { + defaultMessage: 'Color palette not found', + }), +}; + +export const DefaultPalettePicker: FC = (props) => { + const { palette, palettes, onChange = () => {} } = props; + const colorPalettes = palettes.map(prepareColorPalette); + + const onPickerChange = (value: string) => { + const canvasPalette = palettes.find((item) => item.id === value); + if (!canvasPalette) { + throw new Error(strings.getNoPaletteFoundErrorTitle()); + } + + onChange(canvasPalette); + }; + + const foundPalette = findPalette(palette ?? null, palettes); + + return ( + + ); +}; diff --git a/x-pack/plugins/canvas/public/components/palette_picker/palette_picker/index.ts b/x-pack/plugins/canvas/public/components/palette_picker/palette_picker/index.ts new file mode 100644 index 0000000000000..ac9085abf0a5a --- /dev/null +++ b/x-pack/plugins/canvas/public/components/palette_picker/palette_picker/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 { PalettePicker } from './palette_picker'; diff --git a/x-pack/plugins/canvas/public/components/palette_picker/palette_picker/palette_picker.tsx b/x-pack/plugins/canvas/public/components/palette_picker/palette_picker/palette_picker.tsx new file mode 100644 index 0000000000000..064a9bd217abd --- /dev/null +++ b/x-pack/plugins/canvas/public/components/palette_picker/palette_picker/palette_picker.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, { FC } from 'react'; +import PropTypes from 'prop-types'; +import { ClearablePalettePicker } from './clearable_palette_picker'; +import { palettes as defaultPalettes } from '../../../../common/lib/palettes'; +import { PalettePickerProps } from '../types'; +import { DefaultPalettePicker } from './default_palette_picker'; + +export const PalettePicker: FC = (props) => { + const { additionalPalettes = [] } = props; + const palettes = [...defaultPalettes, ...additionalPalettes]; + + if (props.clearable) { + return ( + + ); + } + + return ( + + ); +}; + +PalettePicker.propTypes = { + id: PropTypes.string, + palette: PropTypes.object, + onChange: PropTypes.func, + clearable: PropTypes.bool, +}; diff --git a/x-pack/plugins/canvas/public/components/palette_picker/stops_palette_picker/index.ts b/x-pack/plugins/canvas/public/components/palette_picker/stops_palette_picker/index.ts new file mode 100644 index 0000000000000..8782055147c49 --- /dev/null +++ b/x-pack/plugins/canvas/public/components/palette_picker/stops_palette_picker/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 { StopsPalettePicker } from './stops_palette_picker'; diff --git a/x-pack/plugins/canvas/public/components/palette_picker/stops_palette_picker/stop_color_picker.tsx b/x-pack/plugins/canvas/public/components/palette_picker/stops_palette_picker/stop_color_picker.tsx new file mode 100644 index 0000000000000..6c5ff2923c8d2 --- /dev/null +++ b/x-pack/plugins/canvas/public/components/palette_picker/stops_palette_picker/stop_color_picker.tsx @@ -0,0 +1,133 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { + EuiButtonIcon, + EuiColorPicker, + EuiFieldNumber, + EuiFlexGroup, + EuiFlexItem, +} from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import React, { FC, useEffect, useState } from 'react'; +import useDebounce from 'react-use/lib/useDebounce'; +import { ColorStop } from '../types'; + +interface Props { + removable?: boolean; + stop?: number; + color?: string; + onDelete: () => void; + onChange: (colorStop: ColorStop) => void; +} + +interface ValidationResult { + color: boolean; + stop: boolean; +} + +const strings = { + getDeleteStopColorLabel: () => + i18n.translate('xpack.canvas.stopsColorPicker.deleteColorStopLabel', { + defaultMessage: 'Delete', + }), +}; + +const isValidColorStop = (colorStop: ColorStop): ValidationResult & { valid: boolean } => { + const valid = !isNaN(colorStop.stop); + return { + valid, + stop: valid, + color: true, + }; +}; + +export const StopColorPicker: FC = (props) => { + const { stop, color, onDelete, onChange, removable = true } = props; + + const [colorStop, setColorStop] = useState({ stop: stop ?? 0, color: color ?? '' }); + const [areValidFields, setAreValidFields] = useState({ + stop: true, + color: true, + }); + + const onChangeInput = (updatedColorStop: ColorStop) => { + setColorStop(updatedColorStop); + }; + + const [, cancel] = useDebounce( + () => { + if (color === colorStop.color && stop === colorStop.stop) { + return; + } + + const { valid, ...validationResult } = isValidColorStop(colorStop); + if (!valid) { + setAreValidFields(validationResult); + return; + } + + onChange(colorStop); + }, + 150, + [colorStop] + ); + + useEffect(() => { + const newColorStop = { stop: stop ?? 0, color: color ?? '' }; + setColorStop(newColorStop); + + const { valid, ...validationResult } = isValidColorStop(newColorStop); + setAreValidFields(validationResult); + }, [color, stop]); + + useEffect(() => { + return () => { + cancel(); + }; + }, [cancel]); + + return ( + + + + onChangeInput({ ...colorStop, stop: valueAsNumber }) + } + isInvalid={!areValidFields.stop} + /> + + + + { + onChangeInput({ ...colorStop, color: newColor }); + }} + isInvalid={!areValidFields.color} + /> + + + + + + + ); +}; diff --git a/x-pack/plugins/canvas/public/components/palette_picker/stops_palette_picker/stops_palette_picker.tsx b/x-pack/plugins/canvas/public/components/palette_picker/stops_palette_picker/stops_palette_picker.tsx new file mode 100644 index 0000000000000..aab48528770cb --- /dev/null +++ b/x-pack/plugins/canvas/public/components/palette_picker/stops_palette_picker/stops_palette_picker.tsx @@ -0,0 +1,147 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license 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, useCallback, useMemo } from 'react'; +import { flowRight, identity } from 'lodash'; +import { EuiButtonEmpty, EuiFlexGroup, EuiFlexItem, EuiFormRow, EuiSpacer } from '@elastic/eui'; +import useEffectOnce from 'react-use/lib/useEffectOnce'; +import { i18n } from '@kbn/i18n'; +import { ColorStop, CustomColorPalette, StopsPalettePickerProps } from '../types'; +import { PalettePicker } from '../palette_picker'; +import { StopColorPicker } from './stop_color_picker'; +import { Palette } from './types'; +import { + reduceColorsByStopsSize, + transformPaletteToColorStops, + mergeColorStopsWithPalette, + deleteColorStop, + updateColorStop, + addNewColorStop, + getOverridenPaletteOptions, +} from './utils'; +import { ColorPalette } from '../../../../common/lib/palettes'; + +const strings = { + getAddColorStopLabel: () => + i18n.translate('xpack.canvas.stopsPalettePicker.addColorStopLabel', { + defaultMessage: 'Add color stop', + }), + getColorStopsLabel: () => + i18n.translate('xpack.canvas.stopsPalettePicker.colorStopsLabel', { + defaultMessage: 'Color stops', + }), +}; + +const defaultStops = [0, 1]; +const MIN_STOPS = 2; + +export const StopsPalettePicker: FC = (props) => { + const { palette, onChange } = props; + const stops = useMemo( + () => (!palette?.stops || !palette.stops.length ? defaultStops : palette.stops), + [palette?.stops] + ); + + const colors = useMemo( + () => reduceColorsByStopsSize(palette?.colors, stops.length), + [palette?.colors, stops.length] + ); + + const onChangePalette = useCallback( + (newPalette: ColorPalette | CustomColorPalette | null) => { + if (newPalette) { + const newColors = reduceColorsByStopsSize(newPalette?.colors, stops.length); + props.onChange?.({ + ...palette, + ...newPalette, + colors: newColors, + stops, + }); + } + }, + [palette, props, stops] + ); + + useEffectOnce(() => { + onChangePalette({ ...getOverridenPaletteOptions(), ...palette }); + }); + + const paletteColorStops = useMemo( + () => transformPaletteToColorStops({ stops, colors }), + [colors, stops] + ); + + const updatePalette = useCallback( + (fn: (colorStops: ColorStop[]) => ColorStop[]) => + flowRight( + onChange ?? identity, + mergeColorStopsWithPalette(palette), + fn + ), + [onChange, palette] + ); + + const deleteColorStopAndApply = useCallback( + (index: number) => updatePalette(deleteColorStop(index))(paletteColorStops), + [paletteColorStops, updatePalette] + ); + + const updateColorStopAndApply = useCallback( + (index: number, colorStop: ColorStop) => + updatePalette(updateColorStop(index, colorStop))(paletteColorStops), + [paletteColorStops, updatePalette] + ); + + const addColorStopAndApply = useCallback( + () => updatePalette(addNewColorStop(palette))(paletteColorStops), + [palette, paletteColorStops, updatePalette] + ); + + const stopColorPickers = paletteColorStops.map(({ id, ...rest }, index) => ( + + = MIN_STOPS} + onDelete={() => deleteColorStopAndApply(index)} + onChange={(cp: ColorStop) => updateColorStopAndApply(index, cp)} + /> + + )); + + return ( + <> + + + + + + + {stopColorPickers} + + + + + + + {strings.getAddColorStopLabel()} + + + ); +}; diff --git a/x-pack/plugins/canvas/public/components/palette_picker/stops_palette_picker/types.ts b/x-pack/plugins/canvas/public/components/palette_picker/stops_palette_picker/types.ts new file mode 100644 index 0000000000000..fab6fd218608d --- /dev/null +++ b/x-pack/plugins/canvas/public/components/palette_picker/stops_palette_picker/types.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 { CustomColorPalette } from '../types'; +import { ColorPalette } from '../../../../common/lib/palettes'; + +export type Palette = ColorPalette | CustomColorPalette; +export type PaletteColorStops = Pick; diff --git a/x-pack/plugins/canvas/public/components/palette_picker/stops_palette_picker/utils.ts b/x-pack/plugins/canvas/public/components/palette_picker/stops_palette_picker/utils.ts new file mode 100644 index 0000000000000..0dad7f7c3abe3 --- /dev/null +++ b/x-pack/plugins/canvas/public/components/palette_picker/stops_palette_picker/utils.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 { zip, take } from 'lodash'; +import { htmlIdGenerator } from '@elastic/eui'; +import { ColorPalette } from '../../../../common/lib'; +import { ColorStop } from '../types'; +import { Palette, PaletteColorStops } from './types'; + +const id = htmlIdGenerator(); + +export const getOverridenPaletteOptions = (): Pick => ({ + range: 'number', + continuity: 'below', +}); + +export const createColorStop = (stop: number = 0, color: string = '') => ({ + stop, + color, + id: id(), +}); + +export const transformPaletteToColorStops = ({ stops = [], colors }: PaletteColorStops) => + zip(stops, colors).map(([stop, color]) => createColorStop(stop, color)); + +export const mergeColorStopsWithPalette = + (palette: Palette) => + (colorStops: ColorStop[]): Palette => { + const stopsWithColors = colorStops.reduce<{ colors: string[]; stops: number[] }>( + (acc, { color, stop }) => { + acc.colors.push(color ?? ''); + acc.stops.push(stop); + return acc; + }, + { colors: [], stops: [] } + ); + return { ...palette, ...stopsWithColors }; + }; + +export const updateColorStop = + (index: number, colorStop: ColorStop) => (colorStops: ColorStop[]) => { + colorStops.splice(index, 1, colorStop); + return colorStops; + }; + +export const deleteColorStop = (index: number) => (colorStops: ColorStop[]) => { + colorStops.splice(index, 1); + return colorStops; +}; + +export const addNewColorStop = (palette: Palette) => (colorStops: ColorStop[]) => { + const lastColorStopIndex = colorStops.length - 1; + const lastStop = lastColorStopIndex >= 0 ? colorStops[lastColorStopIndex].stop + 1 : 0; + const newIndex = lastColorStopIndex + 1; + return [ + ...colorStops, + { + stop: lastStop, + color: + palette.colors.length > newIndex + 1 + ? palette.colors[newIndex] + : palette.colors[palette.colors.length - 1], + }, + ]; +}; + +export const reduceColorsByStopsSize = (colors: string[] = [], stopsSize: number) => { + const reducedColors = take(colors, stopsSize); + const colorsLength = reducedColors.length; + if (colorsLength === stopsSize) { + return reducedColors; + } + + return [ + ...reducedColors, + ...Array(stopsSize - colorsLength).fill(reducedColors[colorsLength - 1] ?? ''), + ]; +}; diff --git a/x-pack/plugins/canvas/public/components/palette_picker/types.ts b/x-pack/plugins/canvas/public/components/palette_picker/types.ts new file mode 100644 index 0000000000000..f297c0064a265 --- /dev/null +++ b/x-pack/plugins/canvas/public/components/palette_picker/types.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 { ColorPalette } from '../../../common/lib/palettes'; + +export type CustomColorPalette = ColorPalette<'custom'>; + +export interface RequiredProps { + id?: string; + onChange?: (palette: ColorPalette | CustomColorPalette) => void; + palette: ColorPalette | CustomColorPalette; + clearable?: false; + additionalPalettes?: Array; +} + +export interface ClearableProps { + id?: string; + onChange?: (palette: ColorPalette | CustomColorPalette | null) => void; + palette: ColorPalette | CustomColorPalette | null; + clearable: true; + additionalPalettes?: Array; +} + +export type PalettePickerProps = RequiredProps | ClearableProps; +export type StopsPalettePickerProps = RequiredProps; + +export type ClearableComponentProps = { + palettes: Array; +} & Partial> & + Pick; + +export type RequiredComponentProps = { + palettes: Array; +} & Partial>; + +export interface ColorStop { + color: string; + stop: number; +} diff --git a/x-pack/plugins/canvas/public/components/palette_picker/utils.ts b/x-pack/plugins/canvas/public/components/palette_picker/utils.ts new file mode 100644 index 0000000000000..57d9220069ffd --- /dev/null +++ b/x-pack/plugins/canvas/public/components/palette_picker/utils.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 { EuiColorPalettePickerPaletteProps } from '@elastic/eui'; +import { isEqual } from 'lodash'; +import { ColorPalette } from '../../../common/lib/palettes'; +import { CustomColorPalette } from './types'; + +export const findPalette = ( + colorPalette: ColorPalette | CustomColorPalette | null, + colorPalettes: Array = [] +) => { + const palette = colorPalettes.filter((cp) => cp.id === colorPalette?.id)[0] ?? null; + if (palette === null) { + return colorPalettes.filter((cp) => isEqual(cp.colors, colorPalette?.colors))[0] ?? null; + } + + return palette; +}; + +export const prepareColorPalette = ({ + id, + label, + gradient, + colors, +}: ColorPalette | CustomColorPalette): EuiColorPalettePickerPaletteProps => ({ + value: id, + title: label, + type: gradient ? 'gradient' : 'fixed', + palette: colors, +}); diff --git a/x-pack/plugins/canvas/public/components/sidebar/element_settings/element_settings.component.tsx b/x-pack/plugins/canvas/public/components/sidebar/element_settings/element_settings.component.tsx index e8f2c7a559f58..a912668d91432 100644 --- a/x-pack/plugins/canvas/public/components/sidebar/element_settings/element_settings.component.tsx +++ b/x-pack/plugins/canvas/public/components/sidebar/element_settings/element_settings.component.tsx @@ -61,7 +61,6 @@ export const ElementSettings: FunctionComponent = ({ element }) => { ), }, ]; - return ; }; diff --git a/x-pack/plugins/canvas/public/components/sidebar/element_settings/element_settings.tsx b/x-pack/plugins/canvas/public/components/sidebar/element_settings/element_settings.tsx index 4935647ca6810..2de52c996e7dd 100644 --- a/x-pack/plugins/canvas/public/components/sidebar/element_settings/element_settings.tsx +++ b/x-pack/plugins/canvas/public/components/sidebar/element_settings/element_settings.tsx @@ -6,31 +6,24 @@ */ import React from 'react'; -import { connect } from 'react-redux'; +import deepEqual from 'react-fast-compare'; +import { useSelector } from 'react-redux'; import { getElementById, getSelectedPage } from '../../../state/selectors/workpad'; import { ElementSettings as Component } from './element_settings.component'; -import { State, PositionedElement } from '../../../../types'; +import { State } from '../../../../types'; interface Props { selectedElementId: string | null; } -const mapStateToProps = (state: State, { selectedElementId }: Props): StateProps => ({ - element: getElementById(state, selectedElementId, getSelectedPage(state)), -}); +export const ElementSettings: React.FC = ({ selectedElementId }) => { + const element = useSelector((state: State) => { + return getElementById(state, selectedElementId, getSelectedPage(state)); + }, deepEqual); -interface StateProps { - element: PositionedElement | undefined; -} - -const renderIfElement: React.FunctionComponent = (props) => { - if (props.element) { - return ; + if (element) { + return ; } return null; }; - -export const ElementSettings = connect(mapStateToProps)( - renderIfElement -); diff --git a/x-pack/plugins/canvas/public/components/sidebar/sidebar_content/sidebar_content.tsx b/x-pack/plugins/canvas/public/components/sidebar/sidebar_content/sidebar_content.tsx index e53f5d6d515df..cc7bfa7d11195 100644 --- a/x-pack/plugins/canvas/public/components/sidebar/sidebar_content/sidebar_content.tsx +++ b/x-pack/plugins/canvas/public/components/sidebar/sidebar_content/sidebar_content.tsx @@ -6,7 +6,7 @@ */ import React from 'react'; -import { useSelector } from 'react-redux'; +import { shallowEqual, useSelector } from 'react-redux'; import { getSelectedToplevelNodes, getSelectedElementId } from '../../../state/selectors/workpad'; import { State } from '../../../../types'; import { SidebarContent as Component } from './sidebar_content.component'; @@ -16,12 +16,14 @@ interface SidebarContentProps { } export const SidebarContent: React.FC = ({ commit }) => { - const selectedToplevelNodes = useSelector((state) => - getSelectedToplevelNodes(state) + const selectedToplevelNodes = useSelector( + (state) => getSelectedToplevelNodes(state), + shallowEqual ); - const selectedElementId = useSelector((state) => - getSelectedElementId(state) + const selectedElementId = useSelector( + (state) => getSelectedElementId(state), + shallowEqual ); return ( diff --git a/x-pack/plugins/canvas/public/components/workpad/workpad.tsx b/x-pack/plugins/canvas/public/components/workpad/workpad.tsx index 622c885b6ef28..7cc077203c737 100644 --- a/x-pack/plugins/canvas/public/components/workpad/workpad.tsx +++ b/x-pack/plugins/canvas/public/components/workpad/workpad.tsx @@ -27,6 +27,7 @@ import { WorkpadRoutingContext } from '../../routes/workpad'; import { usePlatformService } from '../../services'; import { Workpad as WorkpadComponent, Props } from './workpad.component'; import { State } from '../../../types'; +import { useIncomingEmbeddable } from '../hooks'; type ContainerProps = Pick; @@ -58,6 +59,9 @@ export const Workpad: FC = (props) => { }; }); + const selectedPage = propsFromState.pages[propsFromState.selectedPageNumber - 1]; + useIncomingEmbeddable(selectedPage); + const fetchAllRenderables = useCallback(() => { dispatch(fetchAllRenderablesAction()); }, [dispatch]); diff --git a/x-pack/plugins/canvas/public/components/workpad_app/workpad_app.component.tsx b/x-pack/plugins/canvas/public/components/workpad_app/workpad_app.component.tsx index 6e380088bd84b..0d9ff68c9f46f 100644 --- a/x-pack/plugins/canvas/public/components/workpad_app/workpad_app.component.tsx +++ b/x-pack/plugins/canvas/public/components/workpad_app/workpad_app.component.tsx @@ -16,7 +16,7 @@ import { CommitFn } from '../../../types'; export const WORKPAD_CONTAINER_ID = 'canvasWorkpadContainer'; -interface Props { +export interface Props { deselectElement?: MouseEventHandler; isWriteable: boolean; } diff --git a/x-pack/plugins/canvas/public/components/workpad_app/workpad_app.scss b/x-pack/plugins/canvas/public/components/workpad_app/workpad_app.scss index 4acdca10d61cc..0ddd44ed8f9a8 100644 --- a/x-pack/plugins/canvas/public/components/workpad_app/workpad_app.scss +++ b/x-pack/plugins/canvas/public/components/workpad_app/workpad_app.scss @@ -31,7 +31,7 @@ $canvasLayoutFontSize: $euiFontSizeS; .canvasLayout__stageHeader { flex-grow: 0; flex-basis: auto; - padding: $euiSizeS; + padding: $euiSizeS $euiSize; font-size: $canvasLayoutFontSize; border-bottom: $euiBorderThin; background: $euiColorLightestShade; diff --git a/x-pack/plugins/canvas/public/components/workpad_app/workpad_app.ts b/x-pack/plugins/canvas/public/components/workpad_app/workpad_app.ts index fe98e0f4b1bff..bf859ceb43161 100644 --- a/x-pack/plugins/canvas/public/components/workpad_app/workpad_app.ts +++ b/x-pack/plugins/canvas/public/components/workpad_app/workpad_app.ts @@ -6,8 +6,8 @@ */ import { MouseEventHandler } from 'react'; -import { Dispatch } from 'redux'; import { connect } from 'react-redux'; +import { Dispatch } from 'redux'; // @ts-expect-error untyped local import { selectToplevelNodes } from '../../state/actions/transient'; import { canUserWrite } from '../../state/selectors/app'; @@ -18,6 +18,8 @@ import { State } from '../../../types'; export { WORKPAD_CONTAINER_ID } from './workpad_app.component'; +const WorkpadAppComponent = withElementsLoadedTelemetry(Component); + const mapDispatchToProps = (dispatch: Dispatch): { deselectElement: MouseEventHandler } => ({ deselectElement: (ev) => { ev.stopPropagation(); @@ -31,4 +33,4 @@ export const WorkpadApp = connect( workpad: getWorkpad(state), }), mapDispatchToProps -)(withElementsLoadedTelemetry(Component)); +)(WorkpadAppComponent); diff --git a/x-pack/plugins/canvas/public/components/workpad_app/workpad_telemetry.test.tsx b/x-pack/plugins/canvas/public/components/workpad_app/workpad_telemetry.test.tsx index 54fd871203f1f..3c2ab0bf8175f 100644 --- a/x-pack/plugins/canvas/public/components/workpad_app/workpad_telemetry.test.tsx +++ b/x-pack/plugins/canvas/public/components/workpad_app/workpad_telemetry.test.tsx @@ -6,6 +6,7 @@ */ import React from 'react'; +import { useSelector } from 'react-redux'; import { render } from '@testing-library/react'; import { withUnconnectedElementsLoadedTelemetry, @@ -13,148 +14,137 @@ import { WorkpadLoadedWithErrorsMetric, } from './workpad_telemetry'; import { METRIC_TYPE } from '../../lib/ui_metric'; -import { ResolvedArgType } from '../../../types'; +import { ExpressionContext, ResolvedArgType } from '../../../types'; + +jest.mock('react-redux', () => { + const originalModule = jest.requireActual('react-redux'); + + return { + ...originalModule, + useSelector: jest.fn(), + }; +}); const trackMetric = jest.fn(); +const useSelectorMock = useSelector as jest.Mock; + const Component = withUnconnectedElementsLoadedTelemetry(() =>
    , trackMetric); const mockWorkpad = { id: 'workpadid', pages: [ { - elements: [{ id: '1' }, { id: '2' }, { id: '3' }, { id: '4' }], + elements: [{ id: '0' }, { id: '1' }, { id: '2' }, { id: '3' }], }, { - elements: [{ id: '5' }], + elements: [{ id: '4' }], }, ], }; -const resolvedArgsMatchWorkpad = { - '1': {} as ResolvedArgType, - '2': {} as ResolvedArgType, - '3': {} as ResolvedArgType, - '4': {} as ResolvedArgType, - '5': {} as ResolvedArgType, -}; +const getMockState = (resolvedArgs: Record) => ({ + transient: { resolvedArgs }, +}); -const resolvedArgsNotMatchWorkpad = { - 'non-matching-id': {} as ResolvedArgType, -}; +const getResolveArgWithState = (state: 'pending' | 'ready' | 'error') => + ({ + expressionRenderable: { value: { as: state, type: 'render' }, state, error: null }, + expressionContext: {} as ExpressionContext, + } as ResolvedArgType); -const pendingCounts = { - pending: 5, - error: 0, - ready: 0, -}; +const arrayToObject = (array: ResolvedArgType[]) => + array.reduce>((acc, el, index) => { + acc[index] = el; + return acc; + }, {}); -const readyCounts = { - pending: 0, - error: 0, - ready: 5, -}; +const pendingMockState = getMockState( + arrayToObject(Array(5).fill(getResolveArgWithState('pending'))) +); -const errorCounts = { - pending: 0, - error: 1, - ready: 4, -}; +const readyMockState = getMockState(arrayToObject(Array(5).fill(getResolveArgWithState('ready')))); + +const errorMockState = getMockState( + arrayToObject([ + ...Array(4).fill(getResolveArgWithState('ready')), + ...Array(1).fill(getResolveArgWithState('error')), + ]) +); + +const emptyElementsMockState = getMockState({}); + +const notMatchedMockState = getMockState({ + 'non-matching-id': getResolveArgWithState('ready'), +}); describe('Elements Loaded Telemetry', () => { beforeEach(() => { trackMetric.mockReset(); }); + afterEach(() => { + useSelectorMock.mockClear(); + }); + it('tracks when all resolvedArgs are completed', () => { - const { rerender } = render( - - ); + useSelectorMock.mockImplementation((callback) => { + return callback(pendingMockState); + }); + const { rerender } = render(); expect(trackMetric).not.toBeCalled(); - rerender( - - ); + useSelectorMock.mockClear(); + useSelectorMock.mockImplementation((callback) => { + return callback(readyMockState); + }); + rerender(); expect(trackMetric).toBeCalledWith(METRIC_TYPE.LOADED, WorkpadLoadedMetric); }); it('only tracks loaded once', () => { - const { rerender } = render( - - ); + useSelectorMock.mockImplementation((callback) => { + return callback(pendingMockState); + }); + const { rerender } = render(); expect(trackMetric).not.toBeCalled(); - rerender( - - ); - rerender( - - ); + useSelectorMock.mockClear(); + useSelectorMock.mockImplementation((callback) => { + return callback(readyMockState); + }); + rerender(); + rerender(); expect(trackMetric).toBeCalledTimes(1); }); it('does not track if resolvedArgs are never pending', () => { - const { rerender } = render( - - ); - - rerender( - - ); + useSelectorMock.mockImplementation((callback) => { + return callback(readyMockState); + }); + const { rerender } = render(); + rerender(); expect(trackMetric).not.toBeCalled(); }); it('tracks if elements are in error state after load', () => { - const { rerender } = render( - - ); + useSelectorMock.mockImplementation((callback) => { + return callback(pendingMockState); + }); + const { rerender } = render(); expect(trackMetric).not.toBeCalled(); - rerender( - - ); + useSelectorMock.mockClear(); + useSelectorMock.mockImplementation((callback) => { + return callback(errorMockState); + }); + rerender(); expect(trackMetric).toBeCalledWith(METRIC_TYPE.LOADED, [ WorkpadLoadedMetric, WorkpadLoadedWithErrorsMetric, @@ -166,42 +156,30 @@ describe('Elements Loaded Telemetry', () => { id: 'otherworkpad', pages: [ { - elements: [{ id: '1' }, { id: '2' }, { id: '3' }, { id: '4' }], + elements: [{ id: '0' }, { id: '1' }, { id: '2' }, { id: '3' }], }, { - elements: [{ id: '5' }], + elements: [{ id: '4' }], }, ], }; - const { rerender } = render( - - ); + useSelectorMock.mockImplementation((callback) => { + return callback(notMatchedMockState); + }); + const { rerender } = render(); expect(trackMetric).not.toBeCalled(); - rerender( - - ); - + rerender(); expect(trackMetric).not.toBeCalled(); - rerender( - - ); + useSelectorMock.mockClear(); + useSelectorMock.mockImplementation((callback) => { + return callback(readyMockState); + }); + rerender(); expect(trackMetric).toBeCalledWith(METRIC_TYPE.LOADED, WorkpadLoadedMetric); }); @@ -211,24 +189,12 @@ describe('Elements Loaded Telemetry', () => { pages: [], }; - const resolvedArgs = {}; - - const { rerender } = render( - - ); - - rerender( - - ); + useSelectorMock.mockImplementation((callback) => { + return callback(emptyElementsMockState); + }); + const { rerender } = render(); + rerender(); expect(trackMetric).not.toBeCalled(); }); }); diff --git a/x-pack/plugins/canvas/public/components/workpad_app/workpad_telemetry.tsx b/x-pack/plugins/canvas/public/components/workpad_app/workpad_telemetry.tsx index 0915c757ff893..d74f8693bc9bd 100644 --- a/x-pack/plugins/canvas/public/components/workpad_app/workpad_telemetry.tsx +++ b/x-pack/plugins/canvas/public/components/workpad_app/workpad_telemetry.tsx @@ -6,21 +6,18 @@ */ import React, { useState, useEffect } from 'react'; -import { connect, ConnectedProps } from 'react-redux'; +import { shallowEqual, useSelector } from 'react-redux'; +import deepEqual from 'react-fast-compare'; import { trackCanvasUiMetric, METRIC_TYPE } from '../../lib/ui_metric'; import { getElementCounts } from '../../state/selectors/workpad'; import { getArgs } from '../../state/selectors/resolved_args'; +import { State } from '../../../types'; const WorkpadLoadedMetric = 'workpad-loaded'; const WorkpadLoadedWithErrorsMetric = 'workpad-loaded-with-errors'; export { WorkpadLoadedMetric, WorkpadLoadedWithErrorsMetric }; -const mapStateToProps = (state: any) => ({ - telemetryElementCounts: getElementCounts(state), - telemetryResolvedArgs: getArgs(state), -}); - // TODO: Build out full workpad types /** Individual Page of a Workpad @@ -47,7 +44,7 @@ interface ResolvedArgs { [keys: string]: any; } -export interface ElementsLoadedTelemetryProps extends PropsFromRedux { +export interface ElementsLoadedTelemetryProps { workpad: Workpad; } @@ -65,33 +62,32 @@ export const withUnconnectedElementsLoadedTelemetry =

    ( Component: React.ComponentType

    , trackMetric = trackCanvasUiMetric ) => - function ElementsLoadedTelemetry(props: ElementsLoadedTelemetryProps) { - const { telemetryElementCounts, workpad, telemetryResolvedArgs, ...other } = props; - const { error, pending } = telemetryElementCounts; + function ElementsLoadedTelemetry(props: P & ElementsLoadedTelemetryProps) { + const { workpad } = props; const [currentWorkpadId, setWorkpadId] = useState(undefined); const [hasReported, setHasReported] = useState(false); + const telemetryElementCounts = useSelector( + (state: State) => getElementCounts(state), + shallowEqual + ); - useEffect(() => { - const resolvedArgsAreForWorkpad = areAllElementsInResolvedArgs( - workpad, - telemetryResolvedArgs - ); + const telemetryResolvedArgs = useSelector((state: State) => getArgs(state), deepEqual); - if (workpad.id !== currentWorkpadId) { - setWorkpadId(workpad.id); + const resolvedArgsAreForWorkpad = areAllElementsInResolvedArgs(workpad, telemetryResolvedArgs); + const { error, pending } = telemetryElementCounts; + const resolved = resolvedArgsAreForWorkpad && pending === 0; + useEffect(() => { + if (workpad.id !== currentWorkpadId) { const workpadElementCount = workpad.pages.reduce( (reduction, page) => reduction + page.elements.length, 0 ); - if (workpadElementCount === 0 || (resolvedArgsAreForWorkpad && pending === 0)) { - setHasReported(true); - } else { - setHasReported(false); - } - } else if (!hasReported && pending === 0 && resolvedArgsAreForWorkpad) { + setWorkpadId(workpad.id); + setHasReported(workpadElementCount === 0 || resolved); + } else if (!hasReported && resolved) { if (error > 0) { trackMetric(METRIC_TYPE.LOADED, [WorkpadLoadedMetric, WorkpadLoadedWithErrorsMetric]); } else { @@ -99,16 +95,9 @@ export const withUnconnectedElementsLoadedTelemetry =

    ( } setHasReported(true); } - }, [currentWorkpadId, hasReported, error, pending, telemetryResolvedArgs, workpad]); - - return ; + }, [currentWorkpadId, hasReported, error, workpad.id, resolved, workpad.pages]); + return ; }; -const connector = connect(mapStateToProps, {}); - -type PropsFromRedux = ConnectedProps; - -export const withElementsLoadedTelemetry =

    (Component: React.ComponentType

    ) => { - const telemetry = withUnconnectedElementsLoadedTelemetry(Component); - return connector(telemetry); -}; +export const withElementsLoadedTelemetry =

    (Component: React.ComponentType

    ) => + withUnconnectedElementsLoadedTelemetry(Component); diff --git a/x-pack/plugins/canvas/public/components/workpad_header/editor_menu/__stories__/__snapshots__/editor_menu.stories.storyshot b/x-pack/plugins/canvas/public/components/workpad_header/editor_menu/__stories__/__snapshots__/editor_menu.stories.storyshot new file mode 100644 index 0000000000000..f4aab0e59e7ee --- /dev/null +++ b/x-pack/plugins/canvas/public/components/workpad_header/editor_menu/__stories__/__snapshots__/editor_menu.stories.storyshot @@ -0,0 +1,81 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Storyshots components/WorkpadHeader/EditorMenu dark mode 1`] = ` +

    +
    + +
    +
    +`; + +exports[`Storyshots components/WorkpadHeader/EditorMenu default 1`] = ` +
    +
    + +
    +
    +`; diff --git a/x-pack/plugins/canvas/public/components/workpad_header/editor_menu/__stories__/editor_menu.stories.tsx b/x-pack/plugins/canvas/public/components/workpad_header/editor_menu/__stories__/editor_menu.stories.tsx new file mode 100644 index 0000000000000..01048bc0af301 --- /dev/null +++ b/x-pack/plugins/canvas/public/components/workpad_header/editor_menu/__stories__/editor_menu.stories.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 { storiesOf } from '@storybook/react'; +import { action } from '@storybook/addon-actions'; +import React from 'react'; +import { EmbeddableFactoryDefinition, IEmbeddable } from 'src/plugins/embeddable/public'; +import { BaseVisType, VisTypeAlias } from 'src/plugins/visualizations/public'; +import { EditorMenu } from '../editor_menu.component'; + +const testFactories: EmbeddableFactoryDefinition[] = [ + { + type: 'ml_anomaly_swimlane', + getDisplayName: () => 'Anomaly swimlane', + getIconType: () => '', + getDescription: () => 'Description for anomaly swimlane', + isEditable: () => Promise.resolve(true), + create: () => Promise.resolve({ id: 'swimlane_embeddable' } as IEmbeddable), + grouping: [ + { + id: 'ml', + getDisplayName: () => 'machine learning', + getIconType: () => 'machineLearningApp', + }, + ], + }, + { + type: 'ml_anomaly_chart', + getDisplayName: () => 'Anomaly chart', + getIconType: () => '', + getDescription: () => 'Description for anomaly chart', + isEditable: () => Promise.resolve(true), + create: () => Promise.resolve({ id: 'anomaly_chart_embeddable' } as IEmbeddable), + grouping: [ + { + id: 'ml', + getDisplayName: () => 'machine learning', + getIconType: () => 'machineLearningApp', + }, + ], + }, + { + type: 'log_stream', + getDisplayName: () => 'Log stream', + getIconType: () => '', + getDescription: () => 'Description for log stream', + isEditable: () => Promise.resolve(true), + create: () => Promise.resolve({ id: 'anomaly_chart_embeddable' } as IEmbeddable), + }, +]; + +const testVisTypes: BaseVisType[] = [ + { title: 'TSVB', icon: '', description: 'Description of TSVB', name: 'tsvb' } as BaseVisType, + { + titleInWizard: 'Custom visualization', + title: 'Vega', + icon: '', + description: 'Description of Vega', + name: 'vega', + } as BaseVisType, +]; + +const testVisTypeAliases: VisTypeAlias[] = [ + { + title: 'Lens', + aliasApp: 'lens', + aliasPath: 'path/to/lens', + icon: 'lensApp', + name: 'lens', + description: 'Description of Lens app', + stage: 'production', + }, + { + title: 'Maps', + aliasApp: 'maps', + aliasPath: 'path/to/maps', + icon: 'gisApp', + name: 'maps', + description: 'Description of Maps app', + stage: 'production', + }, +]; + +storiesOf('components/WorkpadHeader/EditorMenu', module) + .add('default', () => ( + action('createNewVisType')} + createNewEmbeddable={() => action('createNewEmbeddable')} + /> + )) + .add('dark mode', () => ( + action('createNewVisType')} + createNewEmbeddable={() => action('createNewEmbeddable')} + /> + )); diff --git a/x-pack/plugins/canvas/public/components/workpad_header/editor_menu/editor_menu.component.tsx b/x-pack/plugins/canvas/public/components/workpad_header/editor_menu/editor_menu.component.tsx new file mode 100644 index 0000000000000..e8f762f9731a1 --- /dev/null +++ b/x-pack/plugins/canvas/public/components/workpad_header/editor_menu/editor_menu.component.tsx @@ -0,0 +1,170 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { FC } from 'react'; +import { + EuiContextMenu, + EuiContextMenuPanelItemDescriptor, + EuiContextMenuItemIcon, +} from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { EmbeddableFactoryDefinition } from '../../../../../../../src/plugins/embeddable/public'; +import { BaseVisType, VisTypeAlias } from '../../../../../../../src/plugins/visualizations/public'; +import { SolutionToolbarPopover } from '../../../../../../../src/plugins/presentation_util/public'; + +const strings = { + getEditorMenuButtonLabel: () => + i18n.translate('xpack.canvas.solutionToolbar.editorMenuButtonLabel', { + defaultMessage: 'Select type', + }), +}; + +interface FactoryGroup { + id: string; + appName: string; + icon: EuiContextMenuItemIcon; + panelId: number; + factories: EmbeddableFactoryDefinition[]; +} + +interface Props { + factories: EmbeddableFactoryDefinition[]; + isDarkThemeEnabled?: boolean; + promotedVisTypes: BaseVisType[]; + visTypeAliases: VisTypeAlias[]; + createNewVisType: (visType?: BaseVisType | VisTypeAlias) => () => void; + createNewEmbeddable: (factory: EmbeddableFactoryDefinition) => () => void; +} + +export const EditorMenu: FC = ({ + factories, + isDarkThemeEnabled, + promotedVisTypes, + visTypeAliases, + createNewVisType, + createNewEmbeddable, +}: Props) => { + const factoryGroupMap: Record = {}; + const ungroupedFactories: EmbeddableFactoryDefinition[] = []; + + let panelCount = 1; + + // Maps factories with a group to create nested context menus for each group type + // and pushes ungrouped factories into a separate array + factories.forEach((factory: EmbeddableFactoryDefinition, index) => { + const { grouping } = factory; + + if (grouping) { + grouping.forEach((group) => { + if (factoryGroupMap[group.id]) { + factoryGroupMap[group.id].factories.push(factory); + } else { + factoryGroupMap[group.id] = { + id: group.id, + appName: group.getDisplayName ? group.getDisplayName({}) : group.id, + icon: (group.getIconType ? group.getIconType({}) : 'empty') as EuiContextMenuItemIcon, + factories: [factory], + panelId: panelCount, + }; + + panelCount++; + } + }); + } else { + ungroupedFactories.push(factory); + } + }); + + const getVisTypeMenuItem = (visType: BaseVisType): EuiContextMenuPanelItemDescriptor => { + const { name, title, titleInWizard, description, icon = 'empty' } = visType; + return { + name: titleInWizard || title, + icon: icon as string, + onClick: createNewVisType(visType), + 'data-test-subj': `visType-${name}`, + toolTipContent: description, + }; + }; + + const getVisTypeAliasMenuItem = ( + visTypeAlias: VisTypeAlias + ): EuiContextMenuPanelItemDescriptor => { + const { name, title, description, icon = 'empty' } = visTypeAlias; + + return { + name: title, + icon, + onClick: createNewVisType(visTypeAlias), + 'data-test-subj': `visType-${name}`, + toolTipContent: description, + }; + }; + + const getEmbeddableFactoryMenuItem = ( + factory: EmbeddableFactoryDefinition + ): EuiContextMenuPanelItemDescriptor => { + const icon = factory?.getIconType ? factory.getIconType() : 'empty'; + + const toolTipContent = factory?.getDescription ? factory.getDescription() : undefined; + + return { + name: factory.getDisplayName(), + icon, + toolTipContent, + onClick: createNewEmbeddable(factory), + 'data-test-subj': `createNew-${factory.type}`, + }; + }; + + const editorMenuPanels = [ + { + id: 0, + items: [ + ...visTypeAliases.map(getVisTypeAliasMenuItem), + ...Object.values(factoryGroupMap).map(({ id, appName, icon, panelId }) => ({ + name: appName, + icon, + panel: panelId, + 'data-test-subj': `canvasEditorMenu-${id}Group`, + })), + ...ungroupedFactories.map(getEmbeddableFactoryMenuItem), + ...promotedVisTypes.map(getVisTypeMenuItem), + ], + }, + ...Object.values(factoryGroupMap).map( + ({ appName, panelId, factories: groupFactories }: FactoryGroup) => ({ + id: panelId, + title: appName, + items: groupFactories.map(getEmbeddableFactoryMenuItem), + }) + ), + ]; + + return ( + + {() => ( + + )} + + ); +}; diff --git a/x-pack/plugins/canvas/public/components/workpad_header/editor_menu/editor_menu.tsx b/x-pack/plugins/canvas/public/components/workpad_header/editor_menu/editor_menu.tsx new file mode 100644 index 0000000000000..dad34e6983c5d --- /dev/null +++ b/x-pack/plugins/canvas/public/components/workpad_header/editor_menu/editor_menu.tsx @@ -0,0 +1,147 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license 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, useCallback } from 'react'; +import { useLocation } from 'react-router-dom'; +import { trackCanvasUiMetric, METRIC_TYPE } from '../../../../public/lib/ui_metric'; +import { + useEmbeddablesService, + usePlatformService, + useVisualizationsService, +} from '../../../services'; +import { + BaseVisType, + VisGroups, + VisTypeAlias, +} from '../../../../../../../src/plugins/visualizations/public'; +import { + EmbeddableFactoryDefinition, + EmbeddableInput, +} from '../../../../../../../src/plugins/embeddable/public'; +import { CANVAS_APP } from '../../../../common/lib'; +import { encode } from '../../../../common/lib/embeddable_dataurl'; +import { ElementSpec } from '../../../../types'; +import { EditorMenu as Component } from './editor_menu.component'; + +interface Props { + /** + * Handler for adding a selected element to the workpad + */ + addElement: (element: Partial) => void; +} + +export const EditorMenu: FC = ({ addElement }) => { + const embeddablesService = useEmbeddablesService(); + const { pathname, search } = useLocation(); + const platformService = usePlatformService(); + const stateTransferService = embeddablesService.getStateTransfer(); + const visualizationsService = useVisualizationsService(); + const IS_DARK_THEME = platformService.getUISetting('theme:darkMode'); + + const createNewVisType = useCallback( + (visType?: BaseVisType | VisTypeAlias) => () => { + let path = ''; + let appId = ''; + + if (visType) { + if (trackCanvasUiMetric) { + trackCanvasUiMetric(METRIC_TYPE.CLICK, `${visType.name}:create`); + } + + if ('aliasPath' in visType) { + appId = visType.aliasApp; + path = visType.aliasPath; + } else { + appId = 'visualize'; + path = `#/create?type=${encodeURIComponent(visType.name)}`; + } + } else { + appId = 'visualize'; + path = '#/create?'; + } + + stateTransferService.navigateToEditor(appId, { + path, + state: { + originatingApp: CANVAS_APP, + originatingPath: `#/${pathname}${search}`, + }, + }); + }, + [stateTransferService, pathname, search] + ); + + const createNewEmbeddable = useCallback( + (factory: EmbeddableFactoryDefinition) => async () => { + if (trackCanvasUiMetric) { + trackCanvasUiMetric(METRIC_TYPE.CLICK, factory.type); + } + let embeddableInput; + if (factory.getExplicitInput) { + embeddableInput = await factory.getExplicitInput(); + } else { + const newEmbeddable = await factory.create({} as EmbeddableInput); + embeddableInput = newEmbeddable?.getInput(); + } + + if (embeddableInput) { + const config = encode(embeddableInput); + const expression = `embeddable config="${config}" + type="${factory.type}" +| render`; + + addElement({ expression }); + } + }, + [addElement] + ); + + const getVisTypesByGroup = (group: VisGroups): BaseVisType[] => + visualizationsService + .getByGroup(group) + .sort(({ name: a }: BaseVisType | VisTypeAlias, { name: b }: BaseVisType | VisTypeAlias) => { + if (a < b) { + return -1; + } + if (a > b) { + return 1; + } + return 0; + }) + .filter(({ hidden }: BaseVisType) => !hidden); + + const visTypeAliases = visualizationsService + .getAliases() + .sort(({ promotion: a = false }: VisTypeAlias, { promotion: b = false }: VisTypeAlias) => + a === b ? 0 : a ? -1 : 1 + ); + + const factories = embeddablesService + ? Array.from(embeddablesService.getEmbeddableFactories()).filter( + ({ type, isEditable, canCreateNew, isContainerType }) => + isEditable() && + !isContainerType && + canCreateNew() && + !['visualization', 'ml'].some((factoryType) => { + return type.includes(factoryType); + }) + ) + : []; + + const promotedVisTypes = getVisTypesByGroup(VisGroups.PROMOTED); + + return ( + + ); +}; diff --git a/x-pack/plugins/canvas/public/components/workpad_header/editor_menu/index.ts b/x-pack/plugins/canvas/public/components/workpad_header/editor_menu/index.ts new file mode 100644 index 0000000000000..0f903b1bbbe2e --- /dev/null +++ b/x-pack/plugins/canvas/public/components/workpad_header/editor_menu/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 { EditorMenu } from './editor_menu'; +export { EditorMenu as EditorMenuComponent } from './editor_menu.component'; diff --git a/x-pack/plugins/canvas/public/components/workpad_header/element_menu/element_menu.component.tsx b/x-pack/plugins/canvas/public/components/workpad_header/element_menu/element_menu.component.tsx index 8ac581b0866a4..1cfab236d9a9c 100644 --- a/x-pack/plugins/canvas/public/components/workpad_header/element_menu/element_menu.component.tsx +++ b/x-pack/plugins/canvas/public/components/workpad_header/element_menu/element_menu.component.tsx @@ -12,11 +12,11 @@ import { EuiContextMenu, EuiIcon, EuiContextMenuPanelItemDescriptor } from '@ela import { i18n } from '@kbn/i18n'; import { PrimaryActionPopover } from '../../../../../../../src/plugins/presentation_util/public'; import { getId } from '../../../lib/get_id'; -import { ClosePopoverFn } from '../../popover'; import { CONTEXT_MENU_TOP_BORDER_CLASSNAME } from '../../../../common/lib'; import { ElementSpec } from '../../../../types'; import { flattenPanelTree } from '../../../lib/flatten_panel_tree'; import { AssetManager } from '../../asset_manager'; +import { ClosePopoverFn } from '../../popover'; import { SavedElementsModal } from '../../saved_elements_modal'; interface CategorizedElementLists { @@ -112,7 +112,7 @@ const categorizeElementsByType = (elements: ElementSpec[]): { [key: string]: Ele return categories; }; -interface Props { +export interface Props { /** * Dictionary of elements from elements registry */ @@ -120,7 +120,7 @@ interface Props { /** * Handler for adding a selected element to the workpad */ - addElement: (element: ElementSpec) => void; + addElement: (element: Partial) => void; } export const ElementMenu: FunctionComponent = ({ elements, addElement }) => { diff --git a/x-pack/plugins/canvas/public/components/workpad_header/element_menu/index.ts b/x-pack/plugins/canvas/public/components/workpad_header/element_menu/index.ts index 52c8daece7690..037bb84b0cdba 100644 --- a/x-pack/plugins/canvas/public/components/workpad_header/element_menu/index.ts +++ b/x-pack/plugins/canvas/public/components/workpad_header/element_menu/index.ts @@ -5,5 +5,4 @@ * 2.0. */ -export { ElementMenu } from './element_menu'; -export { ElementMenu as ElementMenuComponent } from './element_menu.component'; +export { ElementMenu } from './element_menu.component'; diff --git a/x-pack/plugins/canvas/public/components/workpad_header/share_menu/share_menu.tsx b/x-pack/plugins/canvas/public/components/workpad_header/share_menu/share_menu.tsx index 50a3890673ffa..c66336a9153c0 100644 --- a/x-pack/plugins/canvas/public/components/workpad_header/share_menu/share_menu.tsx +++ b/x-pack/plugins/canvas/public/components/workpad_header/share_menu/share_menu.tsx @@ -49,6 +49,7 @@ export const ShareMenu = () => { getJobParams={() => getPdfJobParams(sharingData, platformService.getKibanaVersion())} layoutOption="canvas" onClose={onClose} + objectId={workpad.id} /> ) : null; diff --git a/x-pack/plugins/canvas/public/components/workpad_header/workpad_header.component.tsx b/x-pack/plugins/canvas/public/components/workpad_header/workpad_header.component.tsx index f031d7c263199..b84e4faf2925e 100644 --- a/x-pack/plugins/canvas/public/components/workpad_header/workpad_header.component.tsx +++ b/x-pack/plugins/canvas/public/components/workpad_header/workpad_header.component.tsx @@ -27,6 +27,7 @@ import { ElementMenu } from './element_menu'; import { ShareMenu } from './share_menu'; import { ViewMenu } from './view_menu'; import { LabsControl } from './labs_control'; +import { EditorMenu } from './editor_menu'; const strings = { getFullScreenButtonAriaLabel: () => @@ -160,24 +161,22 @@ export const WorkpadHeader: FC = ({ + {isWriteable && ( + + + {{ + primaryActionButton: , + quickButtonGroup: , + addFromLibraryButton: , + extraButtons: [], + }} + + + )} - {isWriteable && ( - - - {{ - primaryActionButton: ( - - ), - quickButtonGroup: , - addFromLibraryButton: , - }} - - - )} @@ -192,6 +191,7 @@ export const WorkpadHeader: FC = ({ + diff --git a/x-pack/plugins/canvas/public/components/workpad_page/workpad_interactive_page/index.js b/x-pack/plugins/canvas/public/components/workpad_page/workpad_interactive_page/index.js index f03547df8de99..25e64091f4aec 100644 --- a/x-pack/plugins/canvas/public/components/workpad_page/workpad_interactive_page/index.js +++ b/x-pack/plugins/canvas/public/components/workpad_page/workpad_interactive_page/index.js @@ -92,6 +92,16 @@ const isEmbeddableBody = (element) => { } }; +const isEuiSelect = (element) => { + const hasClosest = typeof element.closest === 'function'; + + if (hasClosest) { + return element.closest(`.euiSelect`); + } else { + return closest.call(element, `.euiSelect`); + } +}; + // Some elements in an embeddable may be portaled out of the embeddable container. // We do not want clicks on those to trigger drags, etc, in the workpad. This function // will check to make sure the clicked item is actually in the container @@ -243,7 +253,8 @@ export const InteractivePage = compose( })), withProps((...props) => ({ ...props, - canDragElement: (element) => !isEmbeddableBody(element) && isInWorkpad(element), + canDragElement: (element) => + !isEmbeddableBody(element) && !isEuiSelect(element) && isInWorkpad(element), })), withHandlers(eventHandlers), // Captures user intent, needs to have reconciled state () => InteractiveComponent diff --git a/x-pack/plugins/canvas/public/expression_types/datasource.tsx b/x-pack/plugins/canvas/public/expression_types/datasource.tsx index 7566c473a720a..8ecfedbf948d7 100644 --- a/x-pack/plugins/canvas/public/expression_types/datasource.tsx +++ b/x-pack/plugins/canvas/public/expression_types/datasource.tsx @@ -12,6 +12,7 @@ import { RenderToDom } from '../components/render_to_dom'; import { BaseForm, BaseFormProps } from './base_form'; import { ExpressionFormHandlers } from '../../common/lib'; import { ExpressionFunction } from '../../types'; +import { UpdatePropsRef } from '../../types/arguments'; const defaultTemplate = () => (
    @@ -22,7 +23,8 @@ const defaultTemplate = () => ( type TemplateFn = ( domNode: HTMLElement, config: DatasourceRenderProps, - handlers: ExpressionFormHandlers + handlers: ExpressionFormHandlers, + onMount?: (ref: UpdatePropsRef | null) => void ) => void; export type DatasourceProps = { @@ -49,6 +51,8 @@ interface DatasourceWrapperProps { const DatasourceWrapper: React.FunctionComponent = (props) => { const domNodeRef = useRef(); + const datasourceRef = useRef>(); + const { spec, datasourceProps, handlers } = props; const callRenderFn = useCallback(() => { @@ -58,14 +62,23 @@ const DatasourceWrapper: React.FunctionComponent = (prop return; } - template(domNodeRef.current, datasourceProps, handlers); + template(domNodeRef.current, datasourceProps, handlers, (ref) => { + datasourceRef.current = ref ?? undefined; + }); }, [datasourceProps, handlers, spec]); useEffect(() => { callRenderFn(); - }, [callRenderFn, props]); + }, [callRenderFn]); + + useEffect(() => { + if (datasourceRef.current) { + datasourceRef.current.updateProps(datasourceProps); + } + }, [datasourceProps]); useEffectOnce(() => () => { + datasourceRef.current = undefined; handlers.destroy(); }); diff --git a/x-pack/plugins/canvas/public/expression_types/function_form.tsx b/x-pack/plugins/canvas/public/expression_types/function_form.tsx index 70279453ac658..9028a0999149c 100644 --- a/x-pack/plugins/canvas/public/expression_types/function_form.tsx +++ b/x-pack/plugins/canvas/public/expression_types/function_form.tsx @@ -140,7 +140,6 @@ export class FunctionForm extends BaseForm { // Don't instaniate these until render time, to give the registries a chance to populate. const argInstances = this.args.map((argSpec) => new Arg(argSpec)); - if (args === null || !isPlainObject(args)) { throw new Error(`Form "${this.name}" expects "args" object`); } @@ -153,7 +152,6 @@ export class FunctionForm extends BaseForm { // otherwise, leave the value alone (including if the arg is not defined) const isMulti = arg && arg.multi; const argValues = args[argName] && !isMulti ? [last(args[argName]) ?? null] : args[argName]; - return { arg, argValues }; }); diff --git a/x-pack/plugins/canvas/public/lib/template_from_react_component.tsx b/x-pack/plugins/canvas/public/lib/template_from_react_component.tsx index b2dfd67a2d34e..a783d6de2916d 100644 --- a/x-pack/plugins/canvas/public/lib/template_from_react_component.tsx +++ b/x-pack/plugins/canvas/public/lib/template_from_react_component.tsx @@ -5,42 +5,72 @@ * 2.0. */ -import React, { ComponentType, FC } from 'react'; +import React, { + ComponentType, + forwardRef, + ForwardRefRenderFunction, + useImperativeHandle, + useState, +} from 'react'; import { unmountComponentAtNode, render } from 'react-dom'; import PropTypes from 'prop-types'; import { I18nProvider } from '@kbn/i18n/react'; import { ErrorBoundary } from '../components/enhance/error_boundary'; -import { ArgumentHandlers } from '../../types/arguments'; +import { ArgumentHandlers, UpdatePropsRef } from '../../types/arguments'; export interface Props { renderError: Function; } export const templateFromReactComponent = (Component: ComponentType) => { - const WrappedComponent: FC = (props) => ( - - {({ error }) => { - if (error) { - props.renderError(); - return null; - } - - return ( - - - - ); - }} - - ); - - WrappedComponent.propTypes = { + const WrappedComponent: ForwardRefRenderFunction, Props> = (props, ref) => { + const [updatedProps, setUpdatedProps] = useState(props); + + useImperativeHandle(ref, () => ({ + updateProps: (newProps: Props) => { + setUpdatedProps(newProps); + }, + })); + + return ( + + {({ error }) => { + if (error) { + props.renderError(); + return null; + } + + return ( + + + + ); + }} + + ); + }; + + const ForwardRefWrappedComponent = forwardRef(WrappedComponent); + + ForwardRefWrappedComponent.propTypes = { renderError: PropTypes.func, }; - return (domNode: HTMLElement, config: Props, handlers: ArgumentHandlers) => { + return ( + domNode: HTMLElement, + config: Props, + handlers: ArgumentHandlers, + onMount?: (ref: UpdatePropsRef | null) => void + ) => { try { - const el = React.createElement(WrappedComponent, config); + const el = ( + { + onMount?.(ref); + }} + /> + ); render(el, domNode, () => { handlers.done(); }); diff --git a/x-pack/plugins/canvas/public/plugin.tsx b/x-pack/plugins/canvas/public/plugin.tsx index 5d1f05fdbe8bf..d2375064603c3 100644 --- a/x-pack/plugins/canvas/public/plugin.tsx +++ b/x-pack/plugins/canvas/public/plugin.tsx @@ -8,6 +8,7 @@ import { BehaviorSubject } from 'rxjs'; import type { SharePluginSetup } from 'src/plugins/share/public'; import { ChartsPluginSetup, ChartsPluginStart } from 'src/plugins/charts/public'; +import { VisualizationsStart } from 'src/plugins/visualizations/public'; import { ReportingStart } from '../../reporting/public'; import { CoreSetup, @@ -63,6 +64,7 @@ export interface CanvasStartDeps { charts: ChartsPluginStart; data: DataPublicPluginStart; presentationUtil: PresentationUtilPluginStart; + visualizations: VisualizationsStart; spaces?: SpacesPluginStart; } @@ -122,7 +124,12 @@ export class CanvasPlugin const { pluginServices } = await import('./services'); pluginServices.setRegistry( - pluginServiceRegistry.start({ coreStart, startPlugins, initContext: this.initContext }) + pluginServiceRegistry.start({ + coreStart, + startPlugins, + appUpdater: this.appUpdater, + initContext: this.initContext, + }) ); // Load application bundle diff --git a/x-pack/plugins/canvas/public/routes/workpad/hooks/use_workpad.ts b/x-pack/plugins/canvas/public/routes/workpad/hooks/use_workpad.ts index 963a69a8f11f0..f117998bbd3eb 100644 --- a/x-pack/plugins/canvas/public/routes/workpad/hooks/use_workpad.ts +++ b/x-pack/plugins/canvas/public/routes/workpad/hooks/use_workpad.ts @@ -50,7 +50,7 @@ export const useWorkpad = ( setResolveInfo({ aliasId, outcome, id: workpadId }); // If it's an alias match, we know we are going to redirect so don't even dispatch that we got the workpad - if (outcome !== 'aliasMatch') { + if (storedWorkpad.id !== workpadId && outcome !== 'aliasMatch') { workpad.aliasId = aliasId; dispatch(setAssets(assets)); @@ -61,7 +61,7 @@ export const useWorkpad = ( setError(e as Error | string); } })(); - }, [workpadId, dispatch, setError, loadPages, workpadResolve]); + }, [workpadId, dispatch, setError, loadPages, workpadResolve, storedWorkpad.id]); useEffect(() => { // If the resolved info is not for the current workpad id, bail out diff --git a/x-pack/plugins/canvas/public/services/embeddables.ts b/x-pack/plugins/canvas/public/services/embeddables.ts index 24d7a57e086f2..26b150b7a5349 100644 --- a/x-pack/plugins/canvas/public/services/embeddables.ts +++ b/x-pack/plugins/canvas/public/services/embeddables.ts @@ -5,8 +5,12 @@ * 2.0. */ -import { EmbeddableFactory } from '../../../../../src/plugins/embeddable/public'; +import { + EmbeddableFactory, + EmbeddableStateTransfer, +} from '../../../../../src/plugins/embeddable/public'; export interface CanvasEmbeddablesService { getEmbeddableFactories: () => IterableIterator; + getStateTransfer: () => EmbeddableStateTransfer; } diff --git a/x-pack/plugins/canvas/public/services/index.ts b/x-pack/plugins/canvas/public/services/index.ts index f4292810b8089..ed55f919e4c76 100644 --- a/x-pack/plugins/canvas/public/services/index.ts +++ b/x-pack/plugins/canvas/public/services/index.ts @@ -17,6 +17,7 @@ import { CanvasNavLinkService } from './nav_link'; import { CanvasNotifyService } from './notify'; import { CanvasPlatformService } from './platform'; import { CanvasReportingService } from './reporting'; +import { CanvasVisualizationsService } from './visualizations'; import { CanvasWorkpadService } from './workpad'; export interface CanvasPluginServices { @@ -28,6 +29,7 @@ export interface CanvasPluginServices { notify: CanvasNotifyService; platform: CanvasPlatformService; reporting: CanvasReportingService; + visualizations: CanvasVisualizationsService; workpad: CanvasWorkpadService; } @@ -44,4 +46,6 @@ export const useNavLinkService = () => (() => pluginServices.getHooks().navLink. export const useNotifyService = () => (() => pluginServices.getHooks().notify.useService())(); export const usePlatformService = () => (() => pluginServices.getHooks().platform.useService())(); export const useReportingService = () => (() => pluginServices.getHooks().reporting.useService())(); +export const useVisualizationsService = () => + (() => pluginServices.getHooks().visualizations.useService())(); export const useWorkpadService = () => (() => pluginServices.getHooks().workpad.useService())(); diff --git a/x-pack/plugins/canvas/public/services/kibana/embeddables.ts b/x-pack/plugins/canvas/public/services/kibana/embeddables.ts index 054b9da7409fb..8d1a86edab3d8 100644 --- a/x-pack/plugins/canvas/public/services/kibana/embeddables.ts +++ b/x-pack/plugins/canvas/public/services/kibana/embeddables.ts @@ -16,4 +16,5 @@ export type EmbeddablesServiceFactory = KibanaPluginServiceFactory< export const embeddablesServiceFactory: EmbeddablesServiceFactory = ({ startPlugins }) => ({ getEmbeddableFactories: startPlugins.embeddable.getEmbeddableFactories, + getStateTransfer: startPlugins.embeddable.getStateTransfer, }); diff --git a/x-pack/plugins/canvas/public/services/kibana/index.ts b/x-pack/plugins/canvas/public/services/kibana/index.ts index 1eb010e8d6f9d..91767947bc0a6 100644 --- a/x-pack/plugins/canvas/public/services/kibana/index.ts +++ b/x-pack/plugins/canvas/public/services/kibana/index.ts @@ -22,6 +22,7 @@ import { navLinkServiceFactory } from './nav_link'; import { notifyServiceFactory } from './notify'; import { platformServiceFactory } from './platform'; import { reportingServiceFactory } from './reporting'; +import { visualizationsServiceFactory } from './visualizations'; import { workpadServiceFactory } from './workpad'; export { customElementServiceFactory } from './custom_element'; @@ -31,6 +32,7 @@ export { labsServiceFactory } from './labs'; export { notifyServiceFactory } from './notify'; export { platformServiceFactory } from './platform'; export { reportingServiceFactory } from './reporting'; +export { visualizationsServiceFactory } from './visualizations'; export { workpadServiceFactory } from './workpad'; export const pluginServiceProviders: PluginServiceProviders< @@ -45,6 +47,7 @@ export const pluginServiceProviders: PluginServiceProviders< notify: new PluginServiceProvider(notifyServiceFactory), platform: new PluginServiceProvider(platformServiceFactory), reporting: new PluginServiceProvider(reportingServiceFactory), + visualizations: new PluginServiceProvider(visualizationsServiceFactory), workpad: new PluginServiceProvider(workpadServiceFactory), }; diff --git a/x-pack/plugins/canvas/public/services/kibana/visualizations.ts b/x-pack/plugins/canvas/public/services/kibana/visualizations.ts new file mode 100644 index 0000000000000..e319ec1c1f427 --- /dev/null +++ b/x-pack/plugins/canvas/public/services/kibana/visualizations.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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { KibanaPluginServiceFactory } from '../../../../../../src/plugins/presentation_util/public'; +import { CanvasStartDeps } from '../../plugin'; +import { CanvasVisualizationsService } from '../visualizations'; + +export type VisualizationsServiceFactory = KibanaPluginServiceFactory< + CanvasVisualizationsService, + CanvasStartDeps +>; + +export const visualizationsServiceFactory: VisualizationsServiceFactory = ({ startPlugins }) => ({ + showNewVisModal: startPlugins.visualizations.showNewVisModal, + getByGroup: startPlugins.visualizations.getByGroup, + getAliases: startPlugins.visualizations.getAliases, +}); diff --git a/x-pack/plugins/canvas/public/services/stubs/embeddables.ts b/x-pack/plugins/canvas/public/services/stubs/embeddables.ts index 173d27563e2b2..9c2cf4d0650ab 100644 --- a/x-pack/plugins/canvas/public/services/stubs/embeddables.ts +++ b/x-pack/plugins/canvas/public/services/stubs/embeddables.ts @@ -14,4 +14,5 @@ const noop = (..._args: any[]): any => {}; export const embeddablesServiceFactory: EmbeddablesServiceFactory = () => ({ getEmbeddableFactories: noop, + getStateTransfer: noop, }); diff --git a/x-pack/plugins/canvas/public/services/stubs/index.ts b/x-pack/plugins/canvas/public/services/stubs/index.ts index 06a5ff49e9c04..2216013a29c12 100644 --- a/x-pack/plugins/canvas/public/services/stubs/index.ts +++ b/x-pack/plugins/canvas/public/services/stubs/index.ts @@ -22,6 +22,7 @@ import { navLinkServiceFactory } from './nav_link'; import { notifyServiceFactory } from './notify'; import { platformServiceFactory } from './platform'; import { reportingServiceFactory } from './reporting'; +import { visualizationsServiceFactory } from './visualizations'; import { workpadServiceFactory } from './workpad'; export { customElementServiceFactory } from './custom_element'; @@ -31,6 +32,7 @@ export { navLinkServiceFactory } from './nav_link'; export { notifyServiceFactory } from './notify'; export { platformServiceFactory } from './platform'; export { reportingServiceFactory } from './reporting'; +export { visualizationsServiceFactory } from './visualizations'; export { workpadServiceFactory } from './workpad'; export const pluginServiceProviders: PluginServiceProviders = { @@ -42,6 +44,7 @@ export const pluginServiceProviders: PluginServiceProviders; + +const noop = (..._args: any[]): any => {}; + +export const visualizationsServiceFactory: VisualizationsServiceFactory = () => ({ + showNewVisModal: noop, + getByGroup: noop, + getAliases: noop, +}); diff --git a/x-pack/plugins/canvas/public/services/visualizations.ts b/x-pack/plugins/canvas/public/services/visualizations.ts new file mode 100644 index 0000000000000..c602b1dd39f3d --- /dev/null +++ b/x-pack/plugins/canvas/public/services/visualizations.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. + */ + +import { VisualizationsStart } from '../../../../../src/plugins/visualizations/public'; + +export interface CanvasVisualizationsService { + showNewVisModal: VisualizationsStart['showNewVisModal']; + getByGroup: VisualizationsStart['getByGroup']; + getAliases: VisualizationsStart['getAliases']; +} diff --git a/x-pack/plugins/canvas/public/state/reducers/embeddable.ts b/x-pack/plugins/canvas/public/state/reducers/embeddable.ts index 4cfdc7f21945f..092d4300d86b7 100644 --- a/x-pack/plugins/canvas/public/state/reducers/embeddable.ts +++ b/x-pack/plugins/canvas/public/state/reducers/embeddable.ts @@ -40,7 +40,7 @@ export const embeddableReducer = handleActions< const element = pageWithElement.elements.find((elem) => elem.id === elementId); - if (!element) { + if (!element || element.expression === embeddableExpression) { return workpadState; } diff --git a/x-pack/plugins/canvas/server/plugin.ts b/x-pack/plugins/canvas/server/plugin.ts index 4071b891e4c3d..ebe43ba76a46a 100644 --- a/x-pack/plugins/canvas/server/plugin.ts +++ b/x-pack/plugins/canvas/server/plugin.ts @@ -14,6 +14,7 @@ import { ExpressionsServerSetup } from 'src/plugins/expressions/server'; import { BfetchServerSetup } from 'src/plugins/bfetch/server'; import { UsageCollectionSetup } from 'src/plugins/usage_collection/server'; import { HomeServerPluginSetup } from 'src/plugins/home/server'; +import { EmbeddableSetup } from 'src/plugins/embeddable/server'; import { ESSQL_SEARCH_STRATEGY } from '../common/lib/constants'; import { ReportingSetup } from '../../reporting/server'; import { PluginSetupContract as FeaturesPluginSetup } from '../../features/server'; @@ -30,6 +31,7 @@ import { CanvasRouteHandlerContext, createWorkpadRouteContext } from './workpad_ interface PluginsSetup { expressions: ExpressionsServerSetup; + embeddable: EmbeddableSetup; features: FeaturesPluginSetup; home: HomeServerPluginSetup; bfetch: BfetchServerSetup; @@ -82,7 +84,12 @@ export class CanvasPlugin implements Plugin { const kibanaIndex = coreSetup.savedObjects.getKibanaIndex(); registerCanvasUsageCollector(plugins.usageCollection, kibanaIndex); - setupInterpreter(expressionsFork); + setupInterpreter(expressionsFork, { + embeddablePersistableStateService: { + extract: plugins.embeddable.extract, + inject: plugins.embeddable.inject, + }, + }); coreSetup.getStartServices().then(([_, depsStart]) => { const strategy = essqlSearchStrategyProvider(); diff --git a/x-pack/plugins/canvas/server/setup_interpreter.ts b/x-pack/plugins/canvas/server/setup_interpreter.ts index 2fe23eb86c086..849ad79717056 100644 --- a/x-pack/plugins/canvas/server/setup_interpreter.ts +++ b/x-pack/plugins/canvas/server/setup_interpreter.ts @@ -7,9 +7,15 @@ import { ExpressionsServerSetup } from 'src/plugins/expressions/server'; import { functions } from '../canvas_plugin_src/functions/server'; -import { functions as externalFunctions } from '../canvas_plugin_src/functions/external'; +import { + initFunctions as initExternalFunctions, + InitializeArguments, +} from '../canvas_plugin_src/functions/external'; -export function setupInterpreter(expressions: ExpressionsServerSetup) { +export function setupInterpreter( + expressions: ExpressionsServerSetup, + dependencies: InitializeArguments +) { functions.forEach((f) => expressions.registerFunction(f)); - externalFunctions.forEach((f) => expressions.registerFunction(f)); + initExternalFunctions(dependencies).forEach((f) => expressions.registerFunction(f)); } diff --git a/x-pack/plugins/canvas/shareable_runtime/webpack.config.js b/x-pack/plugins/canvas/shareable_runtime/webpack.config.js index dee608fcf0702..610f9ee0bb197 100644 --- a/x-pack/plugins/canvas/shareable_runtime/webpack.config.js +++ b/x-pack/plugins/canvas/shareable_runtime/webpack.config.js @@ -18,6 +18,14 @@ const { const isProd = process.env.NODE_ENV === 'production'; +const nodeModulesButNotKbnPackages = (_path) => { + if (!_path.includes('node_modules')) { + return false; + } + + return !_path.includes(`node_modules${path.sep}@kbn${path.sep}`); +}; + module.exports = { context: KIBANA_ROOT, entry: { @@ -124,7 +132,7 @@ module.exports = { }, { test: /\.scss$/, - exclude: [/node_modules/, /\.module\.s(a|c)ss$/], + exclude: [nodeModulesButNotKbnPackages, /\.module\.s(a|c)ss$/], use: [ { loader: 'style-loader', diff --git a/x-pack/plugins/canvas/types/arguments.ts b/x-pack/plugins/canvas/types/arguments.ts index 5dfaa3f8e6759..0ecc696165919 100644 --- a/x-pack/plugins/canvas/types/arguments.ts +++ b/x-pack/plugins/canvas/types/arguments.ts @@ -19,6 +19,10 @@ export interface ArgumentHandlers { onDestroy: GenericCallback; } +export interface UpdatePropsRef { + updateProps: (newProps: Props) => void; +} + export interface ArgumentSpec { /** The argument type */ name: string; @@ -33,13 +37,19 @@ export interface ArgumentSpec { simpleTemplate?: ( domNode: HTMLElement, config: ArgumentConfig, - handlers: ArgumentHandlers + handlers: ArgumentHandlers, + onMount: (ref: UpdatePropsRef | null) => void ) => void; /** * A function that renders a complex/large argument * This is nested in an accordian so it can be expanded/collapsed */ - template?: (domNode: HTMLElement, config: ArgumentConfig, handlers: ArgumentHandlers) => void; + template?: ( + domNode: HTMLElement, + config: ArgumentConfig, + handlers: ArgumentHandlers, + onMount: (ref: UpdatePropsRef | null) => void + ) => void; } export type ArgumentFactory = () => ArgumentSpec; diff --git a/x-pack/plugins/canvas/types/embeddables.ts b/x-pack/plugins/canvas/types/embeddables.ts new file mode 100644 index 0000000000000..b78efece59d8f --- /dev/null +++ b/x-pack/plugins/canvas/types/embeddables.ts @@ -0,0 +1,16 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { TimeRange } from 'src/plugins/data/public'; +import { Filter } from '@kbn/es-query'; +import { EmbeddableInput as Input } from '../../../../src/plugins/embeddable/common/'; + +export type EmbeddableInput = Input & { + timeRange?: TimeRange; + filters?: Filter[]; + savedObjectId?: string; +}; diff --git a/x-pack/plugins/canvas/types/functions.ts b/x-pack/plugins/canvas/types/functions.ts index 2569e0b10685b..c80102915ed95 100644 --- a/x-pack/plugins/canvas/types/functions.ts +++ b/x-pack/plugins/canvas/types/functions.ts @@ -10,8 +10,8 @@ import { UnwrapPromiseOrReturn } from '@kbn/utility-types'; import { functions as commonFunctions } from '../canvas_plugin_src/functions/common'; import { functions as browserFunctions } from '../canvas_plugin_src/functions/browser'; import { functions as serverFunctions } from '../canvas_plugin_src/functions/server'; -import { functions as externalFunctions } from '../canvas_plugin_src/functions/external'; -import { initFunctions } from '../public/functions'; +import { initFunctions as initExternalFunctions } from '../canvas_plugin_src/functions/external'; +import { initFunctions as initClientFunctions } from '../public/functions'; /** * A `ExpressionFunctionFactory` is a powerful type used for any function that produces @@ -90,9 +90,11 @@ export type FunctionFactory = type CommonFunction = FunctionFactory; type BrowserFunction = FunctionFactory; type ServerFunction = FunctionFactory; -type ExternalFunction = FunctionFactory; +type ExternalFunction = FunctionFactory< + ReturnType extends Array ? U : never +>; type ClientFunctions = FunctionFactory< - ReturnType extends Array ? U : never + ReturnType extends Array ? U : never >; /** diff --git a/x-pack/plugins/canvas/types/index.ts b/x-pack/plugins/canvas/types/index.ts index 09ae1510be6da..930f337292088 100644 --- a/x-pack/plugins/canvas/types/index.ts +++ b/x-pack/plugins/canvas/types/index.ts @@ -9,6 +9,7 @@ export * from '../../../../src/plugins/expressions/common'; export * from './assets'; export * from './canvas'; export * from './elements'; +export * from './embeddables'; export * from './filters'; export * from './functions'; export * from './renderers'; diff --git a/x-pack/plugins/cases/common/api/runtime_types.ts b/x-pack/plugins/cases/common/api/runtime_types.ts index 7edc1162c0e81..c807d4b31b751 100644 --- a/x-pack/plugins/cases/common/api/runtime_types.ts +++ b/x-pack/plugins/cases/common/api/runtime_types.ts @@ -60,7 +60,7 @@ export const decodeOrThrow = const getExcessProps = (props: rt.Props, r: Record): string[] => { const ex: string[] = []; for (const k of Object.keys(r)) { - if (!props.hasOwnProperty(k)) { + if (!Object.prototype.hasOwnProperty.call(props, k)) { ex.push(k); } } @@ -89,5 +89,5 @@ export function excess | rt.PartialType ({ +export const getFormMock = (sampleData: unknown) => ({ ...mockFormHook, submit: () => Promise.resolve({ diff --git a/x-pack/plugins/cases/public/components/all_cases/all_cases_generic.tsx b/x-pack/plugins/cases/public/components/all_cases/all_cases_generic.tsx index 4a4e31230030e..54867c333c2e2 100644 --- a/x-pack/plugins/cases/public/components/all_cases/all_cases_generic.tsx +++ b/x-pack/plugins/cases/public/components/all_cases/all_cases_generic.tsx @@ -7,7 +7,7 @@ import React, { useCallback, useMemo, useRef, useState } from 'react'; import { EuiProgress, EuiBasicTable, EuiTableSelectionType } from '@elastic/eui'; -import { difference, head, isEmpty, memoize } from 'lodash/fp'; +import { difference, head, isEmpty } from 'lodash/fp'; import styled, { css } from 'styled-components'; import classnames from 'classnames'; @@ -17,7 +17,6 @@ import { CaseType, CommentRequestAlertType, CaseStatusWithAllStatus, - CommentType, FilterOptions, SortFieldCase, SubCase, @@ -207,6 +206,10 @@ export const AllCasesGeneric = React.memo( isSelectorView: !!isSelectorView, userCanCrud, connectors, + onRowClick, + alertData, + postComment, + updateCase, }); const itemIdToExpandedRowMap = useMemo( @@ -241,32 +244,11 @@ export const AllCasesGeneric = React.memo( const isDataEmpty = useMemo(() => data.total === 0, [data]); const tableRowProps = useCallback( - (theCase: Case) => { - const onTableRowClick = memoize(async () => { - if (alertData != null) { - await postComment({ - caseId: theCase.id, - data: { - type: CommentType.alert, - ...alertData, - }, - updateCase, - }); - } - if (onRowClick) { - onRowClick(theCase); - } - }); - - return { - 'data-test-subj': `cases-table-row-${theCase.id}`, - className: classnames({ isDisabled: theCase.type === CaseType.collection }), - ...(isSelectorView && theCase.type !== CaseType.collection - ? { onClick: onTableRowClick } - : {}), - }; - }, - [isSelectorView, alertData, onRowClick, postComment, updateCase] + (theCase: Case) => ({ + 'data-test-subj': `cases-table-row-${theCase.id}`, + className: classnames({ isDisabled: theCase.type === CaseType.collection }), + }), + [] ); return ( diff --git a/x-pack/plugins/cases/public/components/all_cases/columns.tsx b/x-pack/plugins/cases/public/components/all_cases/columns.tsx index c0bd6536f1b73..6a23a293bfb83 100644 --- a/x-pack/plugins/cases/public/components/all_cases/columns.tsx +++ b/x-pack/plugins/cases/public/components/all_cases/columns.tsx @@ -10,6 +10,7 @@ import { EuiAvatar, EuiBadgeGroup, EuiBadge, + EuiButton, EuiLink, EuiTableActionsColumnType, EuiTableComputedColumnType, @@ -24,6 +25,8 @@ import styled from 'styled-components'; import { CaseStatuses, CaseType, + CommentType, + CommentRequestAlertType, DeleteCase, Case, SubCase, @@ -43,6 +46,7 @@ import { useKibana } from '../../common/lib/kibana'; import { StatusContextMenu } from '../case_action_bar/status_context_menu'; import { TruncatedText } from '../truncated_text'; import { getConnectorIcon } from '../utils'; +import { PostComment } from '../../containers/use_post_comment'; export type CasesColumns = | EuiTableActionsColumnType @@ -75,6 +79,10 @@ export interface GetCasesColumn { isSelectorView: boolean; userCanCrud: boolean; connectors?: ActionConnector[]; + onRowClick?: (theCase: Case) => void; + alertData?: Omit; + postComment?: (args: PostComment) => Promise; + updateCase?: (newCase: Case) => void; } export const useCasesColumns = ({ caseDetailsNavigation, @@ -87,6 +95,10 @@ export const useCasesColumns = ({ isSelectorView, userCanCrud, connectors = [], + onRowClick, + alertData, + postComment, + updateCase, }: GetCasesColumn): CasesColumns[] => { // Delete case const { @@ -132,6 +144,25 @@ export const useCasesColumns = ({ [toggleDeleteModal] ); + const assignCaseAction = useCallback( + async (theCase: Case) => { + if (alertData != null) { + await postComment?.({ + caseId: theCase.id, + data: { + type: CommentType.alert, + ...alertData, + }, + updateCase, + }); + } + if (onRowClick) { + onRowClick(theCase); + } + }, + [alertData, onRowClick, postComment, updateCase] + ); + useEffect(() => { handleIsLoading(isDeleting || isLoadingCases.indexOf('caseUpdate') > -1); }, [handleIsLoading, isDeleting, isLoadingCases]); @@ -281,6 +312,30 @@ export const useCasesColumns = ({ return getEmptyTagValue(); }, }, + ...(isSelectorView + ? [ + { + align: RIGHT_ALIGNMENT, + render: (theCase: Case) => { + if (theCase.id != null) { + return ( + { + assignCaseAction(theCase); + }} + size="s" + fill={true} + > + {i18n.SELECT} + + ); + } + return getEmptyTagValue(); + }, + }, + ] + : []), ...(!isSelectorView ? [ { diff --git a/x-pack/plugins/cases/public/components/all_cases/expanded_row.tsx b/x-pack/plugins/cases/public/components/all_cases/expanded_row.tsx index 59efcf868c9ee..2b43fbf63095e 100644 --- a/x-pack/plugins/cases/public/components/all_cases/expanded_row.tsx +++ b/x-pack/plugins/cases/public/components/all_cases/expanded_row.tsx @@ -6,7 +6,7 @@ */ import React from 'react'; -import { EuiBasicTable as _EuiBasicTable } from '@elastic/eui'; +import { EuiBasicTable } from '@elastic/eui'; import styled from 'styled-components'; import { Case, SubCase } from '../../containers/types'; import { CasesColumns } from './columns'; @@ -14,7 +14,7 @@ import { AssociationType } from '../../../common'; type ExpandedRowMap = Record | {}; -const EuiBasicTable: any = _EuiBasicTable; +// @ts-expect-error TS2769 const BasicTable = styled(EuiBasicTable)` thead { display: none; diff --git a/x-pack/plugins/cases/public/components/all_cases/index.test.tsx b/x-pack/plugins/cases/public/components/all_cases/index.test.tsx index a387c5eae3834..7e3ca8729ef63 100644 --- a/x-pack/plugins/cases/public/components/all_cases/index.test.tsx +++ b/x-pack/plugins/cases/public/components/all_cases/index.test.tsx @@ -303,7 +303,10 @@ describe('AllCasesGeneric', () => { await waitFor(() => { result.current.map( - (i, key) => i.name != null && !i.hasOwnProperty('actions') && checkIt(`${i.name}`, key) + (i, key) => + i.name != null && + !Object.prototype.hasOwnProperty.call(i, 'actions') && + checkIt(`${i.name}`, key) ); }); }); @@ -378,7 +381,9 @@ describe('AllCasesGeneric', () => { }) ); await waitFor(() => { - result.current.map((i) => i.name != null && !i.hasOwnProperty('actions')); + result.current.map( + (i) => i.name != null && !Object.prototype.hasOwnProperty.call(i, 'actions') + ); expect(wrapper.find(`a[data-test-subj="case-details-link"]`).exists()).toBeFalsy(); }); }); @@ -744,7 +749,7 @@ describe('AllCasesGeneric', () => { /> ); - wrapper.find('[data-test-subj="cases-table-row-1"]').first().simulate('click'); + wrapper.find('[data-test-subj="cases-table-row-select-1"]').first().simulate('click'); await waitFor(() => { expect(onRowClick).toHaveBeenCalledWith({ closedAt: null, diff --git a/x-pack/plugins/cases/public/components/all_cases/selector_modal/index.tsx b/x-pack/plugins/cases/public/components/all_cases/selector_modal/index.tsx index 4e8334ebceec0..7356764802550 100644 --- a/x-pack/plugins/cases/public/components/all_cases/selector_modal/index.tsx +++ b/x-pack/plugins/cases/public/components/all_cases/selector_modal/index.tsx @@ -6,7 +6,14 @@ */ import React, { useState, useCallback } from 'react'; -import { EuiModal, EuiModalBody, EuiModalHeader, EuiModalHeaderTitle } from '@elastic/eui'; +import { + EuiButton, + EuiModal, + EuiModalBody, + EuiModalFooter, + EuiModalHeader, + EuiModalHeaderTitle, +} from '@elastic/eui'; import styled from 'styled-components'; import { Case, @@ -76,6 +83,11 @@ const AllCasesSelectorModalComponent: React.FC = ({ updateCase={updateCase} /> + + + {i18n.CANCEL} + + ) : null; }; @@ -87,5 +99,8 @@ export const AllCasesSelectorModal: React.FC = React ); }); + +AllCasesSelectorModal.displayName = 'AllCasesSelectorModal'; + // eslint-disable-next-line import/no-default-export export { AllCasesSelectorModal as default }; diff --git a/x-pack/plugins/cases/public/components/all_cases/table.tsx b/x-pack/plugins/cases/public/components/all_cases/table.tsx index 876007494d276..3f80fc8f0d7c4 100644 --- a/x-pack/plugins/cases/public/components/all_cases/table.tsx +++ b/x-pack/plugins/cases/public/components/all_cases/table.tsx @@ -10,7 +10,7 @@ import { EuiEmptyPrompt, EuiLoadingContent, EuiTableSelectionType, - EuiBasicTable as _EuiBasicTable, + EuiBasicTable, EuiBasicTableProps, } from '@elastic/eui'; import classnames from 'classnames'; @@ -40,12 +40,12 @@ interface CasesTableProps { selection: EuiTableSelectionType; showActions: boolean; sorting: EuiBasicTableProps['sorting']; - tableRef: MutableRefObject<_EuiBasicTable | undefined>; + tableRef: MutableRefObject; tableRowProps: EuiBasicTableProps['rowProps']; userCanCrud: boolean; } -const EuiBasicTable: any = _EuiBasicTable; +// @ts-expect-error TS2769 const BasicTable = styled(EuiBasicTable)` ${({ theme }) => ` .euiTableRow-isExpandedRow.euiTableRow-isSelectable .euiTableCellContent { diff --git a/x-pack/plugins/cases/public/components/all_cases/translations.ts b/x-pack/plugins/cases/public/components/all_cases/translations.ts index be1aa256db657..5f5cb274ebf77 100644 --- a/x-pack/plugins/cases/public/components/all_cases/translations.ts +++ b/x-pack/plugins/cases/public/components/all_cases/translations.ts @@ -75,6 +75,10 @@ export const DELETE = i18n.translate('xpack.cases.caseTable.delete', { defaultMessage: 'Delete', }); +export const SELECT = i18n.translate('xpack.cases.caseTable.select', { + defaultMessage: 'Select', +}); + export const REQUIRES_UPDATE = i18n.translate('xpack.cases.caseTable.requiresUpdate', { defaultMessage: ' requires update', }); diff --git a/x-pack/plugins/cases/public/components/case_action_bar/index.tsx b/x-pack/plugins/cases/public/components/case_action_bar/index.tsx index af17ea0dca895..8149fd6591645 100644 --- a/x-pack/plugins/cases/public/components/case_action_bar/index.tsx +++ b/x-pack/plugins/cases/public/components/case_action_bar/index.tsx @@ -86,7 +86,7 @@ const CaseActionBarComponent: React.FC = ({ {caseData.type !== CaseType.collection && ( - + {i18n.STATUS} = ({ )} - + {title} { expect(onStatusChanged).toHaveBeenCalledWith('in-progress'); }); + + it('it does not call onStatusChanged if selection is same as current status', async () => { + const wrapper = mount( + + ); + + wrapper.find(`[data-test-subj="case-view-status-dropdown"] button`).simulate('click'); + wrapper.find(`[data-test-subj="case-view-status-dropdown-open"] button`).simulate('click'); + + expect(onStatusChanged).not.toHaveBeenCalled(); + }); }); diff --git a/x-pack/plugins/cases/public/components/case_action_bar/status_context_menu.tsx b/x-pack/plugins/cases/public/components/case_action_bar/status_context_menu.tsx index 603efb253f051..ab86f589bfdd0 100644 --- a/x-pack/plugins/cases/public/components/case_action_bar/status_context_menu.tsx +++ b/x-pack/plugins/cases/public/components/case_action_bar/status_context_menu.tsx @@ -33,9 +33,11 @@ const StatusContextMenuComponent: React.FC = ({ const onContextMenuItemClick = useCallback( (status: CaseStatuses) => { closePopover(); - onStatusChanged(status); + if (currentStatus !== status) { + onStatusChanged(status); + } }, - [closePopover, onStatusChanged] + [closePopover, currentStatus, onStatusChanged] ); const panelItems = useMemo( diff --git a/x-pack/plugins/cases/public/components/case_view/index.tsx b/x-pack/plugins/cases/public/components/case_view/index.tsx index 87550ab66927b..75ac42ecd24ee 100644 --- a/x-pack/plugins/cases/public/components/case_view/index.tsx +++ b/x-pack/plugins/cases/public/components/case_view/index.tsx @@ -26,7 +26,7 @@ import { UserActionTree } from '../user_action_tree'; import { UserList } from '../user_list'; import { useUpdateCase } from '../../containers/use_update_case'; import { getTypedPayload } from '../../containers/utils'; -import { ContentWrapper, WhitePageWrapper, HeaderWrapper } from '../wrappers'; +import { ContentWrapper, WhitePageWrapper } from '../wrappers'; import { CaseActionBar } from '../case_action_bar'; import { useGetCaseUserActions } from '../../containers/use_get_case_user_actions'; import { EditConnector } from '../edit_connector'; @@ -389,32 +389,31 @@ export const CaseComponent = React.memo( return ( <> - - - } - title={caseData.title} - > - - - + } + title={caseData.title} + > + + + diff --git a/x-pack/plugins/cases/public/components/configure_cases/index.tsx b/x-pack/plugins/cases/public/components/configure_cases/index.tsx index 788c6eeb61b32..cf7962f08db93 100644 --- a/x-pack/plugins/cases/public/components/configure_cases/index.tsx +++ b/x-pack/plugins/cases/public/components/configure_cases/index.tsx @@ -242,5 +242,7 @@ export const ConfigureCases: React.FC = React.memo((props) ); }); +ConfigureCases.displayName = 'ConfigureCases'; + // eslint-disable-next-line import/no-default-export export default ConfigureCases; diff --git a/x-pack/plugins/cases/public/components/connectors/connectors_registry.ts b/x-pack/plugins/cases/public/components/connectors/connectors_registry.ts index 2e02cb290c3c8..8174733301348 100644 --- a/x-pack/plugins/cases/public/components/connectors/connectors_registry.ts +++ b/x-pack/plugins/cases/public/components/connectors/connectors_registry.ts @@ -9,8 +9,25 @@ import { i18n } from '@kbn/i18n'; import { CaseConnector, CaseConnectorsRegistry } from './types'; export const createCaseConnectorsRegistry = (): CaseConnectorsRegistry => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any const connectors: Map> = new Map(); + function assertConnectorExists( + connector: CaseConnector | undefined | null, + id: string + ): asserts connector { + if (!connector) { + throw new Error( + i18n.translate('xpack.cases.connecors.get.missingCaseConnectorErrorMessage', { + defaultMessage: 'Object type "{id}" is not registered.', + values: { + id, + }, + }) + ); + } + } + const registry: CaseConnectorsRegistry = { has: (id: string) => connectors.has(id), register: (connector: CaseConnector) => { @@ -28,17 +45,9 @@ export const createCaseConnectorsRegistry = (): CaseConnectorsRegistry => { connectors.set(connector.id, connector); }, get: (id: string): CaseConnector => { - if (!connectors.has(id)) { - throw new Error( - i18n.translate('xpack.cases.connecors.get.missingCaseConnectorErrorMessage', { - defaultMessage: 'Object type "{id}" is not registered.', - values: { - id, - }, - }) - ); - } - return connectors.get(id)!; + const connector = connectors.get(id); + assertConnectorExists(connector, id); + return connector; }, list: () => { return Array.from(connectors).map(([id, connector]) => connector); diff --git a/x-pack/plugins/cases/public/components/create/description.tsx b/x-pack/plugins/cases/public/components/create/description.tsx index e1bd563a3d798..c1545a42df3f5 100644 --- a/x-pack/plugins/cases/public/components/create/description.tsx +++ b/x-pack/plugins/cases/public/components/create/description.tsx @@ -21,7 +21,7 @@ const DescriptionComponent: React.FC = ({ isLoading }) => { useLensDraftComment(); const { setFieldValue } = useFormContext(); const [{ title, tags }] = useFormData({ watch: ['title', 'tags'] }); - const editorRef = useRef>(); + const editorRef = useRef>(); useEffect(() => { if (draftComment?.commentId === fieldName && editorRef.current) { diff --git a/x-pack/plugins/cases/public/components/create/index.tsx b/x-pack/plugins/cases/public/components/create/index.tsx index d3eaba1ea0bc4..39f10a89290d8 100644 --- a/x-pack/plugins/cases/public/components/create/index.tsx +++ b/x-pack/plugins/cases/public/components/create/index.tsx @@ -98,5 +98,8 @@ export const CreateCase: React.FC = React.memo((props) => ( )); + +CreateCase.displayName = 'CreateCase'; + // eslint-disable-next-line import/no-default-export export { CreateCase as default }; diff --git a/x-pack/plugins/cases/public/components/header_page/index.test.tsx b/x-pack/plugins/cases/public/components/header_page/index.test.tsx index d84a6d9272def..55e5d0907c869 100644 --- a/x-pack/plugins/cases/public/components/header_page/index.test.tsx +++ b/x-pack/plugins/cases/public/components/header_page/index.test.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import euiDarkVars from '@elastic/eui/dist/eui_theme_dark.json'; +import { euiDarkVars } from '@kbn/ui-shared-deps-src/theme'; import { shallow } from 'enzyme'; import React from 'react'; diff --git a/x-pack/plugins/cases/public/components/markdown_editor/editor.tsx b/x-pack/plugins/cases/public/components/markdown_editor/editor.tsx index 4bf25b23403e1..e2067d75e843e 100644 --- a/x-pack/plugins/cases/public/components/markdown_editor/editor.tsx +++ b/x-pack/plugins/cases/public/components/markdown_editor/editor.tsx @@ -96,4 +96,6 @@ const MarkdownEditorComponent = forwardRef + <> {this.renderSearchBar()} {this.renderListing()} - + ); } @@ -481,16 +481,23 @@ export class SavedObjectFinderUi extends React.Component< {items.map((item) => { const currentSavedObjectMetaData = savedObjectMetaData.find( (metaData) => metaData.type === item.type - )!; + ); + + if (currentSavedObjectMetaData == null) { + return null; + } + const fullName = currentSavedObjectMetaData.getTooltipForSavedObject ? currentSavedObjectMetaData.getTooltipForSavedObject(item.savedObject) - : `${item.title} (${currentSavedObjectMetaData!.name})`; + : `${item.title} (${currentSavedObjectMetaData.name})`; + const iconType = ( currentSavedObjectMetaData || ({ getIconForSavedObject: () => 'document', } as Pick, 'getIconForSavedObject'>) ).getIconForSavedObject(item.savedObject); + return ( => { + const MarkdownLinkProcessingComponent: React.FC = memo((props) => ( + + )); + + MarkdownLinkProcessingComponent.displayName = 'MarkdownLinkProcessingComponent'; + + return MarkdownLinkProcessingComponent; +}; + const MarkdownRendererComponent: React.FC = ({ children, disableLinks }) => { const { processingPlugins, parsingPlugins } = usePlugins(); - const MarkdownLinkProcessingComponent: React.FC = useMemo( - () => (props) => , - [disableLinks] - ); // Deep clone of the processing plugins to prevent affecting the markdown editor. const processingPluginList = cloneDeep(processingPlugins); // This line of code is TS-compatible and it will break if [1][1] change in the future. - processingPluginList[1][1].components.a = MarkdownLinkProcessingComponent; + processingPluginList[1][1].components.a = useMemo( + () => withDisabledLinks(disableLinks), + [disableLinks] + ); return ( = React.memo((props) => { ); }); +RecentCases.displayName = 'RecentCases'; + // eslint-disable-next-line import/no-default-export export { RecentCases as default }; diff --git a/x-pack/plugins/cases/public/components/truncated_text/index.tsx b/x-pack/plugins/cases/public/components/truncated_text/index.tsx index 8a480ed9dbdd1..3cf7f8322d797 100644 --- a/x-pack/plugins/cases/public/components/truncated_text/index.tsx +++ b/x-pack/plugins/cases/public/components/truncated_text/index.tsx @@ -16,6 +16,7 @@ const Text = styled.span` -webkit-line-clamp: ${LINE_CLAMP}; -webkit-box-orient: vertical; overflow: hidden; + word-break: normal; `; interface Props { diff --git a/x-pack/plugins/cases/public/components/user_action_tree/helpers.tsx b/x-pack/plugins/cases/public/components/user_action_tree/helpers.tsx index 2eb44f91190c6..2419ac0d048e9 100644 --- a/x-pack/plugins/cases/public/components/user_action_tree/helpers.tsx +++ b/x-pack/plugins/cases/public/components/user_action_tree/helpers.tsx @@ -396,6 +396,8 @@ const ActionIcon = React.memo<{ ); }); +ActionIcon.displayName = 'ActionIcon'; + export const getActionAttachment = ({ comment, userCanCrud, diff --git a/x-pack/plugins/cases/public/components/user_action_tree/index.tsx b/x-pack/plugins/cases/public/components/user_action_tree/index.tsx index 24fc393715f76..95c4f76eae0a2 100644 --- a/x-pack/plugins/cases/public/components/user_action_tree/index.tsx +++ b/x-pack/plugins/cases/public/components/user_action_tree/index.tsx @@ -25,7 +25,7 @@ import * as i18n from './translations'; import { useUpdateComment } from '../../containers/use_update_comment'; import { useCurrentUser } from '../../common/lib/kibana'; -import { AddComment } from '../add_comment'; +import { AddComment, AddCommentRefObject } from '../add_comment'; import { ActionConnector, ActionsCommentRequestRt, @@ -52,7 +52,7 @@ import { getActionAttachment, } from './helpers'; import { UserActionAvatar } from './user_action_avatar'; -import { UserActionMarkdown } from './user_action_markdown'; +import { UserActionMarkdown, UserActionMarkdownRefObject } from './user_action_markdown'; import { UserActionTimestamp } from './user_action_timestamp'; import { UserActionUsername } from './user_action_username'; import { UserActionContentToolbar } from './user_action_content_toolbar'; @@ -131,6 +131,17 @@ const MyEuiCommentList = styled(EuiCommentList)` const DESCRIPTION_ID = 'description'; const NEW_ID = 'newComment'; +const isAddCommentRef = ( + ref: AddCommentRefObject | UserActionMarkdownRefObject | null | undefined +): ref is AddCommentRefObject => { + const commentRef = ref as AddCommentRefObject; + if (commentRef?.addQuote != null) { + return true; + } + + return false; +}; + export const UserActionTree = React.memo( ({ caseServices, @@ -167,7 +178,9 @@ export const UserActionTree = React.memo( const { isLoadingIds, patchComment } = useUpdateComment(); const currentUser = useCurrentUser(); const [manageMarkdownEditIds, setManageMarkdownEditIds] = useState([]); - const commentRefs = useRef>({}); + const commentRefs = useRef< + Record + >({}); const { clearDraftComment, draftComment, hasIncomingLensState, openLensModal } = useLensDraftComment(); @@ -228,8 +241,9 @@ export const UserActionTree = React.memo( const handleManageQuote = useCallback( (quote: string) => { - if (commentRefs.current[NEW_ID]) { - commentRefs.current[NEW_ID].addQuote(quote); + const ref = commentRefs?.current[NEW_ID]; + if (isAddCommentRef(ref)) { + ref.addQuote(quote); } handleOutlineComment('add-comment'); @@ -337,6 +351,8 @@ export const UserActionTree = React.memo( const userActions: EuiCommentProps[] = useMemo( () => caseUserActions.reduce( + // TODO: Decrease complexity. https://github.com/elastic/kibana/issues/115730 + // eslint-disable-next-line complexity (comments, action, index) => { // Comment creation if (action.commentId != null && action.action === 'create') { @@ -664,15 +680,12 @@ export const UserActionTree = React.memo( return prevManageMarkdownEditIds; }); - if ( - commentRefs.current && - commentRefs.current[draftComment.commentId] && - commentRefs.current[draftComment.commentId].editor?.textarea && - commentRefs.current[draftComment.commentId].editor?.toolbar - ) { - commentRefs.current[draftComment.commentId].setComment(draftComment.comment); + const ref = commentRefs?.current?.[draftComment.commentId]; + + if (isAddCommentRef(ref) && ref.editor?.textarea) { + ref.setComment(draftComment.comment); if (hasIncomingLensState) { - openLensModal({ editorRef: commentRefs.current[draftComment.commentId].editor }); + openLensModal({ editorRef: ref.editor }); } else { clearDraftComment(); } diff --git a/x-pack/plugins/cases/public/components/user_action_tree/user_action_markdown.tsx b/x-pack/plugins/cases/public/components/user_action_tree/user_action_markdown.tsx index f7a6932b35856..93212d2b11016 100644 --- a/x-pack/plugins/cases/public/components/user_action_tree/user_action_markdown.tsx +++ b/x-pack/plugins/cases/public/components/user_action_tree/user_action_markdown.tsx @@ -26,7 +26,7 @@ interface UserActionMarkdownProps { onSaveContent: (content: string) => void; } -interface UserActionMarkdownRefObject { +export interface UserActionMarkdownRefObject { setComment: (newComment: string) => void; } diff --git a/x-pack/plugins/cases/public/components/utility_bar/utility_bar.test.tsx b/x-pack/plugins/cases/public/components/utility_bar/utility_bar.test.tsx index 98af25a9af466..43ebd9bee3ca9 100644 --- a/x-pack/plugins/cases/public/components/utility_bar/utility_bar.test.tsx +++ b/x-pack/plugins/cases/public/components/utility_bar/utility_bar.test.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import euiDarkVars from '@elastic/eui/dist/eui_theme_dark.json'; +import { euiDarkVars } from '@kbn/ui-shared-deps-src/theme'; import { mount, shallow } from 'enzyme'; import React from 'react'; import { TestProviders } from '../../common/mock'; diff --git a/x-pack/plugins/cases/public/components/utils.test.ts b/x-pack/plugins/cases/public/components/utils.test.ts index 86d37d2e5e59e..e3cc753e75746 100644 --- a/x-pack/plugins/cases/public/components/utils.test.ts +++ b/x-pack/plugins/cases/public/components/utils.test.ts @@ -39,8 +39,49 @@ describe('Utils', () => { }); describe('isDeprecatedConnector', () => { + const connector = { + id: 'test', + actionTypeId: '.webhook', + name: 'Test', + config: { usesTableApi: false }, + secrets: {}, + isPreconfigured: false, + }; + it('returns false if the connector is not defined', () => { expect(isDeprecatedConnector()).toBe(false); }); + + it('returns false if the connector is not ITSM or SecOps', () => { + expect(isDeprecatedConnector(connector)).toBe(false); + }); + + it('returns false if the connector is .servicenow and the usesTableApi=false', () => { + expect(isDeprecatedConnector({ ...connector, actionTypeId: '.servicenow' })).toBe(false); + }); + + it('returns false if the connector is .servicenow-sir and the usesTableApi=false', () => { + expect(isDeprecatedConnector({ ...connector, actionTypeId: '.servicenow-sir' })).toBe(false); + }); + + it('returns true if the connector is .servicenow and the usesTableApi=true', () => { + expect( + isDeprecatedConnector({ + ...connector, + actionTypeId: '.servicenow', + config: { usesTableApi: true }, + }) + ).toBe(true); + }); + + it('returns true if the connector is .servicenow-sir and the usesTableApi=true', () => { + expect( + isDeprecatedConnector({ + ...connector, + actionTypeId: '.servicenow-sir', + config: { usesTableApi: true }, + }) + ).toBe(true); + }); }); }); diff --git a/x-pack/plugins/cases/public/components/utils.ts b/x-pack/plugins/cases/public/components/utils.ts index 3ac48135edae8..82d2682e65fad 100644 --- a/x-pack/plugins/cases/public/components/utils.ts +++ b/x-pack/plugins/cases/public/components/utils.ts @@ -12,11 +12,6 @@ import { StartPlugins } from '../types'; import { connectorValidator as swimlaneConnectorValidator } from './connectors/swimlane/validator'; import { connectorValidator as servicenowConnectorValidator } from './connectors/servicenow/validator'; import { CaseActionConnector } from './types'; -import { - ENABLE_NEW_SN_ITSM_CONNECTOR, - ENABLE_NEW_SN_SIR_CONNECTOR, - // eslint-disable-next-line @kbn/eslint/no-restricted-paths -} from '../../../actions/server/constants/connectors'; export const getConnectorById = ( id: string, @@ -83,24 +78,16 @@ export const isDeprecatedConnector = (connector?: CaseActionConnector): boolean return false; } - if (!ENABLE_NEW_SN_ITSM_CONNECTOR && connector.actionTypeId === '.servicenow') { - return true; + if (connector.actionTypeId === '.servicenow' || connector.actionTypeId === '.servicenow-sir') { + /** + * Connector's prior to the Elastic ServiceNow application + * use the Table API (https://developer.servicenow.com/dev.do#!/reference/api/rome/rest/c_TableAPI) + * Connectors after the Elastic ServiceNow application use the + * Import Set API (https://developer.servicenow.com/dev.do#!/reference/api/rome/rest/c_ImportSetAPI) + * A ServiceNow connector is considered deprecated if it uses the Table API. + */ + return !!connector.config.usesTableApi; } - if (!ENABLE_NEW_SN_SIR_CONNECTOR && connector.actionTypeId === '.servicenow-sir') { - return true; - } - - /** - * Connector's prior to the Elastic ServiceNow application - * use the Table API (https://developer.servicenow.com/dev.do#!/reference/api/rome/rest/c_TableAPI) - * Connectors after the Elastic ServiceNow application use the - * Import Set API (https://developer.servicenow.com/dev.do#!/reference/api/rome/rest/c_ImportSetAPI) - * A ServiceNow connector is considered deprecated if it uses the Table API. - * - * All other connectors do not have the usesTableApi config property - * so the function will always return false for them. - */ - - return !!connector.config.usesTableApi; + return false; }; diff --git a/x-pack/plugins/cases/public/components/wrappers/index.tsx b/x-pack/plugins/cases/public/components/wrappers/index.tsx index 4c8a3a681f024..7a3d611413be6 100644 --- a/x-pack/plugins/cases/public/components/wrappers/index.tsx +++ b/x-pack/plugins/cases/public/components/wrappers/index.tsx @@ -20,24 +20,10 @@ export const SectionWrapper = styled.div` width: 100%; `; -export const HeaderWrapper = styled.div` - ${({ theme }) => - ` - padding: ${theme.eui.paddingSizes.l} ${theme.eui.paddingSizes.l} 0 ${theme.eui.paddingSizes.l}; - @media only screen and (max-width: ${theme.eui.euiBreakpoints.s}) { - padding: ${theme.eui.paddingSizes.s} ${theme.eui.paddingSizes.s} 0 - ${theme.eui.paddingSizes.s}; - } - `}; -`; const gutterTimeline = '70px'; // seems to be a timeline reference from the original file export const ContentWrapper = styled.div` ${({ theme }) => ` - padding: ${theme.eui.paddingSizes.l} ${theme.eui.paddingSizes.l} ${gutterTimeline} ${theme.eui.paddingSizes.l}; - @media only screen and (max-width: ${theme.eui.euiBreakpoints.s}) { - padding: ${theme.eui.paddingSizes.s} ${theme.eui.paddingSizes.s} ${gutterTimeline} - ${theme.eui.paddingSizes.s}; - } + padding: ${theme.eui.paddingSizes.l} 0 ${gutterTimeline} 0; `}; `; diff --git a/x-pack/plugins/cases/public/containers/api.ts b/x-pack/plugins/cases/public/containers/api.ts index 75e8c8f58705d..14f617b19db52 100644 --- a/x-pack/plugins/cases/public/containers/api.ts +++ b/x-pack/plugins/cases/public/containers/api.ts @@ -87,7 +87,7 @@ export const resolveCase = async ( signal: AbortSignal ): Promise => { const response = await KibanaServices.get().http.fetch( - getCaseDetailsUrl(caseId) + '/resolve', + `${getCaseDetailsUrl(caseId)}/resolve`, { method: 'GET', query: { diff --git a/x-pack/plugins/cases/public/containers/use_post_comment.tsx b/x-pack/plugins/cases/public/containers/use_post_comment.tsx index 8677787fd9af4..2d4437826092a 100644 --- a/x-pack/plugins/cases/public/containers/use_post_comment.tsx +++ b/x-pack/plugins/cases/public/containers/use_post_comment.tsx @@ -41,7 +41,7 @@ const dataFetchReducer = (state: NewCommentState, action: Action): NewCommentSta } }; -interface PostComment { +export interface PostComment { caseId: string; data: CommentRequest; updateCase?: (newCase: Case) => void; diff --git a/x-pack/plugins/cases/public/utils/use_mount_appended.ts b/x-pack/plugins/cases/public/utils/use_mount_appended.ts index d43b0455f47da..48b71e6dbabfe 100644 --- a/x-pack/plugins/cases/public/utils/use_mount_appended.ts +++ b/x-pack/plugins/cases/public/utils/use_mount_appended.ts @@ -5,6 +5,8 @@ * 2.0. */ +/* eslint-disable @typescript-eslint/no-explicit-any */ + // eslint-disable-next-line import/no-extraneous-dependencies import { mount } from 'enzyme'; diff --git a/x-pack/plugins/cases/server/authorization/authorization.ts b/x-pack/plugins/cases/server/authorization/authorization.ts index 3598c5b8956fa..7a474ff4db402 100644 --- a/x-pack/plugins/cases/server/authorization/authorization.ts +++ b/x-pack/plugins/cases/server/authorization/authorization.ts @@ -231,8 +231,10 @@ export class Authorization { ? Array.from(featureCaseOwners) : privileges.kibana.reduce((authorizedOwners, { authorized, privilege }) => { if (authorized && requiredPrivileges.has(privilege)) { - const owner = requiredPrivileges.get(privilege)!; - authorizedOwners.push(owner); + const owner = requiredPrivileges.get(privilege); + if (owner) { + authorizedOwners.push(owner); + } } return authorizedOwners; diff --git a/x-pack/plugins/cases/server/client/attachments/add.ts b/x-pack/plugins/cases/server/client/attachments/add.ts index b84a6bd84c43b..159ff3b41aba9 100644 --- a/x-pack/plugins/cases/server/client/attachments/add.ts +++ b/x-pack/plugins/cases/server/client/attachments/add.ts @@ -263,7 +263,7 @@ async function getCombinedCase({ id, }), ] - : [Promise.reject('case connector feature is disabled')]), + : [Promise.reject(new Error('case connector feature is disabled'))]), ]); if (subCasePromise.status === 'fulfilled') { diff --git a/x-pack/plugins/cases/server/common/error.ts b/x-pack/plugins/cases/server/common/error.ts index 1b53eb9fdb218..3478131f65537 100644 --- a/x-pack/plugins/cases/server/common/error.ts +++ b/x-pack/plugins/cases/server/common/error.ts @@ -8,10 +8,14 @@ import { Boom, isBoom } from '@hapi/boom'; import { Logger } from 'src/core/server'; +export interface HTTPError extends Error { + statusCode: number; +} + /** * Helper class for wrapping errors while preserving the original thrown error. */ -class CaseError extends Error { +export class CaseError extends Error { public readonly wrappedError?: Error; constructor(message?: string, originalError?: Error) { super(message); @@ -51,6 +55,13 @@ export function isCaseError(error: unknown): error is CaseError { return error instanceof CaseError; } +/** + * Type guard for determining if an error is an HTTPError + */ +export function isHTTPError(error: unknown): error is HTTPError { + return (error as HTTPError)?.statusCode != null; +} + /** * Create a CaseError that wraps the original thrown error. This also logs the message that will be placed in the CaseError * if the logger was defined. diff --git a/x-pack/plugins/cases/server/connectors/case/schema.ts b/x-pack/plugins/cases/server/connectors/case/schema.ts index 79d3bf62e8a9e..b8e46fdf5aa8c 100644 --- a/x-pack/plugins/cases/server/connectors/case/schema.ts +++ b/x-pack/plugins/cases/server/connectors/case/schema.ts @@ -83,6 +83,7 @@ const SwimlaneFieldsSchema = schema.object({ const NoneFieldsSchema = schema.nullable(schema.object({})); +// eslint-disable-next-line @typescript-eslint/no-explicit-any const ReducedConnectorFieldsSchema: { [x: string]: any } = { [ConnectorTypes.jira]: JiraFieldsSchema, [ConnectorTypes.resilient]: ResilientFieldsSchema, diff --git a/x-pack/plugins/cases/server/connectors/servicenow/sir_format.test.ts b/x-pack/plugins/cases/server/connectors/servicenow/sir_format.test.ts index b09272d0a5505..706b9f2f23ab5 100644 --- a/x-pack/plugins/cases/server/connectors/servicenow/sir_format.test.ts +++ b/x-pack/plugins/cases/server/connectors/servicenow/sir_format.test.ts @@ -8,7 +8,7 @@ import { CaseResponse } from '../../../common'; import { format } from './sir_format'; -describe('ITSM formatter', () => { +describe('SIR formatter', () => { const theCase = { id: 'case-id', connector: { diff --git a/x-pack/plugins/cases/server/connectors/servicenow/sir_format.ts b/x-pack/plugins/cases/server/connectors/servicenow/sir_format.ts index 9108408c4d089..02c9fe629f4f8 100644 --- a/x-pack/plugins/cases/server/connectors/servicenow/sir_format.ts +++ b/x-pack/plugins/cases/server/connectors/servicenow/sir_format.ts @@ -45,12 +45,16 @@ export const format: ServiceNowSIRFormat = (theCase, alerts) => { if (fieldsToAdd.length > 0) { sirFields = alerts.reduce>((acc, alert) => { + let temp = {}; fieldsToAdd.forEach((alertField) => { const field = get(alertFieldMapping[alertField].alertPath, alert); + if (field && !manageDuplicate[alertFieldMapping[alertField].sirFieldKey].has(field)) { manageDuplicate[alertFieldMapping[alertField].sirFieldKey].add(field); - acc = { + + temp = { ...acc, + ...temp, [alertFieldMapping[alertField].sirFieldKey]: [ ...acc[alertFieldMapping[alertField].sirFieldKey], field, @@ -58,7 +62,8 @@ export const format: ServiceNowSIRFormat = (theCase, alerts) => { }; } }); - return acc; + + return { ...acc, ...temp }; }, sirFields); } diff --git a/x-pack/plugins/cases/server/index.ts b/x-pack/plugins/cases/server/index.ts index 57db6e5565fff..765d21af8538c 100644 --- a/x-pack/plugins/cases/server/index.ts +++ b/x-pack/plugins/cases/server/index.ts @@ -16,7 +16,7 @@ export const config: PluginConfigDescriptor = { markdownPlugins: true, }, deprecations: ({ renameFromRoot }) => [ - renameFromRoot('xpack.case.enabled', 'xpack.cases.enabled'), + renameFromRoot('xpack.case.enabled', 'xpack.cases.enabled', { level: 'critical' }), ], }; export const plugin = (initializerContext: PluginInitializerContext) => diff --git a/x-pack/plugins/cases/server/plugin.ts b/x-pack/plugins/cases/server/plugin.ts index bef8d45bd86f6..9bbc7089c033c 100644 --- a/x-pack/plugins/cases/server/plugin.ts +++ b/x-pack/plugins/cases/server/plugin.ts @@ -126,6 +126,11 @@ export class CasePlugin { }, featuresPluginStart: plugins.features, actionsPluginStart: plugins.actions, + /** + * Lens will be always defined as + * it is declared as required plugin in kibana.json + */ + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion lensEmbeddableFactory: this.lensEmbeddableFactory!, }); diff --git a/x-pack/plugins/cases/server/routes/api/utils.test.ts b/x-pack/plugins/cases/server/routes/api/utils.test.ts index 3fce38b27446e..fd7c038f06bc1 100644 --- a/x-pack/plugins/cases/server/routes/api/utils.test.ts +++ b/x-pack/plugins/cases/server/routes/api/utils.test.ts @@ -5,8 +5,9 @@ * 2.0. */ -import { wrapError } from './utils'; import { isBoom, boomify } from '@hapi/boom'; +import { HTTPError } from '../../common'; +import { wrapError } from './utils'; describe('Utils', () => { describe('wrapError', () => { @@ -25,7 +26,7 @@ describe('Utils', () => { }); it('it set statusCode to errors status code', () => { - const error = new Error('Something happened') as any; + const error = new Error('Something happened') as HTTPError; error.statusCode = 404; const res = wrapError(error); diff --git a/x-pack/plugins/cases/server/routes/api/utils.ts b/x-pack/plugins/cases/server/routes/api/utils.ts index b2b5417ecae0f..cb4804aab0054 100644 --- a/x-pack/plugins/cases/server/routes/api/utils.ts +++ b/x-pack/plugins/cases/server/routes/api/utils.ts @@ -9,18 +9,21 @@ import { Boom, boomify, isBoom } from '@hapi/boom'; import { schema } from '@kbn/config-schema'; import { CustomHttpResponseOptions, ResponseError } from 'kibana/server'; -import { isCaseError } from '../../common'; +import { CaseError, isCaseError, HTTPError, isHTTPError } from '../../common'; /** * Transforms an error into the correct format for a kibana response. */ -export function wrapError(error: any): CustomHttpResponseOptions { + +export function wrapError( + error: CaseError | Boom | HTTPError | Error +): CustomHttpResponseOptions { let boom: Boom; if (isCaseError(error)) { boom = error.boomify(); } else { - const options = { statusCode: error.statusCode ?? 500 }; + const options = { statusCode: isHTTPError(error) ? error.statusCode : 500 }; boom = isBoom(error) ? error : boomify(error, options); } diff --git a/x-pack/plugins/cases/server/scripts/sub_cases/index.ts b/x-pack/plugins/cases/server/scripts/sub_cases/index.ts index 3cb013bd2e3fd..28672160a0737 100644 --- a/x-pack/plugins/cases/server/scripts/sub_cases/index.ts +++ b/x-pack/plugins/cases/server/scripts/sub_cases/index.ts @@ -4,7 +4,11 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ + +/* eslint-disable @typescript-eslint/no-explicit-any */ /* eslint-disable no-console */ +/* eslint-disable no-process-exit */ + import yargs from 'yargs'; import { ToolingLog } from '@kbn/dev-utils'; import { KbnClient } from '@kbn/test'; diff --git a/x-pack/plugins/cases/server/services/alerts/index.ts b/x-pack/plugins/cases/server/services/alerts/index.ts index 6bb2fb3ee3c56..d8a09fe1baf23 100644 --- a/x-pack/plugins/cases/server/services/alerts/index.ts +++ b/x-pack/plugins/cases/server/services/alerts/index.ts @@ -48,8 +48,6 @@ function isEmptyAlert(alert: AlertInfo): boolean { } export class AlertService { - constructor() {} - public async updateAlertsStatus({ alerts, scopedClusterClient, logger }: UpdateAlertsStatusArgs) { try { const bucketedAlerts = bucketAlertsByIndexAndStatus(alerts, logger); diff --git a/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/services/time_buckets.d.ts b/x-pack/plugins/data_visualizer/common/services/time_buckets.d.ts similarity index 96% rename from x-pack/plugins/data_visualizer/public/application/index_data_visualizer/services/time_buckets.d.ts rename to x-pack/plugins/data_visualizer/common/services/time_buckets.d.ts index 9a5410918a099..62a3187be47dc 100644 --- a/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/services/time_buckets.d.ts +++ b/x-pack/plugins/data_visualizer/common/services/time_buckets.d.ts @@ -31,7 +31,7 @@ export declare class TimeBuckets { public setMaxBars(maxBars: number): void; public setInterval(interval: string): void; public setBounds(bounds: TimeRangeBounds): void; - public getBounds(): { min: any; max: any }; + public getBounds(): { min: Moment; max: Moment }; public getInterval(): TimeBucketsInterval; public getScaledDateFormat(): string; } diff --git a/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/services/time_buckets.js b/x-pack/plugins/data_visualizer/common/services/time_buckets.js similarity index 98% rename from x-pack/plugins/data_visualizer/public/application/index_data_visualizer/services/time_buckets.js rename to x-pack/plugins/data_visualizer/common/services/time_buckets.js index 5d54b6c936fb2..49de535ee6c26 100644 --- a/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/services/time_buckets.js +++ b/x-pack/plugins/data_visualizer/common/services/time_buckets.js @@ -5,12 +5,12 @@ * 2.0. */ -import { FIELD_FORMAT_IDS } from '../../../../../../../src/plugins/field_formats/common'; -import { UI_SETTINGS } from '../../../../../../../src/plugins/data/common'; +import { FIELD_FORMAT_IDS } from '../../../../../src/plugins/field_formats/common'; +import { UI_SETTINGS } from '../../../../../src/plugins/data/common'; import { ary, assign, isPlainObject, isString, sortBy } from 'lodash'; import moment from 'moment'; import dateMath from '@elastic/datemath'; -import { parseInterval } from '../../common/util/parse_interval'; +import { parseInterval } from '../utils/parse_interval'; const { duration: d } = moment; diff --git a/x-pack/plugins/data_visualizer/common/types/field_request_config.ts b/x-pack/plugins/data_visualizer/common/types/field_request_config.ts index 36e8fe14b7002..f0ea7079bf750 100644 --- a/x-pack/plugins/data_visualizer/common/types/field_request_config.ts +++ b/x-pack/plugins/data_visualizer/common/types/field_request_config.ts @@ -14,7 +14,7 @@ export interface Percentile { } export interface FieldRequestConfig { - fieldName?: string; + fieldName: string; type: JobFieldType; cardinality: number; } @@ -29,6 +29,7 @@ export interface DocumentCounts { } export interface FieldVisStats { + error?: Error; cardinality?: number; count?: number; sampleCount?: number; @@ -58,3 +59,10 @@ export interface FieldVisStats { timeRangeEarliest?: number; timeRangeLatest?: number; } + +export interface DVErrorObject { + causedBy?: string; + message: string; + statusCode?: number; + fullError?: Error; +} diff --git a/x-pack/plugins/data_visualizer/common/types/field_stats.ts b/x-pack/plugins/data_visualizer/common/types/field_stats.ts new file mode 100644 index 0000000000000..8932a0641cbe6 --- /dev/null +++ b/x-pack/plugins/data_visualizer/common/types/field_stats.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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; +import { Query } from '@kbn/es-query'; +import { isPopulatedObject } from '../utils/object_utils'; +import { IKibanaSearchResponse } from '../../../../../src/plugins/data/common'; +import { TimeBucketsInterval } from '../services/time_buckets'; + +export interface FieldData { + fieldName: string; + existsInDocs: boolean; + stats?: { + sampleCount?: number; + count?: number; + cardinality?: number; + }; +} + +export interface Field { + fieldName: string; + type: string; + cardinality: number; + safeFieldName: string; +} + +// @todo: check +export function isValidField(arg: unknown): arg is Field { + return isPopulatedObject(arg, ['fieldName', 'type']) && typeof arg.fieldName === 'string'; +} + +export interface HistogramField { + fieldName: string; + type: string; +} + +export interface Distribution { + percentiles: Array<{ value?: number; percent: number; minValue: number; maxValue: number }>; + minPercentile: number; + maxPercentile: number; +} + +export interface Bucket { + doc_count: number; +} + +export interface FieldStatsError { + fieldName?: string; + fields?: Field[]; + error: Error; +} + +export const isIKibanaSearchResponse = (arg: unknown): arg is IKibanaSearchResponse => { + return isPopulatedObject(arg, ['rawResponse']); +}; + +export interface NumericFieldStats { + fieldName: string; + count: number; + min: number; + max: number; + avg: number; + isTopValuesSampled: boolean; + topValues: Bucket[]; + topValuesSampleSize: number; + topValuesSamplerShardSize: number; + median?: number; + distribution?: Distribution; +} + +export interface StringFieldStats { + fieldName: string; + isTopValuesSampled: boolean; + topValues: Bucket[]; + topValuesSampleSize: number; + topValuesSamplerShardSize: number; +} + +export interface DateFieldStats { + fieldName: string; + count: number; + earliest: number; + latest: number; +} + +export interface BooleanFieldStats { + fieldName: string; + count: number; + trueCount: number; + falseCount: number; + [key: string]: number | string; +} + +export interface DocumentCountStats { + interval: number; + buckets: { [key: string]: number }; + timeRangeEarliest: number; + timeRangeLatest: number; +} + +export interface FieldExamples { + fieldName: string; + examples: unknown[]; +} + +export interface NumericColumnStats { + interval: number; + min: number; + max: number; +} +export type NumericColumnStatsMap = Record; + +export interface AggHistogram { + histogram: estypes.AggregationsHistogramAggregation; +} + +export interface AggTerms { + terms: { + field: string; + size: number; + }; +} + +export interface NumericDataItem { + key: number; + key_as_string?: string; + doc_count: number; +} + +export interface NumericChartData { + data: NumericDataItem[]; + id: string; + interval: number; + stats: [number, number]; + type: 'numeric'; +} + +export interface OrdinalDataItem { + key: string; + key_as_string?: string; + doc_count: number; +} + +export interface OrdinalChartData { + type: 'ordinal' | 'boolean'; + cardinality: number; + data: OrdinalDataItem[]; + id: string; +} + +export interface UnsupportedChartData { + id: string; + type: 'unsupported'; +} + +export interface AggCardinality { + cardinality: estypes.AggregationsCardinalityAggregation; +} + +export type ChartRequestAgg = AggHistogram | AggCardinality | AggTerms; + +export type ChartData = NumericChartData | OrdinalChartData | UnsupportedChartData; + +export type BatchStats = + | NumericFieldStats + | StringFieldStats + | BooleanFieldStats + | DateFieldStats + | DocumentCountStats + | FieldExamples; + +export type FieldStats = + | NumericFieldStats + | StringFieldStats + | BooleanFieldStats + | DateFieldStats + | FieldExamples + | FieldStatsError; + +export function isValidFieldStats(arg: unknown): arg is FieldStats { + return isPopulatedObject(arg, ['fieldName', 'type', 'count']); +} + +export interface FieldStatsCommonRequestParams { + index: string; + samplerShardSize: number; + timeFieldName?: string; + earliestMs?: number | undefined; + latestMs?: number | undefined; + runtimeFieldMap?: estypes.MappingRuntimeFields; + intervalMs?: number; + query: estypes.QueryDslQueryContainer; + maxExamples?: number; +} + +export interface OverallStatsSearchStrategyParams { + sessionId?: string; + earliest?: number; + latest?: number; + aggInterval: TimeBucketsInterval; + intervalMs?: number; + searchQuery: Query['query']; + samplerShardSize: number; + index: string; + timeFieldName?: string; + runtimeFieldMap?: estypes.MappingRuntimeFields; + aggregatableFields: string[]; + nonAggregatableFields: string[]; +} + +export interface FieldStatsSearchStrategyReturnBase { + progress: DataStatsFetchProgress; + fieldStats: Map | undefined; + startFetch: () => void; + cancelFetch: () => void; +} + +export interface DataStatsFetchProgress { + error?: Error; + isRunning: boolean; + loaded: number; + total: number; +} + +export interface FieldData { + fieldName: string; + existsInDocs: boolean; + stats?: { + sampleCount?: number; + count?: number; + cardinality?: number; + }; +} + +export interface Field { + fieldName: string; + type: string; + cardinality: number; + safeFieldName: string; +} + +export interface Aggs { + [key: string]: estypes.AggregationsAggregationContainer; +} diff --git a/x-pack/plugins/data_visualizer/public/application/common/components/stats_table/types/field_vis_config.ts b/x-pack/plugins/data_visualizer/common/types/field_vis_config.ts similarity index 92% rename from x-pack/plugins/data_visualizer/public/application/common/components/stats_table/types/field_vis_config.ts rename to x-pack/plugins/data_visualizer/common/types/field_vis_config.ts index eeb9fe12692fd..dcd7da74b85ef 100644 --- a/x-pack/plugins/data_visualizer/public/application/common/components/stats_table/types/field_vis_config.ts +++ b/x-pack/plugins/data_visualizer/common/types/field_vis_config.ts @@ -5,8 +5,7 @@ * 2.0. */ -import type { Percentile, JobFieldType, FieldVisStats } from '../../../../../../common/types'; - +import type { Percentile, JobFieldType, FieldVisStats } from './index'; export interface MetricFieldVisStats { avg?: number; distribution?: { @@ -23,7 +22,7 @@ export interface MetricFieldVisStats { // which display the field information. export interface FieldVisConfig { type: JobFieldType; - fieldName?: string; + fieldName: string; displayName?: string; existsInDocs: boolean; aggregatable: boolean; diff --git a/x-pack/plugins/data_visualizer/common/types/index.ts b/x-pack/plugins/data_visualizer/common/types/index.ts index 1153b45e1cce2..381f7a556b18d 100644 --- a/x-pack/plugins/data_visualizer/common/types/index.ts +++ b/x-pack/plugins/data_visualizer/common/types/index.ts @@ -15,7 +15,6 @@ export type { FieldVisStats, Percentile, } from './field_request_config'; -export type InputData = any[]; export interface DataVisualizerTableState { pageSize: number; diff --git a/x-pack/plugins/data_visualizer/public/application/common/util/parse_interval.ts b/x-pack/plugins/data_visualizer/common/utils/parse_interval.ts similarity index 100% rename from x-pack/plugins/data_visualizer/public/application/common/util/parse_interval.ts rename to x-pack/plugins/data_visualizer/common/utils/parse_interval.ts diff --git a/x-pack/plugins/data_visualizer/common/utils/query_utils.ts b/x-pack/plugins/data_visualizer/common/utils/query_utils.ts index 2aa4cd063d1b1..dc21bbcae96c3 100644 --- a/x-pack/plugins/data_visualizer/common/utils/query_utils.ts +++ b/x-pack/plugins/data_visualizer/common/utils/query_utils.ts @@ -6,6 +6,8 @@ */ import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; +import { Query } from '@kbn/es-query'; + /* * Contains utility functions for building and processing queries. */ @@ -16,8 +18,8 @@ export function buildBaseFilterCriteria( timeFieldName?: string, earliestMs?: number, latestMs?: number, - query?: object -) { + query?: Query['query'] +): estypes.QueryDslQueryContainer[] { const filterCriteria = []; if (timeFieldName && earliestMs && latestMs) { filterCriteria.push({ @@ -31,7 +33,7 @@ export function buildBaseFilterCriteria( }); } - if (query) { + if (query && typeof query === 'object') { filterCriteria.push(query); } diff --git a/x-pack/plugins/data_visualizer/kibana.json b/x-pack/plugins/data_visualizer/kibana.json index e63a6b4fa2100..81fc0a2fdfe02 100644 --- a/x-pack/plugins/data_visualizer/kibana.json +++ b/x-pack/plugins/data_visualizer/kibana.json @@ -25,7 +25,8 @@ "kibanaReact", "maps", "esUiShared", - "fieldFormats" + "fieldFormats", + "charts" ], "extraPublicDirs": [ "common" diff --git a/x-pack/plugins/data_visualizer/public/application/common/components/document_count_content/document_count_chart/document_count_chart.tsx b/x-pack/plugins/data_visualizer/public/application/common/components/document_count_content/document_count_chart/document_count_chart.tsx index 6459fc4006cea..13b68d3b192cc 100644 --- a/x-pack/plugins/data_visualizer/public/application/common/components/document_count_content/document_count_chart/document_count_chart.tsx +++ b/x-pack/plugins/data_visualizer/public/application/common/components/document_count_content/document_count_chart/document_count_chart.tsx @@ -6,16 +6,13 @@ */ import React, { FC, useCallback, useMemo } from 'react'; - import { i18n } from '@kbn/i18n'; - import { Axis, BarSeries, BrushEndListener, Chart, ElementClickListener, - niceTimeFormatter, Position, ScaleType, Settings, @@ -23,7 +20,9 @@ import { XYBrushEvent, } from '@elastic/charts'; import moment from 'moment'; +import { IUiSettingsClient } from 'kibana/public'; import { useDataVisualizerKibana } from '../../../../kibana_context'; +import { MULTILAYER_TIME_AXIS_STYLE } from '../../../../../../../../../src/plugins/charts/common'; export interface DocumentCountChartPoint { time: number | string; @@ -40,6 +39,16 @@ interface Props { const SPEC_ID = 'document_count'; +function getTimezone(uiSettings: IUiSettingsClient) { + if (uiSettings.isDefault('dateFormat:tz')) { + const detectedTimezone = moment.tz.guess(); + if (detectedTimezone) return detectedTimezone; + else return moment().format('Z'); + } else { + return uiSettings.get('dateFormat:tz', 'Browser'); + } +} + export const DocumentCountChart: FC = ({ width, chartPoints, @@ -48,9 +57,12 @@ export const DocumentCountChart: FC = ({ interval, }) => { const { - services: { data }, + services: { data, uiSettings, fieldFormats }, } = useDataVisualizerKibana(); + const xAxisFormatter = fieldFormats.deserialize({ id: 'date' }); + const useLegacyTimeAxis = uiSettings.get('visualization:useLegacyTimeAxis', false); + const seriesName = i18n.translate( 'xpack.dataVisualizer.dataGrid.field.documentCountChart.seriesLabel', { @@ -63,8 +75,6 @@ export const DocumentCountChart: FC = ({ max: timeRangeLatest, }; - const dateFormatter = niceTimeFormatter([timeRangeEarliest, timeRangeLatest]); - const adjustedChartPoints = useMemo(() => { // Display empty chart when no data in range if (chartPoints.length < 1) return [{ time: timeRangeEarliest, value: 0 }]; @@ -110,6 +120,8 @@ export const DocumentCountChart: FC = ({ timefilterUpdateHandler(range); }; + const timeZone = getTimezone(uiSettings); + return (
    = ({ id="bottom" position={Position.Bottom} showOverlappingTicks={true} - tickFormat={dateFormatter} + tickFormat={(value) => xAxisFormatter.convert(value)} + timeAxisLayerCount={useLegacyTimeAxis ? 0 : 2} + style={useLegacyTimeAxis ? {} : MULTILAYER_TIME_AXIS_STYLE} /> = ({ xAccessor="time" yAccessors={['value']} data={adjustedChartPoints} + timeZone={timeZone} />
    diff --git a/x-pack/plugins/data_visualizer/public/application/common/components/document_count_content/document_count_content.tsx b/x-pack/plugins/data_visualizer/public/application/common/components/document_count_content/document_count_content.tsx index d49dbdc7cb446..832e18a12369f 100644 --- a/x-pack/plugins/data_visualizer/public/application/common/components/document_count_content/document_count_content.tsx +++ b/x-pack/plugins/data_visualizer/public/application/common/components/document_count_content/document_count_content.tsx @@ -7,30 +7,25 @@ import React, { FC } from 'react'; import { DocumentCountChart, DocumentCountChartPoint } from './document_count_chart'; -import { FieldVisConfig, FileBasedFieldVisConfig } from '../stats_table/types'; import { TotalCountHeader } from './total_count_header'; +import { DocumentCountStats } from '../../../../../common/types/field_stats'; export interface Props { - config?: FieldVisConfig | FileBasedFieldVisConfig; + documentCountStats?: DocumentCountStats; totalCount: number; } -export const DocumentCountContent: FC = ({ config, totalCount }) => { - if (config?.stats === undefined) { +export const DocumentCountContent: FC = ({ documentCountStats, totalCount }) => { + if (documentCountStats === undefined) { return totalCount !== undefined ? : null; } - const { documentCounts, timeRangeEarliest, timeRangeLatest } = config.stats; - if ( - documentCounts === undefined || - timeRangeEarliest === undefined || - timeRangeLatest === undefined - ) - return null; + const { timeRangeEarliest, timeRangeLatest } = documentCountStats; + if (timeRangeEarliest === undefined || timeRangeLatest === undefined) return null; let chartPoints: DocumentCountChartPoint[] = []; - if (documentCounts.buckets !== undefined) { - const buckets: Record = documentCounts?.buckets; + if (documentCountStats.buckets !== undefined) { + const buckets: Record = documentCountStats?.buckets; chartPoints = Object.entries(buckets).map(([time, value]) => ({ time: +time, value })); } @@ -41,7 +36,7 @@ export const DocumentCountContent: FC = ({ config, totalCount }) => { chartPoints={chartPoints} timeRangeEarliest={timeRangeEarliest} timeRangeLatest={timeRangeLatest} - interval={documentCounts.interval} + interval={documentCountStats.interval} /> ); diff --git a/x-pack/plugins/data_visualizer/public/application/common/components/expanded_row/file_based_expanded_row.tsx b/x-pack/plugins/data_visualizer/public/application/common/components/expanded_row/file_based_expanded_row.tsx index 8a9f9a25c16fa..7ba1615e22b43 100644 --- a/x-pack/plugins/data_visualizer/public/application/common/components/expanded_row/file_based_expanded_row.tsx +++ b/x-pack/plugins/data_visualizer/public/application/common/components/expanded_row/file_based_expanded_row.tsx @@ -17,7 +17,7 @@ import { } from '../stats_table/components/field_data_expanded_row'; import { GeoPointContent } from './geo_point_content/geo_point_content'; import { JOB_FIELD_TYPES } from '../../../../../common'; -import type { FileBasedFieldVisConfig } from '../stats_table/types/field_vis_config'; +import type { FileBasedFieldVisConfig } from '../../../../../common/types/field_vis_config'; export const FileBasedDataVisualizerExpandedRow = ({ item }: { item: FileBasedFieldVisConfig }) => { const config = item; diff --git a/x-pack/plugins/data_visualizer/public/application/common/components/expanded_row/index_based_expanded_row.tsx b/x-pack/plugins/data_visualizer/public/application/common/components/expanded_row/index_based_expanded_row.tsx index 79af35f1c8005..b87da2b3da789 100644 --- a/x-pack/plugins/data_visualizer/public/application/common/components/expanded_row/index_based_expanded_row.tsx +++ b/x-pack/plugins/data_visualizer/public/application/common/components/expanded_row/index_based_expanded_row.tsx @@ -23,6 +23,7 @@ import { IndexPattern } from '../../../../../../../../src/plugins/data/common'; import { CombinedQuery } from '../../../index_data_visualizer/types/combined_query'; import { LoadingIndicator } from '../loading_indicator'; import { IndexPatternField } from '../../../../../../../../src/plugins/data/common'; +import { ErrorMessageContent } from '../stats_table/components/field_data_expanded_row/error_message'; export const IndexBasedDataVisualizerExpandedRow = ({ item, @@ -46,6 +47,10 @@ export const IndexBasedDataVisualizerExpandedRow = ({ return ; } + if (config.stats?.error) { + return ; + } + switch (type) { case JOB_FIELD_TYPES.NUMBER: return ; diff --git a/x-pack/plugins/data_visualizer/public/application/common/components/field_names_filter/field_names_filter.tsx b/x-pack/plugins/data_visualizer/public/application/common/components/field_names_filter/field_names_filter.tsx index 88b4cd406b33c..58e9b9b5740dc 100644 --- a/x-pack/plugins/data_visualizer/public/application/common/components/field_names_filter/field_names_filter.tsx +++ b/x-pack/plugins/data_visualizer/public/application/common/components/field_names_filter/field_names_filter.tsx @@ -11,7 +11,7 @@ import { MultiSelectPicker } from '../multi_select_picker'; import type { FileBasedFieldVisConfig, FileBasedUnknownFieldVisConfig, -} from '../stats_table/types/field_vis_config'; +} from '../../../../../common/types/field_vis_config'; interface Props { fields: Array; diff --git a/x-pack/plugins/data_visualizer/public/application/common/components/field_type_icon/__snapshots__/field_type_icon.test.tsx.snap b/x-pack/plugins/data_visualizer/public/application/common/components/field_type_icon/__snapshots__/field_type_icon.test.tsx.snap index 398dc5dad2dc7..af4464cbc6b4e 100644 --- a/x-pack/plugins/data_visualizer/public/application/common/components/field_type_icon/__snapshots__/field_type_icon.test.tsx.snap +++ b/x-pack/plugins/data_visualizer/public/application/common/components/field_type_icon/__snapshots__/field_type_icon.test.tsx.snap @@ -3,15 +3,13 @@ exports[`FieldTypeIcon render component when type matches a field type 1`] = ` - `; diff --git a/x-pack/plugins/data_visualizer/public/application/common/components/field_type_icon/field_type_icon.test.tsx b/x-pack/plugins/data_visualizer/public/application/common/components/field_type_icon/field_type_icon.test.tsx index b6a5ff3e5dbed..0c036dd6c6d76 100644 --- a/x-pack/plugins/data_visualizer/public/application/common/components/field_type_icon/field_type_icon.test.tsx +++ b/x-pack/plugins/data_visualizer/public/application/common/components/field_type_icon/field_type_icon.test.tsx @@ -14,7 +14,7 @@ import { JOB_FIELD_TYPES } from '../../../../../common'; describe('FieldTypeIcon', () => { test(`render component when type matches a field type`, () => { const typeIconComponent = shallow( - + ); expect(typeIconComponent).toMatchSnapshot(); }); @@ -24,7 +24,7 @@ describe('FieldTypeIcon', () => { jest.useFakeTimers(); const typeIconComponent = mount( - + ); expect(typeIconComponent.find('EuiToolTip').children()).toHaveLength(1); diff --git a/x-pack/plugins/data_visualizer/public/application/common/components/field_type_icon/field_type_icon.tsx b/x-pack/plugins/data_visualizer/public/application/common/components/field_type_icon/field_type_icon.tsx index 9d803e3d4a80c..2a9767ccd62b1 100644 --- a/x-pack/plugins/data_visualizer/public/application/common/components/field_type_icon/field_type_icon.tsx +++ b/x-pack/plugins/data_visualizer/public/application/common/components/field_type_icon/field_type_icon.tsx @@ -6,103 +6,32 @@ */ import React, { FC } from 'react'; -import { EuiToken, EuiToolTip } from '@elastic/eui'; +import { EuiToolTip } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { getJobTypeAriaLabel } from '../../util/field_types_utils'; +import { FieldIcon } from '@kbn/react-field/field_icon'; +import { getJobTypeLabel } from '../../util/field_types_utils'; import type { JobFieldType } from '../../../../../common'; import './_index.scss'; interface FieldTypeIconProps { tooltipEnabled: boolean; type: JobFieldType; - needsAria: boolean; } -interface FieldTypeIconContainerProps { - ariaLabel: string | null; - iconType: string; - color?: string; - needsAria: boolean; - [key: string]: any; -} - -// defaultIcon => a unknown datatype -const defaultIcon = { iconType: 'questionInCircle', color: 'gray' }; - -// Extended & modified version of src/plugins/kibana_react/public/field_icon/field_icon.tsx -export const typeToEuiIconMap: Record = { - boolean: { iconType: 'tokenBoolean' }, - // icon for a data view mapping conflict in discover - conflict: { iconType: 'alert', color: 'euiColorVis9' }, - date: { iconType: 'tokenDate' }, - date_range: { iconType: 'tokenDate' }, - geo_point: { iconType: 'tokenGeo' }, - geo_shape: { iconType: 'tokenGeo' }, - ip: { iconType: 'tokenIP' }, - ip_range: { iconType: 'tokenIP' }, - // is a plugin's data type https://www.elastic.co/guide/en/elasticsearch/plugins/current/mapper-murmur3-usage.html - murmur3: { iconType: 'tokenFile' }, - number: { iconType: 'tokenNumber' }, - number_range: { iconType: 'tokenNumber' }, - histogram: { iconType: 'tokenHistogram' }, - _source: { iconType: 'editorCodeBlock', color: 'gray' }, - string: { iconType: 'tokenString' }, - text: { iconType: 'tokenString' }, - keyword: { iconType: 'tokenString' }, - nested: { iconType: 'tokenNested' }, -}; - -export const FieldTypeIcon: FC = ({ - tooltipEnabled = false, - type, - needsAria = true, -}) => { - const ariaLabel = getJobTypeAriaLabel(type); - const token = typeToEuiIconMap[type] || defaultIcon; - const containerProps = { ...token, ariaLabel, needsAria }; - +export const FieldTypeIcon: FC = ({ tooltipEnabled = false, type }) => { + const label = + getJobTypeLabel(type) ?? + i18n.translate('xpack.dataVisualizer.fieldTypeIcon.fieldTypeTooltip', { + defaultMessage: '{type} type', + values: { type }, + }); if (tooltipEnabled === true) { return ( - - + + ); } - return ; -}; - -// If the tooltip is used, it will apply its events to its first inner child. -// To pass on its properties we apply `rest` to the outer `span` element. -const FieldTypeIconContainer: FC = ({ - ariaLabel, - iconType, - color, - needsAria, - ...rest -}) => { - const wrapperProps: { className: string; 'aria-label'?: string } = { - className: 'field-type-icon', - }; - if (needsAria && ariaLabel) { - wrapperProps['aria-label'] = ariaLabel; - } - return ( - - ); + return ; }; diff --git a/x-pack/plugins/data_visualizer/public/application/common/components/field_types_filter/field_types_filter.tsx b/x-pack/plugins/data_visualizer/public/application/common/components/field_types_filter/field_types_filter.tsx index 97dc2077d5931..0fa860bc6f55e 100644 --- a/x-pack/plugins/data_visualizer/public/application/common/components/field_types_filter/field_types_filter.tsx +++ b/x-pack/plugins/data_visualizer/public/application/common/components/field_types_filter/field_types_filter.tsx @@ -12,7 +12,7 @@ import { MultiSelectPicker, Option } from '../multi_select_picker'; import type { FileBasedFieldVisConfig, FileBasedUnknownFieldVisConfig, -} from '../stats_table/types/field_vis_config'; +} from '../../../../../common/types/field_vis_config'; import { FieldTypeIcon } from '../field_type_icon'; import { jobTypeLabels } from '../../util/field_types_utils'; @@ -50,7 +50,7 @@ export const DataVisualizerFieldTypesFilter: FC = ({ {label} {type && ( - + )}
    diff --git a/x-pack/plugins/data_visualizer/public/application/common/components/fields_stats_grid/fields_stats_grid.tsx b/x-pack/plugins/data_visualizer/public/application/common/components/fields_stats_grid/fields_stats_grid.tsx index b57072eed2944..1173ede84e631 100644 --- a/x-pack/plugins/data_visualizer/public/application/common/components/fields_stats_grid/fields_stats_grid.tsx +++ b/x-pack/plugins/data_visualizer/public/application/common/components/fields_stats_grid/fields_stats_grid.tsx @@ -10,7 +10,7 @@ import { EuiFlexGroup, EuiSpacer } from '@elastic/eui'; import type { FindFileStructureResponse } from '../../../../../../file_upload/common'; import type { DataVisualizerTableState } from '../../../../../common'; import { DataVisualizerTable, ItemIdToExpandedRowMap } from '../stats_table'; -import type { FileBasedFieldVisConfig } from '../stats_table/types/field_vis_config'; +import type { FileBasedFieldVisConfig } from '../../../../../common/types/field_vis_config'; import { FileBasedDataVisualizerExpandedRow } from '../expanded_row'; import { DataVisualizerFieldNamesFilter } from '../field_names_filter'; diff --git a/x-pack/plugins/data_visualizer/public/application/common/components/fields_stats_grid/filter_fields.ts b/x-pack/plugins/data_visualizer/public/application/common/components/fields_stats_grid/filter_fields.ts index 6c164233bdbc1..9f1ea4af22537 100644 --- a/x-pack/plugins/data_visualizer/public/application/common/components/fields_stats_grid/filter_fields.ts +++ b/x-pack/plugins/data_visualizer/public/application/common/components/fields_stats_grid/filter_fields.ts @@ -9,7 +9,7 @@ import { JOB_FIELD_TYPES } from '../../../../../common'; import type { FileBasedFieldVisConfig, FileBasedUnknownFieldVisConfig, -} from '../stats_table/types/field_vis_config'; +} from '../../../../../common/types/field_vis_config'; export function filterFields( fields: Array, diff --git a/x-pack/plugins/data_visualizer/public/application/common/components/stats_table/components/field_data_expanded_row/error_message.tsx b/x-pack/plugins/data_visualizer/public/application/common/components/stats_table/components/field_data_expanded_row/error_message.tsx new file mode 100644 index 0000000000000..1d4a685457e25 --- /dev/null +++ b/x-pack/plugins/data_visualizer/public/application/common/components/stats_table/components/field_data_expanded_row/error_message.tsx @@ -0,0 +1,29 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { EuiText } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; +import React from 'react'; +import { DVErrorObject } from '../../../../../index_data_visualizer/utils/error_utils'; + +export const ErrorMessageContent = ({ + fieldName, + error, +}: { + fieldName: string; + error: DVErrorObject; +}) => { + return ( + + + + ); +}; diff --git a/x-pack/plugins/data_visualizer/public/application/common/components/stats_table/components/field_data_expanded_row/ip_content.tsx b/x-pack/plugins/data_visualizer/public/application/common/components/stats_table/components/field_data_expanded_row/ip_content.tsx index a5db86e0c30a0..d32a8a6dfb907 100644 --- a/x-pack/plugins/data_visualizer/public/application/common/components/stats_table/components/field_data_expanded_row/ip_content.tsx +++ b/x-pack/plugins/data_visualizer/public/application/common/components/stats_table/components/field_data_expanded_row/ip_content.tsx @@ -21,12 +21,14 @@ export const IpContent: FC = ({ config, onAddFilter }) => { return ( - + {stats && ( + + )} ); }; diff --git a/x-pack/plugins/data_visualizer/public/application/common/components/stats_table/components/field_data_expanded_row/text_content.tsx b/x-pack/plugins/data_visualizer/public/application/common/components/stats_table/components/field_data_expanded_row/text_content.tsx index 6f946fc1025ed..4fc73f0831dfc 100644 --- a/x-pack/plugins/data_visualizer/public/application/common/components/stats_table/components/field_data_expanded_row/text_content.tsx +++ b/x-pack/plugins/data_visualizer/public/application/common/components/stats_table/components/field_data_expanded_row/text_content.tsx @@ -30,8 +30,9 @@ export const TextContent: FC = ({ config }) => { {numExamples > 0 && } {numExamples === 0 && ( - + { expect(getLegendText(validUnsupportedChartData, 20)).toBe('Chart not supported.'); }); it('should return the chart legend text for empty datasets', () => { - expect(getLegendText(validNumericChartData, 20)).toBe('0 documents contain field.'); + expect(getLegendText(validNumericChartData, 20)).toBe(''); }); it('should return the chart legend text for boolean chart types', () => { const { getByText } = render( @@ -186,7 +186,7 @@ describe('useColumnChart()', () => { ); expect(result.current.data).toStrictEqual([]); - expect(result.current.legendText).toBe('0 documents contain field.'); + expect(result.current.legendText).toBe(''); expect(result.current.xScaleType).toBe('linear'); }); }); diff --git a/x-pack/plugins/data_visualizer/public/application/common/components/stats_table/components/field_data_row/use_column_chart.tsx b/x-pack/plugins/data_visualizer/public/application/common/components/stats_table/components/field_data_row/use_column_chart.tsx index 60e1595c64ece..827e4a7f44857 100644 --- a/x-pack/plugins/data_visualizer/public/application/common/components/stats_table/components/field_data_row/use_column_chart.tsx +++ b/x-pack/plugins/data_visualizer/public/application/common/components/stats_table/components/field_data_row/use_column_chart.tsx @@ -83,9 +83,7 @@ export const getLegendText = (chartData: ChartData, maxChartColumns: number): Le } if (chartData.data.length === 0) { - return i18n.translate('xpack.dataVisualizer.dataGridChart.notEnoughData', { - defaultMessage: `0 documents contain field.`, - }); + return ''; } if (chartData.type === 'boolean') { diff --git a/x-pack/plugins/data_visualizer/public/application/common/components/stats_table/data_visualizer_stats_table.tsx b/x-pack/plugins/data_visualizer/public/application/common/components/stats_table/data_visualizer_stats_table.tsx index 4e1c03aa987bd..976afc464a672 100644 --- a/x-pack/plugins/data_visualizer/public/application/common/components/stats_table/data_visualizer_stats_table.tsx +++ b/x-pack/plugins/data_visualizer/public/application/common/components/stats_table/data_visualizer_stats_table.tsx @@ -33,12 +33,13 @@ import { FieldVisConfig, FileBasedFieldVisConfig, isIndexBasedFieldVisConfig, -} from './types/field_vis_config'; +} from '../../../../../common/types/field_vis_config'; import { FileBasedNumberContentPreview } from '../field_data_row'; import { BooleanContentPreview } from './components/field_data_row'; import { calculateTableColumnsDimensions } from './utils'; import { DistinctValues } from './components/field_data_row/distinct_values'; import { FieldTypeIcon } from '../field_type_icon'; +import './_index.scss'; const FIELD_NAME = 'fieldName'; @@ -54,6 +55,7 @@ interface DataVisualizerTableProps { showPreviewByDefault?: boolean; /** Callback to receive any updates when table or page state is changed **/ onChange?: (update: Partial) => void; + loading?: boolean; } export const DataVisualizerTable = ({ @@ -64,6 +66,7 @@ export const DataVisualizerTable = ({ extendedColumns, showPreviewByDefault, onChange, + loading, }: DataVisualizerTableProps) => { const [expandedRowItemIds, setExpandedRowItemIds] = useState([]); const [expandAll, setExpandAll] = useState(false); @@ -180,7 +183,7 @@ export const DataVisualizerTable = ({ defaultMessage: 'Type', }), render: (fieldType: JobFieldType) => { - return ; + return ; }, width: dimensions.type, sortable: true, @@ -322,6 +325,13 @@ export const DataVisualizerTable = ({ {(resizeRef) => (
    + message={ + loading + ? i18n.translate('xpack.dataVisualizer.dataGrid.searchingMessage', { + defaultMessage: 'Searching', + }) + : undefined + } className={'dvTable'} items={items} itemId={FIELD_NAME} diff --git a/x-pack/plugins/data_visualizer/public/application/common/components/stats_table/hooks/use_color_range.ts b/x-pack/plugins/data_visualizer/public/application/common/components/stats_table/hooks/use_color_range.ts index b1d26a5437b44..92a88f4d60670 100644 --- a/x-pack/plugins/data_visualizer/public/application/common/components/stats_table/hooks/use_color_range.ts +++ b/x-pack/plugins/data_visualizer/public/application/common/components/stats_table/hooks/use_color_range.ts @@ -7,8 +7,10 @@ import d3 from 'd3'; import { useMemo } from 'react'; -import euiThemeLight from '@elastic/eui/dist/eui_theme_light.json'; -import euiThemeDark from '@elastic/eui/dist/eui_theme_dark.json'; +import { + euiLightVars as euiThemeLight, + euiDarkVars as euiThemeDark, +} from '@kbn/ui-shared-deps-src/theme'; import { i18n } from '@kbn/i18n'; diff --git a/x-pack/plugins/data_visualizer/public/application/common/components/stats_table/types/field_data_row.ts b/x-pack/plugins/data_visualizer/public/application/common/components/stats_table/types/field_data_row.ts index 94b704764c93b..3d7678c7b60a5 100644 --- a/x-pack/plugins/data_visualizer/public/application/common/components/stats_table/types/field_data_row.ts +++ b/x-pack/plugins/data_visualizer/public/application/common/components/stats_table/types/field_data_row.ts @@ -5,8 +5,11 @@ * 2.0. */ -import type { FieldVisConfig, FileBasedFieldVisConfig } from './field_vis_config'; import { IndexPatternField } from '../../../../../../../../../src/plugins/data/common'; +import { + FieldVisConfig, + FileBasedFieldVisConfig, +} from '../../../../../../common/types/field_vis_config'; export interface FieldDataRowProps { config: FieldVisConfig | FileBasedFieldVisConfig; diff --git a/x-pack/plugins/data_visualizer/public/application/common/components/stats_table/types/index.ts b/x-pack/plugins/data_visualizer/public/application/common/components/stats_table/types/index.ts index 00f8ac0c74eb9..6d9f4d5b86d28 100644 --- a/x-pack/plugins/data_visualizer/public/application/common/components/stats_table/types/index.ts +++ b/x-pack/plugins/data_visualizer/public/application/common/components/stats_table/types/index.ts @@ -4,11 +4,13 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ - export type { FieldDataRowProps } from './field_data_row'; export type { FieldVisConfig, FileBasedFieldVisConfig, MetricFieldVisStats, -} from './field_vis_config'; -export { isFileBasedFieldVisConfig, isIndexBasedFieldVisConfig } from './field_vis_config'; +} from '../../../../../../common/types/field_vis_config'; +export { + isFileBasedFieldVisConfig, + isIndexBasedFieldVisConfig, +} from '../../../../../../common/types/field_vis_config'; diff --git a/x-pack/plugins/data_visualizer/public/application/common/components/top_values/top_values.tsx b/x-pack/plugins/data_visualizer/public/application/common/components/top_values/top_values.tsx index e2793512e23df..c9b4137a0106d 100644 --- a/x-pack/plugins/data_visualizer/public/application/common/components/top_values/top_values.tsx +++ b/x-pack/plugins/data_visualizer/public/application/common/components/top_values/top_values.tsx @@ -43,7 +43,7 @@ function getPercentLabel(docCount: number, topValuesSampleSize: number): string } export const TopValues: FC = ({ stats, fieldFormat, barColor, compressed, onAddFilter }) => { - if (stats === undefined) return null; + if (stats === undefined || !stats.topValues) return null; const { topValues, topValuesSampleSize, @@ -81,11 +81,11 @@ export const TopValues: FC = ({ stats, fieldFormat, barColor, compressed, size="xs" label={kibanaFieldFormat(value.key, fieldFormat)} className={classNames('eui-textTruncate', 'topValuesValueLabelContainer')} - valueText={ + valueText={`${value.doc_count}${ progressBarMax !== undefined - ? getPercentLabel(value.doc_count, progressBarMax) - : undefined - } + ? ` (${getPercentLabel(value.doc_count, progressBarMax)})` + : '' + }`} /> {fieldName !== undefined && value.key !== undefined && onAddFilter !== undefined ? ( diff --git a/x-pack/plugins/data_visualizer/public/application/common/util/field_types_utils.test.ts b/x-pack/plugins/data_visualizer/public/application/common/util/field_types_utils.test.ts index 5c0867c7a0745..710ba12313f17 100644 --- a/x-pack/plugins/data_visualizer/public/application/common/util/field_types_utils.test.ts +++ b/x-pack/plugins/data_visualizer/public/application/common/util/field_types_utils.test.ts @@ -6,24 +6,23 @@ */ import { JOB_FIELD_TYPES } from '../../../../common'; -import { getJobTypeAriaLabel, jobTypeAriaLabels } from './field_types_utils'; +import { getJobTypeLabel, jobTypeLabels } from './field_types_utils'; describe('field type utils', () => { - describe('getJobTypeAriaLabel: Getting a field type aria label by passing what it is stored in constants', () => { + describe('getJobTypeLabel: Getting a field type aria label by passing what it is stored in constants', () => { test('should returns all JOB_FIELD_TYPES labels exactly as it is for each correct value', () => { const keys = Object.keys(JOB_FIELD_TYPES); const receivedLabels: Record = {}; - const testStorage = jobTypeAriaLabels; - keys.forEach((constant) => { - receivedLabels[constant] = getJobTypeAriaLabel( - JOB_FIELD_TYPES[constant as keyof typeof JOB_FIELD_TYPES] - ); + const testStorage = jobTypeLabels; + keys.forEach((key) => { + const constant = key as keyof typeof JOB_FIELD_TYPES; + receivedLabels[JOB_FIELD_TYPES[constant]] = getJobTypeLabel(JOB_FIELD_TYPES[constant]); }); expect(receivedLabels).toEqual(testStorage); }); test('should returns NULL as JOB_FIELD_TYPES does not contain such a keyword', () => { - expect(getJobTypeAriaLabel('JOB_FIELD_TYPES')).toBe(null); + expect(getJobTypeLabel('JOB_FIELD_TYPES')).toBe(null); }); }); }); diff --git a/x-pack/plugins/data_visualizer/public/application/common/util/field_types_utils.ts b/x-pack/plugins/data_visualizer/public/application/common/util/field_types_utils.ts index 3e459cd2b079b..1fda7140dbab2 100644 --- a/x-pack/plugins/data_visualizer/public/application/common/util/field_types_utils.ts +++ b/x-pack/plugins/data_visualizer/public/application/common/util/field_types_utils.ts @@ -10,40 +10,8 @@ import { JOB_FIELD_TYPES } from '../../../../common'; import type { IndexPatternField } from '../../../../../../../src/plugins/data/common'; import { KBN_FIELD_TYPES } from '../../../../../../../src/plugins/data/common'; -export const jobTypeAriaLabels = { - BOOLEAN: i18n.translate('xpack.dataVisualizer.fieldTypeIcon.booleanTypeAriaLabel', { - defaultMessage: 'boolean type', - }), - DATE: i18n.translate('xpack.dataVisualizer.fieldTypeIcon.dateTypeAriaLabel', { - defaultMessage: 'date type', - }), - GEO_POINT: i18n.translate('xpack.dataVisualizer.fieldTypeIcon.geoPointTypeAriaLabel', { - defaultMessage: '{geoPointParam} type', - values: { - geoPointParam: 'geo point', - }, - }), - GEO_SHAPE: i18n.translate('xpack.dataVisualizer.fieldTypeIcon.geoShapeTypeAriaLabel', { - defaultMessage: 'geo shape type', - }), - IP: i18n.translate('xpack.dataVisualizer.fieldTypeIcon.ipTypeAriaLabel', { - defaultMessage: 'ip type', - }), - KEYWORD: i18n.translate('xpack.dataVisualizer.fieldTypeIcon.keywordTypeAriaLabel', { - defaultMessage: 'keyword type', - }), - NUMBER: i18n.translate('xpack.dataVisualizer.fieldTypeIcon.numberTypeAriaLabel', { - defaultMessage: 'number type', - }), - HISTOGRAM: i18n.translate('xpack.dataVisualizer.fieldTypeIcon.histogramTypeAriaLabel', { - defaultMessage: 'histogram type', - }), - TEXT: i18n.translate('xpack.dataVisualizer.fieldTypeIcon.textTypeAriaLabel', { - defaultMessage: 'text type', - }), - UNKNOWN: i18n.translate('xpack.dataVisualizer.fieldTypeIcon.unknownTypeAriaLabel', { - defaultMessage: 'unknown type', - }), +export const getJobTypeLabel = (type: string) => { + return type in jobTypeLabels ? jobTypeLabels[type as keyof typeof jobTypeLabels] : null; }; export const jobTypeLabels = { @@ -88,16 +56,6 @@ export const jobTypeLabels = { }), }; -export const getJobTypeAriaLabel = (type: string) => { - const requestedFieldType = Object.keys(JOB_FIELD_TYPES).find( - (k) => JOB_FIELD_TYPES[k as keyof typeof JOB_FIELD_TYPES] === type - ); - if (requestedFieldType === undefined) { - return null; - } - return jobTypeAriaLabels[requestedFieldType as keyof typeof jobTypeAriaLabels]; -}; - // convert kibana types to ML Job types // this is needed because kibana types only have string and not text and keyword. // and we can't use ES_FIELD_TYPES because it has no NUMBER type diff --git a/x-pack/plugins/data_visualizer/public/application/common/util/parse_interval.test.ts b/x-pack/plugins/data_visualizer/public/application/common/util/parse_interval.test.ts index a1608960a91bc..c259f82d12bfb 100644 --- a/x-pack/plugins/data_visualizer/public/application/common/util/parse_interval.test.ts +++ b/x-pack/plugins/data_visualizer/public/application/common/util/parse_interval.test.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { parseInterval } from './parse_interval'; +import { parseInterval } from '../../../../common/utils/parse_interval'; describe('ML parse interval util', () => { test('should correctly parse an interval containing a valid unit and value', () => { diff --git a/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/components/full_time_range_selector/full_time_range_selector.tsx b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/components/full_time_range_selector/full_time_range_selector.tsx index e5bd7a0d6f526..ebddd5527f5a2 100644 --- a/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/components/full_time_range_selector/full_time_range_selector.tsx +++ b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/components/full_time_range_selector/full_time_range_selector.tsx @@ -6,7 +6,6 @@ */ import React, { FC } from 'react'; - import { FormattedMessage } from '@kbn/i18n/react'; import { Query, IndexPattern, TimefilterContract } from 'src/plugins/data/public'; import { EuiButton } from '@elastic/eui'; diff --git a/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/components/index_data_visualizer_view/index_data_visualizer_view.tsx b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/components/index_data_visualizer_view/index_data_visualizer_view.tsx index cdf4b718a93b7..f528d8378bcd2 100644 --- a/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/components/index_data_visualizer_view/index_data_visualizer_view.tsx +++ b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/components/index_data_visualizer_view/index_data_visualizer_view.tsx @@ -6,7 +6,6 @@ */ import React, { FC, Fragment, useEffect, useMemo, useState, useCallback, useRef } from 'react'; -import { merge } from 'rxjs'; import { EuiFlexGroup, EuiFlexItem, @@ -16,6 +15,7 @@ import { EuiPageContentHeader, EuiPageContentHeaderSection, EuiPanel, + EuiProgress, EuiSpacer, EuiTitle, } from '@elastic/eui'; @@ -24,12 +24,7 @@ import { FormattedMessage } from '@kbn/i18n/react'; import { Required } from 'utility-types'; import { i18n } from '@kbn/i18n'; import { Filter } from '@kbn/es-query'; -import { - KBN_FIELD_TYPES, - UI_SETTINGS, - Query, - generateFilters, -} from '../../../../../../../../src/plugins/data/public'; +import { Query, generateFilters } from '../../../../../../../../src/plugins/data/public'; import { FullTimeRangeSelector } from '../full_time_range_selector'; import { usePageUrlState, useUrlState } from '../../../common/util/url_state'; import { @@ -37,39 +32,29 @@ import { ItemIdToExpandedRowMap, } from '../../../common/components/stats_table'; import { FieldVisConfig } from '../../../common/components/stats_table/types'; -import type { - MetricFieldsStats, - TotalFieldsStats, -} from '../../../common/components/stats_table/components/field_count_stats'; +import type { TotalFieldsStats } from '../../../common/components/stats_table/components/field_count_stats'; import { OverallStats } from '../../types/overall_stats'; import { getActions } from '../../../common/components/field_data_row/action_menu'; import { IndexBasedDataVisualizerExpandedRow } from '../../../common/components/expanded_row/index_based_expanded_row'; import { DATA_VISUALIZER_INDEX_VIEWER } from '../../constants/index_data_visualizer_viewer'; import { DataVisualizerIndexBasedAppState } from '../../types/index_data_visualizer_state'; import { SEARCH_QUERY_LANGUAGE, SearchQueryLanguage } from '../../types/combined_query'; -import { - FieldRequestConfig, - JobFieldType, - SavedSearchSavedObject, -} from '../../../../../common/types'; +import { JobFieldType, SavedSearchSavedObject } from '../../../../../common/types'; import { useDataVisualizerKibana } from '../../../kibana_context'; import { FieldCountPanel } from '../../../common/components/field_count_panel'; import { DocumentCountContent } from '../../../common/components/document_count_content'; -import { DataLoader } from '../../data_loader/data_loader'; -import { JOB_FIELD_TYPES, OMIT_FIELDS } from '../../../../../common'; -import { useTimefilter } from '../../hooks/use_time_filter'; +import { OMIT_FIELDS } from '../../../../../common'; import { kbnTypeToJobType } from '../../../common/util/field_types_utils'; import { SearchPanel } from '../search_panel'; import { ActionsPanel } from '../actions_panel'; import { DatePickerWrapper } from '../../../common/components/date_picker_wrapper'; -import { dataVisualizerRefresh$ } from '../../services/timefilter_refresh_service'; import { HelpMenu } from '../../../common/components/help_menu'; -import { TimeBuckets } from '../../services/time_buckets'; -import { createMergedEsQuery, getEsQueryFromSavedSearch } from '../../utils/saved_search_utils'; +import { createMergedEsQuery } from '../../utils/saved_search_utils'; import { DataVisualizerIndexPatternManagement } from '../index_pattern_management'; import { ResultLink } from '../../../common/components/results_links'; -import { extractErrorProperties } from '../../utils/error_utils'; import { IndexPatternField, IndexPattern } from '../../../../../../../../src/plugins/data/common'; +import { useDataVisualizerGridData } from '../../hooks/use_data_visualizer_grid_data'; +import { DataVisualizerGridInput } from '../../embeddables/grid_embeddable/grid_embeddable'; import './_index.scss'; interface DataVisualizerPageState { @@ -155,61 +140,14 @@ export const IndexDataVisualizerView: FC = (dataVi } }, [dataVisualizerProps?.currentSavedSearch]); - useEffect(() => { - return () => { - // When navigating away from the data view - // Reset all previously set filters - // to make sure new page doesn't have unrelated filters - data.query.filterManager.removeAll(); - }; - }, [currentIndexPattern.id, data.query.filterManager]); - - const getTimeBuckets = useCallback(() => { - return new TimeBuckets({ - [UI_SETTINGS.HISTOGRAM_MAX_BARS]: uiSettings.get(UI_SETTINGS.HISTOGRAM_MAX_BARS), - [UI_SETTINGS.HISTOGRAM_BAR_TARGET]: uiSettings.get(UI_SETTINGS.HISTOGRAM_BAR_TARGET), - dateFormat: uiSettings.get('dateFormat'), - 'dateFormat:scaled': uiSettings.get('dateFormat:scaled'), - }); - }, [uiSettings]); - - const timefilter = useTimefilter({ - timeRangeSelector: currentIndexPattern?.timeFieldName !== undefined, - autoRefreshSelector: true, - }); - - const dataLoader = useMemo( - () => new DataLoader(currentIndexPattern, toasts), - [currentIndexPattern, toasts] - ); - - useEffect(() => { - if (globalState?.time !== undefined) { - timefilter.setTime({ - from: globalState.time.from, - to: globalState.time.to, - }); - setLastRefresh(Date.now()); - } - }, [globalState, timefilter]); - - useEffect(() => { - if (globalState?.refreshInterval !== undefined) { - timefilter.setRefreshInterval(globalState.refreshInterval); - setLastRefresh(Date.now()); - } - }, [globalState, timefilter]); - - const [lastRefresh, setLastRefresh] = useState(0); - useEffect(() => { if (!currentIndexPattern.isTimeBased()) { toasts.addWarning({ title: i18n.translate( - 'xpack.dataVisualizer.index.dataViewNotBasedOnTimeSeriesNotificationTitle', + 'xpack.dataVisualizer.index.indexPatternNotBasedOnTimeSeriesNotificationTitle', { - defaultMessage: 'The data view {dataViewTitle} is not based on a time series', - values: { dataViewTitle: currentIndexPattern.title }, + defaultMessage: 'The index pattern {indexPatternTitle} is not based on a time series', + values: { indexPatternTitle: currentIndexPattern.title }, } ), text: i18n.translate( @@ -225,7 +163,7 @@ export const IndexDataVisualizerView: FC = (dataVi const indexPatternFields: IndexPatternField[] = currentIndexPattern.fields; const fieldTypes = useMemo(() => { - // Obtain the list of non metric field types which appear in the data view. + // Obtain the list of non metric field types which appear in the index pattern. const indexedFieldTypes: JobFieldType[] = []; indexPatternFields.forEach((field) => { if (!OMIT_FIELDS.includes(field.name) && field.scripted !== true) { @@ -238,35 +176,6 @@ export const IndexDataVisualizerView: FC = (dataVi return indexedFieldTypes.sort(); }, [indexPatternFields]); - const defaults = getDefaultPageState(); - - const { searchQueryLanguage, searchString, searchQuery } = useMemo(() => { - const searchData = getEsQueryFromSavedSearch({ - indexPattern: currentIndexPattern, - uiSettings, - savedSearch: currentSavedSearch, - filterManager: data.query.filterManager, - }); - - if (searchData === undefined || dataVisualizerListState.searchString !== '') { - if (dataVisualizerListState.filters) { - data.query.filterManager.setFilters(dataVisualizerListState.filters); - } - return { - searchQuery: dataVisualizerListState.searchQuery, - searchString: dataVisualizerListState.searchString, - searchQueryLanguage: dataVisualizerListState.searchQueryLanguage, - }; - } else { - return { - searchQuery: searchData.searchQuery, - searchString: searchData.searchString, - searchQueryLanguage: searchData.queryLanguage, - }; - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [currentSavedSearch, currentIndexPattern, dataVisualizerListState, data.query]); - const setSearchParams = useCallback( (searchParams: { searchQuery: Query['query']; @@ -275,7 +184,7 @@ export const IndexDataVisualizerView: FC = (dataVi filters: Filter[]; }) => { // When the user loads saved search and then clear or modify the query - // we should remove the saved search and replace it with the data view id + // we should remove the saved search and replace it with the index pattern id if (currentSavedSearch !== null) { setCurrentSavedSearch(null); } @@ -318,15 +227,58 @@ export const IndexDataVisualizerView: FC = (dataVi }); }; - const [overallStats, setOverallStats] = useState(defaults.overallStats); + const input: DataVisualizerGridInput = useMemo(() => { + return { + indexPattern: currentIndexPattern, + savedSearch: currentSavedSearch, + visibleFieldNames, + }; + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [currentIndexPattern.id, currentSavedSearch?.id, visibleFieldNames]); + + const { + configs, + searchQueryLanguage, + searchString, + overallStats, + searchQuery, + documentCountStats, + metricsStats, + timefilter, + setLastRefresh, + progress, + } = useDataVisualizerGridData(input, dataVisualizerListState, setGlobalState); + + useEffect(() => { + return () => { + // When navigating away from the index pattern + // Reset all previously set filters + // to make sure new page doesn't have unrelated filters + data.query.filterManager.removeAll(); + }; + }, [currentIndexPattern.id, data.query.filterManager]); + + useEffect(() => { + // Force refresh on index pattern change + setLastRefresh(Date.now()); + }, [currentIndexPattern.id, setLastRefresh]); - const [documentCountStats, setDocumentCountStats] = useState(defaults.documentCountStats); - const [metricConfigs, setMetricConfigs] = useState(defaults.metricConfigs); - const [metricsLoaded, setMetricsLoaded] = useState(defaults.metricsLoaded); - const [metricsStats, setMetricsStats] = useState(); + useEffect(() => { + if (globalState?.time !== undefined) { + timefilter.setTime({ + from: globalState.time.from, + to: globalState.time.to, + }); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [JSON.stringify(globalState?.time), timefilter]); - const [nonMetricConfigs, setNonMetricConfigs] = useState(defaults.nonMetricConfigs); - const [nonMetricsLoaded, setNonMetricsLoaded] = useState(defaults.nonMetricsLoaded); + useEffect(() => { + if (globalState?.refreshInterval !== undefined) { + timefilter.setRefreshInterval(globalState.refreshInterval); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [JSON.stringify(globalState?.refreshInterval), timefilter]); const onAddFilter = useCallback( (field: IndexPatternField | string, values: string, operation: '+' | '-') => { @@ -374,422 +326,8 @@ export const IndexDataVisualizerView: FC = (dataVi ] ); - useEffect(() => { - const timeUpdateSubscription = merge( - timefilter.getTimeUpdate$(), - dataVisualizerRefresh$ - ).subscribe(() => { - setGlobalState({ - time: timefilter.getTime(), - refreshInterval: timefilter.getRefreshInterval(), - }); - setLastRefresh(Date.now()); - }); - return () => { - timeUpdateSubscription.unsubscribe(); - }; - }); - - useEffect(() => { - loadOverallStats(); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [searchQuery, samplerShardSize, lastRefresh]); - - useEffect(() => { - createMetricCards(); - createNonMetricCards(); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [overallStats, showEmptyFields]); - - useEffect(() => { - loadMetricFieldStats(); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [metricConfigs]); - - useEffect(() => { - loadNonMetricFieldStats(); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [nonMetricConfigs]); - - useEffect(() => { - createMetricCards(); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [metricsLoaded]); - - useEffect(() => { - createNonMetricCards(); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [nonMetricsLoaded]); - - async function loadOverallStats() { - const tf = timefilter as any; - let earliest; - let latest; - - const activeBounds = tf.getActiveBounds(); - - if (currentIndexPattern.timeFieldName !== undefined && activeBounds === undefined) { - return; - } - - if (currentIndexPattern.timeFieldName !== undefined) { - earliest = activeBounds.min.valueOf(); - latest = activeBounds.max.valueOf(); - } - - try { - const allStats = await dataLoader.loadOverallData( - searchQuery, - samplerShardSize, - earliest, - latest - ); - // Because load overall stats perform queries in batches - // there could be multiple errors - if (Array.isArray(allStats.errors) && allStats.errors.length > 0) { - allStats.errors.forEach((err: any) => { - dataLoader.displayError(extractErrorProperties(err)); - }); - } - setOverallStats(allStats); - } catch (err) { - dataLoader.displayError(err.body ?? err); - } - } - - async function loadMetricFieldStats() { - // Only request data for fields that exist in documents. - if (metricConfigs.length === 0) { - return; - } - - const configsToLoad = metricConfigs.filter( - (config) => config.existsInDocs === true && config.loading === true - ); - if (configsToLoad.length === 0) { - return; - } - - // Pass the field name, type and cardinality in the request. - // Top values will be obtained on a sample if cardinality > 100000. - const existMetricFields: FieldRequestConfig[] = configsToLoad.map((config) => { - const props = { fieldName: config.fieldName, type: config.type, cardinality: 0 }; - if (config.stats !== undefined && config.stats.cardinality !== undefined) { - props.cardinality = config.stats.cardinality; - } - return props; - }); - - // Obtain the interval to use for date histogram aggregations - // (such as the document count chart). Aim for 75 bars. - const buckets = getTimeBuckets(); - - const tf = timefilter as any; - let earliest: number | undefined; - let latest: number | undefined; - if (currentIndexPattern.timeFieldName !== undefined) { - earliest = tf.getActiveBounds().min.valueOf(); - latest = tf.getActiveBounds().max.valueOf(); - } - - const bounds = tf.getActiveBounds(); - const BAR_TARGET = 75; - buckets.setInterval('auto'); - buckets.setBounds(bounds); - buckets.setBarTarget(BAR_TARGET); - const aggInterval = buckets.getInterval(); - - try { - const metricFieldStats = await dataLoader.loadFieldStats( - searchQuery, - samplerShardSize, - earliest, - latest, - existMetricFields, - aggInterval.asMilliseconds() - ); - - // Add the metric stats to the existing stats in the corresponding config. - const configs: FieldVisConfig[] = []; - metricConfigs.forEach((config) => { - const configWithStats = { ...config }; - if (config.fieldName !== undefined) { - configWithStats.stats = { - ...configWithStats.stats, - ...metricFieldStats.find( - (fieldStats: any) => fieldStats.fieldName === config.fieldName - ), - }; - configWithStats.loading = false; - configs.push(configWithStats); - } else { - // Document count card. - configWithStats.stats = metricFieldStats.find( - (fieldStats: any) => fieldStats.fieldName === undefined - ); - - if (configWithStats.stats !== undefined) { - // Add earliest / latest of timefilter for setting x axis domain. - configWithStats.stats.timeRangeEarliest = earliest; - configWithStats.stats.timeRangeLatest = latest; - } - setDocumentCountStats(configWithStats); - } - }); - - setMetricConfigs(configs); - } catch (err) { - dataLoader.displayError(err); - } - } - - async function loadNonMetricFieldStats() { - // Only request data for fields that exist in documents. - if (nonMetricConfigs.length === 0) { - return; - } - - const configsToLoad = nonMetricConfigs.filter( - (config) => config.existsInDocs === true && config.loading === true - ); - if (configsToLoad.length === 0) { - return; - } - - // Pass the field name, type and cardinality in the request. - // Top values will be obtained on a sample if cardinality > 100000. - const existNonMetricFields: FieldRequestConfig[] = configsToLoad.map((config) => { - const props = { fieldName: config.fieldName, type: config.type, cardinality: 0 }; - if (config.stats !== undefined && config.stats.cardinality !== undefined) { - props.cardinality = config.stats.cardinality; - } - return props; - }); - - const tf = timefilter as any; - let earliest; - let latest; - if (currentIndexPattern.timeFieldName !== undefined) { - earliest = tf.getActiveBounds().min.valueOf(); - latest = tf.getActiveBounds().max.valueOf(); - } - - try { - const nonMetricFieldStats = await dataLoader.loadFieldStats( - searchQuery, - samplerShardSize, - earliest, - latest, - existNonMetricFields - ); - - // Add the field stats to the existing stats in the corresponding config. - const configs: FieldVisConfig[] = []; - nonMetricConfigs.forEach((config) => { - const configWithStats = { ...config }; - if (config.fieldName !== undefined) { - configWithStats.stats = { - ...configWithStats.stats, - ...nonMetricFieldStats.find( - (fieldStats: any) => fieldStats.fieldName === config.fieldName - ), - }; - } - configWithStats.loading = false; - configs.push(configWithStats); - }); - - setNonMetricConfigs(configs); - } catch (err) { - dataLoader.displayError(err); - } - } - - const createMetricCards = useCallback(() => { - const configs: FieldVisConfig[] = []; - const aggregatableExistsFields: any[] = overallStats.aggregatableExistsFields || []; - - const allMetricFields = indexPatternFields.filter((f) => { - return ( - f.type === KBN_FIELD_TYPES.NUMBER && - f.displayName !== undefined && - dataLoader.isDisplayField(f.displayName) === true - ); - }); - const metricExistsFields = allMetricFields.filter((f) => { - return aggregatableExistsFields.find((existsF) => { - return existsF.fieldName === f.spec.name; - }); - }); - - // Add a config for 'document count', identified by no field name if indexpattern is time based. - if (currentIndexPattern.timeFieldName !== undefined) { - configs.push({ - type: JOB_FIELD_TYPES.NUMBER, - existsInDocs: true, - loading: true, - aggregatable: true, - }); - } - - if (metricsLoaded === false) { - setMetricsLoaded(true); - return; - } - - let aggregatableFields: any[] = overallStats.aggregatableExistsFields; - if (allMetricFields.length !== metricExistsFields.length && metricsLoaded === true) { - aggregatableFields = aggregatableFields.concat(overallStats.aggregatableNotExistsFields); - } - - const metricFieldsToShow = - metricsLoaded === true && showEmptyFields === true ? allMetricFields : metricExistsFields; - - metricFieldsToShow.forEach((field) => { - const fieldData = aggregatableFields.find((f) => { - return f.fieldName === field.spec.name; - }); - - const metricConfig: FieldVisConfig = { - ...(fieldData ? fieldData : {}), - fieldFormat: currentIndexPattern.getFormatterForField(field), - type: JOB_FIELD_TYPES.NUMBER, - loading: true, - aggregatable: true, - deletable: field.runtimeField !== undefined, - }; - if (field.displayName !== metricConfig.fieldName) { - metricConfig.displayName = field.displayName; - } - - configs.push(metricConfig); - }); - - setMetricsStats({ - totalMetricFieldsCount: allMetricFields.length, - visibleMetricsCount: metricFieldsToShow.length, - }); - setMetricConfigs(configs); - }, [ - currentIndexPattern, - dataLoader, - indexPatternFields, - metricsLoaded, - overallStats, - showEmptyFields, - ]); - - const createNonMetricCards = useCallback(() => { - const allNonMetricFields = indexPatternFields.filter((f) => { - return ( - f.type !== KBN_FIELD_TYPES.NUMBER && - f.displayName !== undefined && - dataLoader.isDisplayField(f.displayName) === true - ); - }); - // Obtain the list of all non-metric fields which appear in documents - // (aggregatable or not aggregatable). - const populatedNonMetricFields: any[] = []; // Kibana data view non metric fields. - let nonMetricFieldData: any[] = []; // Basic non metric field data loaded from requesting overall stats. - const aggregatableExistsFields: any[] = overallStats.aggregatableExistsFields || []; - const nonAggregatableExistsFields: any[] = overallStats.nonAggregatableExistsFields || []; - - allNonMetricFields.forEach((f) => { - const checkAggregatableField = aggregatableExistsFields.find( - (existsField) => existsField.fieldName === f.spec.name - ); - - if (checkAggregatableField !== undefined) { - populatedNonMetricFields.push(f); - nonMetricFieldData.push(checkAggregatableField); - } else { - const checkNonAggregatableField = nonAggregatableExistsFields.find( - (existsField) => existsField.fieldName === f.spec.name - ); - - if (checkNonAggregatableField !== undefined) { - populatedNonMetricFields.push(f); - nonMetricFieldData.push(checkNonAggregatableField); - } - } - }); - - if (nonMetricsLoaded === false) { - setNonMetricsLoaded(true); - return; - } - - if (allNonMetricFields.length !== nonMetricFieldData.length && showEmptyFields === true) { - // Combine the field data obtained from Elasticsearch into a single array. - nonMetricFieldData = nonMetricFieldData.concat( - overallStats.aggregatableNotExistsFields, - overallStats.nonAggregatableNotExistsFields - ); - } - - const nonMetricFieldsToShow = showEmptyFields ? allNonMetricFields : populatedNonMetricFields; - - const configs: FieldVisConfig[] = []; - - nonMetricFieldsToShow.forEach((field) => { - const fieldData = nonMetricFieldData.find((f) => f.fieldName === field.spec.name); - - const nonMetricConfig = { - ...(fieldData ? fieldData : {}), - fieldFormat: currentIndexPattern.getFormatterForField(field), - aggregatable: field.aggregatable, - scripted: field.scripted, - loading: fieldData?.existsInDocs, - deletable: field.runtimeField !== undefined, - }; - - // Map the field type from the Kibana data view to the field type - // used in the data visualizer. - const dataVisualizerType = kbnTypeToJobType(field); - if (dataVisualizerType !== undefined) { - nonMetricConfig.type = dataVisualizerType; - } else { - // Add a flag to indicate that this is one of the 'other' Kibana - // field types that do not yet have a specific card type. - nonMetricConfig.type = field.type; - nonMetricConfig.isUnsupportedType = true; - } - - if (field.displayName !== nonMetricConfig.fieldName) { - nonMetricConfig.displayName = field.displayName; - } - - configs.push(nonMetricConfig); - }); - - setNonMetricConfigs(configs); - }, [ - currentIndexPattern, - dataLoader, - indexPatternFields, - nonMetricsLoaded, - overallStats, - showEmptyFields, - ]); - const wizardPanelWidth = '280px'; - const configs = useMemo(() => { - let combinedConfigs = [...nonMetricConfigs, ...metricConfigs]; - if (visibleFieldTypes && visibleFieldTypes.length > 0) { - combinedConfigs = combinedConfigs.filter( - (config) => visibleFieldTypes.findIndex((field) => field === config.type) > -1 - ); - } - if (visibleFieldNames && visibleFieldNames.length > 0) { - combinedConfigs = combinedConfigs.filter( - (config) => visibleFieldNames.findIndex((field) => field === config.fieldName) > -1 - ); - } - - return combinedConfigs; - }, [nonMetricConfigs, metricConfigs, visibleFieldTypes, visibleFieldNames]); - const fieldsCountStats: TotalFieldsStats | undefined = useMemo(() => { let _visibleFieldsCount = 0; let _totalFieldsCount = 0; @@ -923,7 +461,7 @@ export const IndexDataVisualizerView: FC = (dataVi {overallStats?.totalCount !== undefined && ( @@ -953,12 +491,14 @@ export const IndexDataVisualizerView: FC = (dataVi metricsStats={metricsStats} /> + items={configs} pageState={dataVisualizerListState} updatePageState={setDataVisualizerListState} getItemIdToExpandedRowMap={getItemIdToExpandedRowMap} extendedColumns={extendedColumns} + loading={progress < 100} /> diff --git a/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/components/search_panel/field_type_filter.tsx b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/components/search_panel/field_type_filter.tsx index 7e86425c0a891..ee54683b08435 100644 --- a/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/components/search_panel/field_type_filter.tsx +++ b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/components/search_panel/field_type_filter.tsx @@ -29,7 +29,7 @@ export const DataVisualizerFieldTypeFilter: FC<{ {label} {indexedFieldName && ( - + )} diff --git a/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/components/search_panel/search_panel.tsx b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/components/search_panel/search_panel.tsx index f55114ca36d78..25ed13121fc34 100644 --- a/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/components/search_panel/search_panel.tsx +++ b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/components/search_panel/search_panel.tsx @@ -22,6 +22,7 @@ import { SearchQueryLanguage } from '../../types/combined_query'; import { useDataVisualizerKibana } from '../../../kibana_context'; import './_index.scss'; import { createMergedEsQuery } from '../../utils/saved_search_utils'; +import { OverallStats } from '../../types/overall_stats'; interface Props { indexPattern: IndexPattern; searchString: Query['query']; @@ -29,7 +30,7 @@ interface Props { searchQueryLanguage: SearchQueryLanguage; samplerShardSize: number; setSamplerShardSize(s: number): void; - overallStats: any; + overallStats: OverallStats; indexedFieldTypes: JobFieldType[]; setVisibleFieldTypes(q: string[]): void; visibleFieldTypes: string[]; diff --git a/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/data_loader/data_loader.ts b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/data_loader/data_loader.ts deleted file mode 100644 index e0a2852a57b29..0000000000000 --- a/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/data_loader/data_loader.ts +++ /dev/null @@ -1,142 +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. - */ - -// Maximum number of examples to obtain for text type fields. -import { CoreSetup } from 'kibana/public'; -import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; -import { i18n } from '@kbn/i18n'; -import { IndexPattern } from '../../../../../../../src/plugins/data/common'; -import { NON_AGGREGATABLE_FIELD_TYPES, OMIT_FIELDS } from '../../../../common/constants'; -import { FieldRequestConfig } from '../../../../common/types'; -import { getVisualizerFieldStats, getVisualizerOverallStats } from '../services/visualizer_stats'; - -type IndexPatternTitle = string; -type SavedSearchQuery = Record | null | undefined; - -const MAX_EXAMPLES_DEFAULT: number = 10; - -export class DataLoader { - private _indexPattern: IndexPattern; - private _runtimeMappings: estypes.MappingRuntimeFields; - private _indexPatternTitle: IndexPatternTitle = ''; - private _maxExamples: number = MAX_EXAMPLES_DEFAULT; - private _toastNotifications: CoreSetup['notifications']['toasts']; - - constructor( - indexPattern: IndexPattern, - toastNotifications: CoreSetup['notifications']['toasts'] - ) { - this._indexPattern = indexPattern; - this._runtimeMappings = this._indexPattern.getComputedFields() - .runtimeFields as estypes.MappingRuntimeFields; - this._indexPatternTitle = indexPattern.title; - this._toastNotifications = toastNotifications; - } - - async loadOverallData( - query: string | SavedSearchQuery, - samplerShardSize: number, - earliest: number | undefined, - latest: number | undefined - ): Promise { - const aggregatableFields: string[] = []; - const nonAggregatableFields: string[] = []; - this._indexPattern.fields.forEach((field) => { - const fieldName = field.displayName !== undefined ? field.displayName : field.name; - if (this.isDisplayField(fieldName) === true) { - if (field.aggregatable === true && !NON_AGGREGATABLE_FIELD_TYPES.has(field.type)) { - aggregatableFields.push(field.name); - } else { - nonAggregatableFields.push(field.name); - } - } - }); - - // Need to find: - // 1. List of aggregatable fields that do exist in docs - // 2. List of aggregatable fields that do not exist in docs - // 3. List of non-aggregatable fields that do exist in docs. - // 4. List of non-aggregatable fields that do not exist in docs. - const stats = await getVisualizerOverallStats({ - indexPatternTitle: this._indexPatternTitle, - query, - timeFieldName: this._indexPattern.timeFieldName, - samplerShardSize, - earliest, - latest, - aggregatableFields, - nonAggregatableFields, - runtimeMappings: this._runtimeMappings, - }); - - return stats; - } - - async loadFieldStats( - query: string | SavedSearchQuery, - samplerShardSize: number, - earliest: number | undefined, - latest: number | undefined, - fields: FieldRequestConfig[], - interval?: number - ): Promise { - const stats = await getVisualizerFieldStats({ - indexPatternTitle: this._indexPatternTitle, - query, - timeFieldName: this._indexPattern.timeFieldName, - earliest, - latest, - samplerShardSize, - interval, - fields, - maxExamples: this._maxExamples, - runtimeMappings: this._runtimeMappings, - }); - - return stats; - } - - displayError(err: any) { - if (err.statusCode === 500) { - this._toastNotifications.addError(err, { - title: i18n.translate('xpack.dataVisualizer.index.dataLoader.internalServerErrorMessage', { - defaultMessage: - 'Error loading data in index {index}. {message}. ' + - 'The request may have timed out. Try using a smaller sample size or narrowing the time range.', - values: { - index: this._indexPattern.title, - message: err.error ?? err.message, - }, - }), - }); - } else { - this._toastNotifications.addError(err, { - title: i18n.translate('xpack.dataVisualizer.index.errorLoadingDataMessage', { - defaultMessage: 'Error loading data in index {index}. {message}.', - values: { - index: this._indexPattern.title, - message: err.error ?? err.message, - }, - }), - }); - } - } - - public set maxExamples(max: number) { - this._maxExamples = max; - } - - public get maxExamples(): number { - return this._maxExamples; - } - - // Returns whether the field with the specified name should be displayed, - // as certain fields such as _id and _source should be omitted from the view. - public isDisplayField(fieldName: string): boolean { - return !OMIT_FIELDS.includes(fieldName); - } -} diff --git a/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/embeddables/grid_embeddable/grid_embeddable.tsx b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/embeddables/grid_embeddable/grid_embeddable.tsx index f59225b1c019f..0391d5ae5d5d5 100644 --- a/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/embeddables/grid_embeddable/grid_embeddable.tsx +++ b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/embeddables/grid_embeddable/grid_embeddable.tsx @@ -8,7 +8,7 @@ import { Observable, Subject } from 'rxjs'; import { CoreStart } from 'kibana/public'; import ReactDOM from 'react-dom'; -import React, { Suspense, useCallback, useState } from 'react'; +import React, { Suspense, useCallback, useEffect, useState } from 'react'; import useObservable from 'react-use/lib/useObservable'; import { EuiEmptyPrompt, EuiIcon, EuiSpacer, EuiText } from '@elastic/eui'; import { Filter } from '@kbn/es-query'; @@ -36,15 +36,15 @@ import { } from '../../../common/components/stats_table'; import { FieldVisConfig } from '../../../common/components/stats_table/types'; import { getDefaultDataVisualizerListState } from '../../components/index_data_visualizer_view/index_data_visualizer_view'; -import { DataVisualizerTableState } from '../../../../../common'; +import { DataVisualizerTableState, SavedSearchSavedObject } from '../../../../../common'; import { DataVisualizerIndexBasedAppState } from '../../types/index_data_visualizer_state'; import { IndexBasedDataVisualizerExpandedRow } from '../../../common/components/expanded_row/index_based_expanded_row'; -import { useDataVisualizerGridData } from './use_data_visualizer_grid_data'; +import { useDataVisualizerGridData } from '../../hooks/use_data_visualizer_grid_data'; export type DataVisualizerGridEmbeddableServices = [CoreStart, DataVisualizerStartDependencies]; -export interface DataVisualizerGridEmbeddableInput extends EmbeddableInput { +export interface DataVisualizerGridInput { indexPattern: IndexPattern; - savedSearch?: SavedSearch; + savedSearch?: SavedSearch | SavedSearchSavedObject | null; query?: Query; visibleFieldNames?: string[]; filters?: Filter[]; @@ -54,6 +54,7 @@ export interface DataVisualizerGridEmbeddableInput extends EmbeddableInput { */ onAddFilter?: (field: IndexPatternField | string, value: string, type: '+' | '-') => void; } +export type DataVisualizerGridEmbeddableInput = EmbeddableInput & DataVisualizerGridInput; export type DataVisualizerGridEmbeddableOutput = EmbeddableOutput; export type IDataVisualizerGridEmbeddable = typeof DataVisualizerGridEmbeddable; @@ -79,8 +80,13 @@ export const EmbeddableWrapper = ({ }, [dataVisualizerListState, onOutputChange] ); - const { configs, searchQueryLanguage, searchString, extendedColumns, loaded } = + const { configs, searchQueryLanguage, searchString, extendedColumns, progress, setLastRefresh } = useDataVisualizerGridData(input, dataVisualizerListState); + + useEffect(() => { + setLastRefresh(Date.now()); + }, [input?.lastReloadRequestTime, setLastRefresh]); + const getItemIdToExpandedRowMap = useCallback( function (itemIds: string[], items: FieldVisConfig[]): ItemIdToExpandedRowMap { return itemIds.reduce((m: ItemIdToExpandedRowMap, fieldName: string) => { @@ -101,13 +107,7 @@ export const EmbeddableWrapper = ({ [input, searchQueryLanguage, searchString] ); - if ( - loaded && - (configs.length === 0 || - // FIXME: Configs might have a placeholder document count stats field - // This will be removed in the future - (configs.length === 1 && configs[0].fieldName === undefined)) - ) { + if (progress === 100 && configs.length === 0) { return (
    ); }; diff --git a/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/embeddables/grid_embeddable/use_data_visualizer_grid_data.ts b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/embeddables/grid_embeddable/use_data_visualizer_grid_data.ts deleted file mode 100644 index fc0fc7a2134b4..0000000000000 --- a/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/embeddables/grid_embeddable/use_data_visualizer_grid_data.ts +++ /dev/null @@ -1,587 +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 { Required } from 'utility-types'; -import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; -import { merge } from 'rxjs'; -import { EuiTableActionsColumnType } from '@elastic/eui/src/components/basic_table/table_types'; -import { i18n } from '@kbn/i18n'; -import { DataVisualizerIndexBasedAppState } from '../../types/index_data_visualizer_state'; -import { useDataVisualizerKibana } from '../../../kibana_context'; -import { getEsQueryFromSavedSearch } from '../../utils/saved_search_utils'; -import { MetricFieldsStats } from '../../../common/components/stats_table/components/field_count_stats'; -import { DataLoader } from '../../data_loader/data_loader'; -import { useTimefilter } from '../../hooks/use_time_filter'; -import { dataVisualizerRefresh$ } from '../../services/timefilter_refresh_service'; -import { TimeBuckets } from '../../services/time_buckets'; -import { - DataViewField, - KBN_FIELD_TYPES, - UI_SETTINGS, -} from '../../../../../../../../src/plugins/data/common'; -import { extractErrorProperties } from '../../utils/error_utils'; -import { FieldVisConfig } from '../../../common/components/stats_table/types'; -import { FieldRequestConfig, JOB_FIELD_TYPES } from '../../../../../common'; -import { kbnTypeToJobType } from '../../../common/util/field_types_utils'; -import { getActions } from '../../../common/components/field_data_row/action_menu'; -import { DataVisualizerGridEmbeddableInput } from './grid_embeddable'; -import { getDefaultPageState } from '../../components/index_data_visualizer_view/index_data_visualizer_view'; - -const defaults = getDefaultPageState(); - -export const useDataVisualizerGridData = ( - input: DataVisualizerGridEmbeddableInput, - dataVisualizerListState: Required -) => { - const { services } = useDataVisualizerKibana(); - const { notifications, uiSettings } = services; - const { toasts } = notifications; - const { samplerShardSize, visibleFieldTypes, showEmptyFields } = dataVisualizerListState; - - const [lastRefresh, setLastRefresh] = useState(0); - - const { - currentSavedSearch, - currentIndexPattern, - currentQuery, - currentFilters, - visibleFieldNames, - } = useMemo( - () => ({ - currentSavedSearch: input?.savedSearch, - currentIndexPattern: input.indexPattern, - currentQuery: input?.query, - visibleFieldNames: input?.visibleFieldNames ?? [], - currentFilters: input?.filters, - }), - [input] - ); - - const { searchQueryLanguage, searchString, searchQuery } = useMemo(() => { - const searchData = getEsQueryFromSavedSearch({ - indexPattern: currentIndexPattern, - uiSettings, - savedSearch: currentSavedSearch, - query: currentQuery, - filters: currentFilters, - }); - - if (searchData === undefined || dataVisualizerListState.searchString !== '') { - return { - searchQuery: dataVisualizerListState.searchQuery, - searchString: dataVisualizerListState.searchString, - searchQueryLanguage: dataVisualizerListState.searchQueryLanguage, - }; - } else { - return { - searchQuery: searchData.searchQuery, - searchString: searchData.searchString, - searchQueryLanguage: searchData.queryLanguage, - }; - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [ - currentSavedSearch, - currentIndexPattern, - dataVisualizerListState, - currentQuery, - currentFilters, - ]); - - const [overallStats, setOverallStats] = useState(defaults.overallStats); - - const [documentCountStats, setDocumentCountStats] = useState(defaults.documentCountStats); - const [metricConfigs, setMetricConfigs] = useState(defaults.metricConfigs); - const [metricsLoaded, setMetricsLoaded] = useState(defaults.metricsLoaded); - const [metricsStats, setMetricsStats] = useState(); - - const [nonMetricConfigs, setNonMetricConfigs] = useState(defaults.nonMetricConfigs); - const [nonMetricsLoaded, setNonMetricsLoaded] = useState(defaults.nonMetricsLoaded); - - const dataLoader = useMemo( - () => new DataLoader(currentIndexPattern, toasts), - [currentIndexPattern, toasts] - ); - - const timefilter = useTimefilter({ - timeRangeSelector: currentIndexPattern?.timeFieldName !== undefined, - autoRefreshSelector: true, - }); - - useEffect(() => { - const timeUpdateSubscription = merge( - timefilter.getTimeUpdate$(), - dataVisualizerRefresh$ - ).subscribe(() => { - setLastRefresh(Date.now()); - }); - return () => { - timeUpdateSubscription.unsubscribe(); - }; - }); - - const getTimeBuckets = useCallback(() => { - return new TimeBuckets({ - [UI_SETTINGS.HISTOGRAM_MAX_BARS]: uiSettings.get(UI_SETTINGS.HISTOGRAM_MAX_BARS), - [UI_SETTINGS.HISTOGRAM_BAR_TARGET]: uiSettings.get(UI_SETTINGS.HISTOGRAM_BAR_TARGET), - dateFormat: uiSettings.get('dateFormat'), - 'dateFormat:scaled': uiSettings.get('dateFormat:scaled'), - }); - }, [uiSettings]); - - const indexPatternFields: DataViewField[] = useMemo( - () => currentIndexPattern.fields, - [currentIndexPattern] - ); - - async function loadOverallStats() { - const tf = timefilter as any; - let earliest; - let latest; - - const activeBounds = tf.getActiveBounds(); - - if (currentIndexPattern.timeFieldName !== undefined && activeBounds === undefined) { - return; - } - - if (currentIndexPattern.timeFieldName !== undefined) { - earliest = activeBounds.min.valueOf(); - latest = activeBounds.max.valueOf(); - } - - try { - const allStats = await dataLoader.loadOverallData( - searchQuery, - samplerShardSize, - earliest, - latest - ); - // Because load overall stats perform queries in batches - // there could be multiple errors - if (Array.isArray(allStats.errors) && allStats.errors.length > 0) { - allStats.errors.forEach((err: any) => { - dataLoader.displayError(extractErrorProperties(err)); - }); - } - setOverallStats(allStats); - } catch (err) { - dataLoader.displayError(err.body ?? err); - } - } - - const createMetricCards = useCallback(() => { - const configs: FieldVisConfig[] = []; - const aggregatableExistsFields: any[] = overallStats.aggregatableExistsFields || []; - - const allMetricFields = indexPatternFields.filter((f) => { - return ( - f.type === KBN_FIELD_TYPES.NUMBER && - f.displayName !== undefined && - dataLoader.isDisplayField(f.displayName) === true - ); - }); - const metricExistsFields = allMetricFields.filter((f) => { - return aggregatableExistsFields.find((existsF) => { - return existsF.fieldName === f.spec.name; - }); - }); - - // Add a config for 'document count', identified by no field name if indexpattern is time based. - if (currentIndexPattern.timeFieldName !== undefined) { - configs.push({ - type: JOB_FIELD_TYPES.NUMBER, - existsInDocs: true, - loading: true, - aggregatable: true, - }); - } - - if (metricsLoaded === false) { - setMetricsLoaded(true); - return; - } - - let aggregatableFields: any[] = overallStats.aggregatableExistsFields; - if (allMetricFields.length !== metricExistsFields.length && metricsLoaded === true) { - aggregatableFields = aggregatableFields.concat(overallStats.aggregatableNotExistsFields); - } - - const metricFieldsToShow = - metricsLoaded === true && showEmptyFields === true ? allMetricFields : metricExistsFields; - - metricFieldsToShow.forEach((field) => { - const fieldData = aggregatableFields.find((f) => { - return f.fieldName === field.spec.name; - }); - - const metricConfig: FieldVisConfig = { - ...(fieldData ? fieldData : {}), - fieldFormat: currentIndexPattern.getFormatterForField(field), - type: JOB_FIELD_TYPES.NUMBER, - loading: true, - aggregatable: true, - deletable: field.runtimeField !== undefined, - }; - if (field.displayName !== metricConfig.fieldName) { - metricConfig.displayName = field.displayName; - } - - configs.push(metricConfig); - }); - - setMetricsStats({ - totalMetricFieldsCount: allMetricFields.length, - visibleMetricsCount: metricFieldsToShow.length, - }); - setMetricConfigs(configs); - }, [ - currentIndexPattern, - dataLoader, - indexPatternFields, - metricsLoaded, - overallStats, - showEmptyFields, - ]); - - const createNonMetricCards = useCallback(() => { - const allNonMetricFields = indexPatternFields.filter((f) => { - return ( - f.type !== KBN_FIELD_TYPES.NUMBER && - f.displayName !== undefined && - dataLoader.isDisplayField(f.displayName) === true - ); - }); - // Obtain the list of all non-metric fields which appear in documents - // (aggregatable or not aggregatable). - const populatedNonMetricFields: any[] = []; // Kibana index pattern non metric fields. - let nonMetricFieldData: any[] = []; // Basic non metric field data loaded from requesting overall stats. - const aggregatableExistsFields: any[] = overallStats.aggregatableExistsFields || []; - const nonAggregatableExistsFields: any[] = overallStats.nonAggregatableExistsFields || []; - - allNonMetricFields.forEach((f) => { - const checkAggregatableField = aggregatableExistsFields.find( - (existsField) => existsField.fieldName === f.spec.name - ); - - if (checkAggregatableField !== undefined) { - populatedNonMetricFields.push(f); - nonMetricFieldData.push(checkAggregatableField); - } else { - const checkNonAggregatableField = nonAggregatableExistsFields.find( - (existsField) => existsField.fieldName === f.spec.name - ); - - if (checkNonAggregatableField !== undefined) { - populatedNonMetricFields.push(f); - nonMetricFieldData.push(checkNonAggregatableField); - } - } - }); - - if (nonMetricsLoaded === false) { - setNonMetricsLoaded(true); - return; - } - - if (allNonMetricFields.length !== nonMetricFieldData.length && showEmptyFields === true) { - // Combine the field data obtained from Elasticsearch into a single array. - nonMetricFieldData = nonMetricFieldData.concat( - overallStats.aggregatableNotExistsFields, - overallStats.nonAggregatableNotExistsFields - ); - } - - const nonMetricFieldsToShow = showEmptyFields ? allNonMetricFields : populatedNonMetricFields; - - const configs: FieldVisConfig[] = []; - - nonMetricFieldsToShow.forEach((field) => { - const fieldData = nonMetricFieldData.find((f) => f.fieldName === field.spec.name); - - const nonMetricConfig = { - ...(fieldData ? fieldData : {}), - fieldFormat: currentIndexPattern.getFormatterForField(field), - aggregatable: field.aggregatable, - scripted: field.scripted, - loading: fieldData?.existsInDocs, - deletable: field.runtimeField !== undefined, - }; - - // Map the field type from the Kibana index pattern to the field type - // used in the data visualizer. - const dataVisualizerType = kbnTypeToJobType(field); - if (dataVisualizerType !== undefined) { - nonMetricConfig.type = dataVisualizerType; - } else { - // Add a flag to indicate that this is one of the 'other' Kibana - // field types that do not yet have a specific card type. - nonMetricConfig.type = field.type; - nonMetricConfig.isUnsupportedType = true; - } - - if (field.displayName !== nonMetricConfig.fieldName) { - nonMetricConfig.displayName = field.displayName; - } - - configs.push(nonMetricConfig); - }); - - setNonMetricConfigs(configs); - }, [ - currentIndexPattern, - dataLoader, - indexPatternFields, - nonMetricsLoaded, - overallStats, - showEmptyFields, - ]); - - async function loadMetricFieldStats() { - // Only request data for fields that exist in documents. - if (metricConfigs.length === 0) { - return; - } - - const configsToLoad = metricConfigs.filter( - (config) => config.existsInDocs === true && config.loading === true - ); - if (configsToLoad.length === 0) { - return; - } - - // Pass the field name, type and cardinality in the request. - // Top values will be obtained on a sample if cardinality > 100000. - const existMetricFields: FieldRequestConfig[] = configsToLoad.map((config) => { - const props = { fieldName: config.fieldName, type: config.type, cardinality: 0 }; - if (config.stats !== undefined && config.stats.cardinality !== undefined) { - props.cardinality = config.stats.cardinality; - } - return props; - }); - - // Obtain the interval to use for date histogram aggregations - // (such as the document count chart). Aim for 75 bars. - const buckets = getTimeBuckets(); - - const tf = timefilter as any; - let earliest: number | undefined; - let latest: number | undefined; - if (currentIndexPattern.timeFieldName !== undefined) { - earliest = tf.getActiveBounds().min.valueOf(); - latest = tf.getActiveBounds().max.valueOf(); - } - - const bounds = tf.getActiveBounds(); - const BAR_TARGET = 75; - buckets.setInterval('auto'); - buckets.setBounds(bounds); - buckets.setBarTarget(BAR_TARGET); - const aggInterval = buckets.getInterval(); - - try { - const metricFieldStats = await dataLoader.loadFieldStats( - searchQuery, - samplerShardSize, - earliest, - latest, - existMetricFields, - aggInterval.asMilliseconds() - ); - - // Add the metric stats to the existing stats in the corresponding config. - const configs: FieldVisConfig[] = []; - metricConfigs.forEach((config) => { - const configWithStats = { ...config }; - if (config.fieldName !== undefined) { - configWithStats.stats = { - ...configWithStats.stats, - ...metricFieldStats.find( - (fieldStats: any) => fieldStats.fieldName === config.fieldName - ), - }; - configWithStats.loading = false; - configs.push(configWithStats); - } else { - // Document count card. - configWithStats.stats = metricFieldStats.find( - (fieldStats: any) => fieldStats.fieldName === undefined - ); - - if (configWithStats.stats !== undefined) { - // Add earliest / latest of timefilter for setting x axis domain. - configWithStats.stats.timeRangeEarliest = earliest; - configWithStats.stats.timeRangeLatest = latest; - } - setDocumentCountStats(configWithStats); - } - }); - - setMetricConfigs(configs); - } catch (err) { - dataLoader.displayError(err); - } - } - - async function loadNonMetricFieldStats() { - // Only request data for fields that exist in documents. - if (nonMetricConfigs.length === 0) { - return; - } - - const configsToLoad = nonMetricConfigs.filter( - (config) => config.existsInDocs === true && config.loading === true - ); - if (configsToLoad.length === 0) { - return; - } - - // Pass the field name, type and cardinality in the request. - // Top values will be obtained on a sample if cardinality > 100000. - const existNonMetricFields: FieldRequestConfig[] = configsToLoad.map((config) => { - const props = { fieldName: config.fieldName, type: config.type, cardinality: 0 }; - if (config.stats !== undefined && config.stats.cardinality !== undefined) { - props.cardinality = config.stats.cardinality; - } - return props; - }); - - const tf = timefilter as any; - let earliest; - let latest; - if (currentIndexPattern.timeFieldName !== undefined) { - earliest = tf.getActiveBounds().min.valueOf(); - latest = tf.getActiveBounds().max.valueOf(); - } - - try { - const nonMetricFieldStats = await dataLoader.loadFieldStats( - searchQuery, - samplerShardSize, - earliest, - latest, - existNonMetricFields - ); - - // Add the field stats to the existing stats in the corresponding config. - const configs: FieldVisConfig[] = []; - nonMetricConfigs.forEach((config) => { - const configWithStats = { ...config }; - if (config.fieldName !== undefined) { - configWithStats.stats = { - ...configWithStats.stats, - ...nonMetricFieldStats.find( - (fieldStats: any) => fieldStats.fieldName === config.fieldName - ), - }; - } - configWithStats.loading = false; - configs.push(configWithStats); - }); - - setNonMetricConfigs(configs); - } catch (err) { - dataLoader.displayError(err); - } - } - - useEffect(() => { - loadOverallStats(); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [searchQuery, samplerShardSize, lastRefresh]); - - useEffect(() => { - createMetricCards(); - createNonMetricCards(); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [overallStats, showEmptyFields]); - - useEffect(() => { - loadMetricFieldStats(); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [metricConfigs]); - - useEffect(() => { - loadNonMetricFieldStats(); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [nonMetricConfigs]); - - useEffect(() => { - createMetricCards(); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [metricsLoaded]); - - useEffect(() => { - createNonMetricCards(); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [nonMetricsLoaded]); - - const configs = useMemo(() => { - let combinedConfigs = [...nonMetricConfigs, ...metricConfigs]; - if (visibleFieldTypes && visibleFieldTypes.length > 0) { - combinedConfigs = combinedConfigs.filter( - (config) => visibleFieldTypes.findIndex((field) => field === config.type) > -1 - ); - } - if (visibleFieldNames && visibleFieldNames.length > 0) { - combinedConfigs = combinedConfigs.filter( - (config) => visibleFieldNames.findIndex((field) => field === config.fieldName) > -1 - ); - } - - return combinedConfigs; - }, [nonMetricConfigs, metricConfigs, visibleFieldTypes, visibleFieldNames]); - - // Some actions open up fly-out or popup - // This variable is used to keep track of them and clean up when unmounting - const actionFlyoutRef = useRef<() => void | undefined>(); - useEffect(() => { - const ref = actionFlyoutRef; - return () => { - // Clean up any of the flyout/editor opened from the actions - if (ref.current) { - ref.current(); - } - }; - }, []); - - // Inject custom action column for the index based visualizer - // Hide the column completely if no access to any of the plugins - const extendedColumns = useMemo(() => { - const actions = getActions( - input.indexPattern, - { lens: services.lens }, - { - searchQueryLanguage, - searchString, - }, - actionFlyoutRef - ); - if (!Array.isArray(actions) || actions.length < 1) return; - - const actionColumn: EuiTableActionsColumnType = { - name: i18n.translate('xpack.dataVisualizer.index.dataGrid.actionsColumnLabel', { - defaultMessage: 'Actions', - }), - actions, - width: '70px', - }; - - return [actionColumn]; - }, [input.indexPattern, services, searchQueryLanguage, searchString]); - - return { - configs, - searchQueryLanguage, - searchString, - searchQuery, - extendedColumns, - documentCountStats, - metricsStats, - loaded: metricsLoaded && nonMetricsLoaded, - }; -}; diff --git a/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/hooks/use_data_visualizer_grid_data.ts b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/hooks/use_data_visualizer_grid_data.ts new file mode 100644 index 0000000000000..e6e7a96e0329f --- /dev/null +++ b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/hooks/use_data_visualizer_grid_data.ts @@ -0,0 +1,528 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { Required } from 'utility-types'; +import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; +import { merge } from 'rxjs'; +import { EuiTableActionsColumnType } from '@elastic/eui/src/components/basic_table/table_types'; +import { i18n } from '@kbn/i18n'; +import { DataVisualizerIndexBasedAppState } from '../types/index_data_visualizer_state'; +import { useDataVisualizerKibana } from '../../kibana_context'; +import { getEsQueryFromSavedSearch } from '../utils/saved_search_utils'; +import { MetricFieldsStats } from '../../common/components/stats_table/components/field_count_stats'; +import { useTimefilter } from './use_time_filter'; +import { dataVisualizerRefresh$ } from '../services/timefilter_refresh_service'; +import { TimeBuckets } from '../../../../common/services/time_buckets'; +import { + DataViewField, + KBN_FIELD_TYPES, + UI_SETTINGS, +} from '../../../../../../../src/plugins/data/common'; +import { FieldVisConfig } from '../../common/components/stats_table/types'; +import { + FieldRequestConfig, + JOB_FIELD_TYPES, + JobFieldType, + NON_AGGREGATABLE_FIELD_TYPES, + OMIT_FIELDS, +} from '../../../../common'; +import { kbnTypeToJobType } from '../../common/util/field_types_utils'; +import { getActions } from '../../common/components/field_data_row/action_menu'; +import { DataVisualizerGridInput } from '../embeddables/grid_embeddable/grid_embeddable'; +import { getDefaultPageState } from '../components/index_data_visualizer_view/index_data_visualizer_view'; +import { useFieldStatsSearchStrategy } from './use_field_stats'; +import { useOverallStats } from './use_overall_stats'; +import { OverallStatsSearchStrategyParams } from '../../../../common/types/field_stats'; +import { Dictionary } from '../../common/util/url_state'; +import { AggregatableField, NonAggregatableField } from '../types/overall_stats'; + +const defaults = getDefaultPageState(); + +function isDisplayField(fieldName: string): boolean { + return !OMIT_FIELDS.includes(fieldName); +} + +export const useDataVisualizerGridData = ( + input: DataVisualizerGridInput, + dataVisualizerListState: Required, + onUpdate?: (params: Dictionary) => void +) => { + const { services } = useDataVisualizerKibana(); + const { uiSettings, data } = services; + const { samplerShardSize, visibleFieldTypes, showEmptyFields } = dataVisualizerListState; + const dataVisualizerListStateRef = useRef(dataVisualizerListState); + + const [lastRefresh, setLastRefresh] = useState(0); + const [searchSessionId, setSearchSessionId] = useState(); + + const { + currentSavedSearch, + currentIndexPattern, + currentQuery, + currentFilters, + visibleFieldNames, + } = useMemo( + () => ({ + currentSavedSearch: input?.savedSearch, + currentIndexPattern: input.indexPattern, + currentQuery: input?.query, + visibleFieldNames: input?.visibleFieldNames ?? [], + currentFilters: input?.filters, + }), + [input] + ); + + /** Prepare required params to pass to search strategy **/ + const { searchQueryLanguage, searchString, searchQuery } = useMemo(() => { + const searchData = getEsQueryFromSavedSearch({ + indexPattern: currentIndexPattern, + uiSettings, + savedSearch: currentSavedSearch, + query: currentQuery, + filters: currentFilters, + filterManager: data.query.filterManager, + }); + + if (searchData === undefined || dataVisualizerListState.searchString !== '') { + if (dataVisualizerListState.filters) { + data.query.filterManager.setFilters(dataVisualizerListState.filters); + } + return { + searchQuery: dataVisualizerListState.searchQuery, + searchString: dataVisualizerListState.searchString, + searchQueryLanguage: dataVisualizerListState.searchQueryLanguage, + }; + } else { + return { + searchQuery: searchData.searchQuery, + searchString: searchData.searchString, + searchQueryLanguage: searchData.queryLanguage, + }; + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [ + currentSavedSearch?.id, + currentIndexPattern.id, + dataVisualizerListState.searchString, + dataVisualizerListState.searchQueryLanguage, + // eslint-disable-next-line react-hooks/exhaustive-deps + JSON.stringify({ + searchQuery: dataVisualizerListState.searchQuery, + currentQuery, + currentFilters, + }), + lastRefresh, + ]); + + useEffect(() => { + const currentSearchSessionId = data.search?.session?.getSessionId(); + if (currentSearchSessionId !== undefined) { + setSearchSessionId(currentSearchSessionId); + } + }, [data]); + + const _timeBuckets = useMemo(() => { + return new TimeBuckets({ + [UI_SETTINGS.HISTOGRAM_MAX_BARS]: uiSettings.get(UI_SETTINGS.HISTOGRAM_MAX_BARS), + [UI_SETTINGS.HISTOGRAM_BAR_TARGET]: uiSettings.get(UI_SETTINGS.HISTOGRAM_BAR_TARGET), + dateFormat: uiSettings.get('dateFormat'), + 'dateFormat:scaled': uiSettings.get('dateFormat:scaled'), + }); + }, [uiSettings]); + + const timefilter = useTimefilter({ + timeRangeSelector: currentIndexPattern?.timeFieldName !== undefined, + autoRefreshSelector: true, + }); + + const [metricConfigs, setMetricConfigs] = useState(defaults.metricConfigs); + const [metricsLoaded, setMetricsLoaded] = useState(defaults.metricsLoaded); + const [metricsStats, setMetricsStats] = useState(); + + const [nonMetricConfigs, setNonMetricConfigs] = useState(defaults.nonMetricConfigs); + const [nonMetricsLoaded, setNonMetricsLoaded] = useState(defaults.nonMetricsLoaded); + + /** Search strategy **/ + const fieldStatsRequest: OverallStatsSearchStrategyParams | undefined = useMemo( + () => { + // Obtain the interval to use for date histogram aggregations + // (such as the document count chart). Aim for 75 bars. + const buckets = _timeBuckets; + + const tf = timefilter; + + if (!buckets || !tf || !currentIndexPattern) return; + + const activeBounds = tf.getActiveBounds(); + + let earliest: number | undefined; + let latest: number | undefined; + if (activeBounds !== undefined && currentIndexPattern.timeFieldName !== undefined) { + earliest = activeBounds.min?.valueOf(); + latest = activeBounds.max?.valueOf(); + } + + const bounds = tf.getActiveBounds(); + const BAR_TARGET = 75; + buckets.setInterval('auto'); + + if (bounds) { + buckets.setBounds(bounds); + buckets.setBarTarget(BAR_TARGET); + } + + const aggInterval = buckets.getInterval(); + + const aggregatableFields: string[] = []; + const nonAggregatableFields: string[] = []; + currentIndexPattern.fields.forEach((field) => { + const fieldName = field.displayName !== undefined ? field.displayName : field.name; + if (!OMIT_FIELDS.includes(fieldName)) { + if (field.aggregatable === true && !NON_AGGREGATABLE_FIELD_TYPES.has(field.type)) { + aggregatableFields.push(field.name); + } else { + nonAggregatableFields.push(field.name); + } + } + }); + return { + earliest, + latest, + aggInterval, + intervalMs: aggInterval?.asMilliseconds(), + searchQuery, + samplerShardSize, + sessionId: searchSessionId, + index: currentIndexPattern.title, + timeFieldName: currentIndexPattern.timeFieldName, + runtimeFieldMap: currentIndexPattern.getComputedFields().runtimeFields, + aggregatableFields, + nonAggregatableFields, + }; + }, + // eslint-disable-next-line react-hooks/exhaustive-deps + [ + _timeBuckets, + timefilter, + currentIndexPattern.id, + // eslint-disable-next-line react-hooks/exhaustive-deps + JSON.stringify(searchQuery), + samplerShardSize, + searchSessionId, + lastRefresh, + ] + ); + + const { overallStats, progress: overallStatsProgress } = useOverallStats( + fieldStatsRequest, + lastRefresh + ); + + const configsWithoutStats = useMemo(() => { + if (overallStatsProgress.loaded < 100) return; + const existMetricFields = metricConfigs + .map((config) => { + if (config.existsInDocs === false) return; + return { + fieldName: config.fieldName, + type: config.type, + cardinality: config.stats?.cardinality ?? 0, + }; + }) + .filter((c) => c !== undefined) as FieldRequestConfig[]; + + // Pass the field name, type and cardinality in the request. + // Top values will be obtained on a sample if cardinality > 100000. + const existNonMetricFields: FieldRequestConfig[] = nonMetricConfigs + .map((config) => { + if (config.existsInDocs === false) return; + return { + fieldName: config.fieldName, + type: config.type, + cardinality: config.stats?.cardinality ?? 0, + }; + }) + .filter((c) => c !== undefined) as FieldRequestConfig[]; + + return { metricConfigs: existMetricFields, nonMetricConfigs: existNonMetricFields }; + }, [metricConfigs, nonMetricConfigs, overallStatsProgress.loaded]); + + const strategyResponse = useFieldStatsSearchStrategy( + fieldStatsRequest, + configsWithoutStats, + dataVisualizerListStateRef.current + ); + + const combinedProgress = useMemo( + () => overallStatsProgress.loaded * 0.2 + strategyResponse.progress.loaded * 0.8, + [overallStatsProgress.loaded, strategyResponse.progress.loaded] + ); + + useEffect(() => { + const timeUpdateSubscription = merge( + timefilter.getTimeUpdate$(), + timefilter.getAutoRefreshFetch$(), + dataVisualizerRefresh$ + ).subscribe(() => { + if (onUpdate) { + onUpdate({ + time: timefilter.getTime(), + refreshInterval: timefilter.getRefreshInterval(), + }); + } + setLastRefresh(Date.now()); + }); + return () => { + timeUpdateSubscription.unsubscribe(); + }; + }); + + const indexPatternFields: DataViewField[] = useMemo( + () => currentIndexPattern.fields, + [currentIndexPattern] + ); + + const createMetricCards = useCallback(() => { + const configs: FieldVisConfig[] = []; + const aggregatableExistsFields: AggregatableField[] = + overallStats.aggregatableExistsFields || []; + + const allMetricFields = indexPatternFields.filter((f) => { + return ( + f.type === KBN_FIELD_TYPES.NUMBER && + f.displayName !== undefined && + isDisplayField(f.displayName) === true + ); + }); + const metricExistsFields = allMetricFields.filter((f) => { + return aggregatableExistsFields.find((existsF) => { + return existsF.fieldName === f.spec.name; + }); + }); + + if (metricsLoaded === false) { + setMetricsLoaded(true); + return; + } + + let aggregatableFields: AggregatableField[] = overallStats.aggregatableExistsFields; + if (allMetricFields.length !== metricExistsFields.length && metricsLoaded === true) { + aggregatableFields = aggregatableFields.concat(overallStats.aggregatableNotExistsFields); + } + + const metricFieldsToShow = + metricsLoaded === true && showEmptyFields === true ? allMetricFields : metricExistsFields; + + metricFieldsToShow.forEach((field) => { + const fieldData = aggregatableFields.find((f) => { + return f.fieldName === field.spec.name; + }); + if (!fieldData) return; + + const metricConfig: FieldVisConfig = { + ...fieldData, + fieldFormat: currentIndexPattern.getFormatterForField(field), + type: JOB_FIELD_TYPES.NUMBER, + loading: true, + aggregatable: true, + deletable: field.runtimeField !== undefined, + }; + if (field.displayName !== metricConfig.fieldName) { + metricConfig.displayName = field.displayName; + } + + configs.push(metricConfig); + }); + + setMetricsStats({ + totalMetricFieldsCount: allMetricFields.length, + visibleMetricsCount: metricFieldsToShow.length, + }); + setMetricConfigs(configs); + }, [currentIndexPattern, indexPatternFields, metricsLoaded, overallStats, showEmptyFields]); + + const createNonMetricCards = useCallback(() => { + const allNonMetricFields = indexPatternFields.filter((f) => { + return ( + f.type !== KBN_FIELD_TYPES.NUMBER && + f.displayName !== undefined && + isDisplayField(f.displayName) === true + ); + }); + // Obtain the list of all non-metric fields which appear in documents + // (aggregatable or not aggregatable). + const populatedNonMetricFields: DataViewField[] = []; // Kibana index pattern non metric fields. + let nonMetricFieldData: Array = []; // Basic non metric field data loaded from requesting overall stats. + const aggregatableExistsFields: AggregatableField[] = + overallStats.aggregatableExistsFields || []; + const nonAggregatableExistsFields: NonAggregatableField[] = + overallStats.nonAggregatableExistsFields || []; + + allNonMetricFields.forEach((f) => { + const checkAggregatableField = aggregatableExistsFields.find( + (existsField) => existsField.fieldName === f.spec.name + ); + + if (checkAggregatableField !== undefined) { + populatedNonMetricFields.push(f); + nonMetricFieldData.push(checkAggregatableField); + } else { + const checkNonAggregatableField = nonAggregatableExistsFields.find( + (existsField) => existsField.fieldName === f.spec.name + ); + + if (checkNonAggregatableField !== undefined) { + populatedNonMetricFields.push(f); + nonMetricFieldData.push(checkNonAggregatableField); + } + } + }); + + if (nonMetricsLoaded === false) { + setNonMetricsLoaded(true); + return; + } + + if (allNonMetricFields.length !== nonMetricFieldData.length && showEmptyFields === true) { + // Combine the field data obtained from Elasticsearch into a single array. + nonMetricFieldData = nonMetricFieldData.concat( + overallStats.aggregatableNotExistsFields, + overallStats.nonAggregatableNotExistsFields + ); + } + + const nonMetricFieldsToShow = showEmptyFields ? allNonMetricFields : populatedNonMetricFields; + + const configs: FieldVisConfig[] = []; + + nonMetricFieldsToShow.forEach((field) => { + const fieldData = nonMetricFieldData.find((f) => f.fieldName === field.spec.name); + + const nonMetricConfig: Partial = { + ...(fieldData ? fieldData : {}), + fieldFormat: currentIndexPattern.getFormatterForField(field), + aggregatable: field.aggregatable, + loading: fieldData?.existsInDocs ?? true, + deletable: field.runtimeField !== undefined, + }; + + // Map the field type from the Kibana index pattern to the field type + // used in the data visualizer. + const dataVisualizerType = kbnTypeToJobType(field); + if (dataVisualizerType !== undefined) { + nonMetricConfig.type = dataVisualizerType; + } else { + // Add a flag to indicate that this is one of the 'other' Kibana + // field types that do not yet have a specific card type. + nonMetricConfig.type = field.type as JobFieldType; + nonMetricConfig.isUnsupportedType = true; + } + + if (field.displayName !== nonMetricConfig.fieldName) { + nonMetricConfig.displayName = field.displayName; + } + + configs.push(nonMetricConfig as FieldVisConfig); + }); + + setNonMetricConfigs(configs); + }, [currentIndexPattern, indexPatternFields, nonMetricsLoaded, overallStats, showEmptyFields]); + + useEffect(() => { + createMetricCards(); + createNonMetricCards(); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [overallStats, showEmptyFields]); + + const configs = useMemo(() => { + const fieldStats = strategyResponse.fieldStats; + let combinedConfigs = [...nonMetricConfigs, ...metricConfigs]; + if (visibleFieldTypes && visibleFieldTypes.length > 0) { + combinedConfigs = combinedConfigs.filter( + (config) => visibleFieldTypes.findIndex((field) => field === config.type) > -1 + ); + } + if (visibleFieldNames && visibleFieldNames.length > 0) { + combinedConfigs = combinedConfigs.filter( + (config) => visibleFieldNames.findIndex((field) => field === config.fieldName) > -1 + ); + } + + if (fieldStats) { + combinedConfigs = combinedConfigs.map((c) => { + const loadedFullStats = fieldStats.get(c.fieldName) ?? {}; + return loadedFullStats + ? { + ...c, + loading: false, + stats: { ...c.stats, ...loadedFullStats }, + } + : c; + }); + } + + return combinedConfigs; + }, [ + nonMetricConfigs, + metricConfigs, + visibleFieldTypes, + visibleFieldNames, + strategyResponse.fieldStats, + ]); + + // Some actions open up fly-out or popup + // This variable is used to keep track of them and clean up when unmounting + const actionFlyoutRef = useRef<() => void | undefined>(); + useEffect(() => { + const ref = actionFlyoutRef; + return () => { + // Clean up any of the flyout/editor opened from the actions + if (ref.current) { + ref.current(); + } + }; + }, []); + + // Inject custom action column for the index based visualizer + // Hide the column completely if no access to any of the plugins + const extendedColumns = useMemo(() => { + const actions = getActions( + input.indexPattern, + { lens: services.lens }, + { + searchQueryLanguage, + searchString, + }, + actionFlyoutRef + ); + if (!Array.isArray(actions) || actions.length < 1) return; + + const actionColumn: EuiTableActionsColumnType = { + name: i18n.translate('xpack.dataVisualizer.index.dataGrid.actionsColumnLabel', { + defaultMessage: 'Actions', + }), + actions, + width: '70px', + }; + + return [actionColumn]; + }, [input.indexPattern, services, searchQueryLanguage, searchString]); + + return { + progress: combinedProgress, + configs, + searchQueryLanguage, + searchString, + searchQuery, + extendedColumns, + documentCountStats: overallStats.documentCountStats, + metricsStats, + overallStats, + timefilter, + setLastRefresh, + }; +}; diff --git a/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/hooks/use_field_stats.ts b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/hooks/use_field_stats.ts new file mode 100644 index 0000000000000..64654d56db05b --- /dev/null +++ b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/hooks/use_field_stats.ts @@ -0,0 +1,287 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { useCallback, useEffect, useReducer, useRef, useState } from 'react'; +import { combineLatest, Observable, Subject, Subscription } from 'rxjs'; +import { i18n } from '@kbn/i18n'; +import { last, cloneDeep } from 'lodash'; +import { switchMap } from 'rxjs/operators'; +import type { + DataStatsFetchProgress, + FieldStatsSearchStrategyReturnBase, + OverallStatsSearchStrategyParams, + FieldStatsCommonRequestParams, + Field, +} from '../../../../common/types/field_stats'; +import { useDataVisualizerKibana } from '../../kibana_context'; +import type { FieldRequestConfig } from '../../../../common'; +import type { DataVisualizerIndexBasedAppState } from '../types/index_data_visualizer_state'; +import { + buildBaseFilterCriteria, + getSafeAggregationName, +} from '../../../../common/utils/query_utils'; +import type { FieldStats, FieldStatsError } from '../../../../common/types/field_stats'; +import { getInitialProgress, getReducer } from '../progress_utils'; +import { MAX_EXAMPLES_DEFAULT } from '../search_strategy/requests/constants'; +import type { ISearchOptions } from '../../../../../../../src/plugins/data/common'; +import { getFieldsStats } from '../search_strategy/requests/get_fields_stats'; +interface FieldStatsParams { + metricConfigs: FieldRequestConfig[]; + nonMetricConfigs: FieldRequestConfig[]; +} + +const createBatchedRequests = (fields: Field[], maxBatchSize = 10) => { + // Batch up fields by type, getting stats for multiple fields at a time. + const batches: Field[][] = []; + const batchedFields: { [key: string]: Field[][] } = {}; + + fields.forEach((field) => { + const fieldType = field.type; + if (batchedFields[fieldType] === undefined) { + batchedFields[fieldType] = [[]]; + } + let lastArray: Field[] = last(batchedFields[fieldType]) as Field[]; + if (lastArray.length === maxBatchSize) { + lastArray = []; + batchedFields[fieldType].push(lastArray); + } + lastArray.push(field); + }); + + Object.values(batchedFields).forEach((lists) => { + batches.push(...lists); + }); + return batches; +}; + +export function useFieldStatsSearchStrategy( + searchStrategyParams: OverallStatsSearchStrategyParams | undefined, + fieldStatsParams: FieldStatsParams | undefined, + initialDataVisualizerListState: DataVisualizerIndexBasedAppState +): FieldStatsSearchStrategyReturnBase { + const { + services: { + data, + notifications: { toasts }, + }, + } = useDataVisualizerKibana(); + + const [fieldStats, setFieldStats] = useState>(); + const [fetchState, setFetchState] = useReducer( + getReducer(), + getInitialProgress() + ); + + const abortCtrl = useRef(new AbortController()); + const searchSubscription$ = useRef(); + const retries$ = useRef(); + + const startFetch = useCallback(() => { + searchSubscription$.current?.unsubscribe(); + retries$.current?.unsubscribe(); + + abortCtrl.current.abort(); + abortCtrl.current = new AbortController(); + setFetchState({ + ...getInitialProgress(), + error: undefined, + }); + setFieldStats(undefined); + + if ( + !searchStrategyParams || + !fieldStatsParams || + (fieldStatsParams.metricConfigs.length === 0 && + fieldStatsParams.nonMetricConfigs.length === 0) + ) { + setFetchState({ + loaded: 100, + isRunning: false, + }); + + return; + } + + const { sortField, sortDirection } = initialDataVisualizerListState; + /** + * Sort the list of fields by the initial sort field and sort direction + * Then divide into chunks by the initial page size + */ + + let sortedConfigs = [...fieldStatsParams.metricConfigs, ...fieldStatsParams.nonMetricConfigs]; + + if (sortField === 'fieldName' || sortField === 'type') { + sortedConfigs = sortedConfigs.sort((a, b) => a[sortField].localeCompare(b[sortField])); + } + if (sortDirection === 'desc') { + sortedConfigs = sortedConfigs.reverse(); + } + + const filterCriteria = buildBaseFilterCriteria( + searchStrategyParams.timeFieldName, + searchStrategyParams.earliest, + searchStrategyParams.latest, + searchStrategyParams.searchQuery + ); + + const params: FieldStatsCommonRequestParams = { + index: searchStrategyParams.index, + samplerShardSize: searchStrategyParams.samplerShardSize, + timeFieldName: searchStrategyParams.timeFieldName, + earliestMs: searchStrategyParams.earliest, + latestMs: searchStrategyParams.latest, + runtimeFieldMap: searchStrategyParams.runtimeFieldMap, + intervalMs: searchStrategyParams.intervalMs, + query: { + bool: { + filter: filterCriteria, + }, + }, + maxExamples: MAX_EXAMPLES_DEFAULT, + }; + const searchOptions: ISearchOptions = { + abortSignal: abortCtrl.current.signal, + sessionId: searchStrategyParams?.sessionId, + }; + + const batches = createBatchedRequests( + sortedConfigs.map((config, idx) => ({ + fieldName: config.fieldName, + type: config.type, + cardinality: config.cardinality, + safeFieldName: getSafeAggregationName(config.fieldName, idx), + })), + 10 + ); + + const statsMap$ = new Subject(); + const fieldsToRetry$ = new Subject(); + + const fieldStatsSub = combineLatest( + batches + .map((batch) => getFieldsStats(data.search, params, batch, searchOptions)) + .filter((obs) => obs !== undefined) as Array> + ); + const onError = (error: any) => { + toasts.addError(error, { + title: i18n.translate('xpack.dataVisualizer.index.errorFetchingFieldStatisticsMessage', { + defaultMessage: 'Error fetching field statistics', + }), + }); + setFetchState({ + isRunning: false, + error, + }); + }; + + const onComplete = () => { + setFetchState({ + isRunning: false, + }); + }; + + // First, attempt to fetch field stats in batches of 10 + searchSubscription$.current = fieldStatsSub.subscribe({ + next: (resp) => { + if (resp) { + const statsMap = new Map(); + const failedFields: Field[] = []; + resp.forEach((batchResponse) => { + if (Array.isArray(batchResponse)) { + batchResponse.forEach((f) => { + if (f.fieldName !== undefined) { + statsMap.set(f.fieldName, f); + } + }); + } else { + // If an error occurred during batch + // retry each field in the failed batch individually + failedFields.push(...(batchResponse.fields ?? [])); + } + }); + + setFetchState({ + loaded: (statsMap.size / sortedConfigs.length) * 100, + isRunning: true, + }); + + setFieldStats(statsMap); + + if (failedFields.length > 0) { + statsMap$.next(statsMap); + fieldsToRetry$.next(failedFields); + } + } + }, + error: onError, + complete: onComplete, + }); + + // If any of batches failed, retry each of the failed field at least one time individually + retries$.current = combineLatest([ + statsMap$, + fieldsToRetry$.pipe( + switchMap((failedFields) => { + return combineLatest( + failedFields + .map((failedField) => + getFieldsStats(data.search, params, [failedField], searchOptions) + ) + .filter((obs) => obs !== undefined) + ); + }) + ), + ]).subscribe({ + next: (resp) => { + const statsMap = cloneDeep(resp[0]) as Map; + const fieldBatches = resp[1]; + + if (Array.isArray(fieldBatches)) { + fieldBatches.forEach((f) => { + if (Array.isArray(f) && f.length === 1) { + statsMap.set(f[0].fieldName, f[0]); + } + }); + setFieldStats(statsMap); + setFetchState({ + loaded: (statsMap.size / sortedConfigs.length) * 100, + isRunning: true, + }); + } + }, + error: onError, + complete: onComplete, + }); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [data.search, toasts, fieldStatsParams, initialDataVisualizerListState]); + + const cancelFetch = useCallback(() => { + searchSubscription$.current?.unsubscribe(); + searchSubscription$.current = undefined; + + retries$.current?.unsubscribe(); + retries$.current = undefined; + + abortCtrl.current.abort(); + setFetchState({ + isRunning: false, + }); + }, []); + + // auto-update + useEffect(() => { + startFetch(); + return cancelFetch; + }, [startFetch, cancelFetch]); + + return { + progress: fetchState, + fieldStats, + startFetch, + cancelFetch, + }; +} diff --git a/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/hooks/use_overall_stats.ts b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/hooks/use_overall_stats.ts new file mode 100644 index 0000000000000..92a95bfacea42 --- /dev/null +++ b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/hooks/use_overall_stats.ts @@ -0,0 +1,267 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { useCallback, useEffect, useState, useRef, useMemo, useReducer } from 'react'; +import { forkJoin, of, Subscription } from 'rxjs'; +import { switchMap } from 'rxjs/operators'; +import { i18n } from '@kbn/i18n'; +import type { ToastsStart } from 'kibana/public'; +import { chunk } from 'lodash'; +import { useDataVisualizerKibana } from '../../kibana_context'; +import { + AggregatableFieldOverallStats, + checkAggregatableFieldsExistRequest, + checkNonAggregatableFieldExistsRequest, + processAggregatableFieldsExistResponse, + processNonAggregatableFieldsExistResponse, +} from '../search_strategy/requests/overall_stats'; +import type { + IKibanaSearchRequest, + IKibanaSearchResponse, + ISearchOptions, +} from '../../../../../../../src/plugins/data/common'; +import type { OverallStats } from '../types/overall_stats'; +import { getDefaultPageState } from '../components/index_data_visualizer_view/index_data_visualizer_view'; +import { extractErrorProperties } from '../utils/error_utils'; +import type { + DataStatsFetchProgress, + OverallStatsSearchStrategyParams, +} from '../../../../common/types/field_stats'; +import { + getDocumentCountStatsRequest, + processDocumentCountStats, +} from '../search_strategy/requests/get_document_stats'; +import { getInitialProgress, getReducer } from '../progress_utils'; + +function displayError(toastNotifications: ToastsStart, indexPattern: string, err: any) { + if (err.statusCode === 500) { + toastNotifications.addError(err, { + title: i18n.translate('xpack.dataVisualizer.index.dataLoader.internalServerErrorMessage', { + defaultMessage: + 'Error loading data in index {index}. {message}. ' + + 'The request may have timed out. Try using a smaller sample size or narrowing the time range.', + values: { + index: indexPattern, + message: err.error ?? err.message, + }, + }), + }); + } else { + toastNotifications.addError(err, { + title: i18n.translate('xpack.dataVisualizer.index.errorLoadingDataMessage', { + defaultMessage: 'Error loading data in index {index}. {message}.', + values: { + index: indexPattern, + message: err.error ?? err.message, + }, + }), + }); + } +} + +export function useOverallStats( + searchStrategyParams: TParams | undefined, + lastRefresh: number +): { + progress: DataStatsFetchProgress; + overallStats: OverallStats; +} { + const { + services: { + data, + notifications: { toasts }, + }, + } = useDataVisualizerKibana(); + + const [stats, setOverallStats] = useState(getDefaultPageState().overallStats); + const [fetchState, setFetchState] = useReducer( + getReducer(), + getInitialProgress() + ); + + const abortCtrl = useRef(new AbortController()); + const searchSubscription$ = useRef(); + + const startFetch = useCallback(() => { + searchSubscription$.current?.unsubscribe(); + abortCtrl.current.abort(); + abortCtrl.current = new AbortController(); + + if (!searchStrategyParams || lastRefresh === 0) return; + + setFetchState({ + ...getInitialProgress(), + error: undefined, + }); + + const { + aggregatableFields, + nonAggregatableFields, + index, + searchQuery, + timeFieldName, + earliest, + latest, + intervalMs, + runtimeFieldMap, + samplerShardSize, + } = searchStrategyParams; + + const searchOptions: ISearchOptions = { + abortSignal: abortCtrl.current.signal, + sessionId: searchStrategyParams?.sessionId, + }; + const nonAggregatableOverallStats$ = + nonAggregatableFields.length > 0 + ? forkJoin( + nonAggregatableFields.map((fieldName: string) => + data.search + .search( + { + params: checkNonAggregatableFieldExistsRequest( + index, + searchQuery, + fieldName, + timeFieldName, + earliest, + latest, + runtimeFieldMap + ), + }, + searchOptions + ) + .pipe( + switchMap((resp) => { + return of({ + ...resp, + rawResponse: { ...resp.rawResponse, fieldName }, + } as IKibanaSearchResponse); + }) + ) + ) + ) + : of(undefined); + + // Have to divide into smaller requests to avoid 413 payload too large + const aggregatableFieldsChunks = chunk(aggregatableFields, 30); + + const aggregatableOverallStats$ = forkJoin( + aggregatableFields.length > 0 + ? aggregatableFieldsChunks.map((aggregatableFieldsChunk) => + data.search + .search( + { + params: checkAggregatableFieldsExistRequest( + index, + searchQuery, + aggregatableFieldsChunk, + samplerShardSize, + timeFieldName, + earliest, + latest, + undefined, + runtimeFieldMap + ), + }, + searchOptions + ) + .pipe( + switchMap((resp) => { + return of({ + ...resp, + aggregatableFields: aggregatableFieldsChunk, + } as AggregatableFieldOverallStats); + }) + ) + ) + : of(undefined) + ); + + const documentCountStats$ = + timeFieldName !== undefined && intervalMs !== undefined && intervalMs > 0 + ? data.search.search( + { + params: getDocumentCountStatsRequest(searchStrategyParams), + }, + searchOptions + ) + : of(undefined); + const sub = forkJoin({ + documentCountStatsResp: documentCountStats$, + nonAggregatableOverallStatsResp: nonAggregatableOverallStats$, + aggregatableOverallStatsResp: aggregatableOverallStats$, + }).pipe( + switchMap( + ({ + documentCountStatsResp, + nonAggregatableOverallStatsResp, + aggregatableOverallStatsResp, + }) => { + const aggregatableOverallStats = processAggregatableFieldsExistResponse( + aggregatableOverallStatsResp, + aggregatableFields, + samplerShardSize + ); + const nonAggregatableOverallStats = processNonAggregatableFieldsExistResponse( + nonAggregatableOverallStatsResp, + nonAggregatableFields + ); + + return of({ + documentCountStats: processDocumentCountStats( + documentCountStatsResp?.rawResponse, + searchStrategyParams + ), + ...nonAggregatableOverallStats, + ...aggregatableOverallStats, + }); + } + ) + ); + + searchSubscription$.current = sub.subscribe({ + next: (overallStats) => { + if (overallStats) { + setOverallStats(overallStats); + } + }, + error: (error) => { + displayError(toasts, searchStrategyParams.index, extractErrorProperties(error)); + setFetchState({ + isRunning: false, + error, + }); + }, + complete: () => { + setFetchState({ + loaded: 100, + isRunning: false, + }); + }, + }); + }, [data.search, searchStrategyParams, toasts, lastRefresh]); + + const cancelFetch = useCallback(() => { + searchSubscription$.current?.unsubscribe(); + searchSubscription$.current = undefined; + abortCtrl.current.abort(); + }, []); + + // auto-update + useEffect(() => { + startFetch(); + return cancelFetch; + }, [startFetch, cancelFetch]); + + return useMemo( + () => ({ + progress: fetchState, + overallStats: stats, + }), + [stats, fetchState] + ); +} diff --git a/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/progress_utils.ts b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/progress_utils.ts new file mode 100644 index 0000000000000..f329fe47e75b0 --- /dev/null +++ b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/progress_utils.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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { DataStatsFetchProgress } from '../../../common/types/field_stats'; + +export const getInitialProgress = (): DataStatsFetchProgress => ({ + isRunning: false, + loaded: 0, + total: 100, +}); + +export const getReducer = + () => + (prev: T, update: Partial): T => ({ + ...prev, + ...update, + }); diff --git a/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/search_strategy/requests/constants.ts b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/search_strategy/requests/constants.ts new file mode 100644 index 0000000000000..6da11fd850acc --- /dev/null +++ b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/search_strategy/requests/constants.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. + */ + +export const SAMPLER_TOP_TERMS_THRESHOLD = 100000; +export const SAMPLER_TOP_TERMS_SHARD_SIZE = 5000; +export const AGGREGATABLE_EXISTS_REQUEST_BATCH_SIZE = 200; +export const FIELDS_REQUEST_BATCH_SIZE = 10; + +export const MAX_CHART_COLUMNS = 20; + +export const MAX_EXAMPLES_DEFAULT = 10; +export const MAX_PERCENT = 100; +export const PERCENTILE_SPACING = 5; diff --git a/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/search_strategy/requests/get_boolean_field_stats.ts b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/search_strategy/requests/get_boolean_field_stats.ts new file mode 100644 index 0000000000000..b5359915ef63e --- /dev/null +++ b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/search_strategy/requests/get_boolean_field_stats.ts @@ -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 { get } from 'lodash'; +import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; +import { Observable, of } from 'rxjs'; +import { catchError, map } from 'rxjs/operators'; +import { + buildSamplerAggregation, + getSamplerAggregationsResponsePath, +} from '../../../../../common/utils/query_utils'; +import { isPopulatedObject } from '../../../../../common/utils/object_utils'; +import type { + Field, + BooleanFieldStats, + Aggs, + FieldStatsCommonRequestParams, +} from '../../../../../common/types/field_stats'; +import { FieldStatsError, isIKibanaSearchResponse } from '../../../../../common/types/field_stats'; +import type { + IKibanaSearchRequest, + IKibanaSearchResponse, + ISearchOptions, + ISearchStart, +} from '../../../../../../../../src/plugins/data/public'; +import { extractErrorProperties } from '../../utils/error_utils'; + +export const getBooleanFieldsStatsRequest = ( + params: FieldStatsCommonRequestParams, + fields: Field[] +) => { + const { index, query, runtimeFieldMap, samplerShardSize } = params; + + const size = 0; + const aggs: Aggs = {}; + fields.forEach((field, i) => { + const safeFieldName = field.safeFieldName; + aggs[`${safeFieldName}_value_count`] = { + filter: { exists: { field: field.fieldName } }, + }; + aggs[`${safeFieldName}_values`] = { + terms: { + field: field.fieldName, + size: 2, + }, + }; + }); + const searchBody = { + query, + aggs: buildSamplerAggregation(aggs, samplerShardSize), + ...(isPopulatedObject(runtimeFieldMap) ? { runtime_mappings: runtimeFieldMap } : {}), + }; + + return { + index, + size, + body: searchBody, + }; +}; + +export const fetchBooleanFieldsStats = ( + dataSearch: ISearchStart, + params: FieldStatsCommonRequestParams, + fields: Field[], + options: ISearchOptions +): Observable => { + const { samplerShardSize } = params; + const request: estypes.SearchRequest = getBooleanFieldsStatsRequest(params, fields); + return dataSearch + .search({ params: request }, options) + .pipe( + catchError((e) => + of({ + fields, + error: extractErrorProperties(e), + } as FieldStatsError) + ), + map((resp) => { + if (!isIKibanaSearchResponse(resp)) return resp; + + const aggregations = resp.rawResponse.aggregations; + const aggsPath = getSamplerAggregationsResponsePath(samplerShardSize); + + const batchStats: BooleanFieldStats[] = fields.map((field, i) => { + const safeFieldName = field.fieldName; + const stats: BooleanFieldStats = { + fieldName: field.fieldName, + count: get(aggregations, [...aggsPath, `${safeFieldName}_value_count`, 'doc_count'], 0), + trueCount: 0, + falseCount: 0, + }; + + const valueBuckets: Array<{ [key: string]: number }> = get( + aggregations, + [...aggsPath, `${safeFieldName}_values`, 'buckets'], + [] + ); + valueBuckets.forEach((bucket) => { + stats[`${bucket.key_as_string}Count`] = bucket.doc_count; + }); + return stats; + }); + + return batchStats; + }) + ); +}; diff --git a/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/search_strategy/requests/get_date_field_stats.ts b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/search_strategy/requests/get_date_field_stats.ts new file mode 100644 index 0000000000000..07bdc8c14301c --- /dev/null +++ b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/search_strategy/requests/get_date_field_stats.ts @@ -0,0 +1,106 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; +import { get } from 'lodash'; +import { Observable, of } from 'rxjs'; +import { catchError, map } from 'rxjs/operators'; +import { + buildSamplerAggregation, + getSamplerAggregationsResponsePath, +} from '../../../../../common/utils/query_utils'; +import { isPopulatedObject } from '../../../../../common/utils/object_utils'; +import type { FieldStatsCommonRequestParams } from '../../../../../common/types/field_stats'; +import type { Field, DateFieldStats, Aggs } from '../../../../../common/types/field_stats'; +import type { + IKibanaSearchRequest, + IKibanaSearchResponse, + ISearchOptions, + ISearchStart, +} from '../../../../../../../../src/plugins/data/public'; +import { FieldStatsError, isIKibanaSearchResponse } from '../../../../../common/types/field_stats'; +import { extractErrorProperties } from '../../utils/error_utils'; + +export const getDateFieldsStatsRequest = ( + params: FieldStatsCommonRequestParams, + fields: Field[] +) => { + const { index, query, runtimeFieldMap, samplerShardSize } = params; + + const size = 0; + + const aggs: Aggs = {}; + fields.forEach((field, i) => { + const safeFieldName = field.safeFieldName; + aggs[`${safeFieldName}_field_stats`] = { + filter: { exists: { field: field.fieldName } }, + aggs: { + actual_stats: { + stats: { field: field.fieldName }, + }, + }, + }; + }); + + const searchBody = { + query, + aggs: buildSamplerAggregation(aggs, samplerShardSize), + ...(isPopulatedObject(runtimeFieldMap) ? { runtime_mappings: runtimeFieldMap } : {}), + }; + return { + index, + size, + body: searchBody, + }; +}; + +export const fetchDateFieldsStats = ( + dataSearch: ISearchStart, + params: FieldStatsCommonRequestParams, + fields: Field[], + options: ISearchOptions +): Observable => { + const { samplerShardSize } = params; + + const request: estypes.SearchRequest = getDateFieldsStatsRequest(params, fields); + return dataSearch + .search({ params: request }, options) + .pipe( + catchError((e) => + of({ + fields, + error: extractErrorProperties(e), + } as FieldStatsError) + ), + map((resp) => { + if (!isIKibanaSearchResponse(resp)) return resp; + const aggregations = resp.rawResponse.aggregations; + const aggsPath = getSamplerAggregationsResponsePath(samplerShardSize); + + const batchStats: DateFieldStats[] = fields.map((field, i) => { + const safeFieldName = field.safeFieldName; + const docCount = get( + aggregations, + [...aggsPath, `${safeFieldName}_field_stats`, 'doc_count'], + 0 + ); + const fieldStatsResp = get( + aggregations, + [...aggsPath, `${safeFieldName}_field_stats`, 'actual_stats'], + {} + ); + return { + fieldName: field.fieldName, + count: docCount, + earliest: get(fieldStatsResp, 'min', 0), + latest: get(fieldStatsResp, 'max', 0), + } as DateFieldStats; + }); + return batchStats; + }) + ); +}; diff --git a/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/search_strategy/requests/get_document_stats.ts b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/search_strategy/requests/get_document_stats.ts new file mode 100644 index 0000000000000..cdd69f5d3a369 --- /dev/null +++ b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/search_strategy/requests/get_document_stats.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 { each, get } from 'lodash'; +import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; +import { buildBaseFilterCriteria } from '../../../../../common/utils/query_utils'; +import { isPopulatedObject } from '../../../../../common/utils/object_utils'; +import type { + DocumentCountStats, + OverallStatsSearchStrategyParams, +} from '../../../../../common/types/field_stats'; + +export const getDocumentCountStatsRequest = (params: OverallStatsSearchStrategyParams) => { + const { + index, + timeFieldName, + earliest: earliestMs, + latest: latestMs, + runtimeFieldMap, + searchQuery, + intervalMs, + } = params; + + const size = 0; + const filterCriteria = buildBaseFilterCriteria(timeFieldName, earliestMs, latestMs, searchQuery); + + // Don't use the sampler aggregation as this can lead to some potentially + // confusing date histogram results depending on the date range of data amongst shards. + + const aggs = { + eventRate: { + date_histogram: { + field: timeFieldName, + fixed_interval: `${intervalMs}ms`, + min_doc_count: 1, + }, + }, + }; + + const searchBody = { + query: { + bool: { + filter: filterCriteria, + }, + }, + aggs, + ...(isPopulatedObject(runtimeFieldMap) ? { runtime_mappings: runtimeFieldMap } : {}), + }; + return { + index, + size, + body: searchBody, + }; +}; + +export const processDocumentCountStats = ( + body: estypes.SearchResponse | undefined, + params: OverallStatsSearchStrategyParams +): DocumentCountStats | undefined => { + if ( + !body || + params.intervalMs === undefined || + params.earliest === undefined || + params.latest === undefined + ) { + return undefined; + } + const buckets: { [key: string]: number } = {}; + const dataByTimeBucket: Array<{ key: string; doc_count: number }> = get( + body, + ['aggregations', 'eventRate', 'buckets'], + [] + ); + each(dataByTimeBucket, (dataForTime) => { + const time = dataForTime.key; + buckets[time] = dataForTime.doc_count; + }); + + return { + interval: params.intervalMs, + buckets, + timeRangeEarliest: params.earliest, + timeRangeLatest: params.latest, + }; +}; diff --git a/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/search_strategy/requests/get_field_examples.ts b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/search_strategy/requests/get_field_examples.ts new file mode 100644 index 0000000000000..618e47bb97e1d --- /dev/null +++ b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/search_strategy/requests/get_field_examples.ts @@ -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 type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; +import { get } from 'lodash'; +import { combineLatest, of } from 'rxjs'; +import { catchError, map } from 'rxjs/operators'; +import { buildBaseFilterCriteria } from '../../../../../common/utils/query_utils'; +import { isPopulatedObject } from '../../../../../common/utils/object_utils'; +import type { + Field, + FieldExamples, + FieldStatsCommonRequestParams, +} from '../../../../../common/types/field_stats'; +import type { + IKibanaSearchRequest, + IKibanaSearchResponse, + ISearchOptions, + ISearchStart, +} from '../../../../../../../../src/plugins/data/public'; +import { FieldStatsError, isIKibanaSearchResponse } from '../../../../../common/types/field_stats'; +import { extractErrorProperties } from '../../utils/error_utils'; +import { MAX_EXAMPLES_DEFAULT } from './constants'; + +export const getFieldExamplesRequest = (params: FieldStatsCommonRequestParams, field: Field) => { + const { index, timeFieldName, earliestMs, latestMs, query, runtimeFieldMap, maxExamples } = + params; + + // Request at least 100 docs so that we have a chance of obtaining + // 'maxExamples' of the field. + const size = Math.max(100, maxExamples ?? MAX_EXAMPLES_DEFAULT); + const filterCriteria = buildBaseFilterCriteria(timeFieldName, earliestMs, latestMs, query); + + // Use an exists filter to return examples of the field. + if (Array.isArray(filterCriteria)) { + filterCriteria.push({ + exists: { field: field.fieldName }, + }); + } + + const searchBody = { + fields: [field.fieldName], + _source: false, + query: { + bool: { + filter: filterCriteria, + }, + }, + ...(isPopulatedObject(runtimeFieldMap) ? { runtime_mappings: runtimeFieldMap } : {}), + }; + + return { + index, + size, + body: searchBody, + }; +}; + +export const fetchFieldsExamples = ( + dataSearch: ISearchStart, + params: FieldStatsCommonRequestParams, + fields: Field[], + options: ISearchOptions +) => { + const { maxExamples } = params; + return combineLatest( + fields.map((field) => { + const request: estypes.SearchRequest = getFieldExamplesRequest(params, field); + + return dataSearch + .search({ params: request }, options) + .pipe( + catchError((e) => + of({ + fieldName: field.fieldName, + fields, + error: extractErrorProperties(e), + } as FieldStatsError) + ), + map((resp) => { + if (!isIKibanaSearchResponse(resp)) return resp; + const body = resp.rawResponse; + const stats = { + fieldName: field.fieldName, + examples: [] as unknown[], + } as FieldExamples; + + if (body.hits.total > 0) { + const hits = body.hits.hits; + for (let i = 0; i < hits.length; i++) { + // Use lodash get() to support field names containing dots. + const doc: object[] | undefined = get(hits[i].fields, field.fieldName); + // the results from fields query is always an array + if (Array.isArray(doc) && doc.length > 0) { + const example = doc[0]; + if (example !== undefined && stats.examples.indexOf(example) === -1) { + stats.examples.push(example); + if (stats.examples.length === maxExamples) { + break; + } + } + } + } + } + + return stats; + }) + ); + }) + ); +}; diff --git a/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/search_strategy/requests/get_fields_stats.ts b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/search_strategy/requests/get_fields_stats.ts new file mode 100644 index 0000000000000..aa19aa9fbb495 --- /dev/null +++ b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/search_strategy/requests/get_fields_stats.ts @@ -0,0 +1,50 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { Observable } from 'rxjs'; +import type { FieldStatsCommonRequestParams } from '../../../../../common/types/field_stats'; +import type { FieldStatsError } from '../../../../../common/types/field_stats'; +import type { ISearchOptions } from '../../../../../../../../src/plugins/data/common'; +import { ISearchStart } from '../../../../../../../../src/plugins/data/public'; +import type { FieldStats } from '../../../../../common/types/field_stats'; +import { JOB_FIELD_TYPES } from '../../../../../common'; +import { fetchDateFieldsStats } from './get_date_field_stats'; +import { fetchBooleanFieldsStats } from './get_boolean_field_stats'; +import { fetchFieldsExamples } from './get_field_examples'; +import { fetchNumericFieldsStats } from './get_numeric_field_stats'; +import { fetchStringFieldsStats } from './get_string_field_stats'; + +export const getFieldsStats = ( + dataSearch: ISearchStart, + params: FieldStatsCommonRequestParams, + fields: Array<{ + fieldName: string; + type: string; + cardinality: number; + safeFieldName: string; + }>, + options: ISearchOptions +): Observable | undefined => { + const fieldType = fields[0].type; + switch (fieldType) { + case JOB_FIELD_TYPES.NUMBER: + return fetchNumericFieldsStats(dataSearch, params, fields, options); + case JOB_FIELD_TYPES.KEYWORD: + case JOB_FIELD_TYPES.IP: + return fetchStringFieldsStats(dataSearch, params, fields, options); + case JOB_FIELD_TYPES.DATE: + return fetchDateFieldsStats(dataSearch, params, fields, options); + case JOB_FIELD_TYPES.BOOLEAN: + return fetchBooleanFieldsStats(dataSearch, params, fields, options); + case JOB_FIELD_TYPES.TEXT: + return fetchFieldsExamples(dataSearch, params, fields, options); + default: + // Use an exists filter on the the field name to get + // examples of the field, so cannot batch up. + return fetchFieldsExamples(dataSearch, params, fields, options); + } +}; diff --git a/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/search_strategy/requests/get_numeric_field_stats.ts b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/search_strategy/requests/get_numeric_field_stats.ts new file mode 100644 index 0000000000000..89ae7598b30fd --- /dev/null +++ b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/search_strategy/requests/get_numeric_field_stats.ts @@ -0,0 +1,207 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license 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 * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; +import { find, get } from 'lodash'; +import { catchError, map } from 'rxjs/operators'; +import { Observable, of } from 'rxjs'; +import { AggregationsTermsAggregation } from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; +import { + MAX_PERCENT, + PERCENTILE_SPACING, + SAMPLER_TOP_TERMS_SHARD_SIZE, + SAMPLER_TOP_TERMS_THRESHOLD, +} from './constants'; +import { + buildSamplerAggregation, + getSamplerAggregationsResponsePath, +} from '../../../../../common/utils/query_utils'; +import { isPopulatedObject } from '../../../../../common/utils/object_utils'; +import type { Aggs, FieldStatsCommonRequestParams } from '../../../../../common/types/field_stats'; +import type { + Field, + NumericFieldStats, + Bucket, + FieldStatsError, +} from '../../../../../common/types/field_stats'; +import { processDistributionData } from '../../utils/process_distribution_data'; +import { + IKibanaSearchRequest, + IKibanaSearchResponse, + ISearchOptions, +} from '../../../../../../../../src/plugins/data/common'; +import type { ISearchStart } from '../../../../../../../../src/plugins/data/public'; +import { extractErrorProperties } from '../../utils/error_utils'; +import { isIKibanaSearchResponse } from '../../../../../common/types/field_stats'; + +export const getNumericFieldsStatsRequest = ( + params: FieldStatsCommonRequestParams, + fields: Field[] +) => { + const { index, query, runtimeFieldMap, samplerShardSize } = params; + + const size = 0; + + // Build the percents parameter which defines the percentiles to query + // for the metric distribution data. + // Use a fixed percentile spacing of 5%. + let count = 0; + const percents = Array.from( + Array(MAX_PERCENT / PERCENTILE_SPACING), + () => (count += PERCENTILE_SPACING) + ); + + const aggs: Aggs = {}; + + fields.forEach((field, i) => { + const { safeFieldName } = field; + + aggs[`${safeFieldName}_field_stats`] = { + filter: { exists: { field: field.fieldName } }, + aggs: { + actual_stats: { + stats: { field: field.fieldName }, + }, + }, + }; + aggs[`${safeFieldName}_percentiles`] = { + percentiles: { + field: field.fieldName, + percents, + keyed: false, + }, + }; + + const top = { + terms: { + field: field.fieldName, + size: 10, + order: { + _count: 'desc', + }, + } as AggregationsTermsAggregation, + }; + + // If cardinality >= SAMPLE_TOP_TERMS_THRESHOLD, run the top terms aggregation + // in a sampler aggregation, even if no sampling has been specified (samplerShardSize < 1). + if (samplerShardSize < 1 && field.cardinality >= SAMPLER_TOP_TERMS_THRESHOLD) { + aggs[`${safeFieldName}_top`] = { + sampler: { + shard_size: SAMPLER_TOP_TERMS_SHARD_SIZE, + }, + aggs: { + top, + }, + }; + } else { + aggs[`${safeFieldName}_top`] = top; + } + }); + + const searchBody = { + query, + aggs: buildSamplerAggregation(aggs, samplerShardSize), + ...(isPopulatedObject(runtimeFieldMap) ? { runtime_mappings: runtimeFieldMap } : {}), + }; + + return { + index, + size, + body: searchBody, + }; +}; + +export const fetchNumericFieldsStats = ( + dataSearch: ISearchStart, + params: FieldStatsCommonRequestParams, + fields: Field[], + options: ISearchOptions +): Observable => { + const { samplerShardSize } = params; + const request: estypes.SearchRequest = getNumericFieldsStatsRequest(params, fields); + + return dataSearch + .search({ params: request }, options) + .pipe( + catchError((e) => { + // @todo: kick off another requests individually + return of({ + fields, + error: extractErrorProperties(e), + } as FieldStatsError); + }), + map((resp) => { + if (!isIKibanaSearchResponse(resp)) return resp; + + const aggregations = resp.rawResponse.aggregations; + const aggsPath = getSamplerAggregationsResponsePath(samplerShardSize); + + const batchStats: NumericFieldStats[] = []; + + fields.forEach((field, i) => { + const safeFieldName = field.safeFieldName; + const docCount = get( + aggregations, + [...aggsPath, `${safeFieldName}_field_stats`, 'doc_count'], + 0 + ); + const fieldStatsResp = get( + aggregations, + [...aggsPath, `${safeFieldName}_field_stats`, 'actual_stats'], + {} + ); + + const topAggsPath = [...aggsPath, `${safeFieldName}_top`]; + if (samplerShardSize < 1 && field.cardinality >= SAMPLER_TOP_TERMS_THRESHOLD) { + topAggsPath.push('top'); + } + + const topValues: Bucket[] = get(aggregations, [...topAggsPath, 'buckets'], []); + + const stats: NumericFieldStats = { + fieldName: field.fieldName, + count: docCount, + min: get(fieldStatsResp, 'min', 0), + max: get(fieldStatsResp, 'max', 0), + avg: get(fieldStatsResp, 'avg', 0), + isTopValuesSampled: + field.cardinality >= SAMPLER_TOP_TERMS_THRESHOLD || samplerShardSize > 0, + topValues, + topValuesSampleSize: topValues.reduce( + (acc, curr) => acc + curr.doc_count, + get(aggregations, [...topAggsPath, 'sum_other_doc_count'], 0) + ), + topValuesSamplerShardSize: + field.cardinality >= SAMPLER_TOP_TERMS_THRESHOLD + ? SAMPLER_TOP_TERMS_SHARD_SIZE + : samplerShardSize, + }; + + if (stats.count > 0) { + const percentiles = get( + aggregations, + [...aggsPath, `${safeFieldName}_percentiles`, 'values'], + [] + ); + const medianPercentile: { value: number; key: number } | undefined = find(percentiles, { + key: 50, + }); + stats.median = medianPercentile !== undefined ? medianPercentile!.value : 0; + stats.distribution = processDistributionData( + percentiles, + PERCENTILE_SPACING, + stats.min + ); + } + + batchStats.push(stats); + }); + + return batchStats; + }) + ); +}; diff --git a/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/search_strategy/requests/get_string_field_stats.ts b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/search_strategy/requests/get_string_field_stats.ts new file mode 100644 index 0000000000000..024464c1947c8 --- /dev/null +++ b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/search_strategy/requests/get_string_field_stats.ts @@ -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 type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; +import { get } from 'lodash'; +import { Observable, of } from 'rxjs'; +import { catchError, map } from 'rxjs/operators'; +import { AggregationsTermsAggregation } from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; +import { SAMPLER_TOP_TERMS_SHARD_SIZE, SAMPLER_TOP_TERMS_THRESHOLD } from './constants'; +import { + buildSamplerAggregation, + getSamplerAggregationsResponsePath, +} from '../../../../../common/utils/query_utils'; +import { isPopulatedObject } from '../../../../../common/utils/object_utils'; +import type { + Aggs, + Bucket, + Field, + FieldStatsCommonRequestParams, + StringFieldStats, +} from '../../../../../common/types/field_stats'; +import type { + IKibanaSearchRequest, + IKibanaSearchResponse, + ISearchOptions, + ISearchStart, +} from '../../../../../../../../src/plugins/data/public'; +import { FieldStatsError, isIKibanaSearchResponse } from '../../../../../common/types/field_stats'; +import { extractErrorProperties } from '../../utils/error_utils'; + +export const getStringFieldStatsRequest = ( + params: FieldStatsCommonRequestParams, + fields: Field[] +) => { + const { index, query, runtimeFieldMap, samplerShardSize } = params; + + const size = 0; + + const aggs: Aggs = {}; + fields.forEach((field, i) => { + const safeFieldName = field.safeFieldName; + const top = { + terms: { + field: field.fieldName, + size: 10, + order: { + _count: 'desc', + }, + } as AggregationsTermsAggregation, + }; + + // If cardinality >= SAMPLE_TOP_TERMS_THRESHOLD, run the top terms aggregation + // in a sampler aggregation, even if no sampling has been specified (samplerShardSize < 1). + if (samplerShardSize < 1 && field.cardinality >= SAMPLER_TOP_TERMS_THRESHOLD) { + aggs[`${safeFieldName}_top`] = { + sampler: { + shard_size: SAMPLER_TOP_TERMS_SHARD_SIZE, + }, + aggs: { + top, + }, + }; + } else { + aggs[`${safeFieldName}_top`] = top; + } + }); + + const searchBody = { + query, + aggs: buildSamplerAggregation(aggs, samplerShardSize), + ...(isPopulatedObject(runtimeFieldMap) ? { runtime_mappings: runtimeFieldMap } : {}), + }; + + return { + index, + size, + body: searchBody, + }; +}; + +export const fetchStringFieldsStats = ( + dataSearch: ISearchStart, + params: FieldStatsCommonRequestParams, + fields: Field[], + options: ISearchOptions +): Observable => { + const { samplerShardSize } = params; + const request: estypes.SearchRequest = getStringFieldStatsRequest(params, fields); + + return dataSearch + .search({ params: request }, options) + .pipe( + catchError((e) => + of({ + fields, + error: extractErrorProperties(e), + } as FieldStatsError) + ), + map((resp) => { + if (!isIKibanaSearchResponse(resp)) return resp; + const aggregations = resp.rawResponse.aggregations; + const aggsPath = getSamplerAggregationsResponsePath(samplerShardSize); + const batchStats: StringFieldStats[] = []; + + fields.forEach((field, i) => { + const safeFieldName = field.safeFieldName; + + const topAggsPath = [...aggsPath, `${safeFieldName}_top`]; + if (samplerShardSize < 1 && field.cardinality >= SAMPLER_TOP_TERMS_THRESHOLD) { + topAggsPath.push('top'); + } + + const topValues: Bucket[] = get(aggregations, [...topAggsPath, 'buckets'], []); + + const stats = { + fieldName: field.fieldName, + isTopValuesSampled: + field.cardinality >= SAMPLER_TOP_TERMS_THRESHOLD || samplerShardSize > 0, + topValues, + topValuesSampleSize: topValues.reduce( + (acc, curr) => acc + curr.doc_count, + get(aggregations, [...topAggsPath, 'sum_other_doc_count'], 0) + ), + topValuesSamplerShardSize: + field.cardinality >= SAMPLER_TOP_TERMS_THRESHOLD + ? SAMPLER_TOP_TERMS_SHARD_SIZE + : samplerShardSize, + }; + + batchStats.push(stats); + }); + + return batchStats; + }) + ); +}; diff --git a/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/search_strategy/requests/overall_stats.ts b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/search_strategy/requests/overall_stats.ts new file mode 100644 index 0000000000000..fb392cc17b05b --- /dev/null +++ b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/search_strategy/requests/overall_stats.ts @@ -0,0 +1,227 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license 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 * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; +import { get } from 'lodash'; +import { Query } from '@kbn/es-query'; +import { + buildBaseFilterCriteria, + buildSamplerAggregation, + getSafeAggregationName, + getSamplerAggregationsResponsePath, +} from '../../../../../common/utils/query_utils'; +import { getDatafeedAggregations } from '../../../../../common/utils/datafeed_utils'; +import { isPopulatedObject } from '../../../../../common/utils/object_utils'; +import { IKibanaSearchResponse } from '../../../../../../../../src/plugins/data/common'; +import { AggregatableField, NonAggregatableField } from '../../types/overall_stats'; +import { AggCardinality, Aggs } from '../../../../../common/types/field_stats'; + +export const checkAggregatableFieldsExistRequest = ( + indexPatternTitle: string, + query: Query['query'], + aggregatableFields: string[], + samplerShardSize: number, + timeFieldName: string | undefined, + earliestMs?: number, + latestMs?: number, + datafeedConfig?: estypes.MlDatafeed, + runtimeMappings?: estypes.MappingRuntimeFields +): estypes.SearchRequest => { + const index = indexPatternTitle; + const size = 0; + const filterCriteria = buildBaseFilterCriteria(timeFieldName, earliestMs, latestMs, query); + const datafeedAggregations = getDatafeedAggregations(datafeedConfig); + + // Value count aggregation faster way of checking if field exists than using + // filter aggregation with exists query. + const aggs: Aggs = datafeedAggregations !== undefined ? { ...datafeedAggregations } : {}; + + // Combine runtime fields from the data view as well as the datafeed + const combinedRuntimeMappings: estypes.MappingRuntimeFields = { + ...(isPopulatedObject(runtimeMappings) ? runtimeMappings : {}), + ...(isPopulatedObject(datafeedConfig) && isPopulatedObject(datafeedConfig.runtime_mappings) + ? datafeedConfig.runtime_mappings + : {}), + }; + + aggregatableFields.forEach((field, i) => { + const safeFieldName = getSafeAggregationName(field, i); + aggs[`${safeFieldName}_count`] = { + filter: { exists: { field } }, + }; + + let cardinalityField: AggCardinality; + if (datafeedConfig?.script_fields?.hasOwnProperty(field)) { + cardinalityField = aggs[`${safeFieldName}_cardinality`] = { + cardinality: { script: datafeedConfig?.script_fields[field].script }, + }; + } else { + cardinalityField = { + cardinality: { field }, + }; + } + aggs[`${safeFieldName}_cardinality`] = cardinalityField; + }); + + const searchBody = { + query: { + bool: { + filter: filterCriteria, + }, + }, + ...(isPopulatedObject(aggs) ? { aggs: buildSamplerAggregation(aggs, samplerShardSize) } : {}), + ...(isPopulatedObject(combinedRuntimeMappings) + ? { runtime_mappings: combinedRuntimeMappings } + : {}), + }; + + return { + index, + track_total_hits: true, + size, + body: searchBody, + }; +}; + +export interface AggregatableFieldOverallStats extends IKibanaSearchResponse { + aggregatableFields: string[]; +} +export const processAggregatableFieldsExistResponse = ( + responses: AggregatableFieldOverallStats[] | undefined, + aggregatableFields: string[], + samplerShardSize: number, + datafeedConfig?: estypes.MlDatafeed +) => { + const stats = { + totalCount: 0, + aggregatableExistsFields: [] as AggregatableField[], + aggregatableNotExistsFields: [] as AggregatableField[], + }; + + if (!responses || aggregatableFields.length === 0) return stats; + + responses.forEach(({ rawResponse: body, aggregatableFields: aggregatableFieldsChunk }) => { + const aggregations = body.aggregations; + const totalCount = (body.hits.total as estypes.SearchTotalHits).value ?? body.hits.total; + stats.totalCount = totalCount as number; + + const aggsPath = getSamplerAggregationsResponsePath(samplerShardSize); + const sampleCount = + samplerShardSize > 0 ? get(aggregations, ['sample', 'doc_count'], 0) : totalCount; + aggregatableFieldsChunk.forEach((field, i) => { + const safeFieldName = getSafeAggregationName(field, i); + const count = get(aggregations, [...aggsPath, `${safeFieldName}_count`, 'doc_count'], 0); + if (count > 0) { + const cardinality = get( + aggregations, + [...aggsPath, `${safeFieldName}_cardinality`, 'value'], + 0 + ); + stats.aggregatableExistsFields.push({ + fieldName: field, + existsInDocs: true, + stats: { + sampleCount, + count, + cardinality, + }, + }); + } else { + if ( + datafeedConfig?.script_fields?.hasOwnProperty(field) || + datafeedConfig?.runtime_mappings?.hasOwnProperty(field) + ) { + const cardinality = get( + aggregations, + [...aggsPath, `${safeFieldName}_cardinality`, 'value'], + 0 + ); + stats.aggregatableExistsFields.push({ + fieldName: field, + existsInDocs: true, + stats: { + sampleCount, + count, + cardinality, + }, + }); + } else { + stats.aggregatableNotExistsFields.push({ + fieldName: field, + existsInDocs: false, + stats: {}, + }); + } + } + }); + }); + + return stats as { + totalCount: number; + aggregatableExistsFields: AggregatableField[]; + aggregatableNotExistsFields: AggregatableField[]; + }; +}; + +export const checkNonAggregatableFieldExistsRequest = ( + indexPatternTitle: string, + query: Query['query'], + field: string, + timeFieldName: string | undefined, + earliestMs: number | undefined, + latestMs: number | undefined, + runtimeMappings?: estypes.MappingRuntimeFields +): estypes.SearchRequest => { + const index = indexPatternTitle; + const size = 0; + const filterCriteria = buildBaseFilterCriteria(timeFieldName, earliestMs, latestMs, query); + + const searchBody = { + query: { + bool: { + filter: filterCriteria, + }, + }, + ...(isPopulatedObject(runtimeMappings) ? { runtime_mappings: runtimeMappings } : {}), + }; + if (Array.isArray(filterCriteria)) { + filterCriteria.push({ exists: { field } }); + } + + return { + index, + size, + body: searchBody, + }; +}; + +export const processNonAggregatableFieldsExistResponse = ( + results: IKibanaSearchResponse[] | undefined, + nonAggregatableFields: string[] +) => { + const stats = { + nonAggregatableExistsFields: [] as NonAggregatableField[], + nonAggregatableNotExistsFields: [] as NonAggregatableField[], + }; + + if (!results || nonAggregatableFields.length === 0) return stats; + + nonAggregatableFields.forEach((fieldName) => { + const foundField = results.find((r) => r.rawResponse.fieldName === fieldName); + const existsInDocs = foundField !== undefined && foundField.rawResponse.hits.total > 0; + const fieldData: NonAggregatableField = { + fieldName, + existsInDocs, + }; + if (existsInDocs === true) { + stats.nonAggregatableExistsFields.push(fieldData); + } else { + stats.nonAggregatableNotExistsFields.push(fieldData); + } + }); + return stats; +}; diff --git a/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/services/visualizer_stats.ts b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/services/visualizer_stats.ts deleted file mode 100644 index 3653936f3d12e..0000000000000 --- a/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/services/visualizer_stats.ts +++ /dev/null @@ -1,98 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; -import { lazyLoadModules } from '../../../lazy_load_bundle'; -import type { DocumentCounts, FieldRequestConfig, FieldVisStats } from '../../../../common/types'; -import { OverallStats } from '../types/overall_stats'; - -export function basePath() { - return '/internal/data_visualizer'; -} - -export async function getVisualizerOverallStats({ - indexPatternTitle, - query, - timeFieldName, - earliest, - latest, - samplerShardSize, - aggregatableFields, - nonAggregatableFields, - runtimeMappings, -}: { - indexPatternTitle: string; - query: any; - timeFieldName?: string; - earliest?: number; - latest?: number; - samplerShardSize?: number; - aggregatableFields: string[]; - nonAggregatableFields: string[]; - runtimeMappings?: estypes.MappingRuntimeFields; -}) { - const body = JSON.stringify({ - query, - timeFieldName, - earliest, - latest, - samplerShardSize, - aggregatableFields, - nonAggregatableFields, - runtimeMappings, - }); - - const fileUploadModules = await lazyLoadModules(); - return await fileUploadModules.getHttp().fetch({ - path: `${basePath()}/get_overall_stats/${indexPatternTitle}`, - method: 'POST', - body, - }); -} - -export async function getVisualizerFieldStats({ - indexPatternTitle, - query, - timeFieldName, - earliest, - latest, - samplerShardSize, - interval, - fields, - maxExamples, - runtimeMappings, -}: { - indexPatternTitle: string; - query: any; - timeFieldName?: string; - earliest?: number; - latest?: number; - samplerShardSize?: number; - interval?: number; - fields?: FieldRequestConfig[]; - maxExamples?: number; - runtimeMappings?: estypes.MappingRuntimeFields; -}) { - const body = JSON.stringify({ - query, - timeFieldName, - earliest, - latest, - samplerShardSize, - interval, - fields, - maxExamples, - runtimeMappings, - }); - - const fileUploadModules = await lazyLoadModules(); - return await fileUploadModules.getHttp().fetch<[DocumentCounts, FieldVisStats]>({ - path: `${basePath()}/get_field_stats/${indexPatternTitle}`, - method: 'POST', - body, - }); -} diff --git a/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/types/combined_query.ts b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/types/combined_query.ts index 734a47d7f01b0..13590505a5d1a 100644 --- a/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/types/combined_query.ts +++ b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/types/combined_query.ts @@ -5,6 +5,8 @@ * 2.0. */ +import { Query } from '@kbn/es-query'; + export const SEARCH_QUERY_LANGUAGE = { KUERY: 'kuery', LUCENE: 'lucene', @@ -13,7 +15,7 @@ export const SEARCH_QUERY_LANGUAGE = { export type SearchQueryLanguage = typeof SEARCH_QUERY_LANGUAGE[keyof typeof SEARCH_QUERY_LANGUAGE]; export interface CombinedQuery { - searchString: string | { [key: string]: any }; + searchString: Query['query']; searchQueryLanguage: string; } diff --git a/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/types/overall_stats.ts b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/types/overall_stats.ts index 2672dc69ac29a..84a6142f012da 100644 --- a/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/types/overall_stats.ts +++ b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/types/overall_stats.ts @@ -5,6 +5,8 @@ * 2.0. */ +import { DocumentCountStats } from '../../../../common/types/field_stats'; + export interface AggregatableField { fieldName: string; stats: { @@ -19,8 +21,9 @@ export type NonAggregatableField = Omit; export interface OverallStats { totalCount: number; + documentCountStats?: DocumentCountStats; aggregatableExistsFields: AggregatableField[]; - aggregatableNotExistsFields: NonAggregatableField[]; - nonAggregatableExistsFields: AggregatableField[]; + aggregatableNotExistsFields: AggregatableField[]; + nonAggregatableExistsFields: NonAggregatableField[]; nonAggregatableNotExistsFields: NonAggregatableField[]; } diff --git a/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/utils/error_utils.ts b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/utils/error_utils.ts index 9bb36496a149e..58c4c820c28d7 100644 --- a/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/utils/error_utils.ts +++ b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/utils/error_utils.ts @@ -85,7 +85,7 @@ export function isDVResponseError(error: any): error is DVResponseError { } export function isBoomError(error: any): error is Boom.Boom { - return error.isBoom === true; + return error?.isBoom === true; } export function isWrappedError(error: any): error is WrappedError { diff --git a/x-pack/plugins/data_visualizer/server/models/data_visualizer/process_distribution_data.ts b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/utils/process_distribution_data.ts similarity index 95% rename from x-pack/plugins/data_visualizer/server/models/data_visualizer/process_distribution_data.ts rename to x-pack/plugins/data_visualizer/public/application/index_data_visualizer/utils/process_distribution_data.ts index 4e40c2baaf701..46719c06e2264 100644 --- a/x-pack/plugins/data_visualizer/server/models/data_visualizer/process_distribution_data.ts +++ b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/utils/process_distribution_data.ts @@ -6,7 +6,7 @@ */ import { last } from 'lodash'; -import { Distribution } from '../../types'; +import type { Distribution } from '../../../../common/types/field_stats'; export const processDistributionData = ( percentiles: Array<{ value: number }>, @@ -49,7 +49,7 @@ export const processDistributionData = ( // Add in 0-5 and 95-100% if they don't add more // than 25% to the value range at either end. - const lastValue: number = (last(percentileBuckets) as any).value; + const lastValue: number = (last(percentileBuckets) as { value: number }).value; const maxDiff = 0.25 * (lastValue - lowerBound); if (lowerBound - dataMin < maxDiff) { percentileBuckets.splice(0, 0, percentiles[0]); diff --git a/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/utils/saved_search_utils.test.ts b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/utils/saved_search_utils.test.ts index ad3229676b31b..586f636a088e1 100644 --- a/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/utils/saved_search_utils.test.ts +++ b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/utils/saved_search_utils.test.ts @@ -75,7 +75,7 @@ const kqlSavedSearch: SavedSearch = { title: 'farequote_filter_and_kuery', description: '', columns: ['_source'], - // @ts-expect-error We don't need the full object here + // @ts-expect-error kibanaSavedObjectMeta: { searchSourceJSON: '{"highlightAll":true,"version":true,"query":{"query":"responsetime > 49","language":"kuery"},"filter":[{"meta":{"index":"90a978e0-1c80-11ec-b1d7-f7e5cf21b9e0","negate":false,"disabled":false,"alias":null,"type":"phrase","key":"airline","value":"ASA","params":{"query":"ASA","type":"phrase"}},"query":{"match":{"airline":{"query":"ASA","type":"phrase"}}},"$state":{"store":"appState"}}],"indexRefName":"kibanaSavedObjectMeta.searchSourceJSON.index"}', diff --git a/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/utils/saved_search_utils.ts b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/utils/saved_search_utils.ts index 1401b1038b8f2..5ebdbcff0b26e 100644 --- a/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/utils/saved_search_utils.ts +++ b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/utils/saved_search_utils.ts @@ -15,6 +15,7 @@ import { Query, Filter, } from '@kbn/es-query'; +import { QueryDslQueryContainer } from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; import { isSavedSearchSavedObject, SavedSearchSavedObject } from '../../../../common/types'; import { IndexPattern, SearchSource } from '../../../../../../../src/plugins/data/common'; import { SEARCH_QUERY_LANGUAGE, SearchQueryLanguage } from '../types/combined_query'; @@ -43,7 +44,7 @@ export function getDefaultQuery() { export function getQueryFromSavedSearchObject(savedSearch: SavedSearchSavedObject | SavedSearch) { const search = isSavedSearchSavedObject(savedSearch) ? savedSearch?.attributes?.kibanaSavedObjectMeta - : // @ts-expect-error kibanaSavedObjectMeta does exist + : // @ts-ignore savedSearch?.kibanaSavedObjectMeta; const parsed = @@ -76,7 +77,7 @@ export function createMergedEsQuery( indexPattern?: IndexPattern, uiSettings?: IUiSettingsClient ) { - let combinedQuery: any = getDefaultQuery(); + let combinedQuery: QueryDslQueryContainer = getDefaultQuery(); if (query && query.language === SEARCH_QUERY_LANGUAGE.KUERY) { const ast = fromKueryExpression(query.query); @@ -86,12 +87,12 @@ export function createMergedEsQuery( if (combinedQuery.bool !== undefined) { const filterQuery = buildQueryFromFilters(filters, indexPattern); - if (Array.isArray(combinedQuery.bool.filter) === false) { + if (!Array.isArray(combinedQuery.bool.filter)) { combinedQuery.bool.filter = combinedQuery.bool.filter === undefined ? [] : [combinedQuery.bool.filter]; } - if (Array.isArray(combinedQuery.bool.must_not) === false) { + if (!Array.isArray(combinedQuery.bool.must_not)) { combinedQuery.bool.must_not = combinedQuery.bool.must_not === undefined ? [] : [combinedQuery.bool.must_not]; } @@ -145,8 +146,20 @@ export function getEsQueryFromSavedSearch({ savedSearch.searchSource.getParent() !== undefined && userQuery ) { + // Flattened query from search source may contain a clause that narrows the time range + // which might interfere with global time pickers so we need to remove + const savedQuery = + cloneDeep(savedSearch.searchSource.getSearchRequestBody()?.query) ?? getDefaultQuery(); + const timeField = savedSearch.searchSource.getField('index')?.timeFieldName; + + if (Array.isArray(savedQuery.bool.filter) && timeField !== undefined) { + savedQuery.bool.filter = savedQuery.bool.filter.filter( + (c: QueryDslQueryContainer) => + !(c.hasOwnProperty('range') && c.range?.hasOwnProperty(timeField)) + ); + } return { - searchQuery: savedSearch.searchSource.getSearchRequestBody()?.query ?? getDefaultQuery(), + searchQuery: savedQuery, searchString: userQuery.query, queryLanguage: userQuery.language as SearchQueryLanguage, }; diff --git a/x-pack/plugins/data_visualizer/public/plugin.ts b/x-pack/plugins/data_visualizer/public/plugin.ts index df1a5ea406d76..dd1d2acccf8cd 100644 --- a/x-pack/plugins/data_visualizer/public/plugin.ts +++ b/x-pack/plugins/data_visualizer/public/plugin.ts @@ -22,6 +22,7 @@ import { getFileDataVisualizerComponent, getIndexDataVisualizerComponent } from import { getMaxBytesFormatted } from './application/common/util/get_max_bytes'; import { registerHomeAddData, registerHomeFeatureCatalogue } from './register_home'; import { registerEmbeddables } from './application/index_data_visualizer/embeddables'; +import { FieldFormatsStart } from '../../../../src/plugins/field_formats/public'; export interface DataVisualizerSetupDependencies { home?: HomePublicPluginSetup; @@ -36,6 +37,7 @@ export interface DataVisualizerStartDependencies { share: SharePluginStart; lens?: LensPublicStart; indexPatternFieldEditor?: IndexPatternFieldEditorStart; + fieldFormats: FieldFormatsStart; } export type DataVisualizerPluginSetup = ReturnType; diff --git a/x-pack/plugins/data_visualizer/server/models/data_visualizer/check_fields_exist.ts b/x-pack/plugins/data_visualizer/server/models/data_visualizer/check_fields_exist.ts deleted file mode 100644 index 24b4deeecdddd..0000000000000 --- a/x-pack/plugins/data_visualizer/server/models/data_visualizer/check_fields_exist.ts +++ /dev/null @@ -1,183 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; -import { get } from 'lodash'; -import { IScopedClusterClient } from 'kibana/server'; -import { AggCardinality, Aggs, FieldData } from '../../types'; -import { - buildBaseFilterCriteria, - buildSamplerAggregation, - getSafeAggregationName, - getSamplerAggregationsResponsePath, -} from '../../../common/utils/query_utils'; -import { getDatafeedAggregations } from '../../../common/utils/datafeed_utils'; -import { isPopulatedObject } from '../../../common/utils/object_utils'; - -export const checkAggregatableFieldsExist = async ( - client: IScopedClusterClient, - indexPatternTitle: string, - query: any, - aggregatableFields: string[], - samplerShardSize: number, - timeFieldName: string | undefined, - earliestMs?: number, - latestMs?: number, - datafeedConfig?: estypes.MlDatafeed, - runtimeMappings?: estypes.MappingRuntimeFields -) => { - const { asCurrentUser } = client; - - const index = indexPatternTitle; - const size = 0; - const filterCriteria = buildBaseFilterCriteria(timeFieldName, earliestMs, latestMs, query); - const datafeedAggregations = getDatafeedAggregations(datafeedConfig); - - // Value count aggregation faster way of checking if field exists than using - // filter aggregation with exists query. - const aggs: Aggs = datafeedAggregations !== undefined ? { ...datafeedAggregations } : {}; - - // Combine runtime fields from the data view as well as the datafeed - const combinedRuntimeMappings: estypes.MappingRuntimeFields = { - ...(isPopulatedObject(runtimeMappings) ? runtimeMappings : {}), - ...(isPopulatedObject(datafeedConfig) && isPopulatedObject(datafeedConfig.runtime_mappings) - ? datafeedConfig.runtime_mappings - : {}), - }; - - aggregatableFields.forEach((field, i) => { - const safeFieldName = getSafeAggregationName(field, i); - aggs[`${safeFieldName}_count`] = { - filter: { exists: { field } }, - }; - - let cardinalityField: AggCardinality; - if (datafeedConfig?.script_fields?.hasOwnProperty(field)) { - cardinalityField = aggs[`${safeFieldName}_cardinality`] = { - cardinality: { script: datafeedConfig?.script_fields[field].script }, - }; - } else { - cardinalityField = { - cardinality: { field }, - }; - } - aggs[`${safeFieldName}_cardinality`] = cardinalityField; - }); - - const searchBody = { - query: { - bool: { - filter: filterCriteria, - }, - }, - ...(isPopulatedObject(aggs) ? { aggs: buildSamplerAggregation(aggs, samplerShardSize) } : {}), - ...(isPopulatedObject(combinedRuntimeMappings) - ? { runtime_mappings: combinedRuntimeMappings } - : {}), - }; - - const { body } = await asCurrentUser.search({ - index, - track_total_hits: true, - size, - body: searchBody, - }); - - const aggregations = body.aggregations; - // @ts-expect-error incorrect search response type - const totalCount = body.hits.total.value; - const stats = { - totalCount, - aggregatableExistsFields: [] as FieldData[], - aggregatableNotExistsFields: [] as FieldData[], - }; - - const aggsPath = getSamplerAggregationsResponsePath(samplerShardSize); - const sampleCount = - samplerShardSize > 0 ? get(aggregations, ['sample', 'doc_count'], 0) : totalCount; - aggregatableFields.forEach((field, i) => { - const safeFieldName = getSafeAggregationName(field, i); - const count = get(aggregations, [...aggsPath, `${safeFieldName}_count`, 'doc_count'], 0); - if (count > 0) { - const cardinality = get( - aggregations, - [...aggsPath, `${safeFieldName}_cardinality`, 'value'], - 0 - ); - stats.aggregatableExistsFields.push({ - fieldName: field, - existsInDocs: true, - stats: { - sampleCount, - count, - cardinality, - }, - }); - } else { - if ( - datafeedConfig?.script_fields?.hasOwnProperty(field) || - datafeedConfig?.runtime_mappings?.hasOwnProperty(field) - ) { - const cardinality = get( - aggregations, - [...aggsPath, `${safeFieldName}_cardinality`, 'value'], - 0 - ); - stats.aggregatableExistsFields.push({ - fieldName: field, - existsInDocs: true, - stats: { - sampleCount, - count, - cardinality, - }, - }); - } else { - stats.aggregatableNotExistsFields.push({ - fieldName: field, - existsInDocs: false, - }); - } - } - }); - - return stats; -}; - -export const checkNonAggregatableFieldExists = async ( - client: IScopedClusterClient, - indexPatternTitle: string, - query: any, - field: string, - timeFieldName: string | undefined, - earliestMs: number | undefined, - latestMs: number | undefined, - runtimeMappings?: estypes.MappingRuntimeFields -) => { - const { asCurrentUser } = client; - const index = indexPatternTitle; - const size = 0; - const filterCriteria = buildBaseFilterCriteria(timeFieldName, earliestMs, latestMs, query); - - const searchBody = { - query: { - bool: { - filter: filterCriteria, - }, - }, - ...(isPopulatedObject(runtimeMappings) ? { runtime_mappings: runtimeMappings } : {}), - }; - filterCriteria.push({ exists: { field } }); - - const { body } = await asCurrentUser.search({ - index, - size, - body: searchBody, - }); - // @ts-expect-error incorrect search response type - return body.hits.total.value > 0; -}; diff --git a/x-pack/plugins/data_visualizer/server/models/data_visualizer/constants.ts b/x-pack/plugins/data_visualizer/server/models/data_visualizer/constants.ts deleted file mode 100644 index 91bd394aee797..0000000000000 --- a/x-pack/plugins/data_visualizer/server/models/data_visualizer/constants.ts +++ /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; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -export const SAMPLER_TOP_TERMS_THRESHOLD = 100000; -export const SAMPLER_TOP_TERMS_SHARD_SIZE = 5000; -export const AGGREGATABLE_EXISTS_REQUEST_BATCH_SIZE = 200; -export const FIELDS_REQUEST_BATCH_SIZE = 10; - -export const MAX_CHART_COLUMNS = 20; diff --git a/x-pack/plugins/data_visualizer/server/models/data_visualizer/data_visualizer.ts b/x-pack/plugins/data_visualizer/server/models/data_visualizer/data_visualizer.ts deleted file mode 100644 index 42e7f93cc8789..0000000000000 --- a/x-pack/plugins/data_visualizer/server/models/data_visualizer/data_visualizer.ts +++ /dev/null @@ -1,489 +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 { IScopedClusterClient } from 'kibana/server'; -import { each, last } from 'lodash'; -import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; -import { JOB_FIELD_TYPES } from '../../../common'; -import type { - BatchStats, - FieldData, - HistogramField, - Field, - DocumentCountStats, - FieldExamples, -} from '../../types'; -import { getHistogramsForFields } from './get_histogram_for_fields'; -import { - checkAggregatableFieldsExist, - checkNonAggregatableFieldExists, -} from './check_fields_exist'; -import { AGGREGATABLE_EXISTS_REQUEST_BATCH_SIZE, FIELDS_REQUEST_BATCH_SIZE } from './constants'; -import { getFieldExamples } from './get_field_examples'; -import { - getBooleanFieldsStats, - getDateFieldsStats, - getDocumentCountStats, - getNumericFieldsStats, - getStringFieldsStats, -} from './get_fields_stats'; -import { wrapError } from '../../utils/error_wrapper'; - -export class DataVisualizer { - private _client: IScopedClusterClient; - - constructor(client: IScopedClusterClient) { - this._client = client; - } - - // Obtains overall stats on the fields in the supplied data view, returning an object - // containing the total document count, and four arrays showing which of the supplied - // aggregatable and non-aggregatable fields do or do not exist in documents. - // Sampling will be used if supplied samplerShardSize > 0. - async getOverallStats( - indexPatternTitle: string, - query: object, - aggregatableFields: string[], - nonAggregatableFields: string[], - samplerShardSize: number, - timeFieldName: string | undefined, - earliestMs: number | undefined, - latestMs: number | undefined, - runtimeMappings?: estypes.MappingRuntimeFields - ) { - const stats = { - totalCount: 0, - aggregatableExistsFields: [] as FieldData[], - aggregatableNotExistsFields: [] as FieldData[], - nonAggregatableExistsFields: [] as FieldData[], - nonAggregatableNotExistsFields: [] as FieldData[], - errors: [] as any[], - }; - - // To avoid checking for the existence of too many aggregatable fields in one request, - // split the check into multiple batches (max 200 fields per request). - const batches: string[][] = [[]]; - each(aggregatableFields, (field) => { - let lastArray: string[] = last(batches) as string[]; - if (lastArray.length === AGGREGATABLE_EXISTS_REQUEST_BATCH_SIZE) { - lastArray = []; - batches.push(lastArray); - } - lastArray.push(field); - }); - - await Promise.all( - batches.map(async (fields) => { - try { - const batchStats = await this.checkAggregatableFieldsExist( - indexPatternTitle, - query, - fields, - samplerShardSize, - timeFieldName, - earliestMs, - latestMs, - undefined, - runtimeMappings - ); - - // Total count will be returned with each batch of fields. Just overwrite. - stats.totalCount = batchStats.totalCount; - - // Add to the lists of fields which do and do not exist. - stats.aggregatableExistsFields.push(...batchStats.aggregatableExistsFields); - stats.aggregatableNotExistsFields.push(...batchStats.aggregatableNotExistsFields); - } catch (e) { - // If index not found, no need to proceed with other batches - if (e.statusCode === 404) { - throw e; - } - stats.errors.push(wrapError(e)); - } - }) - ); - - await Promise.all( - nonAggregatableFields.map(async (field) => { - try { - const existsInDocs = await this.checkNonAggregatableFieldExists( - indexPatternTitle, - query, - field, - timeFieldName, - earliestMs, - latestMs, - runtimeMappings - ); - - const fieldData: FieldData = { - fieldName: field, - existsInDocs, - stats: {}, - }; - - if (existsInDocs === true) { - stats.nonAggregatableExistsFields.push(fieldData); - } else { - stats.nonAggregatableNotExistsFields.push(fieldData); - } - } catch (e) { - stats.errors.push(wrapError(e)); - } - }) - ); - - return stats; - } - - // Obtains binned histograms for supplied list of fields. The statistics for each field in the - // returned array depend on the type of the field (keyword, number, date etc). - // Sampling will be used if supplied samplerShardSize > 0. - async getHistogramsForFields( - indexPatternTitle: string, - query: any, - fields: HistogramField[], - samplerShardSize: number, - runtimeMappings?: estypes.MappingRuntimeFields - ): Promise { - return await getHistogramsForFields( - this._client, - indexPatternTitle, - query, - fields, - samplerShardSize, - runtimeMappings - ); - } - - // Obtains statistics for supplied list of fields. The statistics for each field in the - // returned array depend on the type of the field (keyword, number, date etc). - // Sampling will be used if supplied samplerShardSize > 0. - async getStatsForFields( - indexPatternTitle: string, - query: any, - fields: Field[], - samplerShardSize: number, - timeFieldName: string | undefined, - earliestMs: number | undefined, - latestMs: number | undefined, - intervalMs: number | undefined, - maxExamples: number, - runtimeMappings: estypes.MappingRuntimeFields - ): Promise { - // Batch up fields by type, getting stats for multiple fields at a time. - const batches: Field[][] = []; - const batchedFields: { [key: string]: Field[][] } = {}; - each(fields, (field) => { - if (field.fieldName === undefined) { - // undefined fieldName is used for a document count request. - // getDocumentCountStats requires timeField - don't add to batched requests if not defined - if (timeFieldName !== undefined) { - batches.push([field]); - } - } else { - const fieldType = field.type; - if (batchedFields[fieldType] === undefined) { - batchedFields[fieldType] = [[]]; - } - let lastArray: Field[] = last(batchedFields[fieldType]) as Field[]; - if (lastArray.length === FIELDS_REQUEST_BATCH_SIZE) { - lastArray = []; - batchedFields[fieldType].push(lastArray); - } - lastArray.push(field); - } - }); - - each(batchedFields, (lists) => { - batches.push(...lists); - }); - - let results: BatchStats[] = []; - await Promise.all( - batches.map(async (batch) => { - let batchStats: BatchStats[] = []; - const first = batch[0]; - switch (first.type) { - case JOB_FIELD_TYPES.NUMBER: - // undefined fieldName is used for a document count request. - if (first.fieldName !== undefined) { - batchStats = await this.getNumericFieldsStats( - indexPatternTitle, - query, - batch, - samplerShardSize, - timeFieldName, - earliestMs, - latestMs, - runtimeMappings - ); - } else { - // Will only ever be one document count card, - // so no value in batching up the single request. - if (intervalMs !== undefined) { - const stats = await this.getDocumentCountStats( - indexPatternTitle, - query, - timeFieldName, - earliestMs, - latestMs, - intervalMs, - runtimeMappings - ); - batchStats.push(stats); - } - } - break; - case JOB_FIELD_TYPES.KEYWORD: - case JOB_FIELD_TYPES.IP: - batchStats = await this.getStringFieldsStats( - indexPatternTitle, - query, - batch, - samplerShardSize, - timeFieldName, - earliestMs, - latestMs, - runtimeMappings - ); - break; - case JOB_FIELD_TYPES.DATE: - batchStats = await this.getDateFieldsStats( - indexPatternTitle, - query, - batch, - samplerShardSize, - timeFieldName, - earliestMs, - latestMs, - runtimeMappings - ); - break; - case JOB_FIELD_TYPES.BOOLEAN: - batchStats = await this.getBooleanFieldsStats( - indexPatternTitle, - query, - batch, - samplerShardSize, - timeFieldName, - earliestMs, - latestMs, - runtimeMappings - ); - break; - case JOB_FIELD_TYPES.TEXT: - default: - // Use an exists filter on the the field name to get - // examples of the field, so cannot batch up. - await Promise.all( - batch.map(async (field) => { - const stats = await this.getFieldExamples( - indexPatternTitle, - query, - field.fieldName, - timeFieldName, - earliestMs, - latestMs, - maxExamples, - runtimeMappings - ); - batchStats.push(stats); - }) - ); - break; - } - - results = [...results, ...batchStats]; - }) - ); - - return results; - } - - async checkAggregatableFieldsExist( - indexPatternTitle: string, - query: any, - aggregatableFields: string[], - samplerShardSize: number, - timeFieldName: string | undefined, - earliestMs?: number, - latestMs?: number, - datafeedConfig?: estypes.MlDatafeed, - runtimeMappings?: estypes.MappingRuntimeFields - ) { - return await checkAggregatableFieldsExist( - this._client, - indexPatternTitle, - query, - aggregatableFields, - samplerShardSize, - timeFieldName, - earliestMs, - latestMs, - datafeedConfig, - runtimeMappings - ); - } - - async checkNonAggregatableFieldExists( - indexPatternTitle: string, - query: any, - field: string, - timeFieldName: string | undefined, - earliestMs: number | undefined, - latestMs: number | undefined, - runtimeMappings?: estypes.MappingRuntimeFields - ) { - return await checkNonAggregatableFieldExists( - this._client, - indexPatternTitle, - query, - field, - timeFieldName, - earliestMs, - latestMs, - runtimeMappings - ); - } - - async getDocumentCountStats( - indexPatternTitle: string, - query: any, - timeFieldName: string | undefined, - earliestMs: number | undefined, - latestMs: number | undefined, - intervalMs: number, - runtimeMappings: estypes.MappingRuntimeFields - ): Promise { - return await getDocumentCountStats( - this._client, - indexPatternTitle, - query, - timeFieldName, - earliestMs, - latestMs, - intervalMs, - runtimeMappings - ); - } - - async getNumericFieldsStats( - indexPatternTitle: string, - query: object, - fields: Field[], - samplerShardSize: number, - timeFieldName: string | undefined, - earliestMs: number | undefined, - latestMs: number | undefined, - runtimeMappings?: estypes.MappingRuntimeFields - ) { - return await getNumericFieldsStats( - this._client, - indexPatternTitle, - query, - fields, - samplerShardSize, - timeFieldName, - earliestMs, - latestMs, - runtimeMappings - ); - } - - async getStringFieldsStats( - indexPatternTitle: string, - query: object, - fields: Field[], - samplerShardSize: number, - timeFieldName: string | undefined, - earliestMs: number | undefined, - latestMs: number | undefined, - runtimeMappings?: estypes.MappingRuntimeFields - ) { - return await getStringFieldsStats( - this._client, - indexPatternTitle, - query, - fields, - samplerShardSize, - timeFieldName, - earliestMs, - latestMs, - runtimeMappings - ); - } - - async getDateFieldsStats( - indexPatternTitle: string, - query: object, - fields: Field[], - samplerShardSize: number, - timeFieldName: string | undefined, - earliestMs: number | undefined, - latestMs: number | undefined, - runtimeMappings?: estypes.MappingRuntimeFields - ) { - return await getDateFieldsStats( - this._client, - indexPatternTitle, - query, - fields, - samplerShardSize, - timeFieldName, - earliestMs, - latestMs, - runtimeMappings - ); - } - - async getBooleanFieldsStats( - indexPatternTitle: string, - query: object, - fields: Field[], - samplerShardSize: number, - timeFieldName: string | undefined, - earliestMs: number | undefined, - latestMs: number | undefined, - runtimeMappings?: estypes.MappingRuntimeFields - ) { - return await getBooleanFieldsStats( - this._client, - indexPatternTitle, - query, - fields, - samplerShardSize, - timeFieldName, - earliestMs, - latestMs, - runtimeMappings - ); - } - - async getFieldExamples( - indexPatternTitle: string, - query: any, - field: string, - timeFieldName: string | undefined, - earliestMs: number | undefined, - latestMs: number | undefined, - maxExamples: number, - runtimeMappings?: estypes.MappingRuntimeFields - ): Promise { - return await getFieldExamples( - this._client, - indexPatternTitle, - query, - field, - timeFieldName, - earliestMs, - latestMs, - maxExamples, - runtimeMappings - ); - } -} diff --git a/x-pack/plugins/data_visualizer/server/models/data_visualizer/get_field_examples.ts b/x-pack/plugins/data_visualizer/server/models/data_visualizer/get_field_examples.ts deleted file mode 100644 index 78adfb9e81b95..0000000000000 --- a/x-pack/plugins/data_visualizer/server/models/data_visualizer/get_field_examples.ts +++ /dev/null @@ -1,80 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; -import { get } from 'lodash'; -import { IScopedClusterClient } from 'kibana/server'; -import { buildBaseFilterCriteria } from '../../../common/utils/query_utils'; -import { isPopulatedObject } from '../../../common/utils/object_utils'; -import { FieldExamples } from '../../types/chart_data'; - -export const getFieldExamples = async ( - client: IScopedClusterClient, - indexPatternTitle: string, - query: any, - field: string, - timeFieldName: string | undefined, - earliestMs: number | undefined, - latestMs: number | undefined, - maxExamples: number, - runtimeMappings?: estypes.MappingRuntimeFields -): Promise => { - const { asCurrentUser } = client; - - const index = indexPatternTitle; - - // Request at least 100 docs so that we have a chance of obtaining - // 'maxExamples' of the field. - const size = Math.max(100, maxExamples); - const filterCriteria = buildBaseFilterCriteria(timeFieldName, earliestMs, latestMs, query); - - // Use an exists filter to return examples of the field. - filterCriteria.push({ - exists: { field }, - }); - - const searchBody = { - fields: [field], - _source: false, - query: { - bool: { - filter: filterCriteria, - }, - }, - ...(isPopulatedObject(runtimeMappings) ? { runtime_mappings: runtimeMappings } : {}), - }; - - const { body } = await asCurrentUser.search({ - index, - size, - body: searchBody, - }); - const stats = { - fieldName: field, - examples: [] as any[], - }; - // @ts-expect-error incorrect search response type - if (body.hits.total.value > 0) { - const hits = body.hits.hits; - for (let i = 0; i < hits.length; i++) { - // Use lodash get() to support field names containing dots. - const doc: object[] | undefined = get(hits[i].fields, field); - // the results from fields query is always an array - if (Array.isArray(doc) && doc.length > 0) { - const example = doc[0]; - if (example !== undefined && stats.examples.indexOf(example) === -1) { - stats.examples.push(example); - if (stats.examples.length === maxExamples) { - break; - } - } - } - } - } - - return stats; -}; diff --git a/x-pack/plugins/data_visualizer/server/models/data_visualizer/get_fields_stats.ts b/x-pack/plugins/data_visualizer/server/models/data_visualizer/get_fields_stats.ts deleted file mode 100644 index da93719e9ed93..0000000000000 --- a/x-pack/plugins/data_visualizer/server/models/data_visualizer/get_fields_stats.ts +++ /dev/null @@ -1,478 +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 type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; -import { each, find, get } from 'lodash'; -import { IScopedClusterClient } from 'kibana/server'; -import { - Aggs, - BooleanFieldStats, - Bucket, - DateFieldStats, - DocumentCountStats, - Field, - NumericFieldStats, - StringFieldStats, -} from '../../types'; -import { - buildBaseFilterCriteria, - buildSamplerAggregation, - getSafeAggregationName, - getSamplerAggregationsResponsePath, -} from '../../../common/utils/query_utils'; -import { isPopulatedObject } from '../../../common/utils/object_utils'; -import { processDistributionData } from './process_distribution_data'; -import { SAMPLER_TOP_TERMS_SHARD_SIZE, SAMPLER_TOP_TERMS_THRESHOLD } from './constants'; - -export const getDocumentCountStats = async ( - client: IScopedClusterClient, - indexPatternTitle: string, - query: any, - timeFieldName: string | undefined, - earliestMs: number | undefined, - latestMs: number | undefined, - intervalMs: number, - runtimeMappings: estypes.MappingRuntimeFields -): Promise => { - const { asCurrentUser } = client; - - const index = indexPatternTitle; - const size = 0; - const filterCriteria = buildBaseFilterCriteria(timeFieldName, earliestMs, latestMs, query); - - // Don't use the sampler aggregation as this can lead to some potentially - // confusing date histogram results depending on the date range of data amongst shards. - - const aggs = { - eventRate: { - date_histogram: { - field: timeFieldName, - fixed_interval: `${intervalMs}ms`, - min_doc_count: 1, - }, - }, - }; - - const searchBody = { - query: { - bool: { - filter: filterCriteria, - }, - }, - aggs, - ...(isPopulatedObject(runtimeMappings) ? { runtime_mappings: runtimeMappings } : {}), - }; - - const { body } = await asCurrentUser.search({ - index, - size, - body: searchBody, - }); - - const buckets: { [key: string]: number } = {}; - const dataByTimeBucket: Array<{ key: string; doc_count: number }> = get( - body, - ['aggregations', 'eventRate', 'buckets'], - [] - ); - each(dataByTimeBucket, (dataForTime) => { - const time = dataForTime.key; - buckets[time] = dataForTime.doc_count; - }); - - return { - documentCounts: { - interval: intervalMs, - buckets, - }, - }; -}; - -export const getNumericFieldsStats = async ( - client: IScopedClusterClient, - indexPatternTitle: string, - query: object, - fields: Field[], - samplerShardSize: number, - timeFieldName: string | undefined, - earliestMs: number | undefined, - latestMs: number | undefined, - runtimeMappings?: estypes.MappingRuntimeFields -) => { - const { asCurrentUser } = client; - const index = indexPatternTitle; - const size = 0; - const filterCriteria = buildBaseFilterCriteria(timeFieldName, earliestMs, latestMs, query); - - // Build the percents parameter which defines the percentiles to query - // for the metric distribution data. - // Use a fixed percentile spacing of 5%. - const MAX_PERCENT = 100; - const PERCENTILE_SPACING = 5; - let count = 0; - const percents = Array.from( - Array(MAX_PERCENT / PERCENTILE_SPACING), - () => (count += PERCENTILE_SPACING) - ); - - const aggs: { [key: string]: any } = {}; - fields.forEach((field, i) => { - const safeFieldName = getSafeAggregationName(field.fieldName, i); - aggs[`${safeFieldName}_field_stats`] = { - filter: { exists: { field: field.fieldName } }, - aggs: { - actual_stats: { - stats: { field: field.fieldName }, - }, - }, - }; - aggs[`${safeFieldName}_percentiles`] = { - percentiles: { - field: field.fieldName, - percents, - keyed: false, - }, - }; - - const top = { - terms: { - field: field.fieldName, - size: 10, - order: { - _count: 'desc', - }, - }, - }; - - // If cardinality >= SAMPLE_TOP_TERMS_THRESHOLD, run the top terms aggregation - // in a sampler aggregation, even if no sampling has been specified (samplerShardSize < 1). - if (samplerShardSize < 1 && field.cardinality >= SAMPLER_TOP_TERMS_THRESHOLD) { - aggs[`${safeFieldName}_top`] = { - sampler: { - shard_size: SAMPLER_TOP_TERMS_SHARD_SIZE, - }, - aggs: { - top, - }, - }; - } else { - aggs[`${safeFieldName}_top`] = top; - } - }); - - const searchBody = { - query: { - bool: { - filter: filterCriteria, - }, - }, - aggs: buildSamplerAggregation(aggs, samplerShardSize), - ...(isPopulatedObject(runtimeMappings) ? { runtime_mappings: runtimeMappings } : {}), - }; - - const { body } = await asCurrentUser.search({ - index, - size, - body: searchBody, - }); - const aggregations = body.aggregations; - const aggsPath = getSamplerAggregationsResponsePath(samplerShardSize); - const batchStats: NumericFieldStats[] = []; - fields.forEach((field, i) => { - const safeFieldName = getSafeAggregationName(field.fieldName, i); - const docCount = get( - aggregations, - [...aggsPath, `${safeFieldName}_field_stats`, 'doc_count'], - 0 - ); - const fieldStatsResp = get( - aggregations, - [...aggsPath, `${safeFieldName}_field_stats`, 'actual_stats'], - {} - ); - - const topAggsPath = [...aggsPath, `${safeFieldName}_top`]; - if (samplerShardSize < 1 && field.cardinality >= SAMPLER_TOP_TERMS_THRESHOLD) { - topAggsPath.push('top'); - } - - const topValues: Bucket[] = get(aggregations, [...topAggsPath, 'buckets'], []); - - const stats: NumericFieldStats = { - fieldName: field.fieldName, - count: docCount, - min: get(fieldStatsResp, 'min', 0), - max: get(fieldStatsResp, 'max', 0), - avg: get(fieldStatsResp, 'avg', 0), - isTopValuesSampled: field.cardinality >= SAMPLER_TOP_TERMS_THRESHOLD || samplerShardSize > 0, - topValues, - topValuesSampleSize: topValues.reduce( - (acc, curr) => acc + curr.doc_count, - get(aggregations, [...topAggsPath, 'sum_other_doc_count'], 0) - ), - topValuesSamplerShardSize: - field.cardinality >= SAMPLER_TOP_TERMS_THRESHOLD - ? SAMPLER_TOP_TERMS_SHARD_SIZE - : samplerShardSize, - }; - - if (stats.count > 0) { - const percentiles = get( - aggregations, - [...aggsPath, `${safeFieldName}_percentiles`, 'values'], - [] - ); - const medianPercentile: { value: number; key: number } | undefined = find(percentiles, { - key: 50, - }); - stats.median = medianPercentile !== undefined ? medianPercentile!.value : 0; - stats.distribution = processDistributionData(percentiles, PERCENTILE_SPACING, stats.min); - } - - batchStats.push(stats); - }); - - return batchStats; -}; - -export const getStringFieldsStats = async ( - client: IScopedClusterClient, - indexPatternTitle: string, - query: object, - fields: Field[], - samplerShardSize: number, - timeFieldName: string | undefined, - earliestMs: number | undefined, - latestMs: number | undefined, - runtimeMappings?: estypes.MappingRuntimeFields -) => { - const { asCurrentUser } = client; - - const index = indexPatternTitle; - const size = 0; - const filterCriteria = buildBaseFilterCriteria(timeFieldName, earliestMs, latestMs, query); - - const aggs: Aggs = {}; - fields.forEach((field, i) => { - const safeFieldName = getSafeAggregationName(field.fieldName, i); - const top = { - terms: { - field: field.fieldName, - size: 10, - order: { - _count: 'desc', - }, - }, - }; - - // If cardinality >= SAMPLE_TOP_TERMS_THRESHOLD, run the top terms aggregation - // in a sampler aggregation, even if no sampling has been specified (samplerShardSize < 1). - if (samplerShardSize < 1 && field.cardinality >= SAMPLER_TOP_TERMS_THRESHOLD) { - aggs[`${safeFieldName}_top`] = { - sampler: { - shard_size: SAMPLER_TOP_TERMS_SHARD_SIZE, - }, - aggs: { - top, - }, - }; - } else { - aggs[`${safeFieldName}_top`] = top; - } - }); - - const searchBody = { - query: { - bool: { - filter: filterCriteria, - }, - }, - aggs: buildSamplerAggregation(aggs, samplerShardSize), - ...(isPopulatedObject(runtimeMappings) ? { runtime_mappings: runtimeMappings } : {}), - }; - - const { body } = await asCurrentUser.search({ - index, - size, - body: searchBody, - }); - const aggregations = body.aggregations; - const aggsPath = getSamplerAggregationsResponsePath(samplerShardSize); - const batchStats: StringFieldStats[] = []; - fields.forEach((field, i) => { - const safeFieldName = getSafeAggregationName(field.fieldName, i); - - const topAggsPath = [...aggsPath, `${safeFieldName}_top`]; - if (samplerShardSize < 1 && field.cardinality >= SAMPLER_TOP_TERMS_THRESHOLD) { - topAggsPath.push('top'); - } - - const topValues: Bucket[] = get(aggregations, [...topAggsPath, 'buckets'], []); - - const stats = { - fieldName: field.fieldName, - isTopValuesSampled: field.cardinality >= SAMPLER_TOP_TERMS_THRESHOLD || samplerShardSize > 0, - topValues, - topValuesSampleSize: topValues.reduce( - (acc, curr) => acc + curr.doc_count, - get(aggregations, [...topAggsPath, 'sum_other_doc_count'], 0) - ), - topValuesSamplerShardSize: - field.cardinality >= SAMPLER_TOP_TERMS_THRESHOLD - ? SAMPLER_TOP_TERMS_SHARD_SIZE - : samplerShardSize, - }; - - batchStats.push(stats); - }); - - return batchStats; -}; - -export const getDateFieldsStats = async ( - client: IScopedClusterClient, - indexPatternTitle: string, - query: object, - fields: Field[], - samplerShardSize: number, - timeFieldName: string | undefined, - earliestMs: number | undefined, - latestMs: number | undefined, - runtimeMappings?: estypes.MappingRuntimeFields -) => { - const { asCurrentUser } = client; - - const index = indexPatternTitle; - const size = 0; - const filterCriteria = buildBaseFilterCriteria(timeFieldName, earliestMs, latestMs, query); - - const aggs: Aggs = {}; - fields.forEach((field, i) => { - const safeFieldName = getSafeAggregationName(field.fieldName, i); - aggs[`${safeFieldName}_field_stats`] = { - filter: { exists: { field: field.fieldName } }, - aggs: { - actual_stats: { - stats: { field: field.fieldName }, - }, - }, - }; - }); - - const searchBody = { - query: { - bool: { - filter: filterCriteria, - }, - }, - aggs: buildSamplerAggregation(aggs, samplerShardSize), - ...(isPopulatedObject(runtimeMappings) ? { runtime_mappings: runtimeMappings } : {}), - }; - - const { body } = await asCurrentUser.search({ - index, - size, - body: searchBody, - }); - const aggregations = body.aggregations; - const aggsPath = getSamplerAggregationsResponsePath(samplerShardSize); - const batchStats: DateFieldStats[] = []; - fields.forEach((field, i) => { - const safeFieldName = getSafeAggregationName(field.fieldName, i); - const docCount = get( - aggregations, - [...aggsPath, `${safeFieldName}_field_stats`, 'doc_count'], - 0 - ); - const fieldStatsResp = get( - aggregations, - [...aggsPath, `${safeFieldName}_field_stats`, 'actual_stats'], - {} - ); - batchStats.push({ - fieldName: field.fieldName, - count: docCount, - earliest: get(fieldStatsResp, 'min', 0), - latest: get(fieldStatsResp, 'max', 0), - }); - }); - - return batchStats; -}; - -export const getBooleanFieldsStats = async ( - client: IScopedClusterClient, - indexPatternTitle: string, - query: object, - fields: Field[], - samplerShardSize: number, - timeFieldName: string | undefined, - earliestMs: number | undefined, - latestMs: number | undefined, - runtimeMappings?: estypes.MappingRuntimeFields -) => { - const { asCurrentUser } = client; - - const index = indexPatternTitle; - const size = 0; - const filterCriteria = buildBaseFilterCriteria(timeFieldName, earliestMs, latestMs, query); - - const aggs: Aggs = {}; - fields.forEach((field, i) => { - const safeFieldName = getSafeAggregationName(field.fieldName, i); - aggs[`${safeFieldName}_value_count`] = { - filter: { exists: { field: field.fieldName } }, - }; - aggs[`${safeFieldName}_values`] = { - terms: { - field: field.fieldName, - size: 2, - }, - }; - }); - - const searchBody = { - query: { - bool: { - filter: filterCriteria, - }, - }, - aggs: buildSamplerAggregation(aggs, samplerShardSize), - ...(isPopulatedObject(runtimeMappings) ? { runtime_mappings: runtimeMappings } : {}), - }; - - const { body } = await asCurrentUser.search({ - index, - size, - body: searchBody, - }); - const aggregations = body.aggregations; - const aggsPath = getSamplerAggregationsResponsePath(samplerShardSize); - const batchStats: BooleanFieldStats[] = []; - fields.forEach((field, i) => { - const safeFieldName = getSafeAggregationName(field.fieldName, i); - const stats: BooleanFieldStats = { - fieldName: field.fieldName, - count: get(aggregations, [...aggsPath, `${safeFieldName}_value_count`, 'doc_count'], 0), - trueCount: 0, - falseCount: 0, - }; - - const valueBuckets: Array<{ [key: string]: number }> = get( - aggregations, - [...aggsPath, `${safeFieldName}_values`, 'buckets'], - [] - ); - valueBuckets.forEach((bucket) => { - stats[`${bucket.key_as_string}Count`] = bucket.doc_count; - }); - - batchStats.push(stats); - }); - - return batchStats; -}; diff --git a/x-pack/plugins/data_visualizer/server/models/data_visualizer/get_histogram_for_fields.ts b/x-pack/plugins/data_visualizer/server/models/data_visualizer/get_histogram_for_fields.ts deleted file mode 100644 index 1cbf40a22b056..0000000000000 --- a/x-pack/plugins/data_visualizer/server/models/data_visualizer/get_histogram_for_fields.ts +++ /dev/null @@ -1,186 +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 { IScopedClusterClient } from 'kibana/server'; -import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; -import { get } from 'lodash'; -import { ChartData, ChartRequestAgg, HistogramField, NumericColumnStatsMap } from '../../types'; -import { KBN_FIELD_TYPES } from '../../../../../../src/plugins/data/common'; -import { stringHash } from '../../../common/utils/string_utils'; -import { - buildSamplerAggregation, - getSamplerAggregationsResponsePath, -} from '../../../common/utils/query_utils'; -import { isPopulatedObject } from '../../../common/utils/object_utils'; -import { MAX_CHART_COLUMNS } from './constants'; - -export const getAggIntervals = async ( - { asCurrentUser }: IScopedClusterClient, - indexPatternTitle: string, - query: any, - fields: HistogramField[], - samplerShardSize: number, - runtimeMappings?: estypes.MappingRuntimeFields -): Promise => { - const numericColumns = fields.filter((field) => { - return field.type === KBN_FIELD_TYPES.NUMBER || field.type === KBN_FIELD_TYPES.DATE; - }); - - if (numericColumns.length === 0) { - return {}; - } - - const minMaxAggs = numericColumns.reduce((aggs, c) => { - const id = stringHash(c.fieldName); - aggs[id] = { - stats: { - field: c.fieldName, - }, - }; - return aggs; - }, {} as Record); - - const { body } = await asCurrentUser.search({ - index: indexPatternTitle, - size: 0, - body: { - query, - aggs: buildSamplerAggregation(minMaxAggs, samplerShardSize), - size: 0, - ...(isPopulatedObject(runtimeMappings) ? { runtime_mappings: runtimeMappings } : {}), - }, - }); - - const aggsPath = getSamplerAggregationsResponsePath(samplerShardSize); - const aggregations = aggsPath.length > 0 ? get(body.aggregations, aggsPath) : body.aggregations; - - return Object.keys(aggregations).reduce((p, aggName) => { - const stats = [aggregations[aggName].min, aggregations[aggName].max]; - if (!stats.includes(null)) { - const delta = aggregations[aggName].max - aggregations[aggName].min; - - let aggInterval = 1; - - if (delta > MAX_CHART_COLUMNS || delta <= 1) { - aggInterval = delta / (MAX_CHART_COLUMNS - 1); - } - - p[aggName] = { interval: aggInterval, min: stats[0], max: stats[1] }; - } - - return p; - }, {} as NumericColumnStatsMap); -}; - -export const getHistogramsForFields = async ( - client: IScopedClusterClient, - indexPatternTitle: string, - query: any, - fields: HistogramField[], - samplerShardSize: number, - runtimeMappings?: estypes.MappingRuntimeFields -) => { - const { asCurrentUser } = client; - const aggIntervals = await getAggIntervals( - client, - indexPatternTitle, - query, - fields, - samplerShardSize, - runtimeMappings - ); - - const chartDataAggs = fields.reduce((aggs, field) => { - const fieldName = field.fieldName; - const fieldType = field.type; - const id = stringHash(fieldName); - if (fieldType === KBN_FIELD_TYPES.NUMBER || fieldType === KBN_FIELD_TYPES.DATE) { - if (aggIntervals[id] !== undefined) { - aggs[`${id}_histogram`] = { - histogram: { - field: fieldName, - interval: aggIntervals[id].interval !== 0 ? aggIntervals[id].interval : 1, - }, - }; - } - } else if (fieldType === KBN_FIELD_TYPES.STRING || fieldType === KBN_FIELD_TYPES.BOOLEAN) { - if (fieldType === KBN_FIELD_TYPES.STRING) { - aggs[`${id}_cardinality`] = { - cardinality: { - field: fieldName, - }, - }; - } - aggs[`${id}_terms`] = { - terms: { - field: fieldName, - size: MAX_CHART_COLUMNS, - }, - }; - } - return aggs; - }, {} as Record); - - if (Object.keys(chartDataAggs).length === 0) { - return []; - } - - const { body } = await asCurrentUser.search({ - index: indexPatternTitle, - size: 0, - body: { - query, - aggs: buildSamplerAggregation(chartDataAggs, samplerShardSize), - size: 0, - ...(isPopulatedObject(runtimeMappings) ? { runtime_mappings: runtimeMappings } : {}), - }, - }); - - const aggsPath = getSamplerAggregationsResponsePath(samplerShardSize); - const aggregations = aggsPath.length > 0 ? get(body.aggregations, aggsPath) : body.aggregations; - - const chartsData: ChartData[] = fields.map((field): ChartData => { - const fieldName = field.fieldName; - const fieldType = field.type; - const id = stringHash(field.fieldName); - - if (fieldType === KBN_FIELD_TYPES.NUMBER || fieldType === KBN_FIELD_TYPES.DATE) { - if (aggIntervals[id] === undefined) { - return { - type: 'numeric', - data: [], - interval: 0, - stats: [0, 0], - id: fieldName, - }; - } - - return { - data: aggregations[`${id}_histogram`].buckets, - interval: aggIntervals[id].interval, - stats: [aggIntervals[id].min, aggIntervals[id].max], - type: 'numeric', - id: fieldName, - }; - } else if (fieldType === KBN_FIELD_TYPES.STRING || fieldType === KBN_FIELD_TYPES.BOOLEAN) { - return { - type: fieldType === KBN_FIELD_TYPES.STRING ? 'ordinal' : 'boolean', - cardinality: - fieldType === KBN_FIELD_TYPES.STRING ? aggregations[`${id}_cardinality`].value : 2, - data: aggregations[`${id}_terms`].buckets, - id: fieldName, - }; - } - - return { - type: 'unsupported', - id: fieldName, - }; - }); - - return chartsData; -}; diff --git a/x-pack/plugins/data_visualizer/server/models/data_visualizer/index.ts b/x-pack/plugins/data_visualizer/server/models/data_visualizer/index.ts deleted file mode 100644 index a29957b159b7e..0000000000000 --- a/x-pack/plugins/data_visualizer/server/models/data_visualizer/index.ts +++ /dev/null @@ -1,8 +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 * from './data_visualizer'; diff --git a/x-pack/plugins/data_visualizer/server/plugin.ts b/x-pack/plugins/data_visualizer/server/plugin.ts index e2e0637ef8f3f..9ef6ca5ae6a69 100644 --- a/x-pack/plugins/data_visualizer/server/plugin.ts +++ b/x-pack/plugins/data_visualizer/server/plugin.ts @@ -7,15 +7,12 @@ import { CoreSetup, CoreStart, Plugin } from 'src/core/server'; import { StartDeps, SetupDeps } from './types'; -import { dataVisualizerRoutes } from './routes'; import { registerWithCustomIntegrations } from './register_custom_integration'; export class DataVisualizerPlugin implements Plugin { constructor() {} setup(coreSetup: CoreSetup, plugins: SetupDeps) { - dataVisualizerRoutes(coreSetup); - // home-plugin required if (plugins.home && plugins.customIntegrations) { registerWithCustomIntegrations(plugins.customIntegrations); diff --git a/x-pack/plugins/data_visualizer/server/routes/index.ts b/x-pack/plugins/data_visualizer/server/routes/index.ts deleted file mode 100644 index 892f6cbd77361..0000000000000 --- a/x-pack/plugins/data_visualizer/server/routes/index.ts +++ /dev/null @@ -1,8 +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 { dataVisualizerRoutes } from './routes'; diff --git a/x-pack/plugins/data_visualizer/server/routes/routes.ts b/x-pack/plugins/data_visualizer/server/routes/routes.ts deleted file mode 100644 index 1ec2eaa242c1c..0000000000000 --- a/x-pack/plugins/data_visualizer/server/routes/routes.ts +++ /dev/null @@ -1,262 +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 type { CoreSetup, IScopedClusterClient } from 'kibana/server'; -import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; -import { - dataVisualizerFieldHistogramsSchema, - dataVisualizerFieldStatsSchema, - dataVisualizerOverallStatsSchema, - dataViewTitleSchema, -} from './schemas'; -import type { Field, StartDeps, HistogramField } from '../types'; -import { DataVisualizer } from '../models/data_visualizer'; -import { wrapError } from '../utils/error_wrapper'; - -function getOverallStats( - client: IScopedClusterClient, - indexPatternTitle: string, - query: object, - aggregatableFields: string[], - nonAggregatableFields: string[], - samplerShardSize: number, - timeFieldName: string | undefined, - earliestMs: number | undefined, - latestMs: number | undefined, - runtimeMappings: estypes.MappingRuntimeFields -) { - const dv = new DataVisualizer(client); - return dv.getOverallStats( - indexPatternTitle, - query, - aggregatableFields, - nonAggregatableFields, - samplerShardSize, - timeFieldName, - earliestMs, - latestMs, - runtimeMappings - ); -} - -function getStatsForFields( - client: IScopedClusterClient, - indexPatternTitle: string, - query: any, - fields: Field[], - samplerShardSize: number, - timeFieldName: string | undefined, - earliestMs: number | undefined, - latestMs: number | undefined, - interval: number | undefined, - maxExamples: number, - runtimeMappings: estypes.MappingRuntimeFields -) { - const dv = new DataVisualizer(client); - return dv.getStatsForFields( - indexPatternTitle, - query, - fields, - samplerShardSize, - timeFieldName, - earliestMs, - latestMs, - interval, - maxExamples, - runtimeMappings - ); -} - -function getHistogramsForFields( - client: IScopedClusterClient, - indexPatternTitle: string, - query: any, - fields: HistogramField[], - samplerShardSize: number, - runtimeMappings: estypes.MappingRuntimeFields -) { - const dv = new DataVisualizer(client); - return dv.getHistogramsForFields( - indexPatternTitle, - query, - fields, - samplerShardSize, - runtimeMappings - ); -} -/** - * Routes for the index data visualizer. - */ -export function dataVisualizerRoutes(coreSetup: CoreSetup) { - const router = coreSetup.http.createRouter(); - - /** - * @apiGroup DataVisualizer - * - * @api {post} /internal/data_visualizer/get_field_histograms/:dataViewTitle Get histograms for fields - * @apiName GetHistogramsForFields - * @apiDescription Returns the histograms on a list fields in the specified data view. - * - * @apiSchema (params) dataViewTitleSchema - * @apiSchema (body) dataVisualizerFieldHistogramsSchema - * - * @apiSuccess {Object} fieldName histograms by field, keyed on the name of the field. - */ - router.post( - { - path: '/internal/data_visualizer/get_field_histograms/{dataViewTitle}', - validate: { - params: dataViewTitleSchema, - body: dataVisualizerFieldHistogramsSchema, - }, - }, - async (context, request, response) => { - try { - const { - params: { dataViewTitle }, - body: { query, fields, samplerShardSize, runtimeMappings }, - } = request; - - const results = await getHistogramsForFields( - context.core.elasticsearch.client, - dataViewTitle, - query, - fields, - samplerShardSize, - runtimeMappings - ); - - return response.ok({ - body: results, - }); - } catch (e) { - return response.customError(wrapError(e)); - } - } - ); - - /** - * @apiGroup DataVisualizer - * - * @api {post} /internal/data_visualizer/get_field_stats/:dataViewTitle Get stats for fields - * @apiName GetStatsForFields - * @apiDescription Returns the stats on individual fields in the specified data view. - * - * @apiSchema (params) dataViewTitleSchema - * @apiSchema (body) dataVisualizerFieldStatsSchema - * - * @apiSuccess {Object} fieldName stats by field, keyed on the name of the field. - */ - router.post( - { - path: '/internal/data_visualizer/get_field_stats/{dataViewTitle}', - validate: { - params: dataViewTitleSchema, - body: dataVisualizerFieldStatsSchema, - }, - }, - async (context, request, response) => { - try { - const { - params: { dataViewTitle }, - body: { - query, - fields, - samplerShardSize, - timeFieldName, - earliest, - latest, - interval, - maxExamples, - runtimeMappings, - }, - } = request; - const results = await getStatsForFields( - context.core.elasticsearch.client, - dataViewTitle, - query, - fields, - samplerShardSize, - timeFieldName, - earliest, - latest, - interval, - maxExamples, - runtimeMappings - ); - - return response.ok({ - body: results, - }); - } catch (e) { - return response.customError(wrapError(e)); - } - } - ); - - /** - * @apiGroup DataVisualizer - * - * @api {post} /internal/data_visualizer/get_overall_stats/:dataViewTitle Get overall stats - * @apiName GetOverallStats - * @apiDescription Returns the top level overall stats for the specified data view. - * - * @apiSchema (params) dataViewTitleSchema - * @apiSchema (body) dataVisualizerOverallStatsSchema - * - * @apiSuccess {number} totalCount total count of documents. - * @apiSuccess {Object} aggregatableExistsFields stats on aggregatable fields that exist in documents. - * @apiSuccess {Object} aggregatableNotExistsFields stats on aggregatable fields that do not exist in documents. - * @apiSuccess {Object} nonAggregatableExistsFields stats on non-aggregatable fields that exist in documents. - * @apiSuccess {Object} nonAggregatableNotExistsFields stats on non-aggregatable fields that do not exist in documents. - */ - router.post( - { - path: '/internal/data_visualizer/get_overall_stats/{dataViewTitle}', - validate: { - params: dataViewTitleSchema, - body: dataVisualizerOverallStatsSchema, - }, - }, - async (context, request, response) => { - try { - const { - params: { dataViewTitle }, - body: { - query, - aggregatableFields, - nonAggregatableFields, - samplerShardSize, - timeFieldName, - earliest, - latest, - runtimeMappings, - }, - } = request; - - const results = await getOverallStats( - context.core.elasticsearch.client, - dataViewTitle, - query, - aggregatableFields, - nonAggregatableFields, - samplerShardSize, - timeFieldName, - earliest, - latest, - runtimeMappings - ); - - return response.ok({ - body: results, - }); - } catch (e) { - return response.customError(wrapError(e)); - } - } - ); -} diff --git a/x-pack/plugins/data_visualizer/server/routes/schemas/index.ts b/x-pack/plugins/data_visualizer/server/routes/schemas/index.ts deleted file mode 100644 index 156336feef29e..0000000000000 --- a/x-pack/plugins/data_visualizer/server/routes/schemas/index.ts +++ /dev/null @@ -1,8 +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 * from './index_data_visualizer_schemas'; diff --git a/x-pack/plugins/data_visualizer/server/routes/schemas/index_data_visualizer_schemas.ts b/x-pack/plugins/data_visualizer/server/routes/schemas/index_data_visualizer_schemas.ts deleted file mode 100644 index 3b5797622734f..0000000000000 --- a/x-pack/plugins/data_visualizer/server/routes/schemas/index_data_visualizer_schemas.ts +++ /dev/null @@ -1,76 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { schema } from '@kbn/config-schema'; -import { isRuntimeField } from '../../../common/utils/runtime_field_utils'; - -export const runtimeMappingsSchema = schema.object( - {}, - { - unknowns: 'allow', - validate: (v: object) => { - if (Object.values(v).some((o) => !isRuntimeField(o))) { - return 'Invalid runtime field'; - } - }, - } -); - -export const dataViewTitleSchema = schema.object({ - /** Title of the data view for which to return stats. */ - dataViewTitle: schema.string(), -}); - -export const dataVisualizerFieldHistogramsSchema = schema.object({ - /** Query to match documents in the index. */ - query: schema.any(), - /** The fields to return histogram data. */ - fields: schema.arrayOf(schema.any()), - /** Number of documents to be collected in the sample processed on each shard, or -1 for no sampling. */ - samplerShardSize: schema.number(), - /** Optional search time runtime fields */ - runtimeMappings: runtimeMappingsSchema, -}); - -export const dataVisualizerFieldStatsSchema = schema.object({ - /** Query to match documents in the index. */ - query: schema.any(), - fields: schema.arrayOf(schema.any()), - /** Number of documents to be collected in the sample processed on each shard, or -1 for no sampling. */ - samplerShardSize: schema.number(), - /** Name of the time field in the index (optional). */ - timeFieldName: schema.maybe(schema.string()), - /** Earliest timestamp for search, as epoch ms (optional). */ - earliest: schema.maybe(schema.number()), - /** Latest timestamp for search, as epoch ms (optional). */ - latest: schema.maybe(schema.number()), - /** Aggregation interval, in milliseconds, to use for obtaining document counts over time (optional). */ - interval: schema.maybe(schema.number()), - /** Maximum number of examples to return for text type fields. */ - maxExamples: schema.number(), - /** Optional search time runtime fields */ - runtimeMappings: runtimeMappingsSchema, -}); - -export const dataVisualizerOverallStatsSchema = schema.object({ - /** Query to match documents in the index. */ - query: schema.any(), - /** Names of aggregatable fields for which to return stats. */ - aggregatableFields: schema.arrayOf(schema.string()), - /** Names of non-aggregatable fields for which to return stats. */ - nonAggregatableFields: schema.arrayOf(schema.string()), - /** Number of documents to be collected in the sample processed on each shard, or -1 for no sampling. */ - samplerShardSize: schema.number(), - /** Name of the time field in the index (optional). */ - timeFieldName: schema.maybe(schema.string()), - /** Earliest timestamp for search, as epoch ms (optional). */ - earliest: schema.maybe(schema.number()), - /** Latest timestamp for search, as epoch ms (optional). */ - latest: schema.maybe(schema.number()), - /** Optional search time runtime fields */ - runtimeMappings: runtimeMappingsSchema, -}); diff --git a/x-pack/plugins/data_visualizer/server/types/chart_data.ts b/x-pack/plugins/data_visualizer/server/types/chart_data.ts deleted file mode 100644 index 99c23cf88b5ba..0000000000000 --- a/x-pack/plugins/data_visualizer/server/types/chart_data.ts +++ /dev/null @@ -1,168 +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 interface FieldData { - fieldName: string; - existsInDocs: boolean; - stats?: { - sampleCount?: number; - count?: number; - cardinality?: number; - }; -} - -export interface Field { - fieldName: string; - type: string; - cardinality: number; -} - -export interface HistogramField { - fieldName: string; - type: string; -} - -export interface Distribution { - percentiles: any[]; - minPercentile: number; - maxPercentile: number; -} - -export interface Aggs { - [key: string]: any; -} - -export interface Bucket { - doc_count: number; -} - -export interface NumericFieldStats { - fieldName: string; - count: number; - min: number; - max: number; - avg: number; - isTopValuesSampled: boolean; - topValues: Bucket[]; - topValuesSampleSize: number; - topValuesSamplerShardSize: number; - median?: number; - distribution?: Distribution; -} - -export interface StringFieldStats { - fieldName: string; - isTopValuesSampled: boolean; - topValues: Bucket[]; - topValuesSampleSize: number; - topValuesSamplerShardSize: number; -} - -export interface DateFieldStats { - fieldName: string; - count: number; - earliest: number; - latest: number; -} - -export interface BooleanFieldStats { - fieldName: string; - count: number; - trueCount: number; - falseCount: number; - [key: string]: number | string; -} - -export interface DocumentCountStats { - documentCounts: { - interval: number; - buckets: { [key: string]: number }; - }; -} - -export interface FieldExamples { - fieldName: string; - examples: any[]; -} - -export interface NumericColumnStats { - interval: number; - min: number; - max: number; -} -export type NumericColumnStatsMap = Record; - -export interface AggHistogram { - histogram: { - field: string; - interval: number; - }; -} - -export interface AggTerms { - terms: { - field: string; - size: number; - }; -} - -export interface NumericDataItem { - key: number; - key_as_string?: string; - doc_count: number; -} - -export interface NumericChartData { - data: NumericDataItem[]; - id: string; - interval: number; - stats: [number, number]; - type: 'numeric'; -} - -export interface OrdinalDataItem { - key: string; - key_as_string?: string; - doc_count: number; -} - -export interface OrdinalChartData { - type: 'ordinal' | 'boolean'; - cardinality: number; - data: OrdinalDataItem[]; - id: string; -} - -export interface UnsupportedChartData { - id: string; - type: 'unsupported'; -} - -export interface FieldAggCardinality { - field: string; - percent?: any; -} - -export interface ScriptAggCardinality { - script: any; -} - -export interface AggCardinality { - cardinality: FieldAggCardinality | ScriptAggCardinality; -} - -export type ChartRequestAgg = AggHistogram | AggCardinality | AggTerms; - -export type ChartData = NumericChartData | OrdinalChartData | UnsupportedChartData; - -export type BatchStats = - | NumericFieldStats - | StringFieldStats - | BooleanFieldStats - | DateFieldStats - | DocumentCountStats - | FieldExamples; diff --git a/x-pack/plugins/data_visualizer/server/types/deps.ts b/x-pack/plugins/data_visualizer/server/types/deps.ts index 1f6dba0592f6f..8ee8c75abe543 100644 --- a/x-pack/plugins/data_visualizer/server/types/deps.ts +++ b/x-pack/plugins/data_visualizer/server/types/deps.ts @@ -9,12 +9,18 @@ import type { SecurityPluginStart } from '../../../security/server'; import type { UsageCollectionSetup } from '../../../../../src/plugins/usage_collection/server'; import { CustomIntegrationsPluginSetup } from '../../../../../src/plugins/custom_integrations/server'; import { HomeServerPluginSetup } from '../../../../../src/plugins/home/server'; +import { + PluginSetup as DataPluginSetup, + PluginStart as DataPluginStart, +} from '../../../../../src/plugins/data/server'; export interface StartDeps { security?: SecurityPluginStart; + data: DataPluginStart; } export interface SetupDeps { usageCollection: UsageCollectionSetup; customIntegrations?: CustomIntegrationsPluginSetup; home?: HomeServerPluginSetup; + data: DataPluginSetup; } diff --git a/x-pack/plugins/data_visualizer/server/types/index.ts b/x-pack/plugins/data_visualizer/server/types/index.ts index e0379b514de32..2fc0fb2a6173b 100644 --- a/x-pack/plugins/data_visualizer/server/types/index.ts +++ b/x-pack/plugins/data_visualizer/server/types/index.ts @@ -5,4 +5,3 @@ * 2.0. */ export * from './deps'; -export * from './chart_data'; diff --git a/x-pack/plugins/enterprise_search/common/__mocks__/initial_app_data.ts b/x-pack/plugins/enterprise_search/common/__mocks__/initial_app_data.ts index 5d4844c3296d7..aa3020a9577f9 100644 --- a/x-pack/plugins/enterprise_search/common/__mocks__/initial_app_data.ts +++ b/x-pack/plugins/enterprise_search/common/__mocks__/initial_app_data.ts @@ -7,7 +7,6 @@ export const DEFAULT_INITIAL_APP_DATA = { readOnlyMode: false, - ilmEnabled: true, searchOAuth: { clientId: 'someUID', redirectUrl: 'http://localhost:3002/ws/search_callback', diff --git a/x-pack/plugins/enterprise_search/common/types/index.ts b/x-pack/plugins/enterprise_search/common/types/index.ts index b0b9eb6274875..8addf17f97476 100644 --- a/x-pack/plugins/enterprise_search/common/types/index.ts +++ b/x-pack/plugins/enterprise_search/common/types/index.ts @@ -16,7 +16,6 @@ import { export interface InitialAppData { readOnlyMode?: boolean; - ilmEnabled?: boolean; searchOAuth?: SearchOAuth; configuredLimits?: ConfiguredLimits; access?: ProductAccess; diff --git a/x-pack/plugins/enterprise_search/public/applications/__mocks__/kea_logic/flash_messages_logic.mock.ts b/x-pack/plugins/enterprise_search/public/applications/__mocks__/kea_logic/flash_messages_logic.mock.ts index 36a10fd234bfe..d55c69d478b16 100644 --- a/x-pack/plugins/enterprise_search/public/applications/__mocks__/kea_logic/flash_messages_logic.mock.ts +++ b/x-pack/plugins/enterprise_search/public/applications/__mocks__/kea_logic/flash_messages_logic.mock.ts @@ -27,6 +27,7 @@ export const mockFlashMessageHelpers = { clearFlashMessages: jest.fn(), flashSuccessToast: jest.fn(), flashErrorToast: jest.fn(), + toastAPIErrors: jest.fn(), }; jest.mock('../../shared/flash_messages', () => ({ diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/app_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/app_logic.test.ts index f69e3492d26eb..09b4292a29008 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/app_logic.test.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/app_logic.test.ts @@ -25,7 +25,6 @@ describe('AppLogic', () => { mount({}, DEFAULT_INITIAL_APP_DATA); expect(AppLogic.values).toEqual({ - ilmEnabled: true, configuredLimits: { engine: { maxDocumentByteSize: 102400, diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/app_logic.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/app_logic.ts index 90b37e6a4d4ee..96bf4c062dbaf 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/app_logic.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/app_logic.ts @@ -16,7 +16,6 @@ import { ConfiguredLimits, Account, Role } from './types'; import { getRoleAbilities } from './utils/role'; interface AppValues { - ilmEnabled: boolean; configuredLimits: ConfiguredLimits; account: Account; myRole: Role; @@ -41,7 +40,6 @@ export const AppLogic = kea ({ EngineLogic: { values: { engineName: 'test-engine' } }, @@ -18,6 +13,8 @@ jest.mock('../engine', () => ({ import { nextTick } from '@kbn/test/jest'; +import { itShowsServerErrorAsFlashMessage } from '../../../test_helpers'; + import { DEFAULT_START_DATE, DEFAULT_END_DATE } from './constants'; import { AnalyticsLogic } from './'; @@ -26,7 +23,6 @@ describe('AnalyticsLogic', () => { const { mount } = new LogicMounter(AnalyticsLogic); const { history } = mockKibanaValues; const { http } = mockHttpValues; - const { flashAPIErrors } = mockFlashMessageHelpers; const DEFAULT_VALUES = { dataLoading: true, @@ -197,14 +193,9 @@ describe('AnalyticsLogic', () => { ); }); - it('handles errors', async () => { - http.get.mockReturnValueOnce(Promise.reject('error')); + itShowsServerErrorAsFlashMessage(http.get, () => { mount(); - AnalyticsLogic.actions.loadAnalyticsData(); - await nextTick(); - - expect(flashAPIErrors).toHaveBeenCalledWith('error'); }); }); @@ -259,14 +250,9 @@ describe('AnalyticsLogic', () => { ); }); - it('handles errors', async () => { - http.get.mockReturnValueOnce(Promise.reject('error')); + itShowsServerErrorAsFlashMessage(http.get, () => { mount(); - AnalyticsLogic.actions.loadQueryData('some-query'); - await nextTick(); - - expect(flashAPIErrors).toHaveBeenCalledWith('error'); }); }); }); 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 index 2f3aedc8fa11d..51d51b5aee88c 100644 --- 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 @@ -17,12 +17,14 @@ import { nextTick } from '@kbn/test/jest'; import { DEFAULT_META } from '../../../shared/constants'; +import { itShowsServerErrorAsFlashMessage } from '../../../test_helpers'; + import { ApiLogsLogic } from './'; describe('ApiLogsLogic', () => { const { mount, unmount } = new LogicMounter(ApiLogsLogic); const { http } = mockHttpValues; - const { flashAPIErrors, flashErrorToast } = mockFlashMessageHelpers; + const { flashErrorToast } = mockFlashMessageHelpers; const DEFAULT_VALUES = { dataLoading: true, @@ -176,14 +178,9 @@ describe('ApiLogsLogic', () => { expect(ApiLogsLogic.actions.updateView).toHaveBeenCalledWith(MOCK_API_RESPONSE); }); - it('handles API errors', async () => { - http.get.mockReturnValueOnce(Promise.reject('error')); + itShowsServerErrorAsFlashMessage(http.get, () => { mount(); - ApiLogsLogic.actions.fetchApiLogs(); - await nextTick(); - - expect(flashAPIErrors).toHaveBeenCalledWith('error'); }); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/domains_table.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/domains_table.test.tsx index 76622f9c12822..7511f4ae2c2c3 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/domains_table.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/domains_table.test.tsx @@ -12,8 +12,9 @@ import React from 'react'; import { shallow, ShallowWrapper } from 'enzyme'; -import { EuiBasicTable, EuiButtonIcon, EuiInMemoryTable } from '@elastic/eui'; +import { EuiBasicTable, EuiButtonIcon } from '@elastic/eui'; +import { DEFAULT_META } from '../../../../shared/constants'; import { mountWithIntl } from '../../../../test_helpers'; import { CrawlerDomain } from '../types'; @@ -51,15 +52,19 @@ const domains: CrawlerDomain[] = [ const values = { // EngineLogic engineName: 'some-engine', - // CrawlerOverviewLogic + // CrawlerDomainsLogic domains, + meta: DEFAULT_META, + dataLoading: false, // AppLogic myRole: { canManageEngineCrawler: false }, }; const actions = { - // CrawlerOverviewLogic + // CrawlerDomainsLogic deleteDomain: jest.fn(), + fetchCrawlerDomainsData: jest.fn(), + onPaginate: jest.fn(), }; describe('DomainsTable', () => { @@ -69,17 +74,28 @@ describe('DomainsTable', () => { beforeEach(() => { jest.clearAllMocks(); }); + beforeAll(() => { setMockValues(values); setMockActions(actions); wrapper = shallow(); tableContent = mountWithIntl() - .find(EuiInMemoryTable) + .find(EuiBasicTable) .text(); }); it('renders', () => { - expect(wrapper.find(EuiInMemoryTable)).toHaveLength(1); + expect(wrapper.find(EuiBasicTable)).toHaveLength(1); + + expect(wrapper.find(EuiBasicTable).prop('pagination')).toEqual({ + hidePerPageOptions: true, + pageIndex: 0, + pageSize: 10, + totalItemCount: 0, + }); + + wrapper.find(EuiBasicTable).simulate('change', { page: { index: 2 } }); + expect(actions.onPaginate).toHaveBeenCalledWith(3); }); describe('columns', () => { @@ -88,7 +104,7 @@ describe('DomainsTable', () => { }); it('renders a clickable domain url', () => { - const basicTable = wrapper.find(EuiInMemoryTable).dive().find(EuiBasicTable).dive(); + const basicTable = wrapper.find(EuiBasicTable).dive(); const link = basicTable.find('[data-test-subj="CrawlerDomainURL"]').at(0); expect(link.dive().text()).toContain('elastic.co'); @@ -110,7 +126,7 @@ describe('DomainsTable', () => { }); describe('actions column', () => { - const getTable = () => wrapper.find(EuiInMemoryTable).dive().find(EuiBasicTable).dive(); + const getTable = () => wrapper.find(EuiBasicTable).dive(); const getActions = () => getTable().find('ExpandedItemActions'); const getActionItems = () => getActions().first().dive().find('DefaultItemAction'); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/domains_table.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/domains_table.tsx index 1f0f6be22102f..b8d8159be7b16 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/domains_table.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/domains_table.tsx @@ -5,11 +5,11 @@ * 2.0. */ -import React from 'react'; +import React, { useEffect } from 'react'; import { useActions, useValues } from 'kea'; -import { EuiInMemoryTable, EuiBasicTableColumn } from '@elastic/eui'; +import { EuiBasicTableColumn, EuiBasicTable } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; @@ -18,11 +18,11 @@ import { FormattedNumber } from '@kbn/i18n/react'; import { DELETE_BUTTON_LABEL, MANAGE_BUTTON_LABEL } from '../../../../shared/constants'; import { KibanaLogic } from '../../../../shared/kibana'; import { EuiLinkTo } from '../../../../shared/react_router_helpers'; +import { convertMetaToPagination, handlePageChange } from '../../../../shared/table_pagination'; import { AppLogic } from '../../../app_logic'; import { ENGINE_CRAWLER_DOMAIN_PATH } from '../../../routes'; import { generateEnginePath } from '../../engine'; -import { CrawlerLogic } from '../crawler_logic'; -import { CrawlerOverviewLogic } from '../crawler_overview_logic'; +import { CrawlerDomainsLogic } from '../crawler_domains_logic'; import { CrawlerDomain } from '../types'; import { getDeleteDomainConfirmationMessage } from '../utils'; @@ -30,9 +30,12 @@ import { getDeleteDomainConfirmationMessage } from '../utils'; import { CustomFormattedTimestamp } from './custom_formatted_timestamp'; export const DomainsTable: React.FC = () => { - const { domains } = useValues(CrawlerLogic); + const { domains, meta, dataLoading } = useValues(CrawlerDomainsLogic); + const { fetchCrawlerDomainsData, onPaginate, deleteDomain } = useActions(CrawlerDomainsLogic); - const { deleteDomain } = useActions(CrawlerOverviewLogic); + useEffect(() => { + fetchCrawlerDomainsData(); + }, [meta.page.current]); const { myRole: { canManageEngineCrawler }, @@ -125,5 +128,16 @@ export const DomainsTable: React.FC = () => { columns.push(actionsColumn); } - return ; + return ( + + ); }; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_domains_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_domains_logic.test.ts new file mode 100644 index 0000000000000..fda96ca5f8381 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_domains_logic.test.ts @@ -0,0 +1,203 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { + LogicMounter, + mockHttpValues, + mockFlashMessageHelpers, +} from '../../../__mocks__/kea_logic'; +import '../../__mocks__/engine_logic.mock'; + +import { nextTick } from '@kbn/test/jest'; + +import { Meta } from '../../../../../common/types'; + +import { DEFAULT_META } from '../../../shared/constants'; + +import { itShowsServerErrorAsFlashMessage } from '../../../test_helpers/error_handling'; + +import { CrawlerDomainsLogic, CrawlerDomainsValues } from './crawler_domains_logic'; +import { CrawlerDataFromServer, CrawlerDomain, CrawlerDomainFromServer } from './types'; +import { crawlerDataServerToClient } from './utils'; + +const DEFAULT_VALUES: CrawlerDomainsValues = { + dataLoading: true, + domains: [], + meta: DEFAULT_META, +}; + +const crawlerDataResponse: CrawlerDataFromServer = { + domains: [ + { + id: '507f1f77bcf86cd799439011', + name: 'elastic.co', + created_on: 'Mon, 31 Aug 2020 17:00:00 +0000', + document_count: 13, + sitemaps: [], + entry_points: [], + crawl_rules: [], + deduplication_enabled: false, + deduplication_fields: ['title'], + available_deduplication_fields: ['title', 'description'], + }, + ], + events: [], + most_recent_crawl_request: null, +}; + +const clientCrawlerData = crawlerDataServerToClient(crawlerDataResponse); + +const domainsFromServer: CrawlerDomainFromServer[] = [ + { + name: 'http://www.example.com', + created_on: 'foo', + document_count: 10, + id: '1', + crawl_rules: [], + entry_points: [], + sitemaps: [], + deduplication_enabled: true, + deduplication_fields: [], + available_deduplication_fields: [], + }, +]; + +const domains: CrawlerDomain[] = [ + { + createdOn: 'foo', + documentCount: 10, + id: '1', + url: 'http://www.example.com', + crawlRules: [], + entryPoints: [], + sitemaps: [], + deduplicationEnabled: true, + deduplicationFields: [], + availableDeduplicationFields: [], + }, +]; + +const meta: Meta = { + page: { + current: 2, + size: 100, + total_pages: 5, + total_results: 500, + }, +}; + +describe('CrawlerDomainsLogic', () => { + const { mount } = new LogicMounter(CrawlerDomainsLogic); + const { http } = mockHttpValues; + const { flashAPIErrors } = mockFlashMessageHelpers; + + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('has expected default values', () => { + mount(); + expect(CrawlerDomainsLogic.values).toEqual(DEFAULT_VALUES); + }); + + describe('actions', () => { + describe('onReceiveData', () => { + it('sets state from an API call', () => { + mount(); + + CrawlerDomainsLogic.actions.onReceiveData(domains, meta); + + expect(CrawlerDomainsLogic.values).toEqual({ + ...DEFAULT_VALUES, + domains, + meta, + dataLoading: false, + }); + }); + }); + + describe('onPaginate', () => { + it('sets dataLoading to true & sets meta state', () => { + mount({ dataLoading: false }); + CrawlerDomainsLogic.actions.onPaginate(5); + + expect(CrawlerDomainsLogic.values).toEqual({ + ...DEFAULT_VALUES, + dataLoading: true, + meta: { + ...DEFAULT_META, + page: { + ...DEFAULT_META.page, + current: 5, + }, + }, + }); + }); + }); + }); + + describe('listeners', () => { + describe('fetchCrawlerDomainsData', () => { + it('updates logic with data that has been converted from server to client', async () => { + mount(); + jest.spyOn(CrawlerDomainsLogic.actions, 'onReceiveData'); + + http.get.mockReturnValueOnce( + Promise.resolve({ + results: domainsFromServer, + meta, + }) + ); + + CrawlerDomainsLogic.actions.fetchCrawlerDomainsData(); + await nextTick(); + + expect(http.get).toHaveBeenCalledWith( + '/internal/app_search/engines/some-engine/crawler/domains', + { + query: { 'page[current]': 1, 'page[size]': 10 }, + } + ); + expect(CrawlerDomainsLogic.actions.onReceiveData).toHaveBeenCalledWith(domains, meta); + }); + + it('displays any errors to the user', async () => { + mount(); + http.get.mockReturnValueOnce(Promise.reject('error')); + + CrawlerDomainsLogic.actions.fetchCrawlerDomainsData(); + await nextTick(); + + expect(flashAPIErrors).toHaveBeenCalledWith('error'); + }); + }); + + describe('deleteDomain', () => { + it('deletes the domain and then calls crawlerDomainDeleted with the response', async () => { + jest.spyOn(CrawlerDomainsLogic.actions, 'crawlerDomainDeleted'); + http.delete.mockReturnValue(Promise.resolve(crawlerDataResponse)); + + CrawlerDomainsLogic.actions.deleteDomain({ id: '1234' } as CrawlerDomain); + await nextTick(); + + expect(http.delete).toHaveBeenCalledWith( + '/internal/app_search/engines/some-engine/crawler/domains/1234', + { + query: { respond_with: 'crawler_details' }, + } + ); + expect(CrawlerDomainsLogic.actions.crawlerDomainDeleted).toHaveBeenCalledWith( + clientCrawlerData + ); + }); + + itShowsServerErrorAsFlashMessage(http.delete, () => { + CrawlerDomainsLogic.actions.deleteDomain({ id: '1234' } as CrawlerDomain); + }); + }); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_domains_logic.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_domains_logic.ts new file mode 100644 index 0000000000000..e26e9528ee1d2 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_domains_logic.ts @@ -0,0 +1,125 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { kea, MakeLogicType } from 'kea'; + +import { Meta } from '../../../../../common/types'; +import { DEFAULT_META } from '../../../shared/constants'; +import { flashAPIErrors } from '../../../shared/flash_messages'; +import { HttpLogic } from '../../../shared/http'; +import { updateMetaPageIndex } from '../../../shared/table_pagination'; +import { EngineLogic } from '../engine'; + +import { + CrawlerData, + CrawlerDataFromServer, + CrawlerDomain, + CrawlerDomainFromServer, +} from './types'; +import { crawlerDataServerToClient, crawlerDomainServerToClient } from './utils'; + +export interface CrawlerDomainsValues { + dataLoading: boolean; + domains: CrawlerDomain[]; + meta: Meta; +} + +interface CrawlerDomainsResponse { + results: CrawlerDomainFromServer[]; + meta: Meta; +} + +interface CrawlerDomainsActions { + deleteDomain(domain: CrawlerDomain): { domain: CrawlerDomain }; + fetchCrawlerDomainsData(): void; + onPaginate(newPageIndex: number): { newPageIndex: number }; + onReceiveData(domains: CrawlerDomain[], meta: Meta): { domains: CrawlerDomain[]; meta: Meta }; + crawlerDomainDeleted(data: CrawlerData): { data: CrawlerData }; +} + +export const CrawlerDomainsLogic = kea>({ + path: ['enterprise_search', 'app_search', 'crawler', 'crawler_domains_logic'], + actions: { + deleteDomain: (domain) => ({ domain }), + fetchCrawlerDomainsData: true, + onReceiveData: (domains, meta) => ({ domains, meta }), + onPaginate: (newPageIndex) => ({ newPageIndex }), + crawlerDomainDeleted: (data) => ({ data }), + }, + reducers: { + dataLoading: [ + true, + { + onReceiveData: () => false, + onPaginate: () => true, + }, + ], + domains: [ + [], + { + onReceiveData: (_, { domains }) => domains, + }, + ], + meta: [ + DEFAULT_META, + { + onReceiveData: (_, { meta }) => meta, + onPaginate: (state, { newPageIndex }) => updateMetaPageIndex(state, newPageIndex), + }, + ], + }, + listeners: ({ actions, values }) => ({ + fetchCrawlerDomainsData: async () => { + const { http } = HttpLogic.values; + const { engineName } = EngineLogic.values; + const { meta } = values; + + const query = { + 'page[current]': meta.page.current, + 'page[size]': meta.page.size, + }; + + try { + const response = await http.get( + `/internal/app_search/engines/${engineName}/crawler/domains`, + { + query, + } + ); + + const domains = response.results.map(crawlerDomainServerToClient); + + actions.onReceiveData(domains, response.meta); + } catch (e) { + flashAPIErrors(e); + } + }, + + deleteDomain: async ({ domain }) => { + const { http } = HttpLogic.values; + const { engineName } = EngineLogic.values; + + try { + const response = await http.delete( + `/internal/app_search/engines/${engineName}/crawler/domains/${domain.id}`, + { + query: { + respond_with: 'crawler_details', + }, + } + ); + + const crawlerData = crawlerDataServerToClient(response); + // Publish for other logic files to listen for + actions.crawlerDomainDeleted(crawlerData); + actions.fetchCrawlerDomainsData(); + } catch (e) { + flashAPIErrors(e); + } + }, + }), +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_logic.test.ts index 53c980c9750f5..b2321073f3d95 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_logic.test.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_logic.test.ts @@ -14,6 +14,9 @@ import '../../__mocks__/engine_logic.mock'; import { nextTick } from '@kbn/test/jest'; +import { itShowsServerErrorAsFlashMessage } from '../../../test_helpers'; + +import { CrawlerDomainsLogic } from './crawler_domains_logic'; import { CrawlerLogic, CrawlerValues } from './crawler_logic'; import { CrawlerData, @@ -159,6 +162,16 @@ describe('CrawlerLogic', () => { }); describe('listeners', () => { + describe('CrawlerDomainsLogic.actionTypes.crawlerDomainDeleted', () => { + it('updates data in state when a domain is deleted', () => { + jest.spyOn(CrawlerLogic.actions, 'onReceiveCrawlerData'); + CrawlerDomainsLogic.actions.crawlerDomainDeleted(MOCK_CLIENT_CRAWLER_DATA); + expect(CrawlerLogic.actions.onReceiveCrawlerData).toHaveBeenCalledWith( + MOCK_CLIENT_CRAWLER_DATA + ); + }); + }); + describe('fetchCrawlerData', () => { it('updates logic with data that has been converted from server to client', async () => { jest.spyOn(CrawlerLogic.actions, 'onReceiveCrawlerData'); @@ -269,15 +282,8 @@ describe('CrawlerLogic', () => { }); }); - describe('on failure', () => { - it('flashes an error message', async () => { - http.post.mockReturnValueOnce(Promise.reject('error')); - - CrawlerLogic.actions.startCrawl(); - await nextTick(); - - expect(flashAPIErrors).toHaveBeenCalledWith('error'); - }); + itShowsServerErrorAsFlashMessage(http.post, () => { + CrawlerLogic.actions.startCrawl(); }); }); @@ -297,16 +303,8 @@ describe('CrawlerLogic', () => { }); }); - describe('on failure', () => { - it('flashes an error message', async () => { - jest.spyOn(CrawlerLogic.actions, 'fetchCrawlerData'); - http.post.mockReturnValueOnce(Promise.reject('error')); - - CrawlerLogic.actions.stopCrawl(); - await nextTick(); - - expect(flashAPIErrors).toHaveBeenCalledWith('error'); - }); + itShowsServerErrorAsFlashMessage(http.post, () => { + CrawlerLogic.actions.stopCrawl(); }); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_logic.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_logic.ts index d1530c79a6821..08a01af67ece6 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_logic.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_logic.ts @@ -12,6 +12,8 @@ import { flashAPIErrors } from '../../../shared/flash_messages'; import { HttpLogic } from '../../../shared/http'; import { EngineLogic } from '../engine'; +import { CrawlerDomainsLogic } from './crawler_domains_logic'; + import { CrawlerData, CrawlerDomain, @@ -166,6 +168,9 @@ export const CrawlerLogic = kea>({ actions.onCreateNewTimeout(timeoutIdId); }, + [CrawlerDomainsLogic.actionTypes.crawlerDomainDeleted]: ({ data }) => { + actions.onReceiveCrawlerData(data); + }, }), events: ({ values }) => ({ beforeUnmount: () => { diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_overview_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_overview_logic.test.ts deleted file mode 100644 index a701c43d4775c..0000000000000 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_overview_logic.test.ts +++ /dev/null @@ -1,93 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { - LogicMounter, - mockHttpValues, - mockFlashMessageHelpers, -} from '../../../__mocks__/kea_logic'; -import '../../__mocks__/engine_logic.mock'; - -jest.mock('./crawler_logic', () => ({ - CrawlerLogic: { - actions: { - onReceiveCrawlerData: jest.fn(), - }, - }, -})); - -import { nextTick } from '@kbn/test/jest'; - -import { CrawlerLogic } from './crawler_logic'; -import { CrawlerOverviewLogic } from './crawler_overview_logic'; - -import { CrawlerDataFromServer, CrawlerDomain } from './types'; -import { crawlerDataServerToClient } from './utils'; - -const MOCK_SERVER_CRAWLER_DATA: CrawlerDataFromServer = { - domains: [ - { - id: '507f1f77bcf86cd799439011', - name: 'elastic.co', - created_on: 'Mon, 31 Aug 2020 17:00:00 +0000', - document_count: 13, - sitemaps: [], - entry_points: [], - crawl_rules: [], - deduplication_enabled: false, - deduplication_fields: ['title'], - available_deduplication_fields: ['title', 'description'], - }, - ], - events: [], - most_recent_crawl_request: null, -}; - -const MOCK_CLIENT_CRAWLER_DATA = crawlerDataServerToClient(MOCK_SERVER_CRAWLER_DATA); - -describe('CrawlerOverviewLogic', () => { - const { mount } = new LogicMounter(CrawlerOverviewLogic); - const { http } = mockHttpValues; - const { flashAPIErrors, flashSuccessToast } = mockFlashMessageHelpers; - - beforeEach(() => { - jest.clearAllMocks(); - mount(); - }); - - describe('listeners', () => { - describe('deleteDomain', () => { - it('calls onReceiveCrawlerData with retrieved data that has been converted from server to client', async () => { - jest.spyOn(CrawlerLogic.actions, 'onReceiveCrawlerData'); - http.delete.mockReturnValue(Promise.resolve(MOCK_SERVER_CRAWLER_DATA)); - - CrawlerOverviewLogic.actions.deleteDomain({ id: '1234' } as CrawlerDomain); - await nextTick(); - - expect(http.delete).toHaveBeenCalledWith( - '/internal/app_search/engines/some-engine/crawler/domains/1234', - { - query: { respond_with: 'crawler_details' }, - } - ); - expect(CrawlerLogic.actions.onReceiveCrawlerData).toHaveBeenCalledWith( - MOCK_CLIENT_CRAWLER_DATA - ); - expect(flashSuccessToast).toHaveBeenCalled(); - }); - - it('calls flashApiErrors when there is an error', async () => { - http.delete.mockReturnValue(Promise.reject('error')); - - CrawlerOverviewLogic.actions.deleteDomain({ id: '1234' } as CrawlerDomain); - await nextTick(); - - expect(flashAPIErrors).toHaveBeenCalledWith('error'); - }); - }); - }); -}); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_overview_logic.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_overview_logic.ts deleted file mode 100644 index 605d45effaa24..0000000000000 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_overview_logic.ts +++ /dev/null @@ -1,50 +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 { kea, MakeLogicType } from 'kea'; - -import { flashAPIErrors, flashSuccessToast } from '../../../shared/flash_messages'; - -import { HttpLogic } from '../../../shared/http'; -import { EngineLogic } from '../engine'; - -import { CrawlerLogic } from './crawler_logic'; -import { CrawlerDataFromServer, CrawlerDomain } from './types'; -import { crawlerDataServerToClient, getDeleteDomainSuccessMessage } from './utils'; - -interface CrawlerOverviewActions { - deleteDomain(domain: CrawlerDomain): { domain: CrawlerDomain }; -} - -export const CrawlerOverviewLogic = kea>({ - path: ['enterprise_search', 'app_search', 'crawler', 'crawler_overview'], - actions: { - deleteDomain: (domain) => ({ domain }), - }, - listeners: () => ({ - deleteDomain: async ({ domain }) => { - const { http } = HttpLogic.values; - const { engineName } = EngineLogic.values; - - try { - const response = await http.delete( - `/internal/app_search/engines/${engineName}/crawler/domains/${domain.id}`, - { - query: { - respond_with: 'crawler_details', - }, - } - ); - const crawlerData = crawlerDataServerToClient(response); - CrawlerLogic.actions.onReceiveCrawlerData(crawlerData); - flashSuccessToast(getDeleteDomainSuccessMessage(domain.url)); - } catch (e) { - flashAPIErrors(e); - } - }, - }), -}); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_single_domain.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_single_domain.test.tsx index beb1e65af47a4..ed445b923ea2a 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_single_domain.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_single_domain.test.tsx @@ -28,9 +28,6 @@ const MOCK_VALUES = { domain: { url: 'https://elastic.co', }, - // CrawlerOverviewLogic - domains: [], - crawlRequests: [], }; const MOCK_ACTIONS = { diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_single_domain_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_single_domain_logic.test.ts index 03e20ea988f98..547218ad6a2c1 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_single_domain_logic.test.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_single_domain_logic.test.ts @@ -23,6 +23,8 @@ jest.mock('./crawler_logic', () => ({ import { nextTick } from '@kbn/test/jest'; +import { itShowsServerErrorAsFlashMessage } from '../../../test_helpers'; + import { CrawlerLogic } from './crawler_logic'; import { CrawlerSingleDomainLogic, CrawlerSingleDomainValues } from './crawler_single_domain_logic'; import { CrawlerDomain, CrawlerPolicies, CrawlerRules } from './types'; @@ -35,7 +37,7 @@ const DEFAULT_VALUES: CrawlerSingleDomainValues = { describe('CrawlerSingleDomainLogic', () => { const { mount } = new LogicMounter(CrawlerSingleDomainLogic); const { http } = mockHttpValues; - const { flashAPIErrors, flashSuccessToast } = mockFlashMessageHelpers; + const { flashSuccessToast } = mockFlashMessageHelpers; beforeEach(() => { jest.clearAllMocks(); @@ -176,13 +178,8 @@ describe('CrawlerSingleDomainLogic', () => { expect(navigateToUrl).toHaveBeenCalledWith('/engines/some-engine/crawler'); }); - it('calls flashApiErrors when there is an error', async () => { - http.delete.mockReturnValue(Promise.reject('error')); - + itShowsServerErrorAsFlashMessage(http.delete, () => { CrawlerSingleDomainLogic.actions.deleteDomain({ id: '1234' } as CrawlerDomain); - await nextTick(); - - expect(flashAPIErrors).toHaveBeenCalledWith('error'); }); }); @@ -218,13 +215,8 @@ describe('CrawlerSingleDomainLogic', () => { }); }); - it('displays any errors to the user', async () => { - http.get.mockReturnValueOnce(Promise.reject('error')); - + itShowsServerErrorAsFlashMessage(http.get, () => { CrawlerSingleDomainLogic.actions.fetchDomainData('507f1f77bcf86cd799439011'); - await nextTick(); - - expect(flashAPIErrors).toHaveBeenCalledWith('error'); }); }); @@ -272,16 +264,11 @@ describe('CrawlerSingleDomainLogic', () => { }); }); - it('displays any errors to the user', async () => { - http.put.mockReturnValueOnce(Promise.reject('error')); - + itShowsServerErrorAsFlashMessage(http.put, () => { CrawlerSingleDomainLogic.actions.submitDeduplicationUpdate( { id: '507f1f77bcf86cd799439011' } as CrawlerDomain, { fields: ['title'], enabled: true } ); - await nextTick(); - - expect(flashAPIErrors).toHaveBeenCalledWith('error'); }); }); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_logic.test.ts index 3afa531239dc1..decb98d227975 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_logic.test.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_logic.test.ts @@ -20,6 +20,7 @@ jest.mock('../../app_logic', () => ({ selectors: { myRole: jest.fn(() => ({})) }, }, })); +import { itShowsServerErrorAsFlashMessage } from '../../../test_helpers'; import { AppLogic } from '../../app_logic'; import { EngineTypes } from '../engine/types'; @@ -31,7 +32,7 @@ import { CredentialsLogic } from './credentials_logic'; describe('CredentialsLogic', () => { const { mount } = new LogicMounter(CredentialsLogic); const { http } = mockHttpValues; - const { clearFlashMessages, flashSuccessToast, flashAPIErrors } = mockFlashMessageHelpers; + const { clearFlashMessages, flashSuccessToast } = mockFlashMessageHelpers; const DEFAULT_VALUES = { activeApiToken: { @@ -1059,14 +1060,9 @@ describe('CredentialsLogic', () => { expect(CredentialsLogic.actions.setCredentialsData).toHaveBeenCalledWith(meta, results); }); - it('handles errors', async () => { + itShowsServerErrorAsFlashMessage(http.get, () => { mount(); - http.get.mockReturnValue(Promise.reject('An error occured')); - CredentialsLogic.actions.fetchCredentials(); - await nextTick(); - - expect(flashAPIErrors).toHaveBeenCalledWith('An error occured'); }); }); @@ -1086,14 +1082,9 @@ describe('CredentialsLogic', () => { ); }); - it('handles errors', async () => { + itShowsServerErrorAsFlashMessage(http.get, () => { mount(); - http.get.mockReturnValue(Promise.reject('An error occured')); - CredentialsLogic.actions.fetchDetails(); - await nextTick(); - - expect(flashAPIErrors).toHaveBeenCalledWith('An error occured'); }); }); @@ -1113,14 +1104,9 @@ describe('CredentialsLogic', () => { expect(flashSuccessToast).toHaveBeenCalled(); }); - it('handles errors', async () => { + itShowsServerErrorAsFlashMessage(http.delete, () => { mount(); - http.delete.mockReturnValue(Promise.reject('An error occured')); - CredentialsLogic.actions.deleteApiKey(tokenName); - await nextTick(); - - expect(flashAPIErrors).toHaveBeenCalledWith('An error occured'); }); }); @@ -1172,14 +1158,9 @@ describe('CredentialsLogic', () => { expect(flashSuccessToast).toHaveBeenCalled(); }); - it('handles errors', async () => { + itShowsServerErrorAsFlashMessage(http.post, () => { mount(); - http.post.mockReturnValue(Promise.reject('An error occured')); - CredentialsLogic.actions.onApiTokenChange(); - await nextTick(); - - expect(flashAPIErrors).toHaveBeenCalledWith('An error occured'); }); describe('token type data', () => { diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/components/suggestions_callout.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/components/suggestions_callout.tsx index 490e6323290f0..ed46a878f0cea 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/components/suggestions_callout.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/components/suggestions_callout.tsx @@ -28,6 +28,7 @@ interface SuggestionsCalloutProps { description: string; buttonTo: string; lastUpdatedTimestamp: string; // ISO string like '2021-10-04T18:53:02.784Z' + style?: React.CSSProperties; } export const SuggestionsCallout: React.FC = ({ @@ -35,6 +36,7 @@ export const SuggestionsCallout: React.FC = ({ description, buttonTo, lastUpdatedTimestamp, + style, }) => { const { pathname } = useLocation(); @@ -49,7 +51,7 @@ export const SuggestionsCallout: React.FC = ({ return ( <> - +

    {description}

    @@ -80,7 +82,6 @@ export const SuggestionsCallout: React.FC = ({
    - ); }; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/components/suggestions_logic.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/components/suggestions_logic.test.tsx index 3e12aa7b629f0..a3ca646bd9f54 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/components/suggestions_logic.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/components/suggestions_logic.test.tsx @@ -5,17 +5,15 @@ * 2.0. */ -import { - LogicMounter, - mockFlashMessageHelpers, - mockHttpValues, -} from '../../../../__mocks__/kea_logic'; +import { LogicMounter, mockHttpValues } from '../../../../__mocks__/kea_logic'; import '../../../__mocks__/engine_logic.mock'; import { nextTick } from '@kbn/test/jest'; import { DEFAULT_META } from '../../../../shared/constants'; +import { itShowsServerErrorAsFlashMessage } from '../../../../test_helpers'; + import { SuggestionsAPIResponse, SuggestionsLogic } from './suggestions_logic'; const DEFAULT_VALUES = { @@ -52,7 +50,6 @@ const MOCK_RESPONSE: SuggestionsAPIResponse = { describe('SuggestionsLogic', () => { const { mount } = new LogicMounter(SuggestionsLogic); - const { flashAPIErrors } = mockFlashMessageHelpers; const { http } = mockHttpValues; beforeEach(() => { @@ -140,14 +137,9 @@ describe('SuggestionsLogic', () => { expect(SuggestionsLogic.actions.onSuggestionsLoaded).toHaveBeenCalledWith(MOCK_RESPONSE); }); - it('handles errors', async () => { - http.post.mockReturnValueOnce(Promise.reject('error')); + itShowsServerErrorAsFlashMessage(http.post, () => { mount(); - SuggestionsLogic.actions.loadSuggestions(); - await nextTick(); - - expect(flashAPIErrors).toHaveBeenCalledWith('error'); }); }); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curation/automated_curation_history.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curation/automated_curation_history.test.tsx index b7d1b6f9ed809..cd8ba123b5843 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curation/automated_curation_history.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curation/automated_curation_history.test.tsx @@ -17,7 +17,7 @@ describe('AutomatedCurationHistory', () => { it('renders', () => { const wrapper = shallow(); expect(wrapper.find(EntSearchLogStream).prop('query')).toEqual( - 'appsearch.search_relevance_suggestions.query: some text and event.kind: event and event.dataset: search-relevance-suggestions and appsearch.search_relevance_suggestions.engine: foo and event.action: curation_suggestion and appsearch.search_relevance_suggestions.suggestion.new_status: automated' + 'appsearch.adaptive_relevance.query: some text and event.kind: event and event.dataset: search-relevance-suggestions and appsearch.adaptive_relevance.engine: foo and event.action: curation_suggestion and appsearch.adaptive_relevance.suggestion.new_status: automated' ); }); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curation/automated_curation_history.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curation/automated_curation_history.tsx index debe8f86cfe2b..7fb91daf2e590 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curation/automated_curation_history.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curation/automated_curation_history.tsx @@ -19,12 +19,12 @@ interface Props { export const AutomatedCurationHistory: React.FC = ({ query, engineName }) => { const filters = [ - `appsearch.search_relevance_suggestions.query: ${query}`, + `appsearch.adaptive_relevance.query: ${query}`, 'event.kind: event', 'event.dataset: search-relevance-suggestions', - `appsearch.search_relevance_suggestions.engine: ${engineName}`, + `appsearch.adaptive_relevance.engine: ${engineName}`, 'event.action: curation_suggestion', - 'appsearch.search_relevance_suggestions.suggestion.new_status: automated', + 'appsearch.adaptive_relevance.suggestion.new_status: automated', ]; return ( diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curation/curation_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curation/curation_logic.test.ts index 2b51cbb884ff9..644139250c07c 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curation/curation_logic.test.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curation/curation_logic.test.ts @@ -15,6 +15,8 @@ import '../../../__mocks__/engine_logic.mock'; import { nextTick } from '@kbn/test/jest'; +import { itShowsServerErrorAsFlashMessage } from '../../../../test_helpers'; + import { CurationLogic } from './'; describe('CurationLogic', () => { @@ -309,14 +311,8 @@ describe('CurationLogic', () => { expect(CurationLogic.actions.loadCuration).toHaveBeenCalled(); }); - it('flashes any error messages', async () => { - http.put.mockReturnValueOnce(Promise.reject('error')); - mount({ activeQuery: 'some query' }); - + itShowsServerErrorAsFlashMessage(http.put, () => { CurationLogic.actions.convertToManual(); - await nextTick(); - - expect(flashAPIErrors).toHaveBeenCalledWith('error'); }); }); @@ -336,14 +332,9 @@ describe('CurationLogic', () => { expect(navigateToUrl).toHaveBeenCalledWith('/engines/some-engine/curations'); }); - it('flashes any errors', async () => { - http.delete.mockReturnValueOnce(Promise.reject('error')); + itShowsServerErrorAsFlashMessage(http.delete, () => { mount({}, { curationId: 'cur-404' }); - CurationLogic.actions.deleteCuration(); - await nextTick(); - - expect(flashAPIErrors).toHaveBeenCalledWith('error'); }); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curation/suggested_documents_callout.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curation/suggested_documents_callout.tsx index ec296089a1086..cd9b57651c00a 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curation/suggested_documents_callout.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curation/suggested_documents_callout.tsx @@ -35,6 +35,7 @@ export const SuggestedDocumentsCallout: React.FC = () => { return ( { @@ -130,14 +132,9 @@ describe('CurationsLogic', () => { ); }); - it('handles errors', async () => { - http.get.mockReturnValueOnce(Promise.reject('error')); + itShowsServerErrorAsFlashMessage(http.get, () => { mount(); - CurationsLogic.actions.loadCurations(); - await nextTick(); - - expect(flashAPIErrors).toHaveBeenCalledWith('error'); }); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/views/curation_suggestion/curation_suggestion_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/views/curation_suggestion/curation_suggestion_logic.test.ts index 171c774d8add2..01d8107067e18 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/views/curation_suggestion/curation_suggestion_logic.test.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/views/curation_suggestion/curation_suggestion_logic.test.ts @@ -16,6 +16,7 @@ import '../../../../__mocks__/engine_logic.mock'; import { nextTick } from '@kbn/test/jest'; +import { itShowsServerErrorAsFlashMessage } from '../../../../../test_helpers'; import { HydratedCurationSuggestion } from '../../types'; import { CurationSuggestionLogic } from './curation_suggestion_logic'; @@ -180,20 +181,6 @@ describe('CurationSuggestionLogic', () => { }); }; - const itHandlesErrors = (httpMethod: any, callback: () => void) => { - it('handles errors', async () => { - httpMethod.mockReturnValueOnce(Promise.reject('error')); - mountLogic({ - suggestion, - }); - - callback(); - await nextTick(); - - expect(flashAPIErrors).toHaveBeenCalledWith('error'); - }); - }; - beforeEach(() => { jest.clearAllMocks(); }); @@ -271,7 +258,7 @@ describe('CurationSuggestionLogic', () => { expect(navigateToUrl).toHaveBeenCalledWith('/engines/some-engine/curations'); }); - itHandlesErrors(http.get, () => { + itShowsServerErrorAsFlashMessage(http.get, () => { CurationSuggestionLogic.actions.loadSuggestion(); }); }); @@ -350,7 +337,8 @@ describe('CurationSuggestionLogic', () => { }); }); - itHandlesErrors(http.put, () => { + itShowsServerErrorAsFlashMessage(http.put, () => { + jest.spyOn(global, 'confirm').mockReturnValueOnce(true); CurationSuggestionLogic.actions.acceptSuggestion(); }); @@ -433,7 +421,8 @@ describe('CurationSuggestionLogic', () => { }); }); - itHandlesErrors(http.put, () => { + itShowsServerErrorAsFlashMessage(http.put, () => { + jest.spyOn(global, 'confirm').mockReturnValueOnce(true); CurationSuggestionLogic.actions.acceptAndAutomateSuggestion(); }); @@ -478,7 +467,7 @@ describe('CurationSuggestionLogic', () => { expect(navigateToUrl).toHaveBeenCalledWith('/engines/some-engine/curations'); }); - itHandlesErrors(http.put, () => { + itShowsServerErrorAsFlashMessage(http.put, () => { CurationSuggestionLogic.actions.rejectSuggestion(); }); @@ -523,7 +512,7 @@ describe('CurationSuggestionLogic', () => { expect(navigateToUrl).toHaveBeenCalledWith('/engines/some-engine/curations'); }); - itHandlesErrors(http.put, () => { + itShowsServerErrorAsFlashMessage(http.put, () => { CurationSuggestionLogic.actions.rejectAndDisableSuggestion(); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/views/curations_history/components/automated_curations_history_panel.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/views/curations_history/components/automated_curations_history_panel.test.tsx index 044637ff1c823..36703dc0d0d85 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/views/curations_history/components/automated_curations_history_panel.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/views/curations_history/components/automated_curations_history_panel.test.tsx @@ -29,7 +29,7 @@ describe('AutomatedCurationsHistoryPanel', () => { expect(wrapper.is(DataPanel)).toBe(true); expect(wrapper.find(EntSearchLogStream).prop('query')).toEqual( - 'event.kind: event and event.dataset: search-relevance-suggestions and appsearch.search_relevance_suggestions.engine: some-engine and event.action: curation_suggestion and appsearch.search_relevance_suggestions.suggestion.new_status: automated' + 'event.kind: event and event.dataset: search-relevance-suggestions and appsearch.adaptive_relevance.engine: some-engine and event.action: curation_suggestion and appsearch.adaptive_relevance.suggestion.new_status: automated' ); }); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/views/curations_history/components/automated_curations_history_panel.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/views/curations_history/components/automated_curations_history_panel.tsx index 901609718c8ec..04f786b1ee1e1 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/views/curations_history/components/automated_curations_history_panel.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/views/curations_history/components/automated_curations_history_panel.tsx @@ -21,9 +21,9 @@ export const AutomatedCurationsHistoryPanel: React.FC = () => { const filters = [ 'event.kind: event', 'event.dataset: search-relevance-suggestions', - `appsearch.search_relevance_suggestions.engine: ${engineName}`, + `appsearch.adaptive_relevance.engine: ${engineName}`, 'event.action: curation_suggestion', - 'appsearch.search_relevance_suggestions.suggestion.new_status: automated', + 'appsearch.adaptive_relevance.suggestion.new_status: automated', ]; return ( @@ -54,7 +54,7 @@ export const AutomatedCurationsHistoryPanel: React.FC = () => { columns={[ { type: 'field', - field: 'appsearch.search_relevance_suggestions.query', + field: 'appsearch.adaptive_relevance.query', header: i18n.translate( 'xpack.enterpriseSearch.appSearch.engine.curations.automatedCurationsHistoryPanel.queryColumnHeader', { defaultMessage: 'Query' } diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/views/curations_history/components/ignored_queries_panel/ignored_queries_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/views/curations_history/components/ignored_queries_panel/ignored_queries_logic.test.ts index 8c2545fad651a..af9f876820790 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/views/curations_history/components/ignored_queries_panel/ignored_queries_logic.test.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/views/curations_history/components/ignored_queries_panel/ignored_queries_logic.test.ts @@ -11,13 +11,13 @@ import { mockHttpValues, } from '../../../../../../../__mocks__/kea_logic'; import '../../../../../../__mocks__/engine_logic.mock'; +import { DEFAULT_META } from '../../../../../../../shared/constants'; +import { itShowsServerErrorAsFlashMessage } from '../../../../../../../test_helpers'; // I don't know why eslint is saying this line is out of order // eslint-disable-next-line import/order import { nextTick } from '@kbn/test/jest'; -import { DEFAULT_META } from '../../../../../../../shared/constants'; - import { IgnoredQueriesLogic } from './ignored_queries_logic'; const DEFAULT_VALUES = { @@ -142,13 +142,9 @@ describe('IgnoredQueriesLogic', () => { ); }); - it('handles errors', async () => { - http.post.mockReturnValueOnce(Promise.reject('error')); - + itShowsServerErrorAsFlashMessage(http.post, () => { + mount(); IgnoredQueriesLogic.actions.loadIgnoredQueries(); - await nextTick(); - - expect(flashAPIErrors).toHaveBeenCalledWith('error'); }); }); @@ -185,13 +181,9 @@ describe('IgnoredQueriesLogic', () => { expect(flashSuccessToast).toHaveBeenCalledWith(expect.any(String)); }); - it('handles errors', async () => { - http.put.mockReturnValueOnce(Promise.reject('error')); - + itShowsServerErrorAsFlashMessage(http.put, () => { + mount(); IgnoredQueriesLogic.actions.allowIgnoredQuery('test query'); - await nextTick(); - - expect(flashAPIErrors).toHaveBeenCalledWith('error'); }); it('handles inline errors', async () => { diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/views/curations_history/components/rejected_curations_history_panel.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/views/curations_history/components/rejected_curations_history_panel.test.tsx index 28bb317941e1c..58bf89a36d5ee 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/views/curations_history/components/rejected_curations_history_panel.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/views/curations_history/components/rejected_curations_history_panel.test.tsx @@ -29,7 +29,7 @@ describe('RejectedCurationsHistoryPanel', () => { expect(wrapper.is(DataPanel)).toBe(true); expect(wrapper.find(EntSearchLogStream).prop('query')).toEqual( - 'event.kind: event and event.dataset: search-relevance-suggestions and appsearch.search_relevance_suggestions.engine: some-engine and event.action: curation_suggestion and appsearch.search_relevance_suggestions.suggestion.new_status: rejected' + 'event.kind: event and event.dataset: search-relevance-suggestions and appsearch.adaptive_relevance.engine: some-engine and event.action: curation_suggestion and appsearch.adaptive_relevance.suggestion.new_status: rejected' ); }); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/views/curations_history/components/rejected_curations_history_panel.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/views/curations_history/components/rejected_curations_history_panel.tsx index 275083f91c0fb..e7d66fc35a506 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/views/curations_history/components/rejected_curations_history_panel.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/views/curations_history/components/rejected_curations_history_panel.tsx @@ -21,9 +21,9 @@ export const RejectedCurationsHistoryPanel: React.FC = () => { const filters = [ 'event.kind: event', 'event.dataset: search-relevance-suggestions', - `appsearch.search_relevance_suggestions.engine: ${engineName}`, + `appsearch.adaptive_relevance.engine: ${engineName}`, 'event.action: curation_suggestion', - 'appsearch.search_relevance_suggestions.suggestion.new_status: rejected', + 'appsearch.adaptive_relevance.suggestion.new_status: rejected', ]; return ( @@ -53,7 +53,7 @@ export const RejectedCurationsHistoryPanel: React.FC = () => { columns={[ { type: 'field', - field: 'appsearch.search_relevance_suggestions.query', + field: 'appsearch.adaptive_relevance.query', header: i18n.translate( 'xpack.enterpriseSearch.appSearch.engine.curations.rejectedCurationsHistoryPanel.queryColumnHeader', { defaultMessage: 'Query' } diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/views/curations_settings/curations_settings_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/views/curations_settings/curations_settings_logic.test.ts index 0d09f2d28f396..e9643f92f2f71 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/views/curations_settings/curations_settings_logic.test.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/views/curations_settings/curations_settings_logic.test.ts @@ -5,11 +5,7 @@ * 2.0. */ -import { - LogicMounter, - mockHttpValues, - mockFlashMessageHelpers, -} from '../../../../../__mocks__/kea_logic'; +import { LogicMounter, mockHttpValues } from '../../../../../__mocks__/kea_logic'; import '../../../../__mocks__/engine_logic.mock'; jest.mock('../../curations_logic', () => ({ @@ -24,6 +20,7 @@ jest.mock('../../curations_logic', () => ({ import { nextTick } from '@kbn/test/jest'; import { CurationsLogic } from '../..'; +import { itShowsServerErrorAsFlashMessage } from '../../../../../test_helpers'; import { EngineLogic } from '../../../engine'; import { CurationsSettingsLogic } from './curations_settings_logic'; @@ -39,7 +36,6 @@ const DEFAULT_VALUES = { describe('CurationsSettingsLogic', () => { const { mount } = new LogicMounter(CurationsSettingsLogic); const { http } = mockHttpValues; - const { flashAPIErrors } = mockFlashMessageHelpers; beforeEach(() => { jest.clearAllMocks(); @@ -105,14 +101,8 @@ describe('CurationsSettingsLogic', () => { }); }); - it('presents any API errors to the user', async () => { - http.get.mockReturnValueOnce(Promise.reject('error')); - mount(); - + itShowsServerErrorAsFlashMessage(http.get, () => { CurationsSettingsLogic.actions.loadCurationsSettings(); - await nextTick(); - - expect(flashAPIErrors).toHaveBeenCalledWith('error'); }); }); @@ -223,14 +213,8 @@ describe('CurationsSettingsLogic', () => { expect(CurationsLogic.actions.loadCurations).toHaveBeenCalled(); }); - it('presents any API errors to the user', async () => { - http.put.mockReturnValueOnce(Promise.reject('error')); - mount(); - + itShowsServerErrorAsFlashMessage(http.put, () => { CurationsSettingsLogic.actions.updateCurationsSetting({}); - await nextTick(); - - expect(flashAPIErrors).toHaveBeenCalledWith('error'); }); }); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/document_detail_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/document_detail_logic.test.ts index 5705e5ae2ee98..848a85f23c2cb 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/document_detail_logic.test.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/document_detail_logic.test.ts @@ -17,6 +17,8 @@ import { nextTick } from '@kbn/test/jest'; import { InternalSchemaType } from '../../../shared/schema/types'; +import { itShowsServerErrorAsFlashMessage } from '../../../test_helpers'; + import { DocumentDetailLogic } from './document_detail_logic'; describe('DocumentDetailLogic', () => { @@ -117,14 +119,9 @@ describe('DocumentDetailLogic', () => { await nextTick(); }); - it('handles errors', async () => { + itShowsServerErrorAsFlashMessage(http.delete, () => { mount(); - http.delete.mockReturnValue(Promise.reject('An error occured')); - DocumentDetailLogic.actions.deleteDocument('1'); - await nextTick(); - - expect(flashAPIErrors).toHaveBeenCalledWith('An error occured'); }); }); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/components/suggested_curations_callout.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/components/suggested_curations_callout.tsx index e1f984581438f..a79c0394fc903 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/components/suggested_curations_callout.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/components/suggested_curations_callout.tsx @@ -34,6 +34,7 @@ export const SuggestedCurationsCallout: React.FC = () => { return ( ({ EngineLogic: { values: { engineName: 'some-engine' } }, @@ -17,12 +13,13 @@ jest.mock('../engine', () => ({ import { nextTick } from '@kbn/test/jest'; +import { itShowsServerErrorAsFlashMessage } from '../../../test_helpers'; + import { EngineOverviewLogic } from './'; describe('EngineOverviewLogic', () => { const { mount } = new LogicMounter(EngineOverviewLogic); const { http } = mockHttpValues; - const { flashAPIErrors } = mockFlashMessageHelpers; const mockEngineMetrics = { documentCount: 10, @@ -83,14 +80,9 @@ describe('EngineOverviewLogic', () => { ); }); - it('handles errors', async () => { + itShowsServerErrorAsFlashMessage(http.get, () => { mount(); - http.get.mockReturnValue(Promise.reject('An error occurred')); - EngineOverviewLogic.actions.loadOverviewMetrics(); - await nextTick(); - - expect(flashAPIErrors).toHaveBeenCalledWith('An error occurred'); }); }); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/engines_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/engines_logic.test.ts index f3dc8a378a7d3..d0a227c8c6fbe 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/engines_logic.test.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/engines_logic.test.ts @@ -15,6 +15,7 @@ import { nextTick } from '@kbn/test/jest'; import { DEFAULT_META } from '../../../shared/constants'; +import { itShowsServerErrorAsFlashMessage } from '../../../test_helpers'; import { EngineDetails, EngineTypes } from '../engine/types'; import { EnginesLogic } from './'; @@ -171,14 +172,9 @@ describe('EnginesLogic', () => { expect(EnginesLogic.actions.onEnginesLoad).toHaveBeenCalledWith(MOCK_ENGINES_API_RESPONSE); }); - it('handles errors', async () => { - http.get.mockReturnValueOnce(Promise.reject('error')); + itShowsServerErrorAsFlashMessage(http.get, () => { mount(); - EnginesLogic.actions.loadEngines(); - await nextTick(); - - expect(flashAPIErrors).toHaveBeenCalledWith('error'); }); }); @@ -203,14 +199,9 @@ describe('EnginesLogic', () => { ); }); - it('handles errors', async () => { - http.get.mockReturnValueOnce(Promise.reject('error')); + itShowsServerErrorAsFlashMessage(http.get, () => { mount(); - EnginesLogic.actions.loadMetaEngines(); - await nextTick(); - - expect(flashAPIErrors).toHaveBeenCalledWith('error'); }); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/log_retention/messaging/constants.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/log_retention/messaging/constants.tsx index e1e6820204d94..e30b6cec34d18 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/log_retention/messaging/constants.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/log_retention/messaging/constants.tsx @@ -83,14 +83,6 @@ export const NoLogging: React.FC = ({ type, disabledAt }) => { ); }; -export const ILMDisabled: React.FC = ({ type }) => ( - -); - export const CustomPolicy: React.FC = ({ type }) => ( { const analytics = LogRetentionOptions.Analytics; const api = LogRetentionOptions.API; - const setLogRetention = (logRetention: object, ilmEnabled: boolean = true) => { + const setLogRetention = (logRetention: object) => { const logRetentionSettings = { disabledAt: null, enabled: true, @@ -30,7 +30,6 @@ describe('LogRetentionMessage', () => { }; setMockValues({ - ilmEnabled, logRetention: { [LogRetentionOptions.API]: logRetentionSettings, [LogRetentionOptions.Analytics]: logRetentionSettings, @@ -155,22 +154,4 @@ describe('LogRetentionMessage', () => { }); }); }); - - describe('when ILM is disabled entirely', () => { - describe('an ILM disabled message renders', () => { - beforeEach(() => { - setLogRetention({}, false); - }); - - it('for analytics', () => { - const wrapper = mountWithIntl(); - expect(wrapper.text()).toEqual("App Search isn't managing analytics log retention."); - }); - - it('for api', () => { - const wrapper = mountWithIntl(); - expect(wrapper.text()).toEqual("App Search isn't managing API log retention."); - }); - }); - }); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/log_retention/messaging/log_retention_message.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/log_retention/messaging/log_retention_message.tsx index 7d34a2567ba14..c461de72edb88 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/log_retention/messaging/log_retention_message.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/log_retention/messaging/log_retention_message.tsx @@ -9,18 +9,15 @@ import React from 'react'; import { useValues } from 'kea'; -import { AppLogic } from '../../../app_logic'; import { LogRetentionLogic } from '../log_retention_logic'; import { LogRetentionOptions } from '../types'; -import { NoLogging, ILMDisabled, CustomPolicy, DefaultPolicy } from './constants'; +import { NoLogging, CustomPolicy, DefaultPolicy } from './constants'; interface Props { type: LogRetentionOptions; } export const LogRetentionMessage: React.FC = ({ type }) => { - const { ilmEnabled } = useValues(AppLogic); - const { logRetention } = useValues(LogRetentionLogic); if (!logRetention) return null; @@ -30,9 +27,6 @@ export const LogRetentionMessage: React.FC = ({ type }) => { if (!logRetentionSettings.enabled) { return ; } - if (!ilmEnabled) { - return ; - } if (!logRetentionSettings.retentionPolicy?.isDefault) { return ; } else { diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_logic.test.ts index 193c5dbe8ac24..6ac1c27a27959 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_logic.test.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_logic.test.ts @@ -14,6 +14,8 @@ import { mockEngineValues, mockEngineActions } from '../../__mocks__'; import { nextTick } from '@kbn/test/jest'; +import { itShowsServerErrorAsFlashMessage } from '../../../test_helpers'; + import { Boost, BoostOperation, BoostType, FunctionalBoostFunction } from './types'; import { RelevanceTuningLogic } from './'; @@ -319,14 +321,9 @@ describe('RelevanceTuningLogic', () => { }); }); - it('handles errors', async () => { + itShowsServerErrorAsFlashMessage(http.get, () => { mount(); - http.get.mockReturnValueOnce(Promise.reject('error')); - RelevanceTuningLogic.actions.initializeRelevanceTuning(); - await nextTick(); - - expect(flashAPIErrors).toHaveBeenCalledWith('error'); }); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/result_settings_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/result_settings_logic.test.ts index 94d5e84c67f6d..92cb2346e0a26 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/result_settings_logic.test.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/result_settings_logic.test.ts @@ -5,11 +5,7 @@ * 2.0. */ -import { - LogicMounter, - mockFlashMessageHelpers, - mockHttpValues, -} from '../../../__mocks__/kea_logic'; +import { LogicMounter, mockHttpValues } from '../../../__mocks__/kea_logic'; import { mockEngineValues } from '../../__mocks__'; import { omit } from 'lodash'; @@ -18,6 +14,8 @@ import { nextTick } from '@kbn/test/jest'; import { Schema, SchemaConflicts, SchemaType } from '../../../shared/schema/types'; +import { itShowsServerErrorAsFlashMessage } from '../../../test_helpers'; + import { ServerFieldResultSettingObject } from './types'; import { ResultSettingsLogic } from '.'; @@ -508,7 +506,6 @@ describe('ResultSettingsLogic', () => { describe('listeners', () => { const { http } = mockHttpValues; - const { flashAPIErrors } = mockFlashMessageHelpers; let confirmSpy: jest.SpyInstance; beforeAll(() => { @@ -844,14 +841,9 @@ describe('ResultSettingsLogic', () => { ); }); - it('handles errors', async () => { + itShowsServerErrorAsFlashMessage(http.get, () => { mount(); - http.get.mockReturnValueOnce(Promise.reject('error')); - ResultSettingsLogic.actions.initializeResultSettingsData(); - await nextTick(); - - expect(flashAPIErrors).toHaveBeenCalledWith('error'); }); }); @@ -923,14 +915,9 @@ describe('ResultSettingsLogic', () => { ); }); - it('handles errors', async () => { + itShowsServerErrorAsFlashMessage(http.put, () => { mount(); - http.put.mockReturnValueOnce(Promise.reject('error')); - ResultSettingsLogic.actions.saveResultSettings(); - await nextTick(); - - expect(flashAPIErrors).toHaveBeenCalledWith('error'); }); it('does nothing if the user does not confirm', async () => { diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/role_mappings/role_mappings_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/role_mappings/role_mappings_logic.test.ts index 14d97c7dd3f4d..b35865f279817 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/role_mappings/role_mappings_logic.test.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/role_mappings/role_mappings_logic.test.ts @@ -23,13 +23,15 @@ import { } from '../../../shared/role_mapping/__mocks__/roles'; import { ANY_AUTH_PROVIDER } from '../../../shared/role_mapping/constants'; +import { itShowsServerErrorAsFlashMessage } from '../../../test_helpers'; + import { RoleMappingsLogic } from './role_mappings_logic'; const emptyUser = { username: '', email: '' }; describe('RoleMappingsLogic', () => { const { http } = mockHttpValues; - const { clearFlashMessages, flashAPIErrors, flashSuccessToast } = mockFlashMessageHelpers; + const { clearFlashMessages, flashSuccessToast } = mockFlashMessageHelpers; const { mount } = new LogicMounter(RoleMappingsLogic); const DEFAULT_VALUES = { attributes: [], @@ -391,12 +393,8 @@ describe('RoleMappingsLogic', () => { expect(setRoleMappingsSpy).toHaveBeenCalledWith(mappingsServerProps); }); - it('handles error', async () => { - http.post.mockReturnValue(Promise.reject('this is an error')); + itShowsServerErrorAsFlashMessage(http.post, () => { RoleMappingsLogic.actions.enableRoleBasedAccess(); - await nextTick(); - - expect(flashAPIErrors).toHaveBeenCalledWith('this is an error'); }); }); @@ -411,12 +409,8 @@ describe('RoleMappingsLogic', () => { expect(setRoleMappingsDataSpy).toHaveBeenCalledWith(mappingsServerProps); }); - it('handles error', async () => { - http.get.mockReturnValue(Promise.reject('this is an error')); + itShowsServerErrorAsFlashMessage(http.get, () => { RoleMappingsLogic.actions.initializeRoleMappings(); - await nextTick(); - - expect(flashAPIErrors).toHaveBeenCalledWith('this is an error'); }); it('resets roleMapping state', () => { @@ -691,13 +685,9 @@ describe('RoleMappingsLogic', () => { expect(flashSuccessToast).toHaveBeenCalled(); }); - it('handles error', async () => { + itShowsServerErrorAsFlashMessage(http.delete, () => { mount(mappingsServerProps); - http.delete.mockReturnValue(Promise.reject('this is an error')); RoleMappingsLogic.actions.handleDeleteMapping(roleMappingId); - await nextTick(); - - expect(flashAPIErrors).toHaveBeenCalledWith('this is an error'); }); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/schema/schema_base_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/schema/schema_base_logic.test.ts index c5611420442c8..5b40b362bc665 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/schema/schema_base_logic.test.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/schema/schema_base_logic.test.ts @@ -5,23 +5,20 @@ * 2.0. */ -import { - LogicMounter, - mockFlashMessageHelpers, - mockHttpValues, -} from '../../../__mocks__/kea_logic'; +import { LogicMounter, mockHttpValues } from '../../../__mocks__/kea_logic'; import '../../__mocks__/engine_logic.mock'; import { nextTick } from '@kbn/test/jest'; import { SchemaType } from '../../../shared/schema/types'; +import { itShowsServerErrorAsFlashMessage } from '../../../test_helpers'; + import { SchemaBaseLogic } from './schema_base_logic'; describe('SchemaBaseLogic', () => { const { mount } = new LogicMounter(SchemaBaseLogic); const { http } = mockHttpValues; - const { flashAPIErrors } = mockFlashMessageHelpers; const MOCK_SCHEMA = { some_text_field: SchemaType.Text, @@ -99,14 +96,9 @@ describe('SchemaBaseLogic', () => { expect(SchemaBaseLogic.actions.onSchemaLoad).toHaveBeenCalledWith(MOCK_RESPONSE); }); - it('handles errors', async () => { - http.get.mockReturnValueOnce(Promise.reject('error')); + itShowsServerErrorAsFlashMessage(http.get, () => { mount(); - SchemaBaseLogic.actions.loadSchema(); - await nextTick(); - - expect(flashAPIErrors).toHaveBeenCalledWith('error'); }); }); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/search_ui/search_ui_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/search_ui/search_ui_logic.test.ts index 33144d4188ec1..49444fbd0c5c5 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/search_ui/search_ui_logic.test.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/search_ui/search_ui_logic.test.ts @@ -14,6 +14,8 @@ import { mockEngineValues } from '../../__mocks__'; import { nextTick } from '@kbn/test/jest'; +import { itShowsServerErrorAsFlashMessage } from '../../../test_helpers'; + import { ActiveField } from './types'; import { SearchUILogic } from './'; @@ -21,7 +23,7 @@ import { SearchUILogic } from './'; describe('SearchUILogic', () => { const { mount } = new LogicMounter(SearchUILogic); const { http } = mockHttpValues; - const { flashAPIErrors, setErrorMessage } = mockFlashMessageHelpers; + const { setErrorMessage } = mockFlashMessageHelpers; const DEFAULT_VALUES = { dataLoading: true, @@ -182,14 +184,9 @@ describe('SearchUILogic', () => { ); }); - it('handles errors', async () => { - http.get.mockReturnValueOnce(Promise.reject('error')); + itShowsServerErrorAsFlashMessage(http.get, () => { mount(); - SearchUILogic.actions.loadFieldData(); - await nextTick(); - - expect(flashAPIErrors).toHaveBeenCalledWith('error'); }); }); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/synonyms/synonyms_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/synonyms/synonyms_logic.test.ts index 0ff84ad4cb9cb..7376bc11df79e 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/synonyms/synonyms_logic.test.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/synonyms/synonyms_logic.test.ts @@ -14,6 +14,8 @@ import '../../__mocks__/engine_logic.mock'; import { nextTick } from '@kbn/test/jest'; +import { itShowsServerErrorAsFlashMessage } from '../../../test_helpers'; + import { SYNONYMS_PAGE_META } from './constants'; import { SynonymsLogic } from './'; @@ -146,14 +148,9 @@ describe('SynonymsLogic', () => { expect(SynonymsLogic.actions.onSynonymsLoad).toHaveBeenCalledWith(MOCK_SYNONYMS_RESPONSE); }); - it('handles errors', async () => { - http.get.mockReturnValueOnce(Promise.reject('error')); + itShowsServerErrorAsFlashMessage(http.get, () => { mount(); - SynonymsLogic.actions.loadSynonyms(); - await nextTick(); - - expect(flashAPIErrors).toHaveBeenCalledWith('error'); }); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/utils/recursively_fetch_engines/index.test.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/utils/recursively_fetch_engines/index.test.ts index 555a880d544f4..c03ca8267993a 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/utils/recursively_fetch_engines/index.test.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/utils/recursively_fetch_engines/index.test.ts @@ -5,15 +5,16 @@ * 2.0. */ -import { mockFlashMessageHelpers, mockHttpValues } from '../../../__mocks__/kea_logic'; +import { mockHttpValues } from '../../../__mocks__/kea_logic'; import { nextTick } from '@kbn/test/jest'; +import { itShowsServerErrorAsFlashMessage } from '../../../test_helpers'; + import { recursivelyFetchEngines } from './'; describe('recursivelyFetchEngines', () => { const { http } = mockHttpValues; - const { flashAPIErrors } = mockFlashMessageHelpers; const MOCK_PAGE_1 = { meta: { @@ -100,12 +101,7 @@ describe('recursivelyFetchEngines', () => { }); }); - it('handles errors', async () => { - http.get.mockReturnValueOnce(Promise.reject('error')); - + itShowsServerErrorAsFlashMessage(http.get, () => { recursivelyFetchEngines({ endpoint: '/error', onComplete: MOCK_CALLBACK }); - await nextTick(); - - expect(flashAPIErrors).toHaveBeenCalledWith('error'); }); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/index.test.tsx b/x-pack/plugins/enterprise_search/public/applications/index.test.tsx index 5a82f15a6cf04..356a3c26b910e 100644 --- a/x-pack/plugins/enterprise_search/public/applications/index.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/index.test.tsx @@ -23,7 +23,7 @@ import { renderApp, renderHeaderActions } from './'; describe('renderApp', () => { const kibanaDeps = { - params: coreMock.createAppMountParamters(), + params: coreMock.createAppMountParameters(), core: coreMock.createStart(), plugins: { licensing: licensingMock.createStart(), diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/flash_messages/handle_api_errors.test.ts b/x-pack/plugins/enterprise_search/public/applications/shared/flash_messages/handle_api_errors.test.ts index 47cbef0bfd953..1e3705a3800ed 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/flash_messages/handle_api_errors.test.ts +++ b/x-pack/plugins/enterprise_search/public/applications/shared/flash_messages/handle_api_errors.test.ts @@ -5,11 +5,14 @@ * 2.0. */ +jest.mock('./set_message_helpers', () => ({ + flashErrorToast: jest.fn(), +})); import '../../__mocks__/kea_logic/kibana_logic.mock'; import { FlashMessagesLogic } from './flash_messages_logic'; - -import { flashAPIErrors, getErrorsFromHttpResponse } from './handle_api_errors'; +import { flashAPIErrors, getErrorsFromHttpResponse, toastAPIErrors } from './handle_api_errors'; +import { flashErrorToast } from './set_message_helpers'; describe('flashAPIErrors', () => { const mockHttpError = { @@ -75,6 +78,56 @@ describe('flashAPIErrors', () => { }); }); +describe('toastAPIErrors', () => { + const mockHttpError = { + body: { + statusCode: 404, + error: 'Not Found', + message: 'Could not find X,Could not find Y,Something else bad happened', + attributes: { + errors: ['Could not find X', 'Could not find Y', 'Something else bad happened'], + }, + }, + } as any; + + beforeEach(() => { + jest.clearAllMocks(); + FlashMessagesLogic.mount(); + jest.spyOn(FlashMessagesLogic.actions, 'setFlashMessages'); + jest.spyOn(FlashMessagesLogic.actions, 'setQueuedMessages'); + }); + + it('converts API errors into flash messages', () => { + toastAPIErrors(mockHttpError); + + expect(flashErrorToast).toHaveBeenNthCalledWith(1, 'Could not find X'); + expect(flashErrorToast).toHaveBeenNthCalledWith(2, 'Could not find Y'); + expect(flashErrorToast).toHaveBeenNthCalledWith(3, 'Something else bad happened'); + }); + + it('falls back to the basic message for http responses without an errors array', () => { + toastAPIErrors({ + body: { + statusCode: 404, + error: 'Not Found', + message: 'Not Found', + }, + } as any); + + expect(flashErrorToast).toHaveBeenCalledWith('Not Found'); + }); + + it('displays a generic error message and re-throws non-API errors', () => { + const error = Error('whatever'); + + expect(() => { + toastAPIErrors(error as any); + }).toThrowError(error); + + expect(flashErrorToast).toHaveBeenCalledWith(expect.any(String)); + }); +}); + describe('getErrorsFromHttpResponse', () => { it('should return errors from the response if present', () => { expect( diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/flash_messages/handle_api_errors.ts b/x-pack/plugins/enterprise_search/public/applications/shared/flash_messages/handle_api_errors.ts index abaa67e06f606..40863087004d6 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/flash_messages/handle_api_errors.ts +++ b/x-pack/plugins/enterprise_search/public/applications/shared/flash_messages/handle_api_errors.ts @@ -10,6 +10,7 @@ import { i18n } from '@kbn/i18n'; import { HttpResponse } from 'src/core/public'; import { FlashMessagesLogic } from './flash_messages_logic'; +import { flashErrorToast } from './set_message_helpers'; import { IFlashMessage } from './types'; /** @@ -69,3 +70,16 @@ export const flashAPIErrors = ( throw response; } }; + +export const toastAPIErrors = (response: HttpResponse) => { + const messages = getErrorsFromHttpResponse(response); + + for (const message of messages) { + flashErrorToast(message); + } + // If this was a programming error or a failed request (such as a CORS) error, + // we rethrow the error so it shows up in the developer console + if (!response?.body?.message) { + throw response; + } +}; 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 097d38e0691c5..3d3775aee9607 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 @@ -8,7 +8,7 @@ export { FlashMessages, Toasts } from './flash_messages'; export { FlashMessagesLogic, mountFlashMessagesLogic } from './flash_messages_logic'; export type { IFlashMessage } from './types'; -export { flashAPIErrors } from './handle_api_errors'; +export { flashAPIErrors, toastAPIErrors } from './handle_api_errors'; export { setSuccessMessage, setErrorMessage, diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/tables/generic_endpoint_inline_editable_table/generic_endpoint_inline_editable_table_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/shared/tables/generic_endpoint_inline_editable_table/generic_endpoint_inline_editable_table_logic.test.ts index 5cd4b5af8f517..481013d91bf6f 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/tables/generic_endpoint_inline_editable_table/generic_endpoint_inline_editable_table_logic.test.ts +++ b/x-pack/plugins/enterprise_search/public/applications/shared/tables/generic_endpoint_inline_editable_table/generic_endpoint_inline_editable_table_logic.test.ts @@ -5,6 +5,16 @@ * 2.0. */ +const MOCK_SET_ROW_ERRORS = jest.fn(); + +jest.mock('../inline_editable_table/inline_editable_table_logic', () => ({ + InlineEditableTableLogic: () => ({ + actions: { + setRowErrors: MOCK_SET_ROW_ERRORS, + }, + }), +})); + import { LogicMounter, mockFlashMessageHelpers, @@ -18,7 +28,7 @@ import { GenericEndpointInlineEditableTableLogic } from './generic_endpoint_inli describe('GenericEndpointInlineEditableTableLogic', () => { const { mount } = new LogicMounter(GenericEndpointInlineEditableTableLogic); const { http } = mockHttpValues; - const { flashAPIErrors } = mockFlashMessageHelpers; + const { toastAPIErrors } = mockFlashMessageHelpers; const DEFAULT_VALUES = { isLoading: false, @@ -119,14 +129,13 @@ describe('GenericEndpointInlineEditableTableLogic', () => { expect(logic.actions.clearLoading).toHaveBeenCalled(); }); - it('handles errors', async () => { + it('passes API errors to the nested inline editable table', async () => { http.post.mockReturnValueOnce(Promise.reject('error')); const logic = mountLogic(); - logic.actions.addItem(item, onSuccess); await nextTick(); - expect(flashAPIErrors).toHaveBeenCalledWith('error'); + expect(MOCK_SET_ROW_ERRORS).toHaveBeenCalledWith(['An unexpected error occurred']); }); }); @@ -167,14 +176,13 @@ describe('GenericEndpointInlineEditableTableLogic', () => { expect(logic.actions.clearLoading).toHaveBeenCalled(); }); - it('handles errors', async () => { + it('passes errors to the nested inline editable table', async () => { http.delete.mockReturnValueOnce(Promise.reject('error')); const logic = mountLogic(); - logic.actions.deleteItem(item, onSuccess); await nextTick(); - expect(flashAPIErrors).toHaveBeenCalledWith('error'); + expect(MOCK_SET_ROW_ERRORS).toHaveBeenCalledWith(['An unexpected error occurred']); }); }); @@ -221,14 +229,13 @@ describe('GenericEndpointInlineEditableTableLogic', () => { expect(logic.actions.clearLoading).toHaveBeenCalled(); }); - it('handles errors', async () => { + it('passes errors to the nested inline editable table', async () => { http.put.mockReturnValueOnce(Promise.reject('error')); const logic = mountLogic(); - logic.actions.updateItem(item, onSuccess); await nextTick(); - expect(flashAPIErrors).toHaveBeenCalledWith('error'); + expect(MOCK_SET_ROW_ERRORS).toHaveBeenCalledWith(['An unexpected error occurred']); }); }); @@ -294,7 +301,7 @@ describe('GenericEndpointInlineEditableTableLogic', () => { // It again calls back to the configured 'onReorder' to reset the order expect(DEFAULT_LOGIC_PARAMS.onReorder).toHaveBeenCalledWith(oldItems); - expect(flashAPIErrors).toHaveBeenCalledWith('error'); + expect(toastAPIErrors).toHaveBeenCalledWith('error'); }); it('does nothing if there are no reorder props', async () => { diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/tables/generic_endpoint_inline_editable_table/generic_endpoint_inline_editable_table_logic.ts b/x-pack/plugins/enterprise_search/public/applications/shared/tables/generic_endpoint_inline_editable_table/generic_endpoint_inline_editable_table_logic.ts index 71c993dca9cb9..b5beb2a74757e 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/tables/generic_endpoint_inline_editable_table/generic_endpoint_inline_editable_table_logic.ts +++ b/x-pack/plugins/enterprise_search/public/applications/shared/tables/generic_endpoint_inline_editable_table/generic_endpoint_inline_editable_table_logic.ts @@ -7,9 +7,14 @@ import { kea, MakeLogicType } from 'kea'; -import { flashAPIErrors } from '../../flash_messages'; +import { toastAPIErrors } from '../../flash_messages'; +import { getErrorsFromHttpResponse } from '../../flash_messages/handle_api_errors'; import { HttpLogic } from '../../http'; +import { + InlineEditableTableLogic, + InlineEditableTableProps as InlineEditableTableLogicProps, +} from '../inline_editable_table/inline_editable_table_logic'; import { ItemWithAnID } from '../types'; @@ -91,7 +96,10 @@ export const GenericEndpointInlineEditableTableLogic = kea< onAdd(item, itemsFromResponse); onSuccess(); } catch (e) { - flashAPIErrors(e); + const errors = getErrorsFromHttpResponse(e); + InlineEditableTableLogic({ + instanceId: props.instanceId, + } as InlineEditableTableLogicProps).actions.setRowErrors(errors); } finally { actions.clearLoading(); } @@ -107,7 +115,10 @@ export const GenericEndpointInlineEditableTableLogic = kea< onDelete(item, itemsFromResponse); onSuccess(); } catch (e) { - flashAPIErrors(e); + const errors = getErrorsFromHttpResponse(e); + InlineEditableTableLogic({ + instanceId: props.instanceId, + } as InlineEditableTableLogicProps).actions.setRowErrors(errors); } finally { actions.clearLoading(); } @@ -126,7 +137,10 @@ export const GenericEndpointInlineEditableTableLogic = kea< onUpdate(item, itemsFromResponse); onSuccess(); } catch (e) { - flashAPIErrors(e); + const errors = getErrorsFromHttpResponse(e); + InlineEditableTableLogic({ + instanceId: props.instanceId, + } as InlineEditableTableLogicProps).actions.setRowErrors(errors); } finally { actions.clearLoading(); } @@ -152,7 +166,7 @@ export const GenericEndpointInlineEditableTableLogic = kea< onSuccess(); } catch (e) { onReorder(oldItems); - flashAPIErrors(e); + toastAPIErrors(e); } actions.clearLoading(); diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/tables/inline_editable_table/action_column.test.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/tables/inline_editable_table/action_column.test.tsx index 6328b01cd2be7..4c9532b038a8c 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/tables/inline_editable_table/action_column.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/shared/tables/inline_editable_table/action_column.test.tsx @@ -23,9 +23,10 @@ describe('ActionColumn', () => { const mockValues = { doesEditingItemValueContainEmptyProperty: false, editingItemId: 1, - formErrors: [], + fieldErrors: {}, isEditing: false, isEditingUnsavedItem: false, + rowErrors: [], }; const mockActions = { editExistingItem: jest.fn(), @@ -87,10 +88,20 @@ describe('ActionColumn', () => { expect(subject(wrapper).prop('disabled')).toBe(true); }); - it('which is disabled if there are form errors', () => { + it('which is disabled if there are field errors', () => { setMockValues({ ...mockValues, - formErrors: ['I am an error'], + fieldErrors: { foo: ['I am an error for foo'] }, + }); + + const wrapper = shallow(); + expect(subject(wrapper).prop('disabled')).toBe(true); + }); + + it('which is disabled if there are row errors', () => { + setMockValues({ + ...mockValues, + rowErrors: ['I am a row error'], }); const wrapper = shallow(); diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/tables/inline_editable_table/action_column.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/tables/inline_editable_table/action_column.tsx index ec52b18adf648..3293e8c5021d5 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/tables/inline_editable_table/action_column.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/shared/tables/inline_editable_table/action_column.tsx @@ -41,7 +41,7 @@ export const ActionColumn = ({ lastItemWarning, uneditableItems, }: ActionColumnProps) => { - const { doesEditingItemValueContainEmptyProperty, formErrors, isEditingUnsavedItem } = + const { doesEditingItemValueContainEmptyProperty, fieldErrors, rowErrors, isEditingUnsavedItem } = useValues(InlineEditableTableLogic); const { editExistingItem, deleteItem, doneEditing, saveExistingItem, saveNewItem } = useActions(InlineEditableTableLogic); @@ -50,6 +50,8 @@ export const ActionColumn = ({ return null; } + const isInvalid = Object.keys(fieldErrors).length > 0 || rowErrors.length > 0; + if (isActivelyEditing(item)) { return ( @@ -59,11 +61,7 @@ export const ActionColumn = ({ color="primary" iconType="checkInCircleFilled" onClick={isEditingUnsavedItem ? saveNewItem : saveExistingItem} - disabled={ - isLoading || - Object.keys(formErrors).length > 0 || - doesEditingItemValueContainEmptyProperty - } + disabled={isLoading || isInvalid || doesEditingItemValueContainEmptyProperty} > {SAVE_BUTTON_LABEL} diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/tables/inline_editable_table/editing_column.test.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/tables/inline_editable_table/editing_column.test.tsx index 43ced1bd87492..3dcdc8a84ce38 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/tables/inline_editable_table/editing_column.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/shared/tables/inline_editable_table/editing_column.test.tsx @@ -28,8 +28,9 @@ describe('EditingColumn', () => { }; const mockValues = { - formErrors: [], editingItemValue: { id: 1 }, + fieldErrors: {}, + rowErrors: [], }; const mockActions = { @@ -52,7 +53,7 @@ describe('EditingColumn', () => { beforeEach(() => { setMockValues({ ...mockValues, - formErrors: { + fieldErrors: { foo: 'I am an error for foo and should be displayed', }, }); @@ -70,7 +71,7 @@ describe('EditingColumn', () => { ); }); - it('renders form errors for this field if any are present', () => { + it('renders field errors for this field if any are present', () => { expect(shallow(wrapper.find(EuiFormRow).prop('helpText') as any).html()).toContain( 'I am an error for foo and should be displayed' ); @@ -81,6 +82,22 @@ describe('EditingColumn', () => { }); }); + describe('when there is a form error for this row', () => { + let wrapper: ShallowWrapper; + beforeEach(() => { + setMockValues({ + ...mockValues, + rowErrors: ['I am an error for this row'], + }); + + wrapper = shallow(); + }); + + it('renders as invalid', () => { + expect(wrapper.find(EuiFormRow).prop('isInvalid')).toBe(true); + }); + }); + it('renders nothing if there is no editingItemValue in state', () => { setMockValues({ ...mockValues, @@ -95,7 +112,7 @@ describe('EditingColumn', () => { setMockValues({ ...mockValues, editingItemValue: { id: 1, foo: 'foo', bar: 'bar' }, - formErrors: { foo: ['I am an error for foo'] }, + fieldErrors: { foo: ['I am an error for foo'] }, }); const wrapper = shallow( diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/tables/inline_editable_table/editing_column.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/tables/inline_editable_table/editing_column.tsx index d3d36046dc0a6..99b06ef827ded 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/tables/inline_editable_table/editing_column.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/shared/tables/inline_editable_table/editing_column.tsx @@ -25,20 +25,25 @@ export const EditingColumn = ({ column, isLoading = false, }: EditingColumnProps) => { - const { formErrors, editingItemValue } = useValues(InlineEditableTableLogic); + const { fieldErrors, rowErrors, editingItemValue } = useValues(InlineEditableTableLogic); const { setEditingItemValue } = useActions(InlineEditableTableLogic); if (!editingItemValue) return null; + const fieldError = fieldErrors[column.field]; + const isInvalid = !!fieldError || rowErrors.length > 0; + return ( - {formErrors[column.field]} - + fieldError && ( + + {fieldError} + + ) } - isInvalid={!!formErrors[column.field]} + isInvalid={isInvalid} > <> {column.editingRender( @@ -50,7 +55,7 @@ export const EditingColumn = ({ }); }, { - isInvalid: !!formErrors[column.field], + isInvalid, isLoading, } )} diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/tables/inline_editable_table/inline_editable_table.test.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/tables/inline_editable_table/inline_editable_table.test.tsx index ab59616e9ce78..c8d3079b033c0 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/tables/inline_editable_table/inline_editable_table.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/shared/tables/inline_editable_table/inline_editable_table.test.tsx @@ -153,6 +153,21 @@ describe('InlineEditableTable', () => { expect(rowProps(items[1])).toEqual({ className: 'is-being-edited' }); }); + it('will pass errors for row that is currently being edited', () => { + setMockValues({ + ...mockValues, + isEditing: true, + editingItemId: 2, + rowErrors: ['first error', 'second error'], + }); + const itemList = [{ id: 1 }, { id: 2 }]; + const wrapper = shallow(); + const rowErrors = wrapper.find(ReorderableTable).prop('rowErrors') as (item: any) => object; + expect(rowErrors(items[0])).toEqual(undefined); + // Since editingItemId is 2 and the second item (position 1) in item list has an id of 2, it gets the errors + expect(rowErrors(items[1])).toEqual(['first error', 'second error']); + }); + it('will update the passed columns and pass them through to the underlying table', () => { const updatedColumns = {}; const canRemoveLastItem = true; diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/tables/inline_editable_table/inline_editable_table.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/tables/inline_editable_table/inline_editable_table.tsx index 093692dfde335..3c670264dff22 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/tables/inline_editable_table/inline_editable_table.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/shared/tables/inline_editable_table/inline_editable_table.tsx @@ -95,7 +95,8 @@ export const InlineEditableTableContents = ({ uneditableItems, ...rest }: InlineEditableTableProps) => { - const { editingItemId, isEditing, isEditingUnsavedItem } = useValues(InlineEditableTableLogic); + const { editingItemId, isEditing, isEditingUnsavedItem, rowErrors } = + useValues(InlineEditableTableLogic); const { editNewItem, reorderItems } = useActions(InlineEditableTableLogic); // TODO These two things shoud just be selectors @@ -168,6 +169,7 @@ export const InlineEditableTableContents = ({ 'is-being-edited': isActivelyEditing(item), }), })} + rowErrors={(item) => (isActivelyEditing(item) ? rowErrors : undefined)} noItemsMessage={noItemsMessage(editNewItem)} onReorder={reorderItems} disableDragging={isEditing} diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/tables/inline_editable_table/inline_editable_table_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/shared/tables/inline_editable_table/inline_editable_table_logic.test.ts index f690a38620ecb..5a8a724076223 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/tables/inline_editable_table/inline_editable_table_logic.test.ts +++ b/x-pack/plugins/enterprise_search/public/applications/shared/tables/inline_editable_table/inline_editable_table_logic.test.ts @@ -29,8 +29,9 @@ describe('InlineEditableTableLogic', () => { const DEFAULT_VALUES = { editingItemId: null, editingItemValue: null, - formErrors: {}, + fieldErrors: {}, isEditing: false, + rowErrors: [], }; const SELECTORS = { @@ -91,7 +92,7 @@ describe('InlineEditableTableLogic', () => { isEditing: true, editingItemId: 1, editingItemValue: {}, - formErrors: { foo: 'I am error' }, + fieldErrors: { foo: 'I am error for foo' }, }); logic.actions.doneEditing(); expect(logicValuesWithoutSelectors(logic)).toEqual(DEFAULT_VALUES); @@ -152,29 +153,41 @@ describe('InlineEditableTableLogic', () => { }); }); - describe('setFormErrors', () => { - it('sets formErrors', () => { - const formErrors = { - bar: 'I am an error', + describe('setFieldErrors', () => { + it('sets fieldErrors', () => { + const fieldErrors = { + foo: 'I am an error for foo', }; const logic = mountLogic(); - logic.actions.setFormErrors(formErrors); + logic.actions.setFieldErrors(fieldErrors); expect(logicValuesWithoutSelectors(logic)).toEqual({ ...DEFAULT_VALUES, - formErrors, + fieldErrors, + }); + }); + }); + + describe('setRowErrors', () => { + it('sets rowErrors', () => { + const rowErrors = ['I am a row error']; + const logic = mountLogic(); + logic.actions.setRowErrors(rowErrors); + expect(logicValuesWithoutSelectors(logic)).toEqual({ + ...DEFAULT_VALUES, + rowErrors, }); }); }); describe('setEditingItemValue', () => { - it('updates the state of the item currently being edited and resets form errors', () => { + it('updates the state of the item currently being edited and resets field errors', () => { const logic = mountLogic({ editingItemValue: { id: 1, foo: '', bar: '', }, - formErrors: { foo: 'I am error' }, + fieldErrors: { foo: 'I am error for foo' }, }); logic.actions.setEditingItemValue({ id: 1, @@ -188,7 +201,7 @@ describe('InlineEditableTableLogic', () => { foo: 'blah blah', bar: '', }, - formErrors: {}, + fieldErrors: {}, }); }); }); @@ -297,20 +310,20 @@ describe('InlineEditableTableLogic', () => { ); }); - it('will set form errors and not call the provided onUpdate callback if the item being edited does not validate', () => { + it('will set field errors and not call the provided onUpdate callback if the item being edited does not validate', () => { const editingItemValue = {}; - const formErrors = { + const fieldErrors = { foo: 'some error', }; - DEFAULT_LOGIC_PARAMS.validateItem.mockReturnValue(formErrors); + DEFAULT_LOGIC_PARAMS.validateItem.mockReturnValue(fieldErrors); const logic = mountLogic({ ...DEFAULT_VALUES, editingItemValue, }); - jest.spyOn(logic.actions, 'setFormErrors'); + jest.spyOn(logic.actions, 'setFieldErrors'); logic.actions.saveExistingItem(); expect(DEFAULT_LOGIC_PARAMS.onUpdate).not.toHaveBeenCalled(); - expect(logic.actions.setFormErrors).toHaveBeenCalledWith(formErrors); + expect(logic.actions.setFieldErrors).toHaveBeenCalledWith(fieldErrors); }); it('will do neither if no value is currently being edited', () => { @@ -319,10 +332,10 @@ describe('InlineEditableTableLogic', () => { ...DEFAULT_VALUES, editingItemValue, }); - jest.spyOn(logic.actions, 'setFormErrors'); + jest.spyOn(logic.actions, 'setFieldErrors'); logic.actions.saveExistingItem(); expect(DEFAULT_LOGIC_PARAMS.onUpdate).not.toHaveBeenCalled(); - expect(logic.actions.setFormErrors).not.toHaveBeenCalled(); + expect(logic.actions.setFieldErrors).not.toHaveBeenCalled(); }); it('will always call the provided onUpdate callback if no validateItem param was provided', () => { @@ -382,20 +395,20 @@ describe('InlineEditableTableLogic', () => { ); }); - it('will set form errors and not call the provided onAdd callback if the item being edited does not validate', () => { + it('will set field errors and not call the provided onAdd callback if the item being edited does not validate', () => { const editingItemValue = {}; - const formErrors = { + const fieldErrors = { foo: 'some error', }; - DEFAULT_LOGIC_PARAMS.validateItem.mockReturnValue(formErrors); + DEFAULT_LOGIC_PARAMS.validateItem.mockReturnValue(fieldErrors); const logic = mountLogic({ ...DEFAULT_VALUES, editingItemValue, }); - jest.spyOn(logic.actions, 'setFormErrors'); + jest.spyOn(logic.actions, 'setFieldErrors'); logic.actions.saveNewItem(); expect(DEFAULT_LOGIC_PARAMS.onAdd).not.toHaveBeenCalled(); - expect(logic.actions.setFormErrors).toHaveBeenCalledWith(formErrors); + expect(logic.actions.setFieldErrors).toHaveBeenCalledWith(fieldErrors); }); it('will do nothing if no value is currently being edited', () => { @@ -404,10 +417,10 @@ describe('InlineEditableTableLogic', () => { ...DEFAULT_VALUES, editingItemValue, }); - jest.spyOn(logic.actions, 'setFormErrors'); + jest.spyOn(logic.actions, 'setFieldErrors'); logic.actions.saveNewItem(); expect(DEFAULT_LOGIC_PARAMS.onAdd).not.toHaveBeenCalled(); - expect(logic.actions.setFormErrors).not.toHaveBeenCalled(); + expect(logic.actions.setFieldErrors).not.toHaveBeenCalled(); }); it('will always call the provided onAdd callback if no validateItem param was provided', () => { diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/tables/inline_editable_table/inline_editable_table_logic.ts b/x-pack/plugins/enterprise_search/public/applications/shared/tables/inline_editable_table/inline_editable_table_logic.ts index 0230fc0754120..04b5ceb998851 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/tables/inline_editable_table/inline_editable_table_logic.ts +++ b/x-pack/plugins/enterprise_search/public/applications/shared/tables/inline_editable_table/inline_editable_table_logic.ts @@ -20,7 +20,8 @@ interface InlineEditableTableActions { saveExistingItem(): void; saveNewItem(): void; setEditingItemValue(newValue: Item): { item: Item }; - setFormErrors(formErrors: FormErrors): { formErrors: FormErrors }; + setFieldErrors(fieldErrors: FormErrors): { fieldErrors: FormErrors }; + setRowErrors(rowErrors: string[]): { rowErrors: string[] }; } const generateEmptyItem = ( @@ -39,12 +40,13 @@ interface InlineEditableTableValues { // TODO we should editingItemValue have editingItemValue and editingItemId should be a selector editingItemId: Item['id'] | null; // editingItem is null when the user is editing a new but not saved item editingItemValue: Item | null; - formErrors: FormErrors; + fieldErrors: FormErrors; + rowErrors: string[]; isEditingUnsavedItem: boolean; doesEditingItemValueContainEmptyProperty: boolean; } -interface InlineEditableTableProps { +export interface InlineEditableTableProps { columns: Array>; instanceId: string; // TODO Because these callbacks are params, they are only set on the logic once (i.e., they are cached) @@ -75,7 +77,8 @@ export const InlineEditableTableLogic = kea ({ item: newValue }), - setFormErrors: (formErrors) => ({ formErrors }), + setFieldErrors: (fieldErrors) => ({ fieldErrors }), + setRowErrors: (rowErrors) => ({ rowErrors }), }), reducers: ({ props: { columns } }) => ({ isEditing: [ @@ -103,12 +106,20 @@ export const InlineEditableTableLogic = kea item, }, ], - formErrors: [ + fieldErrors: [ {}, { doneEditing: () => ({}), setEditingItemValue: () => ({}), - setFormErrors: (_, { formErrors }) => formErrors, + setFieldErrors: (_, { fieldErrors }) => fieldErrors, + }, + ], + rowErrors: [ + [], + { + doneEditing: () => [], + setEditingItemValue: () => [], + setRowErrors: (_, { rowErrors }) => rowErrors, }, ], }), @@ -144,7 +155,7 @@ export const InlineEditableTableLogic = kea { const cells = wrapper.find(Cell); expect(cells.length).toBe(3); }); + + it('will render row errors', () => { + const wrapper = shallow( + + ); + const callouts = wrapper.find(EuiCallOut); + expect(callouts.length).toBe(2); + expect(callouts.at(0).prop('title')).toEqual('first error'); + expect(callouts.at(1).prop('title')).toEqual('second error'); + }); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/tables/reorderable_table/body_row.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/tables/reorderable_table/body_row.tsx index 474d49f5eef0f..588f14190d274 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/tables/reorderable_table/body_row.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/shared/tables/reorderable_table/body_row.tsx @@ -7,7 +7,7 @@ import React from 'react'; -import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import { EuiCallOut, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import { Cell } from './cell'; import { DRAGGABLE_UX_STYLE } from './constants'; @@ -19,6 +19,7 @@ export interface BodyRowProps { additionalProps?: object; // Cell to put in first column before other columns leftAction?: React.ReactNode; + errors?: string[]; } export const BodyRow = ({ @@ -26,6 +27,7 @@ export const BodyRow = ({ item, additionalProps, leftAction, + errors = [], }: BodyRowProps) => { return (
    @@ -46,6 +48,15 @@ export const BodyRow = ({ + {errors.length > 0 && ( + + {errors.map((errorMessage, errorMessageIndex) => ( + + + + ))} + + )}
    ); }; diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/tables/reorderable_table/draggable_body_row.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/tables/reorderable_table/draggable_body_row.tsx index 191843a2e6e78..9a21d5a9c8c25 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/tables/reorderable_table/draggable_body_row.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/shared/tables/reorderable_table/draggable_body_row.tsx @@ -18,6 +18,7 @@ export interface DraggableBodyRowProps { rowIndex: number; additionalProps?: object; disableDragging?: boolean; + errors?: string[]; } export const DraggableBodyRow = ({ @@ -26,6 +27,7 @@ export const DraggableBodyRow = ({ rowIndex, additionalProps, disableDragging = false, + errors, }: DraggableBodyRowProps) => { const draggableId = `draggable_row_${rowIndex}`; @@ -42,6 +44,7 @@ export const DraggableBodyRow = ({ item={item} additionalProps={additionalProps} leftAction={!disableDragging ? : <>} + errors={errors} /> ); diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/tables/reorderable_table/reorderable_table.scss b/x-pack/plugins/enterprise_search/public/applications/shared/tables/reorderable_table/reorderable_table.scss index d2ea90bbbfec8..81ae229dcdd48 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/tables/reorderable_table/reorderable_table.scss +++ b/x-pack/plugins/enterprise_search/public/applications/shared/tables/reorderable_table/reorderable_table.scss @@ -9,14 +9,18 @@ border-top: $euiBorderThin; background-color: $euiColorEmptyShade; - > .euiFlexGroup--directionRow.euiFlexGroup--gutterLarge { + > .euiFlexGroup { margin: 0; } + + > .euiFlexGroup:nth-child(2) > .euiFlexItem { + margin-top: 0; + } } &Header { - > .euiFlexGroup--directionRow.euiFlexGroup--gutterLarge { - margin: -12px 0; + > .euiFlexGroup { + margin: ($euiSizeM * -1) 0; } } diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/tables/reorderable_table/reorderable_table.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/tables/reorderable_table/reorderable_table.tsx index 4cb12321bdfcf..88e80f1d5401b 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/tables/reorderable_table/reorderable_table.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/shared/tables/reorderable_table/reorderable_table.tsx @@ -30,6 +30,7 @@ interface ReorderableTableProps { disableReordering?: boolean; onReorder?: (items: Item[], oldItems: Item[]) => void; rowProps?: (item: Item) => object; + rowErrors?: (item: Item) => string[] | undefined; } export const ReorderableTable = ({ @@ -42,6 +43,7 @@ export const ReorderableTable = ({ disableReordering = false, onReorder = () => undefined, rowProps = () => ({}), + rowErrors = () => undefined, }: ReorderableTableProps) => { return (
    @@ -67,6 +69,7 @@ export const ReorderableTable = ({ additionalProps={rowProps(item)} disableDragging={disableDragging} rowIndex={itemIndex} + errors={rowErrors(item)} /> )} onReorder={onReorder} @@ -83,6 +86,7 @@ export const ReorderableTable = ({ columns={columns} item={item} additionalProps={rowProps(item)} + errors={rowErrors(item)} /> )} /> @@ -97,6 +101,7 @@ export const ReorderableTable = ({ columns={columns} item={item} additionalProps={rowProps(item)} + errors={rowErrors(item)} leftAction={<>} /> )} diff --git a/x-pack/plugins/enterprise_search/public/applications/test_helpers/error_handling.ts b/x-pack/plugins/enterprise_search/public/applications/test_helpers/error_handling.ts new file mode 100644 index 0000000000000..4f1f4a40aa503 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/test_helpers/error_handling.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 { mockFlashMessageHelpers } from '../__mocks__/kea_logic'; + +import { nextTick } from '@kbn/test/jest'; +import { HttpHandler } from 'src/core/public'; + +export const itShowsServerErrorAsFlashMessage = (httpMethod: HttpHandler, callback: () => void) => { + const { flashAPIErrors } = mockFlashMessageHelpers; + it('shows any server errors as flash messages', async () => { + (httpMethod as jest.Mock).mockReturnValueOnce(Promise.reject('error')); + callback(); + await nextTick(); + + expect(flashAPIErrors).toHaveBeenCalledWith('error'); + }); +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/test_helpers/index.ts b/x-pack/plugins/enterprise_search/public/applications/test_helpers/index.ts index 35836d5526615..b0705dd7e134b 100644 --- a/x-pack/plugins/enterprise_search/public/applications/test_helpers/index.ts +++ b/x-pack/plugins/enterprise_search/public/applications/test_helpers/index.ts @@ -21,3 +21,4 @@ export { // Misc export { expectedAsyncError } from './expected_async_error'; +export { itShowsServerErrorAsFlashMessage } from './error_handling'; 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 5ff3964b8f83a..da4e9cad9e276 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 @@ -15,6 +15,8 @@ import { sourceConfigData } from '../../../../__mocks__/content_sources.mock'; import { nextTick } from '@kbn/test/jest'; +import { itShowsServerErrorAsFlashMessage } from '../../../../../test_helpers'; + jest.mock('../../../../app_logic', () => ({ AppLogic: { values: { isOrganization: true } }, })); @@ -413,13 +415,8 @@ describe('AddSourceLogic', () => { expect(setSourceConfigDataSpy).toHaveBeenCalledWith(sourceConfigData); }); - it('handles error', async () => { - http.get.mockReturnValue(Promise.reject('this is an error')); - + itShowsServerErrorAsFlashMessage(http.get, () => { AddSourceLogic.actions.getSourceConfigData('github'); - await nextTick(); - - expect(flashAPIErrors).toHaveBeenCalledWith('this is an error'); }); }); @@ -474,13 +471,8 @@ describe('AddSourceLogic', () => { ); }); - it('handles error', async () => { - http.get.mockReturnValue(Promise.reject('this is an error')); - + itShowsServerErrorAsFlashMessage(http.get, () => { AddSourceLogic.actions.getSourceConnectData('github', successCallback); - await nextTick(); - - expect(flashAPIErrors).toHaveBeenCalledWith('this is an error'); }); }); @@ -506,13 +498,8 @@ describe('AddSourceLogic', () => { expect(setSourceConnectDataSpy).toHaveBeenCalledWith(sourceConnectData); }); - it('handles error', async () => { - http.get.mockReturnValue(Promise.reject('this is an error')); - + itShowsServerErrorAsFlashMessage(http.get, () => { AddSourceLogic.actions.getSourceReConnectData('github'); - await nextTick(); - - expect(flashAPIErrors).toHaveBeenCalledWith('this is an error'); }); }); @@ -532,13 +519,8 @@ describe('AddSourceLogic', () => { expect(setPreContentSourceConfigDataSpy).toHaveBeenCalledWith(config); }); - it('handles error', async () => { - http.get.mockReturnValue(Promise.reject('this is an error')); - + itShowsServerErrorAsFlashMessage(http.get, () => { AddSourceLogic.actions.getPreContentSourceConfigData(); - await nextTick(); - - expect(flashAPIErrors).toHaveBeenCalledWith('this is an error'); }); }); @@ -601,13 +583,8 @@ describe('AddSourceLogic', () => { ); }); - it('handles error', async () => { - http.put.mockReturnValue(Promise.reject('this is an error')); - + itShowsServerErrorAsFlashMessage(http.put, () => { AddSourceLogic.actions.saveSourceConfig(true); - await nextTick(); - - expect(flashAPIErrors).toHaveBeenCalledWith('this is an error'); }); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/display_settings_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/display_settings_logic.test.ts index 62e305f72365d..81a97c2d19e16 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/display_settings_logic.test.ts +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/display_settings_logic.test.ts @@ -15,6 +15,8 @@ import { exampleResult } from '../../../../__mocks__/content_sources.mock'; import { nextTick } from '@kbn/test/jest'; +import { itShowsServerErrorAsFlashMessage } from '../../../../../test_helpers'; + const contentSource = { id: 'source123' }; jest.mock('../../source_logic', () => ({ SourceLogic: { values: { contentSource } }, @@ -31,7 +33,7 @@ import { DisplaySettingsLogic, defaultSearchResultConfig } from './display_setti describe('DisplaySettingsLogic', () => { const { http } = mockHttpValues; const { navigateToUrl } = mockKibanaValues; - const { clearFlashMessages, flashAPIErrors, flashSuccessToast } = mockFlashMessageHelpers; + const { clearFlashMessages, flashSuccessToast } = mockFlashMessageHelpers; const { mount } = new LogicMounter(DisplaySettingsLogic); const { searchResultConfig, exampleDocuments } = exampleResult; @@ -406,12 +408,8 @@ describe('DisplaySettingsLogic', () => { }); }); - it('handles error', async () => { - http.get.mockReturnValue(Promise.reject('this is an error')); + itShowsServerErrorAsFlashMessage(http.get, () => { DisplaySettingsLogic.actions.initializeDisplaySettings(); - await nextTick(); - - expect(flashAPIErrors).toHaveBeenCalledWith('this is an error'); }); }); @@ -434,12 +432,8 @@ describe('DisplaySettingsLogic', () => { }); }); - it('handles error', async () => { - http.post.mockReturnValue(Promise.reject('this is an error')); + itShowsServerErrorAsFlashMessage(http.post, () => { DisplaySettingsLogic.actions.setServerData(); - await nextTick(); - - expect(flashAPIErrors).toHaveBeenCalledWith('this is an error'); }); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/schema/schema_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/schema/schema_logic.test.ts index af9d85237335c..d284f5c741eb3 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/schema/schema_logic.test.ts +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/schema/schema_logic.test.ts @@ -29,6 +29,7 @@ Object.defineProperty(global.window, 'scrollTo', { value: spyScrollTo }); import { ADD, UPDATE } from '../../../../../shared/constants/operations'; import { defaultErrorMessage } from '../../../../../shared/flash_messages/handle_api_errors'; import { SchemaType } from '../../../../../shared/schema/types'; +import { itShowsServerErrorAsFlashMessage } from '../../../../../test_helpers'; import { AppLogic } from '../../../../app_logic'; import { @@ -40,8 +41,7 @@ import { SchemaLogic, dataTypeOptions } from './schema_logic'; describe('SchemaLogic', () => { const { http } = mockHttpValues; - const { clearFlashMessages, flashAPIErrors, flashSuccessToast, setErrorMessage } = - mockFlashMessageHelpers; + const { clearFlashMessages, flashSuccessToast, setErrorMessage } = mockFlashMessageHelpers; const { mount } = new LogicMounter(SchemaLogic); const defaultValues = { @@ -224,12 +224,8 @@ describe('SchemaLogic', () => { expect(onInitializeSchemaSpy).toHaveBeenCalledWith(serverResponse); }); - it('handles error', async () => { - http.get.mockReturnValue(Promise.reject('this is an error')); + itShowsServerErrorAsFlashMessage(http.get, () => { SchemaLogic.actions.initializeSchema(); - await nextTick(); - - expect(flashAPIErrors).toHaveBeenCalledWith('this is an error'); }); }); @@ -447,12 +443,8 @@ describe('SchemaLogic', () => { expect(onSchemaSetSuccessSpy).toHaveBeenCalledWith(serverResponse); }); - it('handles error', async () => { - http.post.mockReturnValue(Promise.reject('this is an error')); + itShowsServerErrorAsFlashMessage(http.post, () => { SchemaLogic.actions.setServerField(schema, UPDATE); - await nextTick(); - - expect(flashAPIErrors).toHaveBeenCalledWith('this is an error'); }); }); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/synchronization/synchronization_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/synchronization/synchronization_logic.test.ts index 25fb256e85f01..0ccfd6aa63ae4 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/synchronization/synchronization_logic.test.ts +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/synchronization/synchronization_logic.test.ts @@ -15,7 +15,7 @@ import { fullContentSources } from '../../../../__mocks__/content_sources.mock'; import { nextTick } from '@kbn/test/jest'; -import { expectedAsyncError } from '../../../../../test_helpers'; +import { itShowsServerErrorAsFlashMessage } from '../../../../../test_helpers'; jest.mock('../../source_logic', () => ({ SourceLogic: { actions: { setContentSource: jest.fn() } }, @@ -34,7 +34,7 @@ import { describe('SynchronizationLogic', () => { const { http } = mockHttpValues; - const { flashAPIErrors, flashSuccessToast } = mockFlashMessageHelpers; + const { flashSuccessToast } = mockFlashMessageHelpers; const { navigateToUrl } = mockKibanaValues; const { mount } = new LogicMounter(SynchronizationLogic); const contentSource = fullContentSources[0]; @@ -328,19 +328,8 @@ describe('SynchronizationLogic', () => { expect(flashSuccessToast).toHaveBeenCalledWith('Source synchronization settings updated.'); }); - it('handles error', async () => { - const error = { - response: { - error: 'this is an error', - status: 400, - }, - }; - const promise = Promise.reject(error); - http.patch.mockReturnValue(promise); + itShowsServerErrorAsFlashMessage(http.patch, () => { SynchronizationLogic.actions.updateServerSettings(body); - await expectedAsyncError(promise); - - expect(flashAPIErrors).toHaveBeenCalledWith(error); }); }); }); 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 fb88360de5df0..be288ea208858 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 @@ -20,6 +20,7 @@ import { expectedAsyncError } from '../../../test_helpers'; jest.mock('../../app_logic', () => ({ AppLogic: { values: { isOrganization: true } }, })); +import { itShowsServerErrorAsFlashMessage } from '../../../test_helpers'; import { AppLogic } from '../../app_logic'; import { SourceLogic } from './source_logic'; @@ -235,19 +236,8 @@ describe('SourceLogic', () => { expect(onUpdateSummarySpy).toHaveBeenCalledWith(contentSource.summary); }); - it('handles error', async () => { - const error = { - response: { - error: 'this is an error', - status: 400, - }, - }; - const promise = Promise.reject(error); - http.get.mockReturnValue(promise); + itShowsServerErrorAsFlashMessage(http.get, () => { SourceLogic.actions.initializeFederatedSummary(contentSource.id); - await expectedAsyncError(promise); - - expect(flashAPIErrors).toHaveBeenCalledWith(error); }); }); @@ -295,20 +285,8 @@ describe('SourceLogic', () => { expect(actions.setSearchResults).toHaveBeenCalledWith(searchServerResponse); }); - it('handles error', async () => { - const error = { - response: { - error: 'this is an error', - status: 400, - }, - }; - const promise = Promise.reject(error); - http.post.mockReturnValue(promise); - - await searchContentSourceDocuments({ sourceId: contentSource.id }, mockBreakpoint); - await expectedAsyncError(promise); - - expect(flashAPIErrors).toHaveBeenCalledWith(error); + itShowsServerErrorAsFlashMessage(http.post, () => { + searchContentSourceDocuments({ sourceId: contentSource.id }, mockBreakpoint); }); }); @@ -367,19 +345,8 @@ describe('SourceLogic', () => { expect(onUpdateSourceNameSpy).toHaveBeenCalledWith(contentSource.name); }); - it('handles error', async () => { - const error = { - response: { - error: 'this is an error', - status: 400, - }, - }; - const promise = Promise.reject(error); - http.patch.mockReturnValue(promise); + itShowsServerErrorAsFlashMessage(http.patch, () => { SourceLogic.actions.updateContentSource(contentSource.id, contentSource); - await expectedAsyncError(promise); - - expect(flashAPIErrors).toHaveBeenCalledWith(error); }); }); @@ -413,19 +380,8 @@ describe('SourceLogic', () => { expect(setButtonNotLoadingSpy).toHaveBeenCalled(); }); - it('handles error', async () => { - const error = { - response: { - error: 'this is an error', - status: 400, - }, - }; - const promise = Promise.reject(error); - http.delete.mockReturnValue(promise); + itShowsServerErrorAsFlashMessage(http.delete, () => { SourceLogic.actions.removeContentSource(contentSource.id); - await expectedAsyncError(promise); - - expect(flashAPIErrors).toHaveBeenCalledWith(error); }); }); @@ -441,19 +397,8 @@ describe('SourceLogic', () => { expect(initializeSourceSpy).toHaveBeenCalledWith(contentSource.id); }); - it('handles error', async () => { - const error = { - response: { - error: 'this is an error', - status: 400, - }, - }; - const promise = Promise.reject(error); - http.post.mockReturnValue(promise); + itShowsServerErrorAsFlashMessage(http.post, () => { SourceLogic.actions.initializeSourceSynchronization(contentSource.id); - await expectedAsyncError(promise); - - expect(flashAPIErrors).toHaveBeenCalledWith(error); }); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/sources_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/sources_logic.test.ts index 8518485c98b24..f7e41f6512017 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/sources_logic.test.ts +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/sources_logic.test.ts @@ -12,11 +12,10 @@ import { } from '../../../__mocks__/kea_logic'; import { configuredSources, contentSources } from '../../__mocks__/content_sources.mock'; -import { expectedAsyncError } from '../../../test_helpers'; - jest.mock('../../app_logic', () => ({ AppLogic: { values: { isOrganization: true } }, })); +import { itShowsServerErrorAsFlashMessage } from '../../../test_helpers'; import { AppLogic } from '../../app_logic'; import { SourcesLogic, fetchSourceStatuses, POLLING_INTERVAL } from './sources_logic'; @@ -185,19 +184,8 @@ describe('SourcesLogic', () => { expect(http.get).toHaveBeenCalledWith('/internal/workplace_search/account/sources'); }); - it('handles error', async () => { - const error = { - response: { - error: 'this is an error', - status: 400, - }, - }; - const promise = Promise.reject(error); - http.get.mockReturnValue(promise); + itShowsServerErrorAsFlashMessage(http.get, () => { SourcesLogic.actions.initializeSources(); - await expectedAsyncError(promise); - - expect(flashAPIErrors).toHaveBeenCalledWith(error); }); it('handles early logic unmount gracefully in org context', async () => { @@ -259,19 +247,8 @@ describe('SourcesLogic', () => { ); }); - it('handles error', async () => { - const error = { - response: { - error: 'this is an error', - status: 400, - }, - }; - const promise = Promise.reject(error); - http.put.mockReturnValue(promise); + itShowsServerErrorAsFlashMessage(http.put, () => { SourcesLogic.actions.setSourceSearchability(id, true); - await expectedAsyncError(promise); - - expect(flashAPIErrors).toHaveBeenCalledWith(error); }); }); @@ -367,19 +344,8 @@ describe('SourcesLogic', () => { expect(http.get).toHaveBeenCalledWith('/internal/workplace_search/account/sources/status'); }); - it('handles error', async () => { - const error = { - response: { - error: 'this is an error', - status: 400, - }, - }; - const promise = Promise.reject(error); - http.get.mockReturnValue(promise); + itShowsServerErrorAsFlashMessage(http.get, () => { fetchSourceStatuses(true, mockBreakpoint); - await expectedAsyncError(promise); - - expect(flashAPIErrors).toHaveBeenCalledWith(error); }); }); }); 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 6f811ce364290..3048dcedef26f 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 @@ -16,6 +16,7 @@ import { mockGroupValues } from './__mocks__/group_logic.mock'; import { nextTick } from '@kbn/test/jest'; +import { itShowsServerErrorAsFlashMessage } from '../../../test_helpers'; import { GROUPS_PATH } from '../../routes'; import { GroupLogic } from './group_logic'; @@ -24,8 +25,7 @@ describe('GroupLogic', () => { const { mount } = new LogicMounter(GroupLogic); const { http } = mockHttpValues; const { navigateToUrl } = mockKibanaValues; - const { clearFlashMessages, flashAPIErrors, flashSuccessToast, setQueuedErrorMessage } = - mockFlashMessageHelpers; + const { clearFlashMessages, flashSuccessToast, setQueuedErrorMessage } = mockFlashMessageHelpers; const group = groups[0]; const sourceIds = ['123', '124']; @@ -222,13 +222,8 @@ describe('GroupLogic', () => { expect(flashSuccessToast).toHaveBeenCalledWith('Group "group" was successfully deleted.'); }); - it('handles error', async () => { - http.delete.mockReturnValue(Promise.reject('this is an error')); - + itShowsServerErrorAsFlashMessage(http.delete, () => { GroupLogic.actions.deleteGroup(); - await nextTick(); - - expect(flashAPIErrors).toHaveBeenCalledWith('this is an error'); }); }); @@ -253,13 +248,8 @@ describe('GroupLogic', () => { ); }); - it('handles error', async () => { - http.put.mockReturnValue(Promise.reject('this is an error')); - + itShowsServerErrorAsFlashMessage(http.put, () => { GroupLogic.actions.updateGroupName(); - await nextTick(); - - expect(flashAPIErrors).toHaveBeenCalledWith('this is an error'); }); }); @@ -284,13 +274,8 @@ describe('GroupLogic', () => { ); }); - it('handles error', async () => { - http.post.mockReturnValue(Promise.reject('this is an error')); - + itShowsServerErrorAsFlashMessage(http.post, () => { GroupLogic.actions.saveGroupSources(); - await nextTick(); - - expect(flashAPIErrors).toHaveBeenCalledWith('this is an error'); }); }); @@ -322,13 +307,8 @@ describe('GroupLogic', () => { expect(onGroupPrioritiesChangedSpy).toHaveBeenCalledWith(group); }); - it('handles error', async () => { - http.put.mockReturnValue(Promise.reject('this is an error')); - + itShowsServerErrorAsFlashMessage(http.put, () => { GroupLogic.actions.saveGroupSourcePrioritization(); - await nextTick(); - - expect(flashAPIErrors).toHaveBeenCalledWith('this is an error'); }); }); 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 c8b725f7131a6..15951a9f8b9ca 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 @@ -20,6 +20,8 @@ import { nextTick } from '@kbn/test/jest'; import { JSON_HEADER as headers } from '../../../../../common/constants'; import { DEFAULT_META } from '../../../shared/constants'; +import { itShowsServerErrorAsFlashMessage } from '../../../test_helpers'; + import { GroupsLogic } from './groups_logic'; // We need to mock out the debounced functionality @@ -227,13 +229,8 @@ describe('GroupsLogic', () => { expect(onInitializeGroupsSpy).toHaveBeenCalledWith(groupsResponse); }); - it('handles error', async () => { - http.get.mockReturnValue(Promise.reject('this is an error')); - + itShowsServerErrorAsFlashMessage(http.get, () => { GroupsLogic.actions.initializeGroups(); - await nextTick(); - - expect(flashAPIErrors).toHaveBeenCalledWith('this is an error'); }); }); @@ -310,13 +307,8 @@ describe('GroupsLogic', () => { expect(setGroupUsersSpy).toHaveBeenCalledWith(users); }); - it('handles error', async () => { - http.get.mockReturnValue(Promise.reject('this is an error')); - + itShowsServerErrorAsFlashMessage(http.get, () => { GroupsLogic.actions.fetchGroupUsers('123'); - await nextTick(); - - expect(flashAPIErrors).toHaveBeenCalledWith('this is an error'); }); }); @@ -336,13 +328,8 @@ describe('GroupsLogic', () => { expect(setNewGroupSpy).toHaveBeenCalledWith(groups[0]); }); - it('handles error', async () => { - http.post.mockReturnValue(Promise.reject('this is an error')); - + itShowsServerErrorAsFlashMessage(http.post, () => { GroupsLogic.actions.saveNewGroup(); - await nextTick(); - - expect(flashAPIErrors).toHaveBeenCalledWith('this is an error'); }); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/role_mappings/role_mappings_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/role_mappings/role_mappings_logic.test.ts index 3f5a63275f05d..b70039636bba0 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/role_mappings/role_mappings_logic.test.ts +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/role_mappings/role_mappings_logic.test.ts @@ -23,6 +23,8 @@ import { } from '../../../shared/role_mapping/__mocks__/roles'; import { ANY_AUTH_PROVIDER } from '../../../shared/role_mapping/constants'; +import { itShowsServerErrorAsFlashMessage } from '../../../test_helpers'; + import { RoleMappingsLogic } from './role_mappings_logic'; const emptyUser = { username: '', email: '' }; @@ -349,12 +351,8 @@ describe('RoleMappingsLogic', () => { expect(setRoleMappingsSpy).toHaveBeenCalledWith(mappingsServerProps); }); - it('handles error', async () => { - http.post.mockReturnValue(Promise.reject('this is an error')); + itShowsServerErrorAsFlashMessage(http.post, () => { RoleMappingsLogic.actions.enableRoleBasedAccess(); - await nextTick(); - - expect(flashAPIErrors).toHaveBeenCalledWith('this is an error'); }); }); @@ -369,12 +367,8 @@ describe('RoleMappingsLogic', () => { expect(setRoleMappingsDataSpy).toHaveBeenCalledWith(mappingsServerProps); }); - it('handles error', async () => { - http.get.mockReturnValue(Promise.reject('this is an error')); + itShowsServerErrorAsFlashMessage(http.get, () => { RoleMappingsLogic.actions.initializeRoleMappings(); - await nextTick(); - - expect(flashAPIErrors).toHaveBeenCalledWith('this is an error'); }); it('resets roleMapping state', () => { diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/security/security_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/security/security_logic.test.ts index bc45609e9e83d..df9035d57e56b 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/security/security_logic.test.ts +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/security/security_logic.test.ts @@ -5,19 +5,16 @@ * 2.0. */ -import { - LogicMounter, - mockHttpValues, - mockFlashMessageHelpers, -} from '../../../__mocks__/kea_logic'; +import { LogicMounter, mockHttpValues } from '../../../__mocks__/kea_logic'; import { nextTick } from '@kbn/test/jest'; +import { itShowsServerErrorAsFlashMessage } from '../../../test_helpers'; + import { SecurityLogic } from './security_logic'; describe('SecurityLogic', () => { const { http } = mockHttpValues; - const { flashAPIErrors } = mockFlashMessageHelpers; const { mount } = new LogicMounter(SecurityLogic); beforeEach(() => { @@ -124,15 +121,8 @@ describe('SecurityLogic', () => { expect(setServerPropsSpy).toHaveBeenCalledWith(serverProps); }); - it('handles error', async () => { - http.get.mockReturnValue(Promise.reject('this is an error')); - + itShowsServerErrorAsFlashMessage(http.get, () => { SecurityLogic.actions.initializeSourceRestrictions(); - try { - await nextTick(); - } catch { - expect(flashAPIErrors).toHaveBeenCalledWith('this is an error'); - } }); }); @@ -150,15 +140,8 @@ describe('SecurityLogic', () => { ); }); - it('handles error', async () => { - http.patch.mockReturnValue(Promise.reject('this is an error')); - + itShowsServerErrorAsFlashMessage(http.patch, () => { SecurityLogic.actions.saveSourceRestrictions(); - try { - await nextTick(); - } catch { - expect(flashAPIErrors).toHaveBeenCalledWith('this is an error'); - } }); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/settings/settings_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/settings/settings_logic.test.ts index ebb790b59c1fa..d98c9efe04d8c 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/settings/settings_logic.test.ts +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/settings/settings_logic.test.ts @@ -15,6 +15,7 @@ import { configuredSources, oauthApplication } from '../../__mocks__/content_sou import { nextTick } from '@kbn/test/jest'; +import { itShowsServerErrorAsFlashMessage } from '../../../test_helpers'; import { ORG_UPDATED_MESSAGE, OAUTH_APP_UPDATED_MESSAGE } from '../../constants'; import { SettingsLogic } from './settings_logic'; @@ -22,7 +23,7 @@ import { SettingsLogic } from './settings_logic'; describe('SettingsLogic', () => { const { http } = mockHttpValues; const { navigateToUrl } = mockKibanaValues; - const { clearFlashMessages, flashAPIErrors, flashSuccessToast } = mockFlashMessageHelpers; + const { clearFlashMessages, flashSuccessToast } = mockFlashMessageHelpers; const { mount } = new LogicMounter(SettingsLogic); const ORG_NAME = 'myOrg'; const defaultValues = { @@ -127,12 +128,8 @@ describe('SettingsLogic', () => { expect(setServerPropsSpy).toHaveBeenCalledWith(configuredSources); }); - it('handles error', async () => { - http.get.mockReturnValue(Promise.reject('this is an error')); + itShowsServerErrorAsFlashMessage(http.get, () => { SettingsLogic.actions.initializeSettings(); - await nextTick(); - - expect(flashAPIErrors).toHaveBeenCalledWith('this is an error'); }); }); @@ -150,12 +147,8 @@ describe('SettingsLogic', () => { expect(onInitializeConnectorsSpy).toHaveBeenCalledWith(serverProps); }); - it('handles error', async () => { - http.get.mockReturnValue(Promise.reject('this is an error')); + itShowsServerErrorAsFlashMessage(http.get, () => { SettingsLogic.actions.initializeConnectors(); - await nextTick(); - - expect(flashAPIErrors).toHaveBeenCalledWith('this is an error'); }); }); @@ -176,12 +169,8 @@ describe('SettingsLogic', () => { expect(setUpdatedNameSpy).toHaveBeenCalledWith({ organizationName: NAME }); }); - it('handles error', async () => { - http.put.mockReturnValue(Promise.reject('this is an error')); + itShowsServerErrorAsFlashMessage(http.put, () => { SettingsLogic.actions.updateOrgName(); - - await nextTick(); - expect(flashAPIErrors).toHaveBeenCalledWith('this is an error'); }); }); @@ -205,12 +194,8 @@ describe('SettingsLogic', () => { expect(setIconSpy).toHaveBeenCalledWith(ICON); }); - it('handles error', async () => { - http.put.mockReturnValue(Promise.reject('this is an error')); + itShowsServerErrorAsFlashMessage(http.put, () => { SettingsLogic.actions.updateOrgIcon(); - - await nextTick(); - expect(flashAPIErrors).toHaveBeenCalledWith('this is an error'); }); }); @@ -234,12 +219,8 @@ describe('SettingsLogic', () => { expect(setLogoSpy).toHaveBeenCalledWith(LOGO); }); - it('handles error', async () => { - http.put.mockReturnValue(Promise.reject('this is an error')); + itShowsServerErrorAsFlashMessage(http.put, () => { SettingsLogic.actions.updateOrgLogo(); - - await nextTick(); - expect(flashAPIErrors).toHaveBeenCalledWith('this is an error'); }); }); @@ -291,12 +272,8 @@ describe('SettingsLogic', () => { expect(flashSuccessToast).toHaveBeenCalledWith(OAUTH_APP_UPDATED_MESSAGE); }); - it('handles error', async () => { - http.put.mockReturnValue(Promise.reject('this is an error')); + itShowsServerErrorAsFlashMessage(http.put, () => { SettingsLogic.actions.updateOauthApplication(); - await nextTick(); - - expect(flashAPIErrors).toHaveBeenCalledWith('this is an error'); }); }); @@ -313,12 +290,8 @@ describe('SettingsLogic', () => { expect(flashSuccessToast).toHaveBeenCalled(); }); - it('handles error', async () => { - http.delete.mockReturnValue(Promise.reject('this is an error')); + itShowsServerErrorAsFlashMessage(http.delete, () => { SettingsLogic.actions.deleteSourceConfig(SERVICE_TYPE, NAME); - await nextTick(); - - expect(flashAPIErrors).toHaveBeenCalledWith('this is an error'); }); }); diff --git a/x-pack/plugins/enterprise_search/server/lib/enterprise_search_config_api.test.ts b/x-pack/plugins/enterprise_search/server/lib/enterprise_search_config_api.test.ts index 3f2a038d8bff3..ba600de298976 100644 --- a/x-pack/plugins/enterprise_search/server/lib/enterprise_search_config_api.test.ts +++ b/x-pack/plugins/enterprise_search/server/lib/enterprise_search_config_api.test.ts @@ -46,7 +46,6 @@ describe('callEnterpriseSearchConfigAPI', () => { settings: { external_url: 'http://some.vanity.url/', read_only_mode: false, - ilm_enabled: true, is_federated_auth: false, search_oauth: { client_id: 'someUID', @@ -139,7 +138,6 @@ describe('callEnterpriseSearchConfigAPI', () => { }, publicUrl: undefined, readOnlyMode: false, - ilmEnabled: false, searchOAuth: { clientId: undefined, redirectUrl: undefined, diff --git a/x-pack/plugins/enterprise_search/server/lib/enterprise_search_config_api.ts b/x-pack/plugins/enterprise_search/server/lib/enterprise_search_config_api.ts index 146b06e4d9a4c..d652d56c28efe 100644 --- a/x-pack/plugins/enterprise_search/server/lib/enterprise_search_config_api.ts +++ b/x-pack/plugins/enterprise_search/server/lib/enterprise_search_config_api.ts @@ -74,7 +74,6 @@ export const callEnterpriseSearchConfigAPI = async ({ }, publicUrl: stripTrailingSlash(data?.settings?.external_url), readOnlyMode: !!data?.settings?.read_only_mode, - ilmEnabled: !!data?.settings?.ilm_enabled, searchOAuth: { clientId: data?.settings?.search_oauth?.client_id, redirectUrl: data?.settings?.search_oauth?.redirect_url, diff --git a/x-pack/plugins/enterprise_search/server/routes/app_search/crawler.test.ts b/x-pack/plugins/enterprise_search/server/routes/app_search/crawler.test.ts index 5dff1b934ae5a..01c2ff42fc010 100644 --- a/x-pack/plugins/enterprise_search/server/routes/app_search/crawler.test.ts +++ b/x-pack/plugins/enterprise_search/server/routes/app_search/crawler.test.ts @@ -109,6 +109,45 @@ describe('crawler routes', () => { }); }); + describe('GET /internal/app_search/engines/{name}/crawler/domains', () => { + let mockRouter: MockRouter; + + beforeEach(() => { + jest.clearAllMocks(); + mockRouter = new MockRouter({ + method: 'get', + path: '/internal/app_search/engines/{name}/crawler/domains', + }); + + registerCrawlerRoutes({ + ...mockDependencies, + router: mockRouter.router, + }); + }); + + it('creates a request to enterprise search', () => { + expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({ + path: '/api/as/v0/engines/:name/crawler/domains', + }); + }); + + it('validates correctly', () => { + const request = { + params: { name: 'some-engine' }, + query: { + 'page[current]': 5, + 'page[size]': 10, + }, + }; + mockRouter.shouldValidate(request); + }); + + it('fails validation without required params', () => { + const request = { params: {} }; + mockRouter.shouldThrow(request); + }); + }); + describe('POST /internal/app_search/engines/{name}/crawler/crawl_requests/cancel', () => { let mockRouter: MockRouter; diff --git a/x-pack/plugins/enterprise_search/server/routes/app_search/crawler.ts b/x-pack/plugins/enterprise_search/server/routes/app_search/crawler.ts index 72a48a013636c..9336d9ac93e70 100644 --- a/x-pack/plugins/enterprise_search/server/routes/app_search/crawler.ts +++ b/x-pack/plugins/enterprise_search/server/routes/app_search/crawler.ts @@ -69,6 +69,24 @@ export function registerCrawlerRoutes({ }) ); + router.get( + { + path: '/internal/app_search/engines/{name}/crawler/domains', + validate: { + params: schema.object({ + name: schema.string(), + }), + query: schema.object({ + 'page[current]': schema.number(), + 'page[size]': schema.number(), + }), + }, + }, + enterpriseSearchRequestHandler.createRequest({ + path: '/api/as/v0/engines/:name/crawler/domains', + }) + ); + router.post( { path: '/internal/app_search/engines/{name}/crawler/domains', diff --git a/x-pack/plugins/fleet/common/constants/agent.ts b/x-pack/plugins/fleet/common/constants/agent.ts index e38b7a6b5832b..832a9027c6517 100644 --- a/x-pack/plugins/fleet/common/constants/agent.ts +++ b/x-pack/plugins/fleet/common/constants/agent.ts @@ -5,8 +5,7 @@ * 2.0. */ -export const AGENT_SAVED_OBJECT_TYPE = 'fleet-agents'; -export const AGENT_ACTION_SAVED_OBJECT_TYPE = 'fleet-agent-actions'; +export const AGENTS_PREFIX = 'fleet-agents'; export const AGENT_TYPE_PERMANENT = 'PERMANENT'; export const AGENT_TYPE_EPHEMERAL = 'EPHEMERAL'; diff --git a/x-pack/plugins/fleet/common/constants/enrollment_api_key.ts b/x-pack/plugins/fleet/common/constants/enrollment_api_key.ts index 25d395893aaec..c9a449939ef3f 100644 --- a/x-pack/plugins/fleet/common/constants/enrollment_api_key.ts +++ b/x-pack/plugins/fleet/common/constants/enrollment_api_key.ts @@ -5,6 +5,4 @@ * 2.0. */ -export const ENROLLMENT_API_KEYS_SAVED_OBJECT_TYPE = 'fleet-enrollment-api-keys'; - export const ENROLLMENT_API_KEYS_INDEX = '.fleet-enrollment-api-keys'; diff --git a/x-pack/plugins/fleet/common/constants/output.ts b/x-pack/plugins/fleet/common/constants/output.ts index 9a236001aca25..c750be12be2df 100644 --- a/x-pack/plugins/fleet/common/constants/output.ts +++ b/x-pack/plugins/fleet/common/constants/output.ts @@ -18,6 +18,7 @@ export const DEFAULT_OUTPUT_ID = 'default'; export const DEFAULT_OUTPUT: NewOutput = { name: DEFAULT_OUTPUT_ID, is_default: true, + is_default_monitoring: true, type: outputType.Elasticsearch, hosts: [''], }; diff --git a/x-pack/plugins/fleet/common/constants/routes.ts b/x-pack/plugins/fleet/common/constants/routes.ts index aa5e0dbcd5ed1..be4103c549f1a 100644 --- a/x-pack/plugins/fleet/common/constants/routes.ts +++ b/x-pack/plugins/fleet/common/constants/routes.ts @@ -12,7 +12,6 @@ export const EPM_API_ROOT = `${API_ROOT}/epm`; export const DATA_STREAM_API_ROOT = `${API_ROOT}/data_streams`; export const PACKAGE_POLICY_API_ROOT = `${API_ROOT}/package_policies`; export const AGENT_POLICY_API_ROOT = `${API_ROOT}/agent_policies`; -export const FLEET_API_ROOT_7_9 = `/api/ingest_manager/fleet`; export const LIMITED_CONCURRENCY_ROUTE_TAG = 'ingest:limited-concurrency'; diff --git a/x-pack/plugins/fleet/common/types/models/agent.ts b/x-pack/plugins/fleet/common/types/models/agent.ts index 6913fc52d8c62..018f591fef79c 100644 --- a/x-pack/plugins/fleet/common/types/models/agent.ts +++ b/x-pack/plugins/fleet/common/types/models/agent.ts @@ -66,15 +66,6 @@ export interface AgentPolicyAction extends NewAgentAction { ack_data?: any; } -// Make policy change action renaming BWC with agent version <= 7.9 -// eslint-disable-next-line @typescript-eslint/naming-convention -export type AgentPolicyActionV7_9 = Omit & { - type: 'CONFIG_CHANGE'; - data: { - config: FullAgentPolicy; - }; -}; - interface CommonAgentActionSOAttributes { type: AgentActionType; sent_at?: string; diff --git a/x-pack/plugins/fleet/common/types/models/output.ts b/x-pack/plugins/fleet/common/types/models/output.ts index 4f70460e89ff8..fada8171b91fc 100644 --- a/x-pack/plugins/fleet/common/types/models/output.ts +++ b/x-pack/plugins/fleet/common/types/models/output.ts @@ -12,6 +12,7 @@ export type OutputType = typeof outputType; export interface NewOutput { is_default: boolean; + is_default_monitoring: boolean; name: string; type: ValueOf; hosts?: string[]; diff --git a/x-pack/plugins/fleet/common/types/rest_spec/package_policy.ts b/x-pack/plugins/fleet/common/types/rest_spec/package_policy.ts index ed5f8e07098d4..b050a7c798a0b 100644 --- a/x-pack/plugins/fleet/common/types/rest_spec/package_policy.ts +++ b/x-pack/plugins/fleet/common/types/rest_spec/package_policy.ts @@ -67,6 +67,12 @@ export type DeletePackagePoliciesResponse = Array<{ export interface UpgradePackagePolicyBaseResponse { name?: string; + + // Support generic errors + statusCode?: number; + body?: { + message: string; + }; } export interface UpgradePackagePolicyDryRunResponseItem extends UpgradePackagePolicyBaseResponse { diff --git a/x-pack/plugins/fleet/dev_docs/data_model.md b/x-pack/plugins/fleet/dev_docs/data_model.md index ec9fa031d09d3..be0e06e5439dc 100644 --- a/x-pack/plugins/fleet/dev_docs/data_model.md +++ b/x-pack/plugins/fleet/dev_docs/data_model.md @@ -37,8 +37,6 @@ All of the code that interacts with this index is currently located in [`x-pack/plugins/fleet/server/services/agents/crud.ts`](../server/services/agents/crud.ts) and the schema of these documents is maintained by the `FleetServerAgent` TypeScript interface. -Prior to Fleet Server, this data was stored in the `fleet-agents` Saved Object type which is now obsolete. - ### `.fleet-actions` index Each document in this index represents an action that was initiated by a user and needs to be processed by Fleet Server @@ -167,46 +165,3 @@ represents the relative file path of the file from the package contents Used as "tombstone record" to indicate that a package that was installed by default through preconfiguration was explicitly deleted by user. Used to avoid recreating a preconfiguration policy that a user explicitly does not want. - -### `fleet-agents` - -**DEPRECATED in favor of `.fleet-agents` index.** - -- Constant in code: `AGENT_SAVED_OBJECT_TYPE` -- Introduced in ? -- [Code Link](../server/saved_objects/index.ts#76) -- Migrations: 7.10.0, 7.12.0 -- References to other objects: - - `policy_id` - ID that points to the policy (`ingest-agent-policies`) this agent is assigned to. - - `access_api_key_id` - - `default_api_key_id` - -Tracks an individual Elastic Agent's enrollment in the Fleet, which policy it is current assigned to, its check in -status, which packages are currently installed, and other metadata about the Agent. - -### `fleet-agent-actions` - -**DEPRECATED in favor of `.fleet-agent-actions` index.** - -- Constant in code: `AGENT_ACTION_SAVED_OBJECT_TYPE` -- Introduced in ? -- [Code Link](../server/saved_objects/index.ts#113) -- Migrations: 7.10.0 -- References to other objects: - - `agent_id` - ID that points to the agent for this action (`fleet-agents`) - - `policy_id`- ID that points to the policy for this action (`ingest-agent-policies`) - - -### `fleet-enrollment-api-keys` - -**DEPRECATED in favor of `.fleet-enrollment-api-keys` index.** - -- Constant in code: `ENROLLMENT_API_KEYS_SAVED_OBJECT_TYPE` -- Introduced in ? -- [Code Link](../server/saved_objects/index.ts#166) -- Migrations: 7.10.0 -- References to other objects: - - `api_key_id` - - `policy_id` - ID that points to an agent policy (`ingest-agent-policies`) - -Contains an enrollment key that can be used to enroll a new agent in a specific agent policy. \ No newline at end of file diff --git a/x-pack/plugins/fleet/public/applications/fleet/app.tsx b/x-pack/plugins/fleet/public/applications/fleet/app.tsx index 682c889d80b97..73be40c3c32d9 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/app.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/app.tsx @@ -8,7 +8,7 @@ import type { FunctionComponent } from 'react'; import React, { memo, useEffect, useState } from 'react'; import type { AppMountParameters } from 'kibana/public'; -import { EuiCode, EuiEmptyPrompt, EuiErrorBoundary, EuiPanel, EuiPortal } from '@elastic/eui'; +import { EuiCode, EuiEmptyPrompt, EuiErrorBoundary, EuiPanel } from '@elastic/eui'; import type { History } from 'history'; import { Router, Redirect, Route, Switch, useRouteMatch } from 'react-router-dom'; import { FormattedMessage } from '@kbn/i18n/react'; @@ -25,7 +25,7 @@ import { } from '../../../../../../src/plugins/kibana_react/public'; import { EuiThemeProvider } from '../../../../../../src/plugins/kibana_react/common'; -import { PackageInstallProvider, useUrlModal } from '../integrations/hooks'; +import { PackageInstallProvider } from '../integrations/hooks'; import { ConfigContext, @@ -37,7 +37,7 @@ import { useStartServices, UIExtensionsContext, } from './hooks'; -import { Error, Loading, SettingFlyout, FleetSetupLoading } from './components'; +import { Error, Loading, FleetSetupLoading } from './components'; import type { UIExtensionsStorage } from './types'; import { FLEET_ROUTING_PATHS } from './constants'; @@ -48,6 +48,7 @@ import { AgentsApp } from './sections/agents'; import { MissingESRequirementsPage } from './sections/agents/agent_requirements_page'; import { CreatePackagePolicyPage } from './sections/agent_policy/create_package_policy_page'; import { EnrollmentTokenListPage } from './sections/agents/enrollment_token_list_page'; +import { SettingsApp } from './sections/settings'; const FEEDBACK_URL = 'https://ela.st/fleet-feedback'; @@ -244,7 +245,6 @@ export const FleetAppContext: React.FC<{ const FleetTopNav = memo( ({ setHeaderActionMenu }: { setHeaderActionMenu: AppMountParameters['setHeaderActionMenu'] }) => { - const { getModalHref } = useUrlModal(); const services = useStartServices(); const { TopNavMenu } = services.navigation.ui; @@ -257,14 +257,6 @@ const FleetTopNav = memo( iconType: 'popout', run: () => window.open(FEEDBACK_URL), }, - - { - label: i18n.translate('xpack.fleet.appNavigation.settingsButton', { - defaultMessage: 'Fleet settings', - }), - iconType: 'gear', - run: () => services.application.navigateToUrl(getModalHref('settings')), - }, ]; return ( { - const { modal, setModal } = useUrlModal(); - return ( <> - {modal === 'settings' && ( - - { - setModal(null); - }} - /> - - )} - @@ -308,6 +288,10 @@ export const AppRoutes = memo( + + + + {/* TODO: Move this route to the Integrations app */} diff --git a/x-pack/plugins/fleet/public/applications/fleet/components/search_bar.tsx b/x-pack/plugins/fleet/public/applications/fleet/components/search_bar.tsx index 70becfe40d8e2..f1a23ea759def 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/components/search_bar.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/components/search_bar.tsx @@ -12,9 +12,9 @@ import { fromKueryExpression } from '@kbn/es-query'; import type { IFieldType } from '../../../../../../../src/plugins/data/public'; import { QueryStringInput } from '../../../../../../../src/plugins/data/public'; import { useStartServices } from '../hooks'; -import { INDEX_NAME, AGENT_SAVED_OBJECT_TYPE } from '../constants'; +import { INDEX_NAME, AGENTS_PREFIX } from '../constants'; -const HIDDEN_FIELDS = [`${AGENT_SAVED_OBJECT_TYPE}.actions`, '_id', '_index']; +const HIDDEN_FIELDS = [`${AGENTS_PREFIX}.actions`, '_id', '_index']; interface Props { value: string; diff --git a/x-pack/plugins/fleet/public/applications/fleet/hooks/use_breadcrumbs.tsx b/x-pack/plugins/fleet/public/applications/fleet/hooks/use_breadcrumbs.tsx index 0202fc3351fc0..d77b243500100 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/hooks/use_breadcrumbs.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/hooks/use_breadcrumbs.tsx @@ -147,6 +147,14 @@ const breadcrumbGetters: { }), }, ], + settings: () => [ + BASE_BREADCRUMB, + { + text: i18n.translate('xpack.fleet.breadcrumbs.settingsPageTitle', { + defaultMessage: 'Settings', + }), + }, + ], }; export function useBreadcrumbs(page: Page, values: DynamicPagePathValues = {}) { diff --git a/x-pack/plugins/fleet/public/applications/fleet/layouts/default/default.tsx b/x-pack/plugins/fleet/public/applications/fleet/layouts/default/default.tsx index dd15020adcc75..c8dd428f0df5e 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/layouts/default/default.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/layouts/default/default.tsx @@ -78,6 +78,17 @@ export const DefaultLayout: React.FunctionComponent = ({ href: getHref('data_streams'), 'data-test-subj': 'fleet-datastreams-tab', }, + { + name: ( + + ), + isSelected: section === 'settings', + href: getHref('settings'), + 'data-test-subj': 'fleet-settings-tab', + }, ]} > {children} diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/components/agent_policy_delete_provider.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/components/agent_policy_delete_provider.tsx index c5d0e5279220e..b8d8f212a5451 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/components/agent_policy_delete_provider.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/components/agent_policy_delete_provider.tsx @@ -10,7 +10,7 @@ import { EuiConfirmModal, EuiCallOut } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; -import { AGENT_SAVED_OBJECT_TYPE } from '../../../constants'; +import { AGENTS_PREFIX } from '../../../constants'; import { sendDeleteAgentPolicy, useStartServices, useConfig, sendRequest } from '../../../hooks'; interface Props { @@ -98,7 +98,7 @@ export const AgentPolicyDeleteProvider: React.FunctionComponent = ({ chil path: `/api/fleet/agents`, method: 'get', query: { - kuery: `${AGENT_SAVED_OBJECT_TYPE}.policy_id : ${agentPolicyToCheck}`, + kuery: `${AGENTS_PREFIX}.policy_id : ${agentPolicyToCheck}`, }, }); setAgentsCount(data?.total || 0); diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_details_page/components/agent_logs/index.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_details_page/components/agent_logs/index.tsx index 5411e6313ebb7..76a7f0514a8a2 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_details_page/components/agent_logs/index.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_details_page/components/agent_logs/index.tsx @@ -56,7 +56,7 @@ export const AgentLogs: React.FunctionComponent(false); useEffect(() => { - const stateStorage = createKbnUrlStateStorage(); + const stateStorage = createKbnUrlStateStorage({ useHashQuery: false, useHash: false }); const { start, stop } = syncState({ storageKey: STATE_STORAGE_KEY, stateContainer: stateContainer as INullableBaseStateContainer, diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/index.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/index.tsx index c6e92cbce8d18..9ba63475aaaad 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/index.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/index.tsx @@ -42,7 +42,7 @@ import { ContextMenuActions, } from '../../../components'; import { AgentStatusKueryHelper, isAgentUpgradeable } from '../../../services'; -import { AGENT_SAVED_OBJECT_TYPE, FLEET_SERVER_PACKAGE } from '../../../constants'; +import { AGENTS_PREFIX, FLEET_SERVER_PACKAGE } from '../../../constants'; import { AgentReassignAgentPolicyModal, AgentHealth, @@ -207,7 +207,7 @@ export const AgentListPage: React.FunctionComponent<{}> = () => { if (kueryBuilder) { kueryBuilder = `(${kueryBuilder}) and`; } - kueryBuilder = `${kueryBuilder} ${AGENT_SAVED_OBJECT_TYPE}.policy_id : (${selectedAgentPolicies + kueryBuilder = `${kueryBuilder} ${AGENTS_PREFIX}.policy_id : (${selectedAgentPolicies .map((agentPolicy) => `"${agentPolicy}"`) .join(' or ')})`; } diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_requirements_page/components/fleet_server_on_prem_instructions.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_requirements_page/components/fleet_server_on_prem_instructions.tsx index 1092b7ac89c07..e3bb252beb96f 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_requirements_page/components/fleet_server_on_prem_instructions.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_requirements_page/components/fleet_server_on_prem_instructions.tsx @@ -41,7 +41,7 @@ import { sendPutSettings, sendGetFleetStatus, useFleetStatus, - useUrlModal, + useLink, } from '../../../../hooks'; import type { PLATFORM_TYPE } from '../../../../hooks'; import type { PackagePolicy } from '../../../../types'; @@ -368,7 +368,7 @@ const AgentPolicySelectionStep = ({ @@ -416,7 +416,8 @@ export const AddFleetServerHostStepContent = ({ const [isLoading, setIsLoading] = useState(false); const [fleetServerHost, setFleetServerHost] = useState(''); const [error, setError] = useState(); - const { getModalHref } = useUrlModal(); + + const { getHref } = useLink(); const validate = useCallback( (host: string) => { @@ -519,7 +520,7 @@ export const AddFleetServerHostStepContent = ({ values={{ host: calloutHost, fleetSettingsLink: ( - + { installCommand, platform, setPlatform, - refresh, deploymentMode, setDeploymentMode, fleetServerHost, addFleetServerHost, } = useFleetServerInstructions(policyId); - const { modal } = useUrlModal(); - useEffect(() => { - // Refresh settings when the settings modal is closed - if (!modal) { - refresh(); - } - }, [modal, refresh]); - const { docLinks } = useStartServices(); const [isWaitingForFleetServer, setIsWaitingForFleetServer] = useState(true); diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_requirements_page/components/install_command_utils.test.ts b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_requirements_page/components/install_command_utils.test.ts index b4e7982c52f7b..62580a1445f06 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_requirements_page/components/install_command_utils.test.ts +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_requirements_page/components/install_command_utils.test.ts @@ -17,9 +17,9 @@ describe('getInstallCommandForPlatform', () => { ); expect(res).toMatchInlineSnapshot(` - "sudo ./elastic-agent install -f \\\\ - --fleet-server-es=http://elasticsearch:9200 \\\\ - --fleet-server-service-token=service-token-1" + "sudo ./elastic-agent install \\\\ + --fleet-server-es=http://elasticsearch:9200 \\\\ + --fleet-server-service-token=service-token-1" `); }); @@ -31,9 +31,9 @@ describe('getInstallCommandForPlatform', () => { ); expect(res).toMatchInlineSnapshot(` - ".\\\\elastic-agent.exe install -f \` - --fleet-server-es=http://elasticsearch:9200 \` - --fleet-server-service-token=service-token-1" + ".\\\\elastic-agent.exe install \` + --fleet-server-es=http://elasticsearch:9200 \` + --fleet-server-service-token=service-token-1" `); }); @@ -45,9 +45,9 @@ describe('getInstallCommandForPlatform', () => { ); expect(res).toMatchInlineSnapshot(` - "sudo elastic-agent enroll -f \\\\ - --fleet-server-es=http://elasticsearch:9200 \\\\ - --fleet-server-service-token=service-token-1" + "sudo elastic-agent enroll \\\\ + --fleet-server-es=http://elasticsearch:9200 \\\\ + --fleet-server-service-token=service-token-1" `); }); }); @@ -62,9 +62,9 @@ describe('getInstallCommandForPlatform', () => { ); expect(res).toMatchInlineSnapshot(` - "sudo ./elastic-agent install -f \\\\ - --fleet-server-es=http://elasticsearch:9200 \\\\ - --fleet-server-service-token=service-token-1 \\\\ + "sudo ./elastic-agent install \\\\ + --fleet-server-es=http://elasticsearch:9200 \\\\ + --fleet-server-service-token=service-token-1 \\\\ --fleet-server-policy=policy-1" `); }); @@ -78,9 +78,9 @@ describe('getInstallCommandForPlatform', () => { ); expect(res).toMatchInlineSnapshot(` - ".\\\\elastic-agent.exe install -f \` - --fleet-server-es=http://elasticsearch:9200 \` - --fleet-server-service-token=service-token-1 \` + ".\\\\elastic-agent.exe install \` + --fleet-server-es=http://elasticsearch:9200 \` + --fleet-server-service-token=service-token-1 \` --fleet-server-policy=policy-1" `); }); @@ -94,9 +94,9 @@ describe('getInstallCommandForPlatform', () => { ); expect(res).toMatchInlineSnapshot(` - "sudo elastic-agent enroll -f \\\\ - --fleet-server-es=http://elasticsearch:9200 \\\\ - --fleet-server-service-token=service-token-1 \\\\ + "sudo elastic-agent enroll \\\\ + --fleet-server-es=http://elasticsearch:9200 \\\\ + --fleet-server-service-token=service-token-1 \\\\ --fleet-server-policy=policy-1" `); }); @@ -115,9 +115,8 @@ describe('getInstallCommandForPlatform', () => { expect(res).toMatchInlineSnapshot(` "sudo ./elastic-agent install --url=http://fleetserver:8220 \\\\ - -f \\\\ - --fleet-server-es=http://elasticsearch:9200 \\\\ - --fleet-server-service-token=service-token-1 \\\\ + --fleet-server-es=http://elasticsearch:9200 \\\\ + --fleet-server-service-token=service-token-1 \\\\ --fleet-server-policy=policy-1 \\\\ --certificate-authorities= \\\\ --fleet-server-es-ca= \\\\ @@ -138,9 +137,8 @@ describe('getInstallCommandForPlatform', () => { expect(res).toMatchInlineSnapshot(` ".\\\\elastic-agent.exe install --url=http://fleetserver:8220 \` - -f \` - --fleet-server-es=http://elasticsearch:9200 \` - --fleet-server-service-token=service-token-1 \` + --fleet-server-es=http://elasticsearch:9200 \` + --fleet-server-service-token=service-token-1 \` --fleet-server-policy=policy-1 \` --certificate-authorities= \` --fleet-server-es-ca= \` @@ -161,9 +159,8 @@ describe('getInstallCommandForPlatform', () => { expect(res).toMatchInlineSnapshot(` "sudo elastic-agent enroll --url=http://fleetserver:8220 \\\\ - -f \\\\ - --fleet-server-es=http://elasticsearch:9200 \\\\ - --fleet-server-service-token=service-token-1 \\\\ + --fleet-server-es=http://elasticsearch:9200 \\\\ + --fleet-server-service-token=service-token-1 \\\\ --fleet-server-policy=policy-1 \\\\ --certificate-authorities= \\\\ --fleet-server-es-ca= \\\\ @@ -181,9 +178,9 @@ describe('getInstallCommandForPlatform', () => { ); expect(res).toMatchInlineSnapshot(` - "sudo elastic-agent enroll -f \\\\ - --fleet-server-es=http://elasticsearch:9200 \\\\ - --fleet-server-service-token=service-token-1" + "sudo elastic-agent enroll \\\\ + --fleet-server-es=http://elasticsearch:9200 \\\\ + --fleet-server-service-token=service-token-1" `); }); }); diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_requirements_page/components/install_command_utils.ts b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_requirements_page/components/install_command_utils.ts index e129d7a4d5b4e..f5c40e8071691 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_requirements_page/components/install_command_utils.ts +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_requirements_page/components/install_command_utils.ts @@ -20,10 +20,12 @@ export function getInstallCommandForPlatform( if (isProductionDeployment && fleetServerHost) { commandArguments += `--url=${fleetServerHost} ${newLineSeparator}\n`; + } else { + commandArguments += ` ${newLineSeparator}\n`; } - commandArguments += ` -f ${newLineSeparator}\n --fleet-server-es=${esHost}`; - commandArguments += ` ${newLineSeparator}\n --fleet-server-service-token=${serviceToken}`; + commandArguments += ` --fleet-server-es=${esHost}`; + commandArguments += ` ${newLineSeparator}\n --fleet-server-service-token=${serviceToken}`; if (policyId) { commandArguments += ` ${newLineSeparator}\n --fleet-server-policy=${policyId}`; } diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/components/agent_health.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/components/agent_health.tsx index fbac6ad74906d..701d68c0e29e3 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/components/agent_health.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/components/agent_health.tsx @@ -9,7 +9,7 @@ import React from 'react'; import { FormattedMessage, FormattedRelative } from '@kbn/i18n/react'; import { EuiBadge, EuiToolTip } from '@elastic/eui'; -import * as euiVars from '@elastic/eui/dist/eui_theme_light.json'; +import { euiLightVars as euiVars } from '@kbn/ui-shared-deps-src/theme'; import type { Agent } from '../../../types'; @@ -29,7 +29,7 @@ const Status = { ), Inactive: ( - + ), diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/services/agent_status.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/services/agent_status.tsx index 74e9879936d42..8eafcef0dc6de 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/services/agent_status.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/services/agent_status.tsx @@ -7,20 +7,20 @@ import { euiPaletteColorBlindBehindText } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import * as euiVars from '@elastic/eui/dist/eui_theme_light.json'; +import { euiLightVars } from '@kbn/ui-shared-deps-src/theme'; import type { SimplifiedAgentStatus } from '../../../types'; const visColors = euiPaletteColorBlindBehindText(); const colorToHexMap = { // using variables as mentioned here https://elastic.github.io/eui/#/guidelines/getting-started - default: euiVars.default.euiColorLightShade, + default: euiLightVars.euiColorLightShade, primary: visColors[1], secondary: visColors[0], accent: visColors[2], warning: visColors[5], danger: visColors[9], - inactive: euiVars.default.euiColorDarkShade, + inactive: euiLightVars.euiColorDarkShade, }; export const AGENT_STATUSES: SimplifiedAgentStatus[] = [ diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/index.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/index.tsx index b36fbf4bb815e..848ceac11c001 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/index.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/index.tsx @@ -9,4 +9,9 @@ export { AgentPolicyApp } from './agent_policy'; export { DataStreamApp } from './data_stream'; export { AgentsApp } from './agents'; -export type Section = 'agents' | 'agent_policies' | 'enrollment_tokens' | 'data_streams'; +export type Section = + | 'agents' + | 'agent_policies' + | 'enrollment_tokens' + | 'data_streams' + | 'settings'; diff --git a/x-pack/plugins/fleet/public/components/settings_flyout/confirm_modal.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/settings/components/legacy_settings_form/confirm_modal.tsx similarity index 100% rename from x-pack/plugins/fleet/public/components/settings_flyout/confirm_modal.tsx rename to x-pack/plugins/fleet/public/applications/fleet/sections/settings/components/legacy_settings_form/confirm_modal.tsx diff --git a/x-pack/plugins/fleet/public/components/settings_flyout/hosts_input.test.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/settings/components/legacy_settings_form/hosts_input.test.tsx similarity index 98% rename from x-pack/plugins/fleet/public/components/settings_flyout/hosts_input.test.tsx rename to x-pack/plugins/fleet/public/applications/fleet/sections/settings/components/legacy_settings_form/hosts_input.test.tsx index 01ec166b74afc..aca3399c4af46 100644 --- a/x-pack/plugins/fleet/public/components/settings_flyout/hosts_input.test.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/settings/components/legacy_settings_form/hosts_input.test.tsx @@ -8,7 +8,7 @@ import React from 'react'; import { fireEvent, act } from '@testing-library/react'; -import { createFleetTestRendererMock } from '../../mock'; +import { createFleetTestRendererMock } from '../../../../../../mock'; import { HostsInput } from './hosts_input'; diff --git a/x-pack/plugins/fleet/public/components/settings_flyout/hosts_input.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/settings/components/legacy_settings_form/hosts_input.tsx similarity index 98% rename from x-pack/plugins/fleet/public/components/settings_flyout/hosts_input.tsx rename to x-pack/plugins/fleet/public/applications/fleet/sections/settings/components/legacy_settings_form/hosts_input.tsx index 49cff905d167f..30ef969aceec7 100644 --- a/x-pack/plugins/fleet/public/components/settings_flyout/hosts_input.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/settings/components/legacy_settings_form/hosts_input.tsx @@ -27,7 +27,7 @@ import { import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; -import type { EuiTheme } from '../../../../../../src/plugins/kibana_react/common'; +import type { EuiTheme } from '../../../../../../../../../../src/plugins/kibana_react/common'; interface Props { id: string; diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/settings/components/legacy_settings_form/index.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/settings/components/legacy_settings_form/index.tsx new file mode 100644 index 0000000000000..6caca7209e0d2 --- /dev/null +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/settings/components/legacy_settings_form/index.tsx @@ -0,0 +1,498 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license 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, useCallback } from 'react'; +import styled from 'styled-components'; +import { i18n } from '@kbn/i18n'; +import { + EuiPortal, + EuiTitle, + EuiFlexGroup, + EuiFlexItem, + EuiSpacer, + EuiButton, + EuiForm, + EuiFormRow, + EuiCode, + EuiLink, + EuiPanel, + EuiTextColor, +} from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { EuiText } from '@elastic/eui'; +import { safeLoad } from 'js-yaml'; + +import { + useComboInput, + useStartServices, + useGetSettings, + useInput, + sendPutSettings, + useDefaultOutput, + sendPutOutput, +} from '../../../../../../hooks'; +import { isDiffPathProtocol, normalizeHostsForAgents } from '../../../../../../../common'; +import { CodeEditor } from '../../../../../../../../../../src/plugins/kibana_react/public'; + +import { SettingsConfirmModal } from './confirm_modal'; +import type { SettingsConfirmModalProps } from './confirm_modal'; +import { HostsInput } from './hosts_input'; + +const CodeEditorContainer = styled.div` + min-height: 0; + position: relative; + height: 250px; +`; + +const CodeEditorPlaceholder = styled(EuiTextColor).attrs((props) => ({ + color: 'subdued', + size: 'xs', +}))` + position: absolute; + top: 0; + left: 0; + // Matches monaco editor + font-family: Menlo, Monaco, 'Courier New', monospace; + font-size: 12px; + line-height: 21px; + pointer-events: none; +`; + +const URL_REGEX = /^(https?):\/\/[^\s$.?#].[^\s]*$/gm; + +function normalizeHosts(hostsInput: string[]) { + return hostsInput.map((host) => { + try { + return normalizeHostsForAgents(host); + } catch (err) { + return host; + } + }); +} + +function isSameArrayValueWithNormalizedHosts(arrayA: string[] = [], arrayB: string[] = []) { + const hostsA = normalizeHosts(arrayA); + const hostsB = normalizeHosts(arrayB); + return hostsA.length === hostsB.length && hostsA.every((val, index) => val === hostsB[index]); +} + +function useSettingsForm(outputId: string | undefined) { + const [isLoading, setIsloading] = React.useState(false); + const { notifications } = useStartServices(); + + const fleetServerHostsInput = useComboInput('fleetServerHostsComboBox', [], (value) => { + if (value.length === 0) { + return [ + { + message: i18n.translate('xpack.fleet.settings.fleetServerHostsEmptyError', { + defaultMessage: 'At least one URL is required', + }), + }, + ]; + } + + const res: Array<{ message: string; index: number }> = []; + const hostIndexes: { [key: string]: number[] } = {}; + value.forEach((val, idx) => { + if (!val.match(URL_REGEX)) { + res.push({ + message: i18n.translate('xpack.fleet.settings.fleetServerHostsError', { + defaultMessage: 'Invalid URL', + }), + index: idx, + }); + } + const curIndexes = hostIndexes[val] || []; + hostIndexes[val] = [...curIndexes, idx]; + }); + + Object.values(hostIndexes) + .filter(({ length }) => length > 1) + .forEach((indexes) => { + indexes.forEach((index) => + res.push({ + message: i18n.translate('xpack.fleet.settings.fleetServerHostsDuplicateError', { + defaultMessage: 'Duplicate URL', + }), + index, + }) + ); + }); + + if (res.length) { + return res; + } + + if (value.length && isDiffPathProtocol(value)) { + return [ + { + message: i18n.translate( + 'xpack.fleet.settings.fleetServerHostsDifferentPathOrProtocolError', + { + defaultMessage: 'Protocol and path must be the same for each URL', + } + ), + }, + ]; + } + }); + + const elasticsearchUrlInput = useComboInput('esHostsComboxBox', [], (value) => { + const res: Array<{ message: string; index: number }> = []; + const urlIndexes: { [key: string]: number[] } = {}; + value.forEach((val, idx) => { + if (!val.match(URL_REGEX)) { + res.push({ + message: i18n.translate('xpack.fleet.settings.elasticHostError', { + defaultMessage: 'Invalid URL', + }), + index: idx, + }); + } + const curIndexes = urlIndexes[val] || []; + urlIndexes[val] = [...curIndexes, idx]; + }); + + Object.values(urlIndexes) + .filter(({ length }) => length > 1) + .forEach((indexes) => { + indexes.forEach((index) => + res.push({ + message: i18n.translate('xpack.fleet.settings.elasticHostDuplicateError', { + defaultMessage: 'Duplicate URL', + }), + index, + }) + ); + }); + + if (res.length) { + return res; + } + }); + + const additionalYamlConfigInput = useInput('', (value) => { + try { + safeLoad(value); + return; + } catch (error) { + return [ + i18n.translate('xpack.fleet.settings.invalidYamlFormatErrorMessage', { + defaultMessage: 'Invalid YAML: {reason}', + values: { reason: error.message }, + }), + ]; + } + }); + + const validate = useCallback(() => { + const fleetServerHostsValid = fleetServerHostsInput.validate(); + const elasticsearchUrlsValid = elasticsearchUrlInput.validate(); + const additionalYamlConfigValid = additionalYamlConfigInput.validate(); + + if (!fleetServerHostsValid || !elasticsearchUrlsValid || !additionalYamlConfigValid) { + return false; + } + + return true; + }, [fleetServerHostsInput, elasticsearchUrlInput, additionalYamlConfigInput]); + + return { + isLoading, + validate, + submit: async () => { + try { + setIsloading(true); + if (!outputId) { + throw new Error('Unable to load outputs'); + } + const outputResponse = await sendPutOutput(outputId, { + hosts: elasticsearchUrlInput.value, + config_yaml: additionalYamlConfigInput.value, + }); + if (outputResponse.error) { + throw outputResponse.error; + } + const settingsResponse = await sendPutSettings({ + fleet_server_hosts: fleetServerHostsInput.value, + }); + if (settingsResponse.error) { + throw settingsResponse.error; + } + notifications.toasts.addSuccess( + i18n.translate('xpack.fleet.settings.success.message', { + defaultMessage: 'Settings saved', + }) + ); + setIsloading(false); + } catch (error) { + setIsloading(false); + notifications.toasts.addError(error, { + title: 'Error', + }); + } + }, + inputs: { + fleetServerHosts: fleetServerHostsInput, + elasticsearchUrl: elasticsearchUrlInput, + additionalYamlConfig: additionalYamlConfigInput, + }, + }; +} + +export const LegacySettingsForm: React.FunctionComponent = () => { + const { docLinks } = useStartServices(); + + const settingsRequest = useGetSettings(); + const settings = settingsRequest?.data?.item; + const { output } = useDefaultOutput(); + const { inputs, submit, validate, isLoading } = useSettingsForm(output?.id); + + const [isConfirmModalVisible, setConfirmModalVisible] = React.useState(false); + + const onSubmit = useCallback(() => { + if (validate()) { + setConfirmModalVisible(true); + } + }, [validate, setConfirmModalVisible]); + + const onConfirm = useCallback(() => { + setConfirmModalVisible(false); + submit(); + }, [submit]); + + const onConfirmModalClose = useCallback(() => { + setConfirmModalVisible(false); + }, [setConfirmModalVisible]); + + useEffect(() => { + if (output) { + inputs.elasticsearchUrl.setValue(output.hosts || []); + inputs.additionalYamlConfig.setValue(output.config_yaml || ''); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [output]); + + useEffect(() => { + if (settings) { + inputs.fleetServerHosts.setValue([...settings.fleet_server_hosts]); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [settings]); + + const isUpdated = React.useMemo(() => { + if (!settings || !output) { + return false; + } + return ( + !isSameArrayValueWithNormalizedHosts( + settings.fleet_server_hosts, + inputs.fleetServerHosts.value + ) || + !isSameArrayValueWithNormalizedHosts(output.hosts, inputs.elasticsearchUrl.value) || + (output.config_yaml || '') !== inputs.additionalYamlConfig.value + ); + }, [settings, inputs, output]); + + const changes = React.useMemo(() => { + if (!settings || !output || !isConfirmModalVisible) { + return []; + } + + const tmpChanges: SettingsConfirmModalProps['changes'] = []; + if (!isSameArrayValueWithNormalizedHosts(output.hosts, inputs.elasticsearchUrl.value)) { + tmpChanges.push( + { + type: 'elasticsearch', + direction: 'removed', + urls: normalizeHosts(output.hosts || []), + }, + { + type: 'elasticsearch', + direction: 'added', + urls: normalizeHosts(inputs.elasticsearchUrl.value), + } + ); + } + + if ( + !isSameArrayValueWithNormalizedHosts( + settings.fleet_server_hosts, + inputs.fleetServerHosts.value + ) + ) { + tmpChanges.push( + { + type: 'fleet_server', + direction: 'removed', + urls: normalizeHosts(settings.fleet_server_hosts || []), + }, + { + type: 'fleet_server', + direction: 'added', + urls: normalizeHosts(inputs.fleetServerHosts.value), + } + ); + } + + return tmpChanges; + }, [settings, inputs, output, isConfirmModalVisible]); + + const body = settings && ( + + + outputs, + }} + /> + + + + + + + ), + }} + /> + } + /> + + + + + + + + + + + {(!inputs.additionalYamlConfig.value || inputs.additionalYamlConfig.value === '') && ( + + {`# YAML settings here will be added to the Elasticsearch output section of each policy`} + + )} + + + + + ); + + return ( + <> + {isConfirmModalVisible && ( + + + + )} + <> + <> + +

    + +

    +
    + + <>{body} + <> + + + + + {isLoading ? ( + + ) : ( + + )} + + + + + + + ); +}; diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/settings/index.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/settings/index.tsx new file mode 100644 index 0000000000000..6117d3249b189 --- /dev/null +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/settings/index.tsx @@ -0,0 +1,23 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; + +import { useBreadcrumbs } from '../../hooks'; +import { DefaultLayout } from '../../layouts'; + +import { LegacySettingsForm } from './components/legacy_settings_form'; + +export const SettingsApp = () => { + useBreadcrumbs('settings'); + + return ( + + + + ); +}; diff --git a/x-pack/plugins/fleet/public/applications/integrations/app.tsx b/x-pack/plugins/fleet/public/applications/integrations/app.tsx index 3a091c30bb792..eca2c0c0612c7 100644 --- a/x-pack/plugins/fleet/public/applications/integrations/app.tsx +++ b/x-pack/plugins/fleet/public/applications/integrations/app.tsx @@ -7,7 +7,7 @@ import React, { memo } from 'react'; import type { AppMountParameters } from 'kibana/public'; -import { EuiErrorBoundary, EuiPortal } from '@elastic/eui'; +import { EuiErrorBoundary } from '@elastic/eui'; import type { History } from 'history'; import { Router, Redirect, Route, Switch } from 'react-router-dom'; import useObservable from 'react-use/lib/useObservable'; @@ -22,11 +22,9 @@ import { } from '../../../../../../src/plugins/kibana_react/public'; import { EuiThemeProvider } from '../../../../../../src/plugins/kibana_react/common'; -import { AgentPolicyContextProvider, useUrlModal } from './hooks'; +import { AgentPolicyContextProvider } from './hooks'; import { INTEGRATIONS_ROUTING_PATHS, pagePathGetters } from './constants'; -import { SettingFlyout } from './components'; - import type { UIExtensionsStorage } from './types'; import { EPMApp } from './sections/epm'; @@ -93,18 +91,8 @@ export const IntegrationsAppContext: React.FC<{ ); export const AppRoutes = memo(() => { - const { modal, setModal } = useUrlModal(); return ( <> - {modal === 'settings' && ( - - { - setModal(null); - }} - /> - - )} diff --git a/x-pack/plugins/fleet/public/applications/integrations/hooks/use_merge_epr_with_replacements.test.ts b/x-pack/plugins/fleet/public/applications/integrations/hooks/use_merge_epr_with_replacements.test.ts index d5d8aa093e300..f69132d9a6452 100644 --- a/x-pack/plugins/fleet/public/applications/integrations/hooks/use_merge_epr_with_replacements.test.ts +++ b/x-pack/plugins/fleet/public/applications/integrations/hooks/use_merge_epr_with_replacements.test.ts @@ -120,6 +120,17 @@ describe('useMergeEprWithReplacements', () => { ]); }); + test('should filter out apm from package list', () => { + const eprPackages: PackageListItem[] = mockEprPackages([ + { + name: 'apm', + release: 'beta', + }, + ]); + + expect(useMergeEprPackagesWithReplacements(eprPackages, [])).toEqual([]); + }); + test('should consists of all 3 types (ga eprs, replacements for non-ga eprs, replacements without epr equivalent', () => { const eprPackages: PackageListItem[] = mockEprPackages([ { @@ -136,6 +147,10 @@ describe('useMergeEprWithReplacements', () => { name: 'activemq', release: 'beta', }, + { + name: 'apm', + release: 'ga', + }, ]); const replacements: CustomIntegration[] = mockIntegrations([ { diff --git a/x-pack/plugins/fleet/public/applications/integrations/hooks/use_merge_epr_with_replacements.ts b/x-pack/plugins/fleet/public/applications/integrations/hooks/use_merge_epr_with_replacements.ts index 4c59f0ef45123..ff1b51ef19a81 100644 --- a/x-pack/plugins/fleet/public/applications/integrations/hooks/use_merge_epr_with_replacements.ts +++ b/x-pack/plugins/fleet/public/applications/integrations/hooks/use_merge_epr_with_replacements.ts @@ -8,6 +8,7 @@ import type { PackageListItem } from '../../../../common/types/models'; import type { CustomIntegration } from '../../../../../../../src/plugins/custom_integrations/common'; import { filterCustomIntegrations } from '../../../../../../../src/plugins/custom_integrations/public'; +import { FLEET_APM_PACKAGE } from '../../../../common/constants'; // Export this as a utility to find replacements for a package (e.g. in the overview-page for an EPR package) function findReplacementsForEprPackage( @@ -22,12 +23,17 @@ function findReplacementsForEprPackage( } export function useMergeEprPackagesWithReplacements( - eprPackages: PackageListItem[], + rawEprPackages: PackageListItem[], replacements: CustomIntegration[] ): Array { const merged: Array = []; const filteredReplacements = replacements; + // APM EPR-packages should _never_ show. They have special handling. + const eprPackages = rawEprPackages.filter((p) => { + return p.name !== FLEET_APM_PACKAGE; + }); + // Either select replacement or select beat eprPackages.forEach((eprPackage: PackageListItem) => { const hits = findReplacementsForEprPackage( diff --git a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/components/package_list_grid.tsx b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/components/package_list_grid.tsx index 91bd29d14d0b3..3dc704399c914 100644 --- a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/components/package_list_grid.tsx +++ b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/components/package_list_grid.tsx @@ -235,7 +235,7 @@ function MissingIntegrationContent({

    @@ -245,11 +245,11 @@ function MissingIntegrationContent({ /> ), - discussForumLink: ( + forumLink: ( ), diff --git a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/index.tsx b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/index.tsx index c8c6f49356810..b6f236852f940 100644 --- a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/index.tsx +++ b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/index.tsx @@ -370,12 +370,12 @@ export function Detail() { content: missingSecurityConfiguration ? ( ) : ( ), } diff --git a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/home/available_packages.tsx b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/home/available_packages.tsx index f1d0717584e2e..62f911ffdbbb7 100644 --- a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/home/available_packages.tsx +++ b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/home/available_packages.tsx @@ -5,10 +5,12 @@ * 2.0. */ +import type { FunctionComponent } from 'react'; import React, { memo, useMemo, useState } from 'react'; import { useLocation, useHistory, useParams } from 'react-router-dom'; import _ from 'lodash'; import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; import { EuiHorizontalRule, EuiFlexItem, @@ -16,6 +18,8 @@ import { EuiSpacer, EuiCard, EuiIcon, + EuiCallOut, + EuiLink, } from '@elastic/eui'; import { useStartServices } from '../../../../hooks'; @@ -52,6 +56,76 @@ import type { CategoryFacet } from './category_facets'; import type { CategoryParams } from '.'; import { getParams, categoryExists, mapToCard } from '.'; +const NoEprCallout: FunctionComponent<{ statusCode?: number }> = ({ + statusCode, +}: { + statusCode?: number; +}) => { + let titleMessage; + let descriptionMessage; + if (statusCode === 502) { + titleMessage = i18n.translate('xpack.fleet.epmList.eprUnavailableBadGatewayCalloutTitle', { + defaultMessage: + 'Kibana cannot reach the Elastic Package Registry, which provides Elastic Agent integrations\n', + }); + descriptionMessage = ( + , + onpremregistry: , + }} + /> + ); + } else { + titleMessage = i18n.translate('xpack.fleet.epmList.eprUnavailable400500CalloutTitle', { + defaultMessage: + 'Kibana cannot connect to the Elastic Package Registry, which provides Elastic Agent integrations\n', + }); + descriptionMessage = ( + , + onpremregistry: , + }} + /> + ); + } + + return ( + +

    {descriptionMessage}

    + + ); +}; + +function ProxyLink() { + const { docLinks } = useStartServices(); + + return ( + + {i18n.translate('xpack.fleet.epmList.proxyLinkSnippedText', { + defaultMessage: 'proxy server', + })} + + ); +} + +function OnPremLink() { + const { docLinks } = useStartServices(); + + return ( + + {i18n.translate('xpack.fleet.epmList.onPremLinkSnippetText', { + defaultMessage: 'your own registry', + })} + + ); +} + function getAllCategoriesFromIntegrations(pkg: PackageListItem) { if (!doesPackageHaveIntegrations(pkg)) { return pkg.categories; @@ -133,10 +207,13 @@ export const AvailablePackages: React.FC = memo(() => { history.replace(pagePathGetters.integrations_all({ searchTerm: search })[1]); } - const { data: eprPackages, isLoading: isLoadingAllPackages } = useGetPackages({ + const { + data: eprPackages, + isLoading: isLoadingAllPackages, + error: eprPackageLoadingError, + } = useGetPackages({ category: '', }); - const eprIntegrationList = useMemo( () => packageListToIntegrationsList(eprPackages?.response || []), [eprPackages] @@ -166,18 +243,23 @@ export const AvailablePackages: React.FC = memo(() => { return a.title.localeCompare(b.title); }); - const { data: eprCategories, isLoading: isLoadingCategories } = useGetCategories({ + const { + data: eprCategories, + isLoading: isLoadingCategories, + error: eprCategoryLoadingError, + } = useGetCategories({ include_policy_templates: true, }); const categories = useMemo(() => { - const eprAndCustomCategories: CategoryFacet[] = - isLoadingCategories || !eprCategories - ? [] - : mergeCategoriesAndCount( - eprCategories.response as Array<{ id: string; title: string; count: number }>, - cards - ); + const eprAndCustomCategories: CategoryFacet[] = isLoadingCategories + ? [] + : mergeCategoriesAndCount( + eprCategories + ? (eprCategories.response as Array<{ id: string; title: string; count: number }>) + : [], + cards + ); return [ { ...ALL_CATEGORY, @@ -221,6 +303,7 @@ export const AvailablePackages: React.FC = memo(() => { if (selectedCategory === '') { return true; } + return c.categories.includes(selectedCategory); }); @@ -255,7 +338,7 @@ export const AvailablePackages: React.FC = memo(() => { defaultMessage: 'Monitor, detect and diagnose complex performance issues from your application.', })} - href={addBasePath('/app/integrations/detail/apm')} + href={addBasePath('/app/home#/tutorial/apm')} icon={} /> @@ -265,7 +348,7 @@ export const AvailablePackages: React.FC = memo(() => { } - href={addBasePath('/app/enterprise_search/app_search')} + href={addBasePath('/app/enterprise_search/app_search/engines/new?method=crawler')} title={i18n.translate('xpack.fleet.featuredSearchTitle', { defaultMessage: 'Web site crawler', })} @@ -280,6 +363,12 @@ export const AvailablePackages: React.FC = memo(() => { ); + let noEprCallout; + if (eprPackageLoadingError || eprCategoryLoadingError) { + const error = eprPackageLoadingError || eprCategoryLoadingError; + noEprCallout = ; + } + return ( { setSelectedCategory={setSelectedCategory} onSearchChange={setSearchTerm} showMissingIntegrationMessage + callout={noEprCallout} /> ); }); diff --git a/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/index.tsx b/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/index.tsx index 87911e5d6c2c7..62bf3e8d6564a 100644 --- a/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/index.tsx +++ b/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/index.tsx @@ -22,7 +22,7 @@ import { } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; -import { useGetSettings, useUrlModal, sendGetOneAgentPolicy, useFleetStatus } from '../../hooks'; +import { useGetSettings, sendGetOneAgentPolicy, useFleetStatus } from '../../hooks'; import { FLEET_SERVER_PACKAGE } from '../../constants'; import type { PackagePolicy } from '../../types'; @@ -52,19 +52,9 @@ export const AgentEnrollmentFlyout: React.FunctionComponent = ({ }) => { const [mode, setMode] = useState(defaultMode); - const { modal } = useUrlModal(); - const [lastModal, setLastModal] = useState(modal); const settings = useGetSettings(); const fleetServerHosts = settings.data?.item?.fleet_server_hosts || []; - // Refresh settings when there is a modal/flyout change - useEffect(() => { - if (modal !== lastModal) { - settings.resendRequest(); - setLastModal(modal); - } - }, [modal, lastModal, settings]); - const fleetStatus = useFleetStatus(); const [policyId, setSelectedPolicyId] = useState(agentPolicy?.id); const [isFleetServerPolicySelected, setIsFleetServerPolicySelected] = useState(false); diff --git a/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/missing_fleet_server_host_callout.tsx b/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/missing_fleet_server_host_callout.tsx index c390b50c498fb..220b98f07cd35 100644 --- a/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/missing_fleet_server_host_callout.tsx +++ b/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/missing_fleet_server_host_callout.tsx @@ -10,11 +10,11 @@ import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import { EuiCallOut, EuiLink, EuiButton, EuiSpacer } from '@elastic/eui'; -import { useUrlModal, useStartServices } from '../../hooks'; +import { useLink, useStartServices } from '../../hooks'; export const MissingFleetServerHostCallout: React.FunctionComponent = () => { - const { setModal } = useUrlModal(); const { docLinks } = useStartServices(); + const { getHref } = useLink(); return ( { }} /> - { - setModal('settings'); - }} - > + = ({ const enrollArgs = getfleetServerHostsEnrollArgs(apiKey, fleetServerHosts); - const linuxMacCommand = `sudo ./elastic-agent install -f ${enrollArgs}`; + const linuxMacCommand = `sudo ./elastic-agent install ${enrollArgs}`; - const windowsCommand = `.\\elastic-agent.exe install -f ${enrollArgs}`; + const windowsCommand = `.\\elastic-agent.exe install ${enrollArgs}`; return ( <> diff --git a/x-pack/plugins/fleet/public/components/home_integration/tutorial_module_notice.tsx b/x-pack/plugins/fleet/public/components/home_integration/tutorial_module_notice.tsx index 24d9dc8e2c100..1b0d90098fa48 100644 --- a/x-pack/plugins/fleet/public/components/home_integration/tutorial_module_notice.tsx +++ b/x-pack/plugins/fleet/public/components/home_integration/tutorial_module_notice.tsx @@ -13,6 +13,7 @@ import type { TutorialModuleNoticeComponent } from 'src/plugins/home/public'; import { useGetPackages, useLink, useCapabilities } from '../../hooks'; import { pkgKeyFromPackageInfo } from '../../services'; +import { FLEET_APM_PACKAGE } from '../../../common/constants'; const TutorialModuleNotice: TutorialModuleNoticeComponent = memo(({ moduleName }) => { const { getHref } = useLink(); @@ -22,7 +23,7 @@ const TutorialModuleNotice: TutorialModuleNoticeComponent = memo(({ moduleName } const pkgInfo = !isLoading && packagesData?.response && - packagesData.response.find((pkg) => pkg.name === moduleName); + packagesData.response.find((pkg) => pkg.name === moduleName && pkg.name !== FLEET_APM_PACKAGE); // APM needs special handling if (hasIngestManager && pkgInfo) { return ( diff --git a/x-pack/plugins/fleet/public/components/index.ts b/x-pack/plugins/fleet/public/components/index.ts index 9015071450bf0..757625d7244a3 100644 --- a/x-pack/plugins/fleet/public/components/index.ts +++ b/x-pack/plugins/fleet/public/components/index.ts @@ -22,5 +22,4 @@ export { PackagePolicyDeleteProvider } from './package_policy_delete_provider'; export { PackagePolicyActionsMenu } from './package_policy_actions_menu'; export { AddAgentHelpPopover } from './add_agent_help_popover'; export * from './link_and_revision'; -export * from './settings_flyout'; export * from './agent_enrollment_flyout'; diff --git a/x-pack/plugins/fleet/public/components/linked_agent_count.tsx b/x-pack/plugins/fleet/public/components/linked_agent_count.tsx index f9b2727f48935..dcbda2e1445c7 100644 --- a/x-pack/plugins/fleet/public/components/linked_agent_count.tsx +++ b/x-pack/plugins/fleet/public/components/linked_agent_count.tsx @@ -11,7 +11,7 @@ import type { EuiLinkAnchorProps } from '@elastic/eui'; import { EuiLink } from '@elastic/eui'; import { useLink } from '../hooks'; -import { AGENT_SAVED_OBJECT_TYPE } from '../constants'; +import { AGENTS_PREFIX } from '../constants'; /** * Displays the provided `count` number as a link to the Agents list if it is greater than zero @@ -37,7 +37,7 @@ export const LinkedAgentCount = memo< {displayValue} diff --git a/x-pack/plugins/fleet/public/components/package_policy_delete_provider.tsx b/x-pack/plugins/fleet/public/components/package_policy_delete_provider.tsx index e2522f40ef966..7c2703ec8437b 100644 --- a/x-pack/plugins/fleet/public/components/package_policy_delete_provider.tsx +++ b/x-pack/plugins/fleet/public/components/package_policy_delete_provider.tsx @@ -11,7 +11,7 @@ import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import { useStartServices, sendRequest, sendDeletePackagePolicy, useConfig } from '../hooks'; -import { AGENT_API_ROUTES, AGENT_SAVED_OBJECT_TYPE } from '../../common/constants'; +import { AGENT_API_ROUTES, AGENTS_PREFIX } from '../../common/constants'; import type { AgentPolicy } from '../types'; interface Props { @@ -53,7 +53,7 @@ export const PackagePolicyDeleteProvider: React.FunctionComponent = ({ query: { page: 1, perPage: 1, - kuery: `${AGENT_SAVED_OBJECT_TYPE}.policy_id : ${agentPolicy.id}`, + kuery: `${AGENTS_PREFIX}.policy_id : ${agentPolicy.id}`, }, }); setAgentsCount(data?.total || 0); diff --git a/x-pack/plugins/fleet/public/components/settings_flyout/index.tsx b/x-pack/plugins/fleet/public/components/settings_flyout/index.tsx deleted file mode 100644 index d10fd8336a37f..0000000000000 --- a/x-pack/plugins/fleet/public/components/settings_flyout/index.tsx +++ /dev/null @@ -1,512 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React, { useEffect, useCallback } from 'react'; -import styled from 'styled-components'; -import { i18n } from '@kbn/i18n'; -import { - EuiFlyout, - EuiFlyoutBody, - EuiFlyoutHeader, - EuiTitle, - EuiFlexGroup, - EuiFlexItem, - EuiButtonEmpty, - EuiSpacer, - EuiButton, - EuiFlyoutFooter, - EuiForm, - EuiFormRow, - EuiCode, - EuiLink, - EuiPanel, - EuiTextColor, -} from '@elastic/eui'; -import { FormattedMessage } from '@kbn/i18n/react'; -import { EuiText } from '@elastic/eui'; -import { safeLoad } from 'js-yaml'; - -import { - useComboInput, - useStartServices, - useGetSettings, - useInput, - sendPutSettings, - useDefaultOutput, - sendPutOutput, -} from '../../hooks'; -import { isDiffPathProtocol, normalizeHostsForAgents } from '../../../common'; -import { CodeEditor } from '../../../../../../src/plugins/kibana_react/public'; - -import { SettingsConfirmModal } from './confirm_modal'; -import type { SettingsConfirmModalProps } from './confirm_modal'; -import { HostsInput } from './hosts_input'; - -const CodeEditorContainer = styled.div` - min-height: 0; - position: relative; - height: 250px; -`; - -const CodeEditorPlaceholder = styled(EuiTextColor).attrs((props) => ({ - color: 'subdued', - size: 'xs', -}))` - position: absolute; - top: 0; - left: 0; - // Matches monaco editor - font-family: Menlo, Monaco, 'Courier New', monospace; - font-size: 12px; - line-height: 21px; - pointer-events: none; -`; - -const URL_REGEX = /^(https?):\/\/[^\s$.?#].[^\s]*$/gm; - -interface Props { - onClose: () => void; -} - -function normalizeHosts(hostsInput: string[]) { - return hostsInput.map((host) => { - try { - return normalizeHostsForAgents(host); - } catch (err) { - return host; - } - }); -} - -function isSameArrayValueWithNormalizedHosts(arrayA: string[] = [], arrayB: string[] = []) { - const hostsA = normalizeHosts(arrayA); - const hostsB = normalizeHosts(arrayB); - return hostsA.length === hostsB.length && hostsA.every((val, index) => val === hostsB[index]); -} - -function useSettingsForm(outputId: string | undefined, onSuccess: () => void) { - const [isLoading, setIsloading] = React.useState(false); - const { notifications } = useStartServices(); - - const fleetServerHostsInput = useComboInput('fleetServerHostsComboBox', [], (value) => { - if (value.length === 0) { - return [ - { - message: i18n.translate('xpack.fleet.settings.fleetServerHostsEmptyError', { - defaultMessage: 'At least one URL is required', - }), - }, - ]; - } - - const res: Array<{ message: string; index: number }> = []; - const hostIndexes: { [key: string]: number[] } = {}; - value.forEach((val, idx) => { - if (!val.match(URL_REGEX)) { - res.push({ - message: i18n.translate('xpack.fleet.settings.fleetServerHostsError', { - defaultMessage: 'Invalid URL', - }), - index: idx, - }); - } - const curIndexes = hostIndexes[val] || []; - hostIndexes[val] = [...curIndexes, idx]; - }); - - Object.values(hostIndexes) - .filter(({ length }) => length > 1) - .forEach((indexes) => { - indexes.forEach((index) => - res.push({ - message: i18n.translate('xpack.fleet.settings.fleetServerHostsDuplicateError', { - defaultMessage: 'Duplicate URL', - }), - index, - }) - ); - }); - - if (res.length) { - return res; - } - - if (value.length && isDiffPathProtocol(value)) { - return [ - { - message: i18n.translate( - 'xpack.fleet.settings.fleetServerHostsDifferentPathOrProtocolError', - { - defaultMessage: 'Protocol and path must be the same for each URL', - } - ), - }, - ]; - } - }); - - const elasticsearchUrlInput = useComboInput('esHostsComboxBox', [], (value) => { - const res: Array<{ message: string; index: number }> = []; - const urlIndexes: { [key: string]: number[] } = {}; - value.forEach((val, idx) => { - if (!val.match(URL_REGEX)) { - res.push({ - message: i18n.translate('xpack.fleet.settings.elasticHostError', { - defaultMessage: 'Invalid URL', - }), - index: idx, - }); - } - const curIndexes = urlIndexes[val] || []; - urlIndexes[val] = [...curIndexes, idx]; - }); - - Object.values(urlIndexes) - .filter(({ length }) => length > 1) - .forEach((indexes) => { - indexes.forEach((index) => - res.push({ - message: i18n.translate('xpack.fleet.settings.elasticHostDuplicateError', { - defaultMessage: 'Duplicate URL', - }), - index, - }) - ); - }); - - if (res.length) { - return res; - } - }); - - const additionalYamlConfigInput = useInput('', (value) => { - try { - safeLoad(value); - return; - } catch (error) { - return [ - i18n.translate('xpack.fleet.settings.invalidYamlFormatErrorMessage', { - defaultMessage: 'Invalid YAML: {reason}', - values: { reason: error.message }, - }), - ]; - } - }); - - const validate = useCallback(() => { - const fleetServerHostsValid = fleetServerHostsInput.validate(); - const elasticsearchUrlsValid = elasticsearchUrlInput.validate(); - const additionalYamlConfigValid = additionalYamlConfigInput.validate(); - - if (!fleetServerHostsValid || !elasticsearchUrlsValid || !additionalYamlConfigValid) { - return false; - } - - return true; - }, [fleetServerHostsInput, elasticsearchUrlInput, additionalYamlConfigInput]); - - return { - isLoading, - validate, - submit: async () => { - try { - setIsloading(true); - if (!outputId) { - throw new Error('Unable to load outputs'); - } - const outputResponse = await sendPutOutput(outputId, { - hosts: elasticsearchUrlInput.value, - config_yaml: additionalYamlConfigInput.value, - }); - if (outputResponse.error) { - throw outputResponse.error; - } - const settingsResponse = await sendPutSettings({ - fleet_server_hosts: fleetServerHostsInput.value, - }); - if (settingsResponse.error) { - throw settingsResponse.error; - } - notifications.toasts.addSuccess( - i18n.translate('xpack.fleet.settings.success.message', { - defaultMessage: 'Settings saved', - }) - ); - setIsloading(false); - onSuccess(); - } catch (error) { - setIsloading(false); - notifications.toasts.addError(error, { - title: 'Error', - }); - } - }, - inputs: { - fleetServerHosts: fleetServerHostsInput, - elasticsearchUrl: elasticsearchUrlInput, - additionalYamlConfig: additionalYamlConfigInput, - }, - }; -} - -export const SettingFlyout: React.FunctionComponent = ({ onClose }) => { - const { docLinks } = useStartServices(); - - const settingsRequest = useGetSettings(); - const settings = settingsRequest?.data?.item; - const { output } = useDefaultOutput(); - const { inputs, submit, validate, isLoading } = useSettingsForm(output?.id, onClose); - - const [isConfirmModalVisible, setConfirmModalVisible] = React.useState(false); - - const onSubmit = useCallback(() => { - if (validate()) { - setConfirmModalVisible(true); - } - }, [validate, setConfirmModalVisible]); - - const onConfirm = useCallback(() => { - setConfirmModalVisible(false); - submit(); - }, [submit]); - - const onConfirmModalClose = useCallback(() => { - setConfirmModalVisible(false); - }, [setConfirmModalVisible]); - - useEffect(() => { - if (output) { - inputs.elasticsearchUrl.setValue(output.hosts || []); - inputs.additionalYamlConfig.setValue(output.config_yaml || ''); - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [output]); - - useEffect(() => { - if (settings) { - inputs.fleetServerHosts.setValue([...settings.fleet_server_hosts]); - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [settings]); - - const isUpdated = React.useMemo(() => { - if (!settings || !output) { - return false; - } - return ( - !isSameArrayValueWithNormalizedHosts( - settings.fleet_server_hosts, - inputs.fleetServerHosts.value - ) || - !isSameArrayValueWithNormalizedHosts(output.hosts, inputs.elasticsearchUrl.value) || - (output.config_yaml || '') !== inputs.additionalYamlConfig.value - ); - }, [settings, inputs, output]); - - const changes = React.useMemo(() => { - if (!settings || !output || !isConfirmModalVisible) { - return []; - } - - const tmpChanges: SettingsConfirmModalProps['changes'] = []; - if (!isSameArrayValueWithNormalizedHosts(output.hosts, inputs.elasticsearchUrl.value)) { - tmpChanges.push( - { - type: 'elasticsearch', - direction: 'removed', - urls: normalizeHosts(output.hosts || []), - }, - { - type: 'elasticsearch', - direction: 'added', - urls: normalizeHosts(inputs.elasticsearchUrl.value), - } - ); - } - - if ( - !isSameArrayValueWithNormalizedHosts( - settings.fleet_server_hosts, - inputs.fleetServerHosts.value - ) - ) { - tmpChanges.push( - { - type: 'fleet_server', - direction: 'removed', - urls: normalizeHosts(settings.fleet_server_hosts || []), - }, - { - type: 'fleet_server', - direction: 'added', - urls: normalizeHosts(inputs.fleetServerHosts.value), - } - ); - } - - return tmpChanges; - }, [settings, inputs, output, isConfirmModalVisible]); - - const body = settings && ( - - - outputs, - }} - /> - - - - - - - ), - }} - /> - } - /> - - - - - - - - - - - {(!inputs.additionalYamlConfig.value || inputs.additionalYamlConfig.value === '') && ( - - {`# YAML settings here will be added to the Elasticsearch output section of each policy`} - - )} - - - - - ); - - return ( - <> - {isConfirmModalVisible && ( - - )} - - - -

    - -

    -
    -
    - {body} - - - - - - - - - - {isLoading ? ( - - ) : ( - - )} - - - - -
    - - ); -}; diff --git a/x-pack/plugins/fleet/public/constants/index.ts b/x-pack/plugins/fleet/public/constants/index.ts index 32dd732c53dec..38b7875c93b3b 100644 --- a/x-pack/plugins/fleet/public/constants/index.ts +++ b/x-pack/plugins/fleet/public/constants/index.ts @@ -12,8 +12,7 @@ export { AGENT_API_ROUTES, SO_SEARCH_LIMIT, AGENT_POLICY_SAVED_OBJECT_TYPE, - AGENT_SAVED_OBJECT_TYPE, - ENROLLMENT_API_KEYS_SAVED_OBJECT_TYPE, + AGENTS_PREFIX, PACKAGE_POLICY_SAVED_OBJECT_TYPE, FLEET_SERVER_PACKAGE, // Fleet Server index diff --git a/x-pack/plugins/fleet/public/constants/page_paths.ts b/x-pack/plugins/fleet/public/constants/page_paths.ts index 0673d50ec9485..821c115cb1cac 100644 --- a/x-pack/plugins/fleet/public/constants/page_paths.ts +++ b/x-pack/plugins/fleet/public/constants/page_paths.ts @@ -14,7 +14,8 @@ export type StaticPage = | 'policies' | 'policies_list' | 'enrollment_tokens' - | 'data_streams'; + | 'data_streams' + | 'settings'; export type DynamicPage = | 'integrations_all' @@ -57,6 +58,7 @@ export const FLEET_ROUTING_PATHS = { upgrade_package_policy: '/policies/:policyId/upgrade-package-policy/:packagePolicyId', enrollment_tokens: '/enrollment-tokens', data_streams: '/data-streams', + settings: '/settings', // TODO: Move this to the integrations app add_integration_to_policy: '/integrations/:pkgkey/add-integration/:integration?', @@ -145,4 +147,5 @@ export const pagePathGetters: { agent_details_logs: ({ agentId }) => [FLEET_BASE_PATH, `/agents/${agentId}/logs`], enrollment_tokens: () => [FLEET_BASE_PATH, '/enrollment-tokens'], data_streams: () => [FLEET_BASE_PATH, '/data-streams'], + settings: () => [FLEET_BASE_PATH, '/settings'], }; diff --git a/x-pack/plugins/fleet/public/hooks/index.ts b/x-pack/plugins/fleet/public/hooks/index.ts index 16454a266c3c4..fa1f09fbf0b79 100644 --- a/x-pack/plugins/fleet/public/hooks/index.ts +++ b/x-pack/plugins/fleet/public/hooks/index.ts @@ -19,7 +19,6 @@ export { usePagination, PAGE_SIZE_OPTIONS } from './use_pagination'; export { useUrlPagination } from './use_url_pagination'; export { useSorting } from './use_sorting'; export { useDebounce } from './use_debounce'; -export { useUrlModal } from './use_url_modal'; export * from './use_request'; export * from './use_input'; export * from './use_url_params'; diff --git a/x-pack/plugins/fleet/public/hooks/use_request/outputs.ts b/x-pack/plugins/fleet/public/hooks/use_request/outputs.ts index 2d623da505c65..214fa5f5ed142 100644 --- a/x-pack/plugins/fleet/public/hooks/use_request/outputs.ts +++ b/x-pack/plugins/fleet/public/hooks/use_request/outputs.ts @@ -5,8 +5,6 @@ * 2.0. */ -import { useMemo, useCallback } from 'react'; - import { outputRoutesService } from '../../services'; import type { PutOutputRequest, GetOutputsResponse } from '../../types'; @@ -21,17 +19,9 @@ export function useGetOutputs() { export function useDefaultOutput() { const outputsRequest = useGetOutputs(); - const output = useMemo(() => { - return outputsRequest.data?.items.find((o) => o.is_default); - }, [outputsRequest.data]); - - const refresh = useCallback(() => { - return outputsRequest.resendRequest(); - }, [outputsRequest]); + const output = outputsRequest.data?.items.find((o) => o.is_default); - return useMemo(() => { - return { output, refresh }; - }, [output, refresh]); + return { output, refresh: outputsRequest.resendRequest }; } export function sendPutOutput(outputId: string, body: PutOutputRequest['body']) { diff --git a/x-pack/plugins/fleet/public/hooks/use_url_modal.ts b/x-pack/plugins/fleet/public/hooks/use_url_modal.ts deleted file mode 100644 index b6bdba5eba844..0000000000000 --- a/x-pack/plugins/fleet/public/hooks/use_url_modal.ts +++ /dev/null @@ -1,67 +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 { useCallback, useMemo } from 'react'; -import { useHistory, useLocation } from 'react-router-dom'; - -import { useUrlParams } from './use_url_params'; - -type Modal = 'settings'; - -/** - * Uses URL params for pagination and also persists those to the URL as they are updated - */ -export const useUrlModal = () => { - const location = useLocation(); - const history = useHistory(); - const { urlParams, toUrlParams } = useUrlParams(); - - const setModal = useCallback( - (modal: Modal | null) => { - const newUrlParams: any = { - ...urlParams, - modal, - }; - - if (modal === null) { - delete newUrlParams.modal; - } - history.push({ - ...location, - search: toUrlParams(newUrlParams), - }); - }, - [history, location, toUrlParams, urlParams] - ); - - const getModalHref = useCallback( - (modal: Modal | null) => { - return history.createHref({ - ...location, - search: toUrlParams({ - ...urlParams, - modal, - }), - }); - }, - [history, location, toUrlParams, urlParams] - ); - - const modal: Modal | null = useMemo(() => { - if (urlParams.modal === 'settings') { - return urlParams.modal; - } - - return null; - }, [urlParams.modal]); - - return { - modal, - setModal, - getModalHref, - }; -}; diff --git a/x-pack/plugins/fleet/server/collectors/agent_collectors.ts b/x-pack/plugins/fleet/server/collectors/agent_collectors.ts index 43c15e603a87a..9b7d48328467d 100644 --- a/x-pack/plugins/fleet/server/collectors/agent_collectors.ts +++ b/x-pack/plugins/fleet/server/collectors/agent_collectors.ts @@ -9,7 +9,6 @@ import type { SavedObjectsClient, ElasticsearchClient } from 'kibana/server'; import type { FleetConfigType } from '../../common/types'; import * as AgentService from '../services/agents'; -import { isFleetServerSetup } from '../services/fleet_server'; export interface AgentUsage { total_enrolled: number; @@ -26,7 +25,7 @@ export const getAgentUsage = async ( esClient?: ElasticsearchClient ): Promise => { // TODO: unsure if this case is possible at all. - if (!soClient || !esClient || !(await isFleetServerSetup())) { + if (!soClient || !esClient) { return { total_enrolled: 0, healthy: 0, diff --git a/x-pack/plugins/fleet/server/collectors/fleet_server_collector.ts b/x-pack/plugins/fleet/server/collectors/fleet_server_collector.ts index 47440e791747c..a08ed450b5b30 100644 --- a/x-pack/plugins/fleet/server/collectors/fleet_server_collector.ts +++ b/x-pack/plugins/fleet/server/collectors/fleet_server_collector.ts @@ -10,7 +10,6 @@ import type { SavedObjectsClient, ElasticsearchClient } from 'kibana/server'; import { packagePolicyService, settingsService } from '../services'; import { getAgentStatusForAgentPolicy } from '../services/agents'; -import { isFleetServerSetup } from '../services/fleet_server'; const DEFAULT_USAGE = { total_all_statuses: 0, @@ -36,7 +35,7 @@ export const getFleetServerUsage = async ( soClient?: SavedObjectsClient, esClient?: ElasticsearchClient ): Promise => { - if (!soClient || !esClient || !(await isFleetServerSetup())) { + if (!soClient || !esClient) { return DEFAULT_USAGE; } diff --git a/x-pack/plugins/fleet/server/constants/index.ts b/x-pack/plugins/fleet/server/constants/index.ts index bfb1f3ec433f2..633390c368957 100644 --- a/x-pack/plugins/fleet/server/constants/index.ts +++ b/x-pack/plugins/fleet/server/constants/index.ts @@ -35,14 +35,12 @@ export { PRECONFIGURATION_API_ROUTES, // Saved object types SO_SEARCH_LIMIT, - AGENT_SAVED_OBJECT_TYPE, - AGENT_ACTION_SAVED_OBJECT_TYPE, + AGENTS_PREFIX, AGENT_POLICY_SAVED_OBJECT_TYPE, PACKAGE_POLICY_SAVED_OBJECT_TYPE, OUTPUT_SAVED_OBJECT_TYPE, PACKAGES_SAVED_OBJECT_TYPE, ASSETS_SAVED_OBJECT_TYPE, - ENROLLMENT_API_KEYS_SAVED_OBJECT_TYPE, GLOBAL_SETTINGS_SAVED_OBJECT_TYPE, // Defaults DEFAULT_AGENT_POLICY, diff --git a/x-pack/plugins/fleet/server/errors/handlers.ts b/x-pack/plugins/fleet/server/errors/handlers.ts index c47b1a07780ec..e171a5bafba90 100644 --- a/x-pack/plugins/fleet/server/errors/handlers.ts +++ b/x-pack/plugins/fleet/server/errors/handlers.ts @@ -24,7 +24,9 @@ import { IngestManagerError, PackageNotFoundError, PackageUnsupportedMediaTypeError, + RegistryConnectionError, RegistryError, + RegistryResponseError, } from './index'; type IngestErrorHandler = ( @@ -40,7 +42,12 @@ interface IngestErrorHandlerParams { // this type is based on BadRequest values observed while debugging https://github.com/elastic/kibana/issues/75862 const getHTTPResponseCode = (error: IngestManagerError): number => { - if (error instanceof RegistryError) { + if (error instanceof RegistryResponseError) { + // 4xx/5xx's from EPR + return 500; + } + if (error instanceof RegistryConnectionError || error instanceof RegistryError) { + // Connection errors (ie. RegistryConnectionError) / fallback (RegistryError) from EPR return 502; // Bad Gateway } if (error instanceof PackageNotFoundError) { diff --git a/x-pack/plugins/fleet/server/index.test.ts b/x-pack/plugins/fleet/server/index.test.ts deleted file mode 100644 index 724bb9ad91ab6..0000000000000 --- a/x-pack/plugins/fleet/server/index.test.ts +++ /dev/null @@ -1,86 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { applyDeprecations, configDeprecationFactory } from '@kbn/config'; - -import { configDeprecationsMock } from '../../../../src/core/server/mocks'; - -import { config } from '.'; - -const deprecationContext = configDeprecationsMock.createContext(); - -const applyConfigDeprecations = (settings: Record = {}) => { - if (!config.deprecations) { - throw new Error('Config is not valid no deprecations'); - } - const deprecations = config.deprecations(configDeprecationFactory); - const deprecationMessages: string[] = []; - const migrated = applyDeprecations( - settings, - deprecations.map((deprecation) => ({ - deprecation, - path: '', - context: deprecationContext, - })), - () => - ({ message }) => - deprecationMessages.push(message) - ); - return { - messages: deprecationMessages, - migrated: migrated.config, - }; -}; - -describe('Config depreciation test', () => { - it('should migrate old xpack.ingestManager.fleet settings to xpack.fleet.agents', () => { - const { migrated } = applyConfigDeprecations({ - xpack: { - ingestManager: { - fleet: { enabled: true, elasticsearch: { host: 'http://testes.fr:9200' } }, - }, - }, - }); - - expect(migrated).toMatchInlineSnapshot(` - Object { - "xpack": Object { - "fleet": Object { - "agents": Object { - "elasticsearch": Object { - "hosts": Array [ - "http://testes.fr:9200", - ], - }, - "enabled": true, - }, - }, - }, - } - `); - }); - - it('should support mixing xpack.ingestManager config and xpack.fleet config', () => { - const { migrated } = applyConfigDeprecations({ - xpack: { - ingestManager: { registryUrl: 'http://registrytest.fr' }, - fleet: { registryProxyUrl: 'http://registryProxy.fr' }, - }, - }); - - expect(migrated).toMatchInlineSnapshot(` - Object { - "xpack": Object { - "fleet": Object { - "registryProxyUrl": "http://registryProxy.fr", - "registryUrl": "http://registrytest.fr", - }, - }, - } - `); - }); -}); diff --git a/x-pack/plugins/fleet/server/index.ts b/x-pack/plugins/fleet/server/index.ts index e1ee2652594cc..c3dd408925cf0 100644 --- a/x-pack/plugins/fleet/server/index.ts +++ b/x-pack/plugins/fleet/server/index.ts @@ -44,52 +44,6 @@ export const config: PluginConfigDescriptor = { agents: true, }, deprecations: ({ renameFromRoot, unused, unusedFromRoot }) => [ - // Fleet plugin was named ingestManager before - renameFromRoot('xpack.ingestManager.enabled', 'xpack.fleet.enabled', { level: 'critical' }), - renameFromRoot('xpack.ingestManager.registryUrl', 'xpack.fleet.registryUrl', { - level: 'critical', - }), - renameFromRoot('xpack.ingestManager.registryProxyUrl', 'xpack.fleet.registryProxyUrl', { - level: 'critical', - }), - renameFromRoot('xpack.ingestManager.fleet', 'xpack.ingestManager.agents', { - level: 'critical', - }), - renameFromRoot('xpack.ingestManager.agents.enabled', 'xpack.fleet.agents.enabled', { - level: 'critical', - }), - renameFromRoot('xpack.ingestManager.agents.elasticsearch', 'xpack.fleet.agents.elasticsearch', { - level: 'critical', - }), - renameFromRoot( - 'xpack.ingestManager.agents.tlsCheckDisabled', - 'xpack.fleet.agents.tlsCheckDisabled', - { level: 'critical' } - ), - renameFromRoot( - 'xpack.ingestManager.agents.pollingRequestTimeout', - 'xpack.fleet.agents.pollingRequestTimeout', - { level: 'critical' } - ), - renameFromRoot( - 'xpack.ingestManager.agents.maxConcurrentConnections', - 'xpack.fleet.agents.maxConcurrentConnections', - { level: 'critical' } - ), - renameFromRoot('xpack.ingestManager.agents.kibana', 'xpack.fleet.agents.kibana', { - level: 'critical', - }), - renameFromRoot( - 'xpack.ingestManager.agents.agentPolicyRolloutRateLimitIntervalMs', - 'xpack.fleet.agents.agentPolicyRolloutRateLimitIntervalMs', - { level: 'critical' } - ), - renameFromRoot( - 'xpack.ingestManager.agents.agentPolicyRolloutRateLimitRequestPerInterval', - 'xpack.fleet.agents.agentPolicyRolloutRateLimitRequestPerInterval', - { level: 'critical' } - ), - unusedFromRoot('xpack.ingestManager', { level: 'critical' }), // Unused settings before Fleet server exists unused('agents.kibana', { level: 'critical' }), unused('agents.maxConcurrentConnections', { level: 'critical' }), diff --git a/x-pack/plugins/fleet/server/integration_tests/router.test.ts b/x-pack/plugins/fleet/server/integration_tests/router.test.ts index 55518923e65f2..eb002f5d731d8 100644 --- a/x-pack/plugins/fleet/server/integration_tests/router.test.ts +++ b/x-pack/plugins/fleet/server/integration_tests/router.test.ts @@ -30,7 +30,7 @@ function createXPackRoot(config: {} = {}) { }); } -describe('ingestManager', () => { +describe('fleet', () => { describe('default. manager, EPM, and Fleet all disabled', () => { let root: ReturnType; @@ -64,11 +64,11 @@ describe('ingestManager', () => { let root: ReturnType; beforeAll(async () => { - const ingestManagerConfig = { + const fleetConfig = { enabled: true, }; root = createXPackRoot({ - ingestManager: ingestManagerConfig, + fleet: fleetConfig, }); await root.preboot(); await root.setup(); @@ -103,12 +103,12 @@ describe('ingestManager', () => { let root: ReturnType; beforeAll(async () => { - const ingestManagerConfig = { + const fleetConfig = { enabled: true, epm: { enabled: true }, }; root = createXPackRoot({ - ingestManager: ingestManagerConfig, + fleet: fleetConfig, }); await root.preboot(); await root.setup(); @@ -138,12 +138,12 @@ describe('ingestManager', () => { let root: ReturnType; beforeAll(async () => { - const ingestManagerConfig = { + const fleetConfig = { enabled: true, fleet: { enabled: true }, }; root = createXPackRoot({ - ingestManager: ingestManagerConfig, + fleet: fleetConfig, }); await root.preboot(); await root.setup(); @@ -173,13 +173,13 @@ describe('ingestManager', () => { let root: ReturnType; beforeAll(async () => { - const ingestManagerConfig = { + const fleetConfig = { enabled: true, epm: { enabled: true }, fleet: { enabled: true }, }; root = createXPackRoot({ - ingestManager: ingestManagerConfig, + fleet: fleetConfig, }); await root.preboot(); await root.setup(); diff --git a/x-pack/plugins/fleet/server/plugin.ts b/x-pack/plugins/fleet/server/plugin.ts index 7cc1b8b1cfcc9..d0c73a0fe42a7 100644 --- a/x-pack/plugins/fleet/server/plugin.ts +++ b/x-pack/plugins/fleet/server/plugin.ts @@ -40,8 +40,6 @@ import { PACKAGE_POLICY_SAVED_OBJECT_TYPE, PACKAGES_SAVED_OBJECT_TYPE, ASSETS_SAVED_OBJECT_TYPE, - AGENT_SAVED_OBJECT_TYPE, - ENROLLMENT_API_KEYS_SAVED_OBJECT_TYPE, PRECONFIGURATION_DELETION_RECORD_SAVED_OBJECT_TYPE, } from './constants'; import { registerSavedObjects, registerEncryptedSavedObjects } from './saved_objects'; @@ -82,7 +80,6 @@ import { import { registerFleetUsageCollector } from './collectors/register'; import { getInstallation, ensureInstalledPackage } from './services/epm/packages'; import { RouterWrappers } from './routes/security'; -import { startFleetServerSetup } from './services/fleet_server'; import { FleetArtifactsClient } from './services/artifacts'; import type { FleetRouter } from './types/request_context'; import { TelemetryEventsSender } from './telemetry/sender'; @@ -131,8 +128,6 @@ const allSavedObjectTypes = [ PACKAGE_POLICY_SAVED_OBJECT_TYPE, PACKAGES_SAVED_OBJECT_TYPE, ASSETS_SAVED_OBJECT_TYPE, - AGENT_SAVED_OBJECT_TYPE, - ENROLLMENT_API_KEYS_SAVED_OBJECT_TYPE, PRECONFIGURATION_DELETION_RECORD_SAVED_OBJECT_TYPE, ]; @@ -335,15 +330,10 @@ export class FleetPlugin }); licenseService.start(this.licensing$); - const fleetServerSetup = startFleetServerSetup(); - this.telemetryEventsSender.start(plugins.telemetry, core); return { - fleetSetupCompleted: () => - new Promise((resolve) => { - Promise.all([fleetServerSetup]).finally(() => resolve()); - }), + fleetSetupCompleted: () => Promise.resolve(), esIndexPatternService: new ESIndexPatternSavedObjectService(), packageService: { getInstallation, diff --git a/x-pack/plugins/fleet/server/routes/agent_policy/handlers.ts b/x-pack/plugins/fleet/server/routes/agent_policy/handlers.ts index c3da75183f581..250bfd13a84c1 100644 --- a/x-pack/plugins/fleet/server/routes/agent_policy/handlers.ts +++ b/x-pack/plugins/fleet/server/routes/agent_policy/handlers.ts @@ -7,13 +7,13 @@ import type { TypeOf } from '@kbn/config-schema'; import type { RequestHandler, ResponseHeaders } from 'src/core/server'; -import bluebird from 'bluebird'; +import pMap from 'p-map'; import { safeDump } from 'js-yaml'; import { fullAgentPolicyToYaml } from '../../../common/services'; import { appContextService, agentPolicyService, packagePolicyService } from '../../services'; import { getAgentsByKuery } from '../../services/agents'; -import { AGENT_SAVED_OBJECT_TYPE } from '../../constants'; +import { AGENTS_PREFIX } from '../../constants'; import type { GetAgentPoliciesRequestSchema, GetOneAgentPolicyRequestSchema, @@ -37,6 +37,7 @@ import type { GetFullAgentConfigMapResponse, } from '../../../common'; import { defaultIngestErrorHandler } from '../../errors'; +import { incrementPackageName } from '../../services/package_policy'; export const getAgentPoliciesHandler: RequestHandler< undefined, @@ -57,14 +58,14 @@ export const getAgentPoliciesHandler: RequestHandler< perPage, }; - await bluebird.map( + await pMap( items, (agentPolicy: GetAgentPoliciesResponseItem) => getAgentsByKuery(esClient, { showInactive: false, perPage: 0, page: 1, - kuery: `${AGENT_SAVED_OBJECT_TYPE}.policy_id:${agentPolicy.id}`, + kuery: `${AGENTS_PREFIX}.policy_id:${agentPolicy.id}`, }).then(({ total: agentTotal }) => (agentPolicy.agents = agentTotal)), { concurrency: 10 } ); @@ -108,6 +109,7 @@ export const createAgentPolicyHandler: RequestHandler< const esClient = context.core.elasticsearch.client.asCurrentUser; const user = (await appContextService.getSecurity()?.authc.getCurrentUser(request)) || undefined; const withSysMonitoring = request.query.sys_monitoring ?? false; + try { // eslint-disable-next-line prefer-const let [agentPolicy, newSysPackagePolicy] = await Promise.all< @@ -131,6 +133,8 @@ export const createAgentPolicyHandler: RequestHandler< if (withSysMonitoring && newSysPackagePolicy !== undefined && agentPolicy !== undefined) { newSysPackagePolicy.policy_id = agentPolicy.id; newSysPackagePolicy.namespace = agentPolicy.namespace; + newSysPackagePolicy.name = await incrementPackageName(soClient, FLEET_SYSTEM_PACKAGE); + await packagePolicyService.create(soClient, esClient, newSysPackagePolicy, { user, bumpRevision: false, diff --git a/x-pack/plugins/fleet/server/routes/package_policy/handlers.ts b/x-pack/plugins/fleet/server/routes/package_policy/handlers.ts index 58463bfa5569d..f61890f852798 100644 --- a/x-pack/plugins/fleet/server/routes/package_policy/handlers.ts +++ b/x-pack/plugins/fleet/server/routes/package_policy/handlers.ts @@ -192,6 +192,8 @@ export const deletePackagePolicyHandler: RequestHandler< } }; +// TODO: Separate the upgrade and dry-run processes into separate endpoints, and address +// duplicate logic in error handling as part of https://github.com/elastic/kibana/issues/63123 export const upgradePackagePolicyHandler: RequestHandler< unknown, unknown, @@ -212,6 +214,16 @@ export const upgradePackagePolicyHandler: RequestHandler< ); body.push(result); } + + const firstFatalError = body.find((item) => item.statusCode && item.statusCode !== 200); + + if (firstFatalError) { + return response.customError({ + statusCode: firstFatalError.statusCode!, + body: { message: firstFatalError.body!.message }, + }); + } + return response.ok({ body, }); @@ -222,6 +234,15 @@ export const upgradePackagePolicyHandler: RequestHandler< request.body.packagePolicyIds, { user } ); + + const firstFatalError = body.find((item) => item.statusCode && item.statusCode !== 200); + + if (firstFatalError) { + return response.customError({ + statusCode: firstFatalError.statusCode!, + body: { message: firstFatalError.body!.message }, + }); + } return response.ok({ body, }); diff --git a/x-pack/plugins/fleet/server/saved_objects/index.ts b/x-pack/plugins/fleet/server/saved_objects/index.ts index e8fda952f17e6..3b459c938b5f0 100644 --- a/x-pack/plugins/fleet/server/saved_objects/index.ts +++ b/x-pack/plugins/fleet/server/saved_objects/index.ts @@ -14,29 +14,19 @@ import { PACKAGE_POLICY_SAVED_OBJECT_TYPE, PACKAGES_SAVED_OBJECT_TYPE, ASSETS_SAVED_OBJECT_TYPE, - AGENT_SAVED_OBJECT_TYPE, - AGENT_ACTION_SAVED_OBJECT_TYPE, - ENROLLMENT_API_KEYS_SAVED_OBJECT_TYPE, GLOBAL_SETTINGS_SAVED_OBJECT_TYPE, PRECONFIGURATION_DELETION_RECORD_SAVED_OBJECT_TYPE, } from '../constants'; import { - migrateAgentActionToV7100, migrateAgentPolicyToV7100, - migrateAgentToV7100, - migrateEnrollmentApiKeysToV7100, migratePackagePolicyToV7100, migrateSettingsToV7100, } from './migrations/to_v7_10_0'; import { migratePackagePolicyToV7110 } from './migrations/to_v7_11_0'; -import { - migrateAgentPolicyToV7120, - migrateAgentToV7120, - migratePackagePolicyToV7120, -} from './migrations/to_v7_12_0'; +import { migrateAgentPolicyToV7120, migratePackagePolicyToV7120 } from './migrations/to_v7_12_0'; import { migratePackagePolicyToV7130, migrateSettingsToV7130, @@ -45,6 +35,7 @@ import { import { migratePackagePolicyToV7140, migrateInstallationToV7140 } from './migrations/to_v7_14_0'; import { migratePackagePolicyToV7150 } from './migrations/to_v7_15_0'; import { migrateInstallationToV7160, migratePackagePolicyToV7160 } from './migrations/to_v7_16_0'; +import { migrateOutputToV800 } from './migrations/to_v8_0_0'; /* * Saved object types and mappings @@ -74,66 +65,6 @@ const getSavedObjectTypes = ( '7.13.0': migrateSettingsToV7130, }, }, - [AGENT_SAVED_OBJECT_TYPE]: { - name: AGENT_SAVED_OBJECT_TYPE, - hidden: false, - namespaceType: 'agnostic', - management: { - importableAndExportable: false, - }, - mappings: { - properties: { - type: { type: 'keyword' }, - active: { type: 'boolean' }, - enrolled_at: { type: 'date' }, - unenrolled_at: { type: 'date' }, - unenrollment_started_at: { type: 'date' }, - upgraded_at: { type: 'date' }, - upgrade_started_at: { type: 'date' }, - access_api_key_id: { type: 'keyword' }, - version: { type: 'keyword' }, - user_provided_metadata: { type: 'flattened' }, - local_metadata: { type: 'flattened' }, - policy_id: { type: 'keyword' }, - policy_revision: { type: 'integer' }, - last_updated: { type: 'date' }, - last_checkin: { type: 'date' }, - last_checkin_status: { type: 'keyword' }, - default_api_key_id: { type: 'keyword' }, - default_api_key: { type: 'binary' }, - updated_at: { type: 'date' }, - current_error_events: { type: 'text', index: false }, - packages: { type: 'keyword' }, - }, - }, - migrations: { - '7.10.0': migrateAgentToV7100, - '7.12.0': migrateAgentToV7120, - }, - }, - [AGENT_ACTION_SAVED_OBJECT_TYPE]: { - name: AGENT_ACTION_SAVED_OBJECT_TYPE, - hidden: false, - namespaceType: 'agnostic', - management: { - importableAndExportable: false, - }, - mappings: { - properties: { - agent_id: { type: 'keyword' }, - policy_id: { type: 'keyword' }, - policy_revision: { type: 'integer' }, - type: { type: 'keyword' }, - data: { type: 'binary' }, - ack_data: { type: 'text' }, - sent_at: { type: 'date' }, - created_at: { type: 'date' }, - }, - }, - migrations: { - '7.10.0': migrateAgentActionToV7100(encryptedSavedObjects), - }, - }, [AGENT_POLICY_SAVED_OBJECT_TYPE]: { name: AGENT_POLICY_SAVED_OBJECT_TYPE, hidden: false, @@ -166,30 +97,6 @@ const getSavedObjectTypes = ( '7.12.0': migrateAgentPolicyToV7120, }, }, - [ENROLLMENT_API_KEYS_SAVED_OBJECT_TYPE]: { - name: ENROLLMENT_API_KEYS_SAVED_OBJECT_TYPE, - hidden: false, - namespaceType: 'agnostic', - management: { - importableAndExportable: false, - }, - mappings: { - properties: { - name: { type: 'keyword' }, - type: { type: 'keyword' }, - api_key: { type: 'binary' }, - api_key_id: { type: 'keyword' }, - policy_id: { type: 'keyword' }, - created_at: { type: 'date' }, - updated_at: { type: 'date' }, - expire_at: { type: 'date' }, - active: { type: 'boolean' }, - }, - }, - migrations: { - '7.10.0': migrateEnrollmentApiKeysToV7100, - }, - }, [OUTPUT_SAVED_OBJECT_TYPE]: { name: OUTPUT_SAVED_OBJECT_TYPE, hidden: false, @@ -203,6 +110,7 @@ const getSavedObjectTypes = ( name: { type: 'keyword' }, type: { type: 'keyword' }, is_default: { type: 'boolean' }, + is_default_monitoring: { type: 'boolean' }, hosts: { type: 'keyword' }, ca_sha256: { type: 'keyword', index: false }, config: { type: 'flattened' }, @@ -212,6 +120,7 @@ const getSavedObjectTypes = ( }, migrations: { '7.13.0': migrateOutputToV7130, + '8.0.0': migrateOutputToV800, }, }, [PACKAGE_POLICY_SAVED_OBJECT_TYPE]: { @@ -396,48 +305,4 @@ export function registerEncryptedSavedObjects( encryptedSavedObjects: EncryptedSavedObjectsPluginSetup ) { // Encrypted saved objects - encryptedSavedObjects.registerType({ - type: ENROLLMENT_API_KEYS_SAVED_OBJECT_TYPE, - attributesToEncrypt: new Set(['api_key']), - attributesToExcludeFromAAD: new Set([ - 'name', - 'type', - 'api_key_id', - 'policy_id', - 'created_at', - 'updated_at', - 'expire_at', - 'active', - ]), - }); - encryptedSavedObjects.registerType({ - type: AGENT_SAVED_OBJECT_TYPE, - attributesToEncrypt: new Set(['default_api_key']), - attributesToExcludeFromAAD: new Set([ - 'type', - 'active', - 'enrolled_at', - 'access_api_key_id', - 'version', - 'user_provided_metadata', - 'local_metadata', - 'policy_id', - 'policy_revision', - 'last_updated', - 'last_checkin', - 'last_checkin_status', - 'updated_at', - 'current_error_events', - 'unenrolled_at', - 'unenrollment_started_at', - 'packages', - 'upgraded_at', - 'upgrade_started_at', - ]), - }); - encryptedSavedObjects.registerType({ - type: AGENT_ACTION_SAVED_OBJECT_TYPE, - attributesToEncrypt: new Set(['data']), - attributesToExcludeFromAAD: new Set(['agent_id', 'type', 'sent_at', 'created_at']), - }); } diff --git a/x-pack/plugins/fleet/server/saved_objects/migrations/to_v7_10_0.ts b/x-pack/plugins/fleet/server/saved_objects/migrations/to_v7_10_0.ts index 64338690977c9..bb54c55ac75a6 100644 --- a/x-pack/plugins/fleet/server/saved_objects/migrations/to_v7_10_0.ts +++ b/x-pack/plugins/fleet/server/saved_objects/migrations/to_v7_10_0.ts @@ -5,33 +5,9 @@ * 2.0. */ -import type { SavedObjectMigrationFn, SavedObjectUnsanitizedDoc } from 'kibana/server'; +import type { SavedObjectMigrationFn } from 'kibana/server'; -import type { EncryptedSavedObjectsPluginSetup } from '../../../../encrypted_saved_objects/server'; -import type { - Agent, - AgentPolicy, - PackagePolicy, - EnrollmentAPIKey, - Settings, - AgentAction, -} from '../../types'; - -export const migrateAgentToV7100: SavedObjectMigrationFn< - Exclude & { - config_id?: string; - config_revision?: number | null; - }, - Agent -> = (agentDoc) => { - agentDoc.attributes.policy_id = agentDoc.attributes.config_id; - delete agentDoc.attributes.config_id; - - agentDoc.attributes.policy_revision = agentDoc.attributes.config_revision; - delete agentDoc.attributes.config_revision; - - return agentDoc; -}; +import type { AgentPolicy, PackagePolicy, Settings } from '../../types'; export const migrateAgentPolicyToV7100: SavedObjectMigrationFn< Exclude & { @@ -46,18 +22,6 @@ export const migrateAgentPolicyToV7100: SavedObjectMigrationFn< return agentPolicyDoc; }; -export const migrateEnrollmentApiKeysToV7100: SavedObjectMigrationFn< - Exclude & { - config_id?: string; - }, - EnrollmentAPIKey -> = (enrollmentApiKeyDoc) => { - enrollmentApiKeyDoc.attributes.policy_id = enrollmentApiKeyDoc.attributes.config_id; - delete enrollmentApiKeyDoc.attributes.config_id; - - return enrollmentApiKeyDoc; -}; - export const migratePackagePolicyToV7100: SavedObjectMigrationFn< Exclude & { config_id: string; @@ -84,45 +48,3 @@ export const migrateSettingsToV7100: SavedObjectMigrationFn< return settingsDoc; }; - -export const migrateAgentActionToV7100 = ( - encryptedSavedObjects: EncryptedSavedObjectsPluginSetup -): SavedObjectMigrationFn => { - return encryptedSavedObjects.createMigration({ - isMigrationNeededPredicate: ( - agentActionDoc - ): agentActionDoc is SavedObjectUnsanitizedDoc => { - // @ts-expect-error - return agentActionDoc.attributes.type === 'CONFIG_CHANGE'; - }, - migration: (agentActionDoc) => { - let agentActionData; - try { - agentActionData = agentActionDoc.attributes.data - ? JSON.parse(agentActionDoc.attributes.data) - : undefined; - } catch (e) { - // Silently swallow JSON parsing error - } - if (agentActionData && agentActionData.config) { - const { - attributes: { data, ...restOfAttributes }, - } = agentActionDoc; - const { config, ...restOfData } = agentActionData; - return { - ...agentActionDoc, - attributes: { - ...restOfAttributes, - type: 'POLICY_CHANGE', - data: JSON.stringify({ - ...restOfData, - policy: config, - }), - }, - }; - } else { - return agentActionDoc; - } - }, - }); -}; diff --git a/x-pack/plugins/fleet/server/saved_objects/migrations/to_v7_12_0.ts b/x-pack/plugins/fleet/server/saved_objects/migrations/to_v7_12_0.ts index ad7a179f50766..fde71388cbbde 100644 --- a/x-pack/plugins/fleet/server/saved_objects/migrations/to_v7_12_0.ts +++ b/x-pack/plugins/fleet/server/saved_objects/migrations/to_v7_12_0.ts @@ -7,18 +7,10 @@ import type { SavedObjectMigrationFn } from 'kibana/server'; -import type { Agent, AgentPolicy } from '../../types'; +import type { AgentPolicy } from '../../types'; export { migratePackagePolicyToV7120 } from './security_solution/to_v7_12_0'; -export const migrateAgentToV7120: SavedObjectMigrationFn = ( - agentDoc -) => { - delete agentDoc.attributes.shared_id; - - return agentDoc; -}; - export const migrateAgentPolicyToV7120: SavedObjectMigrationFn< Exclude, AgentPolicy diff --git a/x-pack/plugins/fleet/server/saved_objects/migrations/to_v8_0_0.ts b/x-pack/plugins/fleet/server/saved_objects/migrations/to_v8_0_0.ts new file mode 100644 index 0000000000000..77797b3d27ba5 --- /dev/null +++ b/x-pack/plugins/fleet/server/saved_objects/migrations/to_v8_0_0.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 type { SavedObjectMigrationFn } from 'kibana/server'; + +import type { Output } from '../../../common'; +import {} from '../../../common'; + +export const migrateOutputToV800: SavedObjectMigrationFn = ( + outputDoc, + migrationContext +) => { + if (outputDoc.attributes.is_default) { + outputDoc.attributes.is_default_monitoring = true; + } + + return outputDoc; +}; diff --git a/x-pack/plugins/fleet/server/services/agent_policies/full_agent_policy.test.ts b/x-pack/plugins/fleet/server/services/agent_policies/full_agent_policy.test.ts index 9a9b200d14130..d720aa72e18f8 100644 --- a/x-pack/plugins/fleet/server/services/agent_policies/full_agent_policy.test.ts +++ b/x-pack/plugins/fleet/server/services/agent_policies/full_agent_policy.test.ts @@ -51,13 +51,15 @@ jest.mock('../agent_policy'); jest.mock('../output', () => { return { outputService: { - getDefaultOutputId: () => 'test-id', + getDefaultDataOutputId: async () => 'test-id', + getDefaultMonitoringOutputId: async () => 'test-id', get: (soClient: any, id: string): Output => { switch (id) { case 'data-output-id': return { id: 'data-output-id', is_default: false, + is_default_monitoring: false, name: 'Data output', // @ts-ignore type: 'elasticsearch', @@ -67,6 +69,7 @@ jest.mock('../output', () => { return { id: 'monitoring-output-id', is_default: false, + is_default_monitoring: false, name: 'Monitoring output', // @ts-ignore type: 'elasticsearch', @@ -76,6 +79,7 @@ jest.mock('../output', () => { return { id: 'test-id', is_default: true, + is_default_monitoring: true, name: 'default', // @ts-ignore type: 'elasticsearch', diff --git a/x-pack/plugins/fleet/server/services/agent_policies/full_agent_policy.ts b/x-pack/plugins/fleet/server/services/agent_policies/full_agent_policy.ts index 60cf9c8d96257..f89a186c1a5f9 100644 --- a/x-pack/plugins/fleet/server/services/agent_policies/full_agent_policy.ts +++ b/x-pack/plugins/fleet/server/services/agent_policies/full_agent_policy.ts @@ -48,13 +48,17 @@ export async function getFullAgentPolicy( return null; } - const defaultOutputId = await outputService.getDefaultOutputId(soClient); - if (!defaultOutputId) { + const defaultDataOutputId = await outputService.getDefaultDataOutputId(soClient); + + if (!defaultDataOutputId) { throw new Error('Default output is not setup'); } - const dataOutputId = agentPolicy.data_output_id || defaultOutputId; - const monitoringOutputId = agentPolicy.monitoring_output_id || defaultOutputId; + const dataOutputId: string = agentPolicy.data_output_id || defaultDataOutputId; + const monitoringOutputId: string = + agentPolicy.monitoring_output_id || + (await outputService.getDefaultMonitoringOutputId(soClient)) || + dataOutputId; const outputs = await Promise.all( Array.from(new Set([dataOutputId, monitoringOutputId])).map((outputId) => diff --git a/x-pack/plugins/fleet/server/services/agent_policy.test.ts b/x-pack/plugins/fleet/server/services/agent_policy.test.ts index 5617f8ef7bd7c..e28e2610b4b45 100644 --- a/x-pack/plugins/fleet/server/services/agent_policy.test.ts +++ b/x-pack/plugins/fleet/server/services/agent_policy.test.ts @@ -235,7 +235,7 @@ describe('agent policy', () => { const soClient = savedObjectsClientMock.create(); const esClient = elasticsearchServiceMock.createClusterClient().asInternalUser; mockedAppContextService.getInternalUserESClient.mockReturnValue(esClient); - mockedOutputService.getDefaultOutputId.mockResolvedValue('default-output'); + mockedOutputService.getDefaultDataOutputId.mockResolvedValue('default-output'); mockedGetFullAgentPolicy.mockResolvedValue(null); soClient.get.mockResolvedValue({ @@ -253,7 +253,7 @@ describe('agent policy', () => { const soClient = savedObjectsClientMock.create(); const esClient = elasticsearchServiceMock.createClusterClient().asInternalUser; mockedAppContextService.getInternalUserESClient.mockReturnValue(esClient); - mockedOutputService.getDefaultOutputId.mockResolvedValue('default-output'); + mockedOutputService.getDefaultDataOutputId.mockResolvedValue('default-output'); mockedGetFullAgentPolicy.mockResolvedValue({ id: 'policy123', revision: 1, diff --git a/x-pack/plugins/fleet/server/services/agent_policy.ts b/x-pack/plugins/fleet/server/services/agent_policy.ts index 7de907b9a15fa..bb9360b834b37 100644 --- a/x-pack/plugins/fleet/server/services/agent_policy.ts +++ b/x-pack/plugins/fleet/server/services/agent_policy.ts @@ -20,7 +20,7 @@ import { SavedObjectsErrorHelpers } from '../../../../../src/core/server'; import type { AuthenticatedUser } from '../../../security/server'; import { AGENT_POLICY_SAVED_OBJECT_TYPE, - AGENT_SAVED_OBJECT_TYPE, + AGENTS_PREFIX, PRECONFIGURATION_DELETION_RECORD_SAVED_OBJECT_TYPE, } from '../constants'; import type { @@ -626,7 +626,7 @@ class AgentPolicyService { showInactive: false, perPage: 0, page: 1, - kuery: `${AGENT_SAVED_OBJECT_TYPE}.policy_id:${id}`, + kuery: `${AGENTS_PREFIX}.policy_id:${id}`, }); if (total > 0) { @@ -672,7 +672,7 @@ class AgentPolicyService { ) { // Use internal ES client so we have permissions to write to .fleet* indices const esClient = appContextService.getInternalUserESClient(); - const defaultOutputId = await outputService.getDefaultOutputId(soClient); + const defaultOutputId = await outputService.getDefaultDataOutputId(soClient); if (!defaultOutputId) { return; diff --git a/x-pack/plugins/fleet/server/services/agents/crud.ts b/x-pack/plugins/fleet/server/services/agents/crud.ts index b8d7c284309df..516acf5a120de 100644 --- a/x-pack/plugins/fleet/server/services/agents/crud.ts +++ b/x-pack/plugins/fleet/server/services/agents/crud.ts @@ -16,7 +16,7 @@ import type { AgentSOAttributes, Agent, BulkActionResult, ListWithKuery } from ' import { appContextService, agentPolicyService } from '../../services'; import type { FleetServerAgent } from '../../../common'; import { isAgentUpgradeable, SO_SEARCH_LIMIT } from '../../../common'; -import { AGENT_SAVED_OBJECT_TYPE, AGENTS_INDEX } from '../../constants'; +import { AGENTS_PREFIX, AGENTS_INDEX } from '../../constants'; import { escapeSearchQueryPhrase, normalizeKuery } from '../saved_object'; import { IngestManagerError, isESClientError, AgentNotFoundError } from '../../errors'; @@ -176,7 +176,7 @@ export async function countInactiveAgents( const filters = [INACTIVE_AGENT_CONDITION]; if (kuery && kuery !== '') { - filters.push(normalizeKuery(AGENT_SAVED_OBJECT_TYPE, kuery)); + filters.push(normalizeKuery(AGENTS_PREFIX, kuery)); } const kueryNode = _joinFilters(filters); diff --git a/x-pack/plugins/fleet/server/services/agents/crud_so.ts b/x-pack/plugins/fleet/server/services/agents/crud_so.ts deleted file mode 100644 index aa3cb4e4ec1a7..0000000000000 --- a/x-pack/plugins/fleet/server/services/agents/crud_so.ts +++ /dev/null @@ -1,255 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import Boom from '@hapi/boom'; -import type { SavedObjectsBulkUpdateObject, SavedObjectsClientContract } from 'src/core/server'; - -import type { KueryNode } from '@kbn/es-query'; -import { fromKueryExpression } from '@kbn/es-query'; - -import { isAgentUpgradeable } from '../../../common'; -import { AGENT_SAVED_OBJECT_TYPE } from '../../constants'; -import type { AgentSOAttributes, Agent, ListWithKuery } from '../../types'; -import { escapeSearchQueryPhrase, normalizeKuery, findAllSOs } from '../saved_object'; -import { appContextService } from '../../services'; - -import { savedObjectToAgent } from './saved_objects'; - -const ACTIVE_AGENT_CONDITION = `${AGENT_SAVED_OBJECT_TYPE}.attributes.active:true`; -const INACTIVE_AGENT_CONDITION = `NOT (${ACTIVE_AGENT_CONDITION})`; - -function _joinFilters(filters: Array) { - return filters - .filter((filter) => filter !== undefined) - .reduce( - ( - acc: KueryNode | undefined, - kuery: string | KueryNode | undefined - ): KueryNode | undefined => { - if (kuery === undefined) { - return acc; - } - const kueryNode: KueryNode = - typeof kuery === 'string' - ? fromKueryExpression(normalizeKuery(AGENT_SAVED_OBJECT_TYPE, kuery)) - : kuery; - - if (!acc) { - return kueryNode; - } - - return { - type: 'function', - function: 'and', - arguments: [acc, kueryNode], - }; - }, - undefined as KueryNode | undefined - ); -} - -export async function listAgents( - soClient: SavedObjectsClientContract, - options: ListWithKuery & { - showInactive: boolean; - } -): Promise<{ - agents: Agent[]; - total: number; - page: number; - perPage: number; -}> { - const { - page = 1, - perPage = 20, - sortField = 'enrolled_at', - sortOrder = 'desc', - kuery, - showInactive = false, - showUpgradeable, - } = options; - const filters: Array = []; - - if (kuery && kuery !== '') { - filters.push(kuery); - } - - if (showInactive === false) { - filters.push(ACTIVE_AGENT_CONDITION); - } - try { - let { saved_objects: agentSOs, total } = await soClient.find({ - type: AGENT_SAVED_OBJECT_TYPE, - filter: _joinFilters(filters) || '', - sortField, - sortOrder, - page, - perPage, - }); - // filtering for a range on the version string will not work, - // nor does filtering on a flattened field (local_metadata), so filter here - if (showUpgradeable) { - agentSOs = agentSOs.filter((agent) => - isAgentUpgradeable(savedObjectToAgent(agent), appContextService.getKibanaVersion()) - ); - total = agentSOs.length; - } - - return { - agents: agentSOs.map(savedObjectToAgent), - total, - page, - perPage, - }; - } catch (e) { - if (e.output?.payload?.message?.startsWith('The key is empty')) { - return { - agents: [], - total: 0, - page: 0, - perPage: 0, - }; - } else { - throw e; - } - } -} - -export async function listAllAgents( - soClient: SavedObjectsClientContract, - options: Omit & { - showInactive: boolean; - } -): Promise<{ - agents: Agent[]; - total: number; -}> { - const { sortField = 'enrolled_at', sortOrder = 'desc', kuery, showInactive = false } = options; - const filters = []; - - if (kuery && kuery !== '') { - filters.push(kuery); - } - - if (showInactive === false) { - filters.push(ACTIVE_AGENT_CONDITION); - } - - const { saved_objects: agentSOs, total } = await findAllSOs(soClient, { - type: AGENT_SAVED_OBJECT_TYPE, - kuery: _joinFilters(filters), - sortField, - sortOrder, - }); - - return { - agents: agentSOs.map(savedObjectToAgent), - total, - }; -} - -export async function countInactiveAgents( - soClient: SavedObjectsClientContract, - options: Pick -): Promise { - const { kuery } = options; - const filters = [INACTIVE_AGENT_CONDITION]; - - if (kuery && kuery !== '') { - filters.push(normalizeKuery(AGENT_SAVED_OBJECT_TYPE, kuery)); - } - - const { total } = await soClient.find({ - type: AGENT_SAVED_OBJECT_TYPE, - filter: _joinFilters(filters), - perPage: 0, - }); - - return total; -} - -export async function getAgent(soClient: SavedObjectsClientContract, agentId: string) { - const agent = savedObjectToAgent( - await soClient.get(AGENT_SAVED_OBJECT_TYPE, agentId) - ); - return agent; -} - -export async function getAgents(soClient: SavedObjectsClientContract, agentIds: string[]) { - const agentSOs = await soClient.bulkGet( - agentIds.map((agentId) => ({ - id: agentId, - type: AGENT_SAVED_OBJECT_TYPE, - })) - ); - const agents = agentSOs.saved_objects.map(savedObjectToAgent); - return agents; -} - -export async function getAgentByAccessAPIKeyId( - soClient: SavedObjectsClientContract, - accessAPIKeyId: string -): Promise { - const response = await soClient.find({ - type: AGENT_SAVED_OBJECT_TYPE, - searchFields: ['access_api_key_id'], - search: escapeSearchQueryPhrase(accessAPIKeyId), - }); - const [agent] = response.saved_objects.map(savedObjectToAgent); - - if (!agent) { - throw Boom.notFound('Agent not found'); - } - if (agent.access_api_key_id !== accessAPIKeyId) { - throw new Error('Agent api key id is not matching'); - } - if (!agent.active) { - throw Boom.forbidden('Agent inactive'); - } - - return agent; -} - -export async function updateAgent( - soClient: SavedObjectsClientContract, - agentId: string, - data: Partial -) { - await soClient.update(AGENT_SAVED_OBJECT_TYPE, agentId, data); -} - -export async function bulkUpdateAgents( - soClient: SavedObjectsClientContract, - updateData: Array<{ - agentId: string; - data: Partial; - }> -) { - const updates: Array> = updateData.map( - ({ agentId, data }) => ({ - type: AGENT_SAVED_OBJECT_TYPE, - id: agentId, - attributes: data, - }) - ); - - const res = await soClient.bulkUpdate(updates); - - return { - items: res.saved_objects.map((so) => ({ - id: so.id, - success: !so.error, - error: so.error, - })), - }; -} - -export async function deleteAgent(soClient: SavedObjectsClientContract, agentId: string) { - await soClient.update(AGENT_SAVED_OBJECT_TYPE, agentId, { - active: false, - }); -} diff --git a/x-pack/plugins/fleet/server/services/agents/status.ts b/x-pack/plugins/fleet/server/services/agents/status.ts index ee30843e74e1a..5c5176ec41352 100644 --- a/x-pack/plugins/fleet/server/services/agents/status.ts +++ b/x-pack/plugins/fleet/server/services/agents/status.ts @@ -11,7 +11,7 @@ import pMap from 'p-map'; import type { KueryNode } from '@kbn/es-query'; import { fromKueryExpression } from '@kbn/es-query'; -import { AGENT_SAVED_OBJECT_TYPE } from '../../constants'; +import { AGENTS_PREFIX } from '../../constants'; import type { AgentStatus } from '../../types'; import { AgentStatusKueryHelper } from '../../../common/services'; @@ -70,8 +70,8 @@ export async function getAgentStatusForAgentPolicy( ...[ kuery, filterKuery, - `${AGENT_SAVED_OBJECT_TYPE}.attributes.active:true`, - agentPolicyId ? `${AGENT_SAVED_OBJECT_TYPE}.policy_id:"${agentPolicyId}"` : undefined, + `${AGENTS_PREFIX}.attributes.active:true`, + agentPolicyId ? `${AGENTS_PREFIX}.policy_id:"${agentPolicyId}"` : undefined, ] ), }), diff --git a/x-pack/plugins/fleet/server/services/agents/update.ts b/x-pack/plugins/fleet/server/services/agents/update.ts index 74386efe65613..cbe7853425fa6 100644 --- a/x-pack/plugins/fleet/server/services/agents/update.ts +++ b/x-pack/plugins/fleet/server/services/agents/update.ts @@ -7,7 +7,7 @@ import type { ElasticsearchClient, SavedObjectsClientContract } from 'src/core/server'; -import { AGENT_SAVED_OBJECT_TYPE } from '../../constants'; +import { AGENTS_PREFIX } from '../../constants'; import { getAgentsByKuery } from './crud'; import { unenrollAgent } from './unenroll'; @@ -21,7 +21,7 @@ export async function unenrollForAgentPolicyId( let page = 1; while (hasMore) { const { agents } = await getAgentsByKuery(esClient, { - kuery: `${AGENT_SAVED_OBJECT_TYPE}.policy_id:"${policyId}"`, + kuery: `${AGENTS_PREFIX}.policy_id:"${policyId}"`, page: page++, perPage: 1000, showInactive: false, diff --git a/x-pack/plugins/fleet/server/services/agents/upgrade.ts b/x-pack/plugins/fleet/server/services/agents/upgrade.ts index 988d3c63223f4..ce5536df359ba 100644 --- a/x-pack/plugins/fleet/server/services/agents/upgrade.ts +++ b/x-pack/plugins/fleet/server/services/agents/upgrade.ts @@ -7,8 +7,7 @@ import type { ElasticsearchClient, SavedObjectsClientContract } from 'src/core/server'; -import type { Agent, AgentAction, AgentActionSOAttributes, BulkActionResult } from '../../types'; -import { AGENT_ACTION_SAVED_OBJECT_TYPE } from '../../constants'; +import type { Agent, BulkActionResult } from '../../types'; import { agentPolicyService } from '../../services'; import { AgentReassignmentError, @@ -68,23 +67,6 @@ export async function sendUpgradeAgentAction({ }); } -export async function ackAgentUpgraded( - soClient: SavedObjectsClientContract, - esClient: ElasticsearchClient, - agentAction: AgentAction -) { - const { - attributes: { ack_data: ackData }, - } = await soClient.get(AGENT_ACTION_SAVED_OBJECT_TYPE, agentAction.id); - if (!ackData) throw new Error('data missing from UPGRADE action'); - const { version } = JSON.parse(ackData); - if (!version) throw new Error('version missing from UPGRADE action'); - await updateAgent(esClient, agentAction.agent_id, { - upgraded_at: new Date().toISOString(), - upgrade_started_at: null, - }); -} - export async function sendUpgradeAgentsActions( soClient: SavedObjectsClientContract, esClient: ElasticsearchClient, diff --git a/x-pack/plugins/fleet/server/services/api_keys/enrollment_api_key_so.ts b/x-pack/plugins/fleet/server/services/api_keys/enrollment_api_key_so.ts deleted file mode 100644 index a2b40200fe136..0000000000000 --- a/x-pack/plugins/fleet/server/services/api_keys/enrollment_api_key_so.ts +++ /dev/null @@ -1,72 +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 type { SavedObjectsClientContract, SavedObject } from 'src/core/server'; - -import type { EnrollmentAPIKey, EnrollmentAPIKeySOAttributes } from '../../types'; -import { ENROLLMENT_API_KEYS_SAVED_OBJECT_TYPE } from '../../constants'; -import { appContextService } from '../app_context'; -import { normalizeKuery } from '../saved_object'; - -export async function listEnrollmentApiKeys( - soClient: SavedObjectsClientContract, - options: { - page?: number; - perPage?: number; - kuery?: string; - showInactive?: boolean; - } -): Promise<{ items: EnrollmentAPIKey[]; total: any; page: any; perPage: any }> { - const { page = 1, perPage = 20, kuery } = options; - - // eslint-disable-next-line @typescript-eslint/naming-convention - const { saved_objects, total } = await soClient.find({ - type: ENROLLMENT_API_KEYS_SAVED_OBJECT_TYPE, - page, - perPage, - sortField: 'created_at', - sortOrder: 'desc', - filter: - kuery && kuery !== '' - ? normalizeKuery(ENROLLMENT_API_KEYS_SAVED_OBJECT_TYPE, kuery) - : undefined, - }); - - const items = saved_objects.map(savedObjectToEnrollmentApiKey); - - return { - items, - total, - page, - perPage, - }; -} - -export async function getEnrollmentAPIKey(soClient: SavedObjectsClientContract, id: string) { - const so = await appContextService - .getEncryptedSavedObjects() - .getDecryptedAsInternalUser( - ENROLLMENT_API_KEYS_SAVED_OBJECT_TYPE, - id - ); - return savedObjectToEnrollmentApiKey(so); -} - -function savedObjectToEnrollmentApiKey({ - error, - attributes, - id, -}: SavedObject): EnrollmentAPIKey { - if (error) { - throw new Error(error.message); - } - - return { - id, - ...attributes, - }; -} diff --git a/x-pack/plugins/fleet/server/services/epm/packages/install.test.ts b/x-pack/plugins/fleet/server/services/epm/packages/install.test.ts new file mode 100644 index 0000000000000..a9bb235c22cb8 --- /dev/null +++ b/x-pack/plugins/fleet/server/services/epm/packages/install.test.ts @@ -0,0 +1,252 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { savedObjectsClientMock } from 'src/core/server/mocks'; + +import type { ElasticsearchClient } from 'kibana/server'; + +import * as Registry from '../registry'; + +import { sendTelemetryEvents } from '../../upgrade_sender'; + +import { licenseService } from '../../license'; + +import { installPackage } from './install'; +import * as install from './_install_package'; +import * as obj from './index'; + +jest.mock('../../app_context', () => { + return { + appContextService: { + getLogger: jest.fn(() => { + return { error: jest.fn(), debug: jest.fn(), warn: jest.fn() }; + }), + getTelemetryEventsSender: jest.fn(), + }, + }; +}); +jest.mock('./index'); +jest.mock('../registry'); +jest.mock('../../upgrade_sender'); +jest.mock('../../license'); +jest.mock('../../upgrade_sender'); +jest.mock('./cleanup'); +jest.mock('./_install_package', () => { + return { + _installPackage: jest.fn(() => Promise.resolve()), + }; +}); +jest.mock('../kibana/index_pattern/install', () => { + return { + installIndexPatterns: jest.fn(() => Promise.resolve()), + }; +}); +jest.mock('../archive', () => { + return { + parseAndVerifyArchiveEntries: jest.fn(() => + Promise.resolve({ packageInfo: { name: 'apache', version: '1.3.0' } }) + ), + unpackBufferToCache: jest.fn(), + setPackageInfo: jest.fn(), + }; +}); + +describe('install', () => { + beforeEach(() => { + jest.spyOn(Registry, 'splitPkgKey').mockImplementation((pkgKey: string) => { + const [pkgName, pkgVersion] = pkgKey.split('-'); + return { pkgName, pkgVersion }; + }); + jest + .spyOn(Registry, 'fetchFindLatestPackage') + .mockImplementation(() => Promise.resolve({ version: '1.3.0' } as any)); + jest + .spyOn(Registry, 'getRegistryPackage') + .mockImplementation(() => Promise.resolve({ packageInfo: { license: 'basic' } } as any)); + }); + + describe('registry', () => { + it('should send telemetry on install failure, out of date', async () => { + await installPackage({ + installSource: 'registry', + pkgkey: 'apache-1.1.0', + savedObjectsClient: savedObjectsClientMock.create(), + esClient: {} as ElasticsearchClient, + }); + + expect(sendTelemetryEvents).toHaveBeenCalledWith(expect.anything(), undefined, { + currentVersion: 'not_installed', + dryRun: false, + errorMessage: 'apache-1.1.0 is out-of-date and cannot be installed or updated', + eventType: 'package-install', + installType: 'install', + newVersion: '1.1.0', + packageName: 'apache', + status: 'failure', + }); + }); + + it('should send telemetry on install failure, license error', async () => { + jest.spyOn(licenseService, 'hasAtLeast').mockReturnValue(false); + await installPackage({ + installSource: 'registry', + pkgkey: 'apache-1.3.0', + savedObjectsClient: savedObjectsClientMock.create(), + esClient: {} as ElasticsearchClient, + }); + + expect(sendTelemetryEvents).toHaveBeenCalledWith(expect.anything(), undefined, { + currentVersion: 'not_installed', + dryRun: false, + errorMessage: 'Requires basic license', + eventType: 'package-install', + installType: 'install', + newVersion: '1.3.0', + packageName: 'apache', + status: 'failure', + }); + }); + + it('should send telemetry on install success', async () => { + jest.spyOn(licenseService, 'hasAtLeast').mockReturnValue(true); + await installPackage({ + installSource: 'registry', + pkgkey: 'apache-1.3.0', + savedObjectsClient: savedObjectsClientMock.create(), + esClient: {} as ElasticsearchClient, + }); + + expect(sendTelemetryEvents).toHaveBeenCalledWith(expect.anything(), undefined, { + currentVersion: 'not_installed', + dryRun: false, + eventType: 'package-install', + installType: 'install', + newVersion: '1.3.0', + packageName: 'apache', + status: 'success', + }); + }); + + it('should send telemetry on update success', async () => { + jest + .spyOn(obj, 'getInstallationObject') + .mockImplementationOnce(() => Promise.resolve({ attributes: { version: '1.2.0' } } as any)); + jest.spyOn(licenseService, 'hasAtLeast').mockReturnValue(true); + await installPackage({ + installSource: 'registry', + pkgkey: 'apache-1.3.0', + savedObjectsClient: savedObjectsClientMock.create(), + esClient: {} as ElasticsearchClient, + }); + + expect(sendTelemetryEvents).toHaveBeenCalledWith(expect.anything(), undefined, { + currentVersion: '1.2.0', + dryRun: false, + eventType: 'package-install', + installType: 'update', + newVersion: '1.3.0', + packageName: 'apache', + status: 'success', + }); + }); + + it('should send telemetry on install failure, async error', async () => { + jest + .spyOn(install, '_installPackage') + .mockImplementation(() => Promise.reject(new Error('error'))); + jest.spyOn(licenseService, 'hasAtLeast').mockReturnValue(true); + await installPackage({ + installSource: 'registry', + pkgkey: 'apache-1.3.0', + savedObjectsClient: savedObjectsClientMock.create(), + esClient: {} as ElasticsearchClient, + }); + + expect(sendTelemetryEvents).toHaveBeenCalledWith(expect.anything(), undefined, { + currentVersion: 'not_installed', + dryRun: false, + errorMessage: 'error', + eventType: 'package-install', + installType: 'install', + newVersion: '1.3.0', + packageName: 'apache', + status: 'failure', + }); + }); + }); + + describe('upload', () => { + it('should send telemetry on install failure', async () => { + jest + .spyOn(obj, 'getInstallationObject') + .mockImplementationOnce(() => Promise.resolve({ attributes: { version: '1.2.0' } } as any)); + await installPackage({ + installSource: 'upload', + archiveBuffer: {} as Buffer, + contentType: '', + savedObjectsClient: savedObjectsClientMock.create(), + esClient: {} as ElasticsearchClient, + }); + + expect(sendTelemetryEvents).toHaveBeenCalledWith(expect.anything(), undefined, { + currentVersion: '1.2.0', + dryRun: false, + errorMessage: + 'Package upload only supports fresh installations. Package apache is already installed, please uninstall first.', + eventType: 'package-install', + installType: 'update', + newVersion: '1.3.0', + packageName: 'apache', + status: 'failure', + }); + }); + + it('should send telemetry on install success', async () => { + await installPackage({ + installSource: 'upload', + archiveBuffer: {} as Buffer, + contentType: '', + savedObjectsClient: savedObjectsClientMock.create(), + esClient: {} as ElasticsearchClient, + }); + + expect(sendTelemetryEvents).toHaveBeenCalledWith(expect.anything(), undefined, { + currentVersion: 'not_installed', + dryRun: false, + eventType: 'package-install', + installType: 'install', + newVersion: '1.3.0', + packageName: 'apache', + status: 'success', + }); + }); + + it('should send telemetry on install failure, async error', async () => { + jest + .spyOn(install, '_installPackage') + .mockImplementation(() => Promise.reject(new Error('error'))); + await installPackage({ + installSource: 'upload', + archiveBuffer: {} as Buffer, + contentType: '', + savedObjectsClient: savedObjectsClientMock.create(), + esClient: {} as ElasticsearchClient, + }); + + expect(sendTelemetryEvents).toHaveBeenCalledWith(expect.anything(), undefined, { + currentVersion: 'not_installed', + dryRun: false, + errorMessage: 'error', + eventType: 'package-install', + installType: 'install', + newVersion: '1.3.0', + packageName: 'apache', + status: 'failure', + }); + }); + }); +}); diff --git a/x-pack/plugins/fleet/server/services/epm/packages/install.ts b/x-pack/plugins/fleet/server/services/epm/packages/install.ts index f57965614adc6..42f4663dc21e3 100644 --- a/x-pack/plugins/fleet/server/services/epm/packages/install.ts +++ b/x-pack/plugins/fleet/server/services/epm/packages/install.ts @@ -41,6 +41,9 @@ import { toAssetReference } from '../kibana/assets/install'; import type { ArchiveAsset } from '../kibana/assets/install'; import { installIndexPatterns } from '../kibana/index_pattern/install'; +import type { PackageUpdateEvent } from '../../upgrade_sender'; +import { sendTelemetryEvents, UpdateEventType } from '../../upgrade_sender'; + import { isUnremovablePackage, getInstallation, getInstallationObject } from './index'; import { removeInstallation } from './remove'; import { getPackageSavedObjects } from './get'; @@ -203,6 +206,26 @@ interface InstallRegistryPackageParams { force?: boolean; } +function getTelemetryEvent(pkgName: string, pkgVersion: string): PackageUpdateEvent { + return { + packageName: pkgName, + currentVersion: 'unknown', + newVersion: pkgVersion, + status: 'failure', + dryRun: false, + eventType: UpdateEventType.PACKAGE_INSTALL, + installType: 'unknown', + }; +} + +function sendEvent(telemetryEvent: PackageUpdateEvent) { + sendTelemetryEvents( + appContextService.getLogger(), + appContextService.getTelemetryEventsSender(), + telemetryEvent + ); +} + async function installPackageFromRegistry({ savedObjectsClient, pkgkey, @@ -216,6 +239,8 @@ async function installPackageFromRegistry({ // if an error happens during getInstallType, report that we don't know let installType: InstallType = 'unknown'; + const telemetryEvent: PackageUpdateEvent = getTelemetryEvent(pkgName, pkgVersion); + try { // get the currently installed package const installedPkg = await getInstallationObject({ savedObjectsClient, pkgName }); @@ -248,6 +273,9 @@ async function installPackageFromRegistry({ } } + telemetryEvent.installType = installType; + telemetryEvent.currentVersion = installedPkg?.attributes.version || 'not_installed'; + // if the requested version is out-of-date of the latest package version, check if we allow it // if we don't allow it, return an error if (semverLt(pkgVersion, latestPackage.version)) { @@ -267,7 +295,12 @@ async function installPackageFromRegistry({ const { paths, packageInfo } = await Registry.getRegistryPackage(pkgName, pkgVersion); if (!licenseService.hasAtLeast(packageInfo.license || 'basic')) { - return { error: new Error(`Requires ${packageInfo.license} license`), installType }; + const err = new Error(`Requires ${packageInfo.license} license`); + sendEvent({ + ...telemetryEvent, + errorMessage: err.message, + }); + return { error: err, installType }; } // try installing the package, if there was an error, call error handler and rethrow @@ -287,6 +320,10 @@ async function installPackageFromRegistry({ pkgName: packageInfo.name, currentVersion: packageInfo.version, }); + sendEvent({ + ...telemetryEvent, + status: 'success', + }); return { assets, status: 'installed', installType }; }) .catch(async (err: Error) => { @@ -299,9 +336,17 @@ async function installPackageFromRegistry({ installedPkg, esClient, }); + sendEvent({ + ...telemetryEvent, + errorMessage: err.message, + }); return { error: err, installType }; }); } catch (e) { + sendEvent({ + ...telemetryEvent, + errorMessage: e.message, + }); return { error: e, installType, @@ -324,6 +369,7 @@ async function installPackageByUpload({ }: InstallUploadedArchiveParams): Promise { // if an error happens during getInstallType, report that we don't know let installType: InstallType = 'unknown'; + const telemetryEvent: PackageUpdateEvent = getTelemetryEvent('', ''); try { const { packageInfo } = await parseAndVerifyArchiveEntries(archiveBuffer, contentType); @@ -333,6 +379,12 @@ async function installPackageByUpload({ }); installType = getInstallType({ pkgVersion: packageInfo.version, installedPkg }); + + telemetryEvent.packageName = packageInfo.name; + telemetryEvent.newVersion = packageInfo.version; + telemetryEvent.installType = installType; + telemetryEvent.currentVersion = installedPkg?.attributes.version || 'not_installed'; + if (installType !== 'install') { throw new PackageOperationNotSupportedError( `Package upload only supports fresh installations. Package ${packageInfo.name} is already installed, please uninstall first.` @@ -364,12 +416,24 @@ async function installPackageByUpload({ installSource, }) .then((assets) => { + sendEvent({ + ...telemetryEvent, + status: 'success', + }); return { assets, status: 'installed', installType }; }) .catch(async (err: Error) => { + sendEvent({ + ...telemetryEvent, + errorMessage: err.message, + }); return { error: err, installType }; }); } catch (e) { + sendEvent({ + ...telemetryEvent, + errorMessage: e.message, + }); return { error: e, installType }; } } diff --git a/x-pack/plugins/fleet/server/services/fleet_server/index.ts b/x-pack/plugins/fleet/server/services/fleet_server/index.ts index 0d386b9ba4995..55b0fb0dff225 100644 --- a/x-pack/plugins/fleet/server/services/fleet_server/index.ts +++ b/x-pack/plugins/fleet/server/services/fleet_server/index.ts @@ -6,23 +6,9 @@ */ import type { ElasticsearchClient } from 'kibana/server'; -import { first } from 'rxjs/operators'; -import { appContextService } from '../app_context'; -import { licenseService } from '../license'; import { FLEET_SERVER_SERVERS_INDEX } from '../../constants'; -import { runFleetServerMigration } from './saved_object_migrations'; - -let _isFleetServerSetup = false; -let _isPending = false; -let _status: Promise | undefined; -let _onResolve: (arg?: any) => void; - -export function isFleetServerSetup() { - return _isFleetServerSetup; -} - /** * Check if at least one fleet server is connected */ @@ -35,48 +21,3 @@ export async function hasFleetServers(esClient: ElasticsearchClient) { // @ts-expect-error value is number | TotalHits return res.body.hits.total.value > 0; } - -export async function awaitIfFleetServerSetupPending() { - if (!_isPending) { - return; - } - - return _status; -} - -export async function startFleetServerSetup() { - _isPending = true; - _status = new Promise((resolve) => { - _onResolve = resolve; - }); - const logger = appContextService.getLogger(); - - // Check for security - if (!appContextService.hasSecurity()) { - // Fleet will not work if security is not enabled - logger?.warn('Fleet requires the security plugin to be enabled.'); - return; - } - - // Log information about custom registry URL - const customUrl = appContextService.getConfig()?.registryUrl; - if (customUrl) { - logger.info( - `Custom registry url is an experimental feature and is unsupported. Using custom registry at ${customUrl}` - ); - } - - try { - // We need licence to be initialized before using the SO service. - await licenseService.getLicenseInformation$()?.pipe(first())?.toPromise(); - await runFleetServerMigration(); - _isFleetServerSetup = true; - } catch (err) { - logger?.error('Setup for central management of agents failed.'); - logger?.error(err); - } - _isPending = false; - if (_onResolve) { - _onResolve(); - } -} 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 deleted file mode 100644 index bbaf9c9479eb4..0000000000000 --- a/x-pack/plugins/fleet/server/services/fleet_server/saved_object_migrations.ts +++ /dev/null @@ -1,201 +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 { isBoom } from '@hapi/boom'; -import type { KibanaRequest } from 'src/core/server'; - -import { - ENROLLMENT_API_KEYS_INDEX, - ENROLLMENT_API_KEYS_SAVED_OBJECT_TYPE, - AGENT_POLICY_INDEX, - AGENTS_INDEX, - AGENT_SAVED_OBJECT_TYPE, - SO_SEARCH_LIMIT, -} from '../../../common'; -import type { - FleetServerEnrollmentAPIKey, - AgentSOAttributes, - FleetServerAgent, -} from '../../../common'; -import { listEnrollmentApiKeys, getEnrollmentAPIKey } from '../api_keys/enrollment_api_key_so'; -import { appContextService } from '../app_context'; -import { agentPolicyService } from '../agent_policy'; -import { invalidateAPIKeys } from '../api_keys'; -import { settingsService } from '..'; - -export async function runFleetServerMigration() { - await settingsService.settingsSetup(getInternalUserSOClient()); - await Promise.all([migrateEnrollmentApiKeys(), migrateAgentPolicies(), migrateAgents()]); -} - -function getInternalUserSOClient() { - const fakeRequest = { - headers: {}, - getBasePath: () => '', - path: '/', - route: { settings: {} }, - url: { - href: '/', - }, - raw: { - req: { - url: '/', - }, - }, - } as unknown as KibanaRequest; - - return appContextService.getInternalUserSOClient(fakeRequest); -} - -async function migrateAgents() { - const esClient = appContextService.getInternalUserESClient(); - const soClient = getInternalUserSOClient(); - const logger = appContextService.getLogger(); - let hasMore = true; - - let hasAgents = false; - - while (hasMore) { - const res = await soClient.find({ - type: AGENT_SAVED_OBJECT_TYPE, - page: 1, - perPage: 100, - }); - - if (res.total === 0) { - hasMore = false; - } else { - hasAgents = true; - } - - for (const so of res.saved_objects) { - try { - const { attributes } = await appContextService - .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: false, - enrolled_at: attributes.enrolled_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, - access_api_key_id: attributes.access_api_key_id, - user_provided_metadata: attributes.user_provided_metadata, - local_metadata: attributes.local_metadata, - policy_id: attributes.policy_id, - policy_revision_idx: attributes.policy_revision || undefined, - last_checkin: attributes.last_checkin, - last_checkin_status: attributes.last_checkin_status, - default_api_key_id: attributes.default_api_key_id, - default_api_key: attributes.default_api_key, - packages: attributes.packages, - }; - await esClient.create({ - index: AGENTS_INDEX, - body, - id: so.id, - refresh: 'wait_for', - }); - - await soClient.delete(AGENT_SAVED_OBJECT_TYPE, so.id); - } catch (error) { - // swallow 404 error has multiple Kibana can run the migration at the same time - if (!is404Error(error)) { - throw error; - } - } - } - } - - // Update settings to show migration modal - if (hasAgents) { - await settingsService.saveSettings(soClient, { - has_seen_fleet_migration_notice: false, - }); - } -} - -async function migrateEnrollmentApiKeys() { - const esClient = appContextService.getInternalUserESClient(); - const soClient = getInternalUserSOClient(); - let hasMore = true; - while (hasMore) { - const res = await listEnrollmentApiKeys(soClient, { - page: 1, - perPage: 100, - }); - if (res.total === 0) { - hasMore = false; - } - for (const item of res.items) { - try { - const key = await getEnrollmentAPIKey(soClient, item.id); - - const body: FleetServerEnrollmentAPIKey = { - api_key: key.api_key, - api_key_id: key.api_key_id, - active: key.active, - created_at: key.created_at, - name: key.name, - policy_id: key.policy_id, - }; - await esClient.create({ - index: ENROLLMENT_API_KEYS_INDEX, - body, - id: key.id, - refresh: 'wait_for', - }); - - await soClient.delete(ENROLLMENT_API_KEYS_SAVED_OBJECT_TYPE, key.id); - } catch (error) { - // swallow 404 error has multiple Kibana can run the migration at the same time - if (!is404Error(error)) { - throw error; - } - } - } - } -} - -async function migrateAgentPolicies() { - const esClient = appContextService.getInternalUserESClient(); - const soClient = getInternalUserSOClient(); - const { items: agentPolicies } = await agentPolicyService.list(soClient, { - perPage: SO_SEARCH_LIMIT, - }); - - await Promise.all( - agentPolicies.map(async (agentPolicy) => { - const res = await esClient.search({ - index: AGENT_POLICY_INDEX, - q: `policy_id:${agentPolicy.id}`, - track_total_hits: true, - ignore_unavailable: true, - }); - - // @ts-expect-error value is number | TotalHits - if (res.body.hits.total.value === 0) { - return agentPolicyService.createFleetServerPolicy(soClient, agentPolicy.id); - } - }) - ); -} - -function is404Error(error: any) { - return isBoom(error) && error.output.statusCode === 404; -} diff --git a/x-pack/plugins/fleet/server/services/managed_package_policies.ts b/x-pack/plugins/fleet/server/services/managed_package_policies.ts index 306725ae01953..e78bc096b8711 100644 --- a/x-pack/plugins/fleet/server/services/managed_package_policies.ts +++ b/x-pack/plugins/fleet/server/services/managed_package_policies.ts @@ -72,7 +72,10 @@ export const upgradeManagedPackagePolicies = async ( ); if (dryRunResults.hasErrors) { - const errors = dryRunResults.diff?.[1].errors; + const errors = dryRunResults.diff + ? dryRunResults.diff?.[1].errors + : dryRunResults.body?.message; + appContextService .getLogger() .error( diff --git a/x-pack/plugins/fleet/server/services/output.test.ts b/x-pack/plugins/fleet/server/services/output.test.ts index 8103794fb0805..23ee77e0f28c2 100644 --- a/x-pack/plugins/fleet/server/services/output.test.ts +++ b/x-pack/plugins/fleet/server/services/output.test.ts @@ -36,23 +36,109 @@ const CONFIG_WITHOUT_ES_HOSTS = { }, }; -function getMockedSoClient() { +function mockOutputSO(id: string, attributes: any = {}) { + return { + id: outputIdToUuid(id), + type: 'ingest-outputs', + references: [], + attributes: { + output_id: id, + ...attributes, + }, + }; +} + +function getMockedSoClient( + options: { defaultOutputId?: string; defaultOutputMonitoringId?: string } = {} +) { const soClient = savedObjectsClientMock.create(); + soClient.get.mockImplementation(async (type: string, id: string) => { switch (id) { case outputIdToUuid('output-test'): { - return { - id: outputIdToUuid('output-test'), - type: 'ingest-outputs', - references: [], - attributes: { - output_id: 'output-test', - }, - }; + return mockOutputSO('output-test'); + } + case outputIdToUuid('existing-default-output'): { + return mockOutputSO('existing-default-output'); } + case outputIdToUuid('existing-default-monitoring-output'): { + return mockOutputSO('existing-default-monitoring-output', { is_default: true }); + } + case outputIdToUuid('existing-preconfigured-default-output'): { + return mockOutputSO('existing-preconfigured-default-output', { + is_default: true, + is_preconfigured: true, + }); + } + default: - throw new Error('not found'); + throw new Error('not found: ' + id); + } + }); + soClient.update.mockImplementation(async (type, id, data) => { + return { + id, + type, + attributes: {}, + references: [], + }; + }); + soClient.create.mockImplementation(async (type, data, createOptions) => { + return { + id: createOptions?.id || 'generated-id', + type, + attributes: {}, + references: [], + }; + }); + soClient.find.mockImplementation(async (findOptions) => { + if ( + options?.defaultOutputMonitoringId && + findOptions.searchFields && + findOptions.searchFields.includes('is_default_monitoring') && + findOptions.search === 'true' + ) { + return { + page: 1, + per_page: 10, + saved_objects: [ + { + score: 0, + ...(await soClient.get( + 'ingest-outputs', + outputIdToUuid(options.defaultOutputMonitoringId) + )), + }, + ], + total: 1, + }; + } + + if ( + options?.defaultOutputId && + findOptions.searchFields && + findOptions.searchFields.includes('is_default') && + findOptions.search === 'true' + ) { + return { + page: 1, + per_page: 10, + saved_objects: [ + { + score: 0, + ...(await soClient.get('ingest-outputs', outputIdToUuid(options.defaultOutputId))), + }, + ], + total: 1, + }; } + + return { + page: 1, + per_page: 10, + saved_objects: [], + total: 0, + }; }); return soClient; @@ -62,16 +148,12 @@ describe('Output Service', () => { describe('create', () => { it('work with a predefined id', async () => { const soClient = getMockedSoClient(); - soClient.create.mockResolvedValue({ - id: outputIdToUuid('output-test'), - type: 'ingest-output', - attributes: {}, - references: [], - }); + await outputService.create( soClient, { is_default: false, + is_default_monitoring: false, name: 'Test', type: 'elasticsearch', }, @@ -86,6 +168,285 @@ describe('Output Service', () => { 'output-test' ); }); + + it('should create a new default output if none exists before', async () => { + const soClient = getMockedSoClient(); + + await outputService.create( + soClient, + { + is_default: true, + is_default_monitoring: false, + name: 'Test', + type: 'elasticsearch', + }, + { id: 'output-test' } + ); + + expect(soClient.update).not.toBeCalled(); + }); + + it('should update existing default output when creating a new default output', async () => { + const soClient = getMockedSoClient({ + defaultOutputId: 'existing-default-output', + }); + + await outputService.create( + soClient, + { + is_default: true, + is_default_monitoring: false, + name: 'Test', + type: 'elasticsearch', + }, + { id: 'output-test' } + ); + + expect(soClient.update).toBeCalledTimes(1); + expect(soClient.update).toBeCalledWith( + expect.anything(), + outputIdToUuid('existing-default-output'), + { is_default: false } + ); + }); + + it('should create a new default monitoring output if none exists before', async () => { + const soClient = getMockedSoClient(); + + await outputService.create( + soClient, + { + is_default: false, + is_default_monitoring: true, + name: 'Test', + type: 'elasticsearch', + }, + { id: 'output-test' } + ); + + expect(soClient.update).not.toBeCalled(); + }); + + it('should update existing default monitoring output when creating a new default output', async () => { + const soClient = getMockedSoClient({ + defaultOutputMonitoringId: 'existing-default-monitoring-output', + }); + + await outputService.create( + soClient, + { + is_default: true, + is_default_monitoring: true, + name: 'Test', + type: 'elasticsearch', + }, + { id: 'output-test' } + ); + + expect(soClient.update).toBeCalledTimes(1); + expect(soClient.update).toBeCalledWith( + expect.anything(), + outputIdToUuid('existing-default-monitoring-output'), + { is_default_monitoring: false } + ); + }); + + // With preconfigured outputs + it('should throw when an existing preconfigured default output and creating a new default output outside of preconfiguration', async () => { + const soClient = getMockedSoClient({ + defaultOutputId: 'existing-preconfigured-default-output', + }); + + await expect( + outputService.create( + soClient, + { + is_default: true, + is_default_monitoring: false, + name: 'Test', + type: 'elasticsearch', + }, + { id: 'output-test' } + ) + ).rejects.toThrow( + `Preconfigured output existing-preconfigured-default-output cannot be updated outside of kibana config file.` + ); + }); + + it('should update existing default preconfigured monitoring output when creating a new default output from preconfiguration', async () => { + const soClient = getMockedSoClient({ + defaultOutputId: 'existing-preconfigured-default-output', + }); + + await outputService.create( + soClient, + { + is_default: true, + is_default_monitoring: true, + name: 'Test', + type: 'elasticsearch', + }, + { id: 'output-test', fromPreconfiguration: true } + ); + + expect(soClient.update).toBeCalledTimes(1); + expect(soClient.update).toBeCalledWith( + expect.anything(), + outputIdToUuid('existing-preconfigured-default-output'), + { is_default: false } + ); + }); + }); + + describe('update', () => { + it('should update existing default output when updating an output to become the default output', async () => { + const soClient = getMockedSoClient({ + defaultOutputId: 'existing-default-output', + }); + + await outputService.update(soClient, 'output-test', { + is_default: true, + }); + + expect(soClient.update).toBeCalledTimes(2); + expect(soClient.update).toBeCalledWith(expect.anything(), outputIdToUuid('output-test'), { + is_default: true, + }); + expect(soClient.update).toBeCalledWith( + expect.anything(), + outputIdToUuid('existing-default-output'), + { is_default: false } + ); + }); + + it('should not update existing default output when the output is already the default one', async () => { + const soClient = getMockedSoClient({ + defaultOutputId: 'existing-default-output', + }); + + await outputService.update(soClient, 'existing-default-output', { + is_default: true, + name: 'Test', + }); + + expect(soClient.update).toBeCalledTimes(1); + expect(soClient.update).toBeCalledWith( + expect.anything(), + outputIdToUuid('existing-default-output'), + { is_default: true, name: 'Test' } + ); + }); + + it('should update existing default monitoring output when updating an output to become the default monitoring output', async () => { + const soClient = getMockedSoClient({ + defaultOutputMonitoringId: 'existing-default-monitoring-output', + }); + + await outputService.update(soClient, 'output-test', { + is_default_monitoring: true, + }); + + expect(soClient.update).toBeCalledTimes(2); + expect(soClient.update).toBeCalledWith(expect.anything(), outputIdToUuid('output-test'), { + is_default_monitoring: true, + }); + expect(soClient.update).toBeCalledWith( + expect.anything(), + outputIdToUuid('existing-default-monitoring-output'), + { is_default_monitoring: false } + ); + }); + + // With preconfigured outputs + it('Do not allow to update a preconfigured output outisde from preconfiguration', async () => { + const soClient = getMockedSoClient(); + await expect( + outputService.update(soClient, 'existing-preconfigured-default-output', { + config_yaml: '', + }) + ).rejects.toThrow( + 'Preconfigured output existing-preconfigured-default-output cannot be updated outside of kibana config file.' + ); + }); + + it('Allow to update a preconfigured output from preconfiguration', async () => { + const soClient = getMockedSoClient(); + await outputService.update( + soClient, + 'existing-preconfigured-default-output', + { + config_yaml: '', + }, + { + fromPreconfiguration: true, + } + ); + + expect(soClient.update).toBeCalled(); + }); + + it('Should throw when an existing preconfigured default output and updating an output to become the default one outside of preconfiguration', async () => { + const soClient = getMockedSoClient({ + defaultOutputId: 'existing-preconfigured-default-output', + }); + + await expect( + outputService.update(soClient, 'output-test', { + is_default: true, + is_default_monitoring: false, + name: 'Test', + type: 'elasticsearch', + }) + ).rejects.toThrow( + `Preconfigured output existing-preconfigured-default-output cannot be updated outside of kibana config file.` + ); + }); + + it('Should update existing default preconfigured monitoring output when updating an output to become the default one from preconfiguration', async () => { + const soClient = getMockedSoClient({ + defaultOutputId: 'existing-default-output', + }); + + await outputService.update( + soClient, + 'output-test', + { + is_default: true, + is_default_monitoring: false, + name: 'Test', + type: 'elasticsearch', + }, + { fromPreconfiguration: true } + ); + + expect(soClient.update).toBeCalledTimes(2); + expect(soClient.update).toBeCalledWith( + expect.anything(), + outputIdToUuid('existing-default-output'), + { is_default: false } + ); + }); + }); + + describe('delete', () => { + // Preconfigured output + it('Do not allow to delete a preconfigured output outisde from preconfiguration', async () => { + const soClient = getMockedSoClient(); + await expect( + outputService.delete(soClient, 'existing-preconfigured-default-output') + ).rejects.toThrow( + 'Preconfigured output existing-preconfigured-default-output cannot be deleted outside of kibana config file.' + ); + }); + + it('Allow to delete a preconfigured output from preconfiguration', async () => { + const soClient = getMockedSoClient(); + await outputService.delete(soClient, 'existing-preconfigured-default-output', { + fromPreconfiguration: true, + }); + + expect(soClient.delete).toBeCalled(); + }); }); describe('get', () => { @@ -99,27 +460,25 @@ describe('Output Service', () => { }); }); - describe('getDefaultOutputId', () => { + describe('getDefaultDataOutputId', () => { it('work with a predefined id', async () => { - const soClient = getMockedSoClient(); - soClient.find.mockResolvedValue({ - page: 1, - per_page: 100, - total: 1, - saved_objects: [ - { - id: outputIdToUuid('output-test'), - type: 'ingest-outputs', - references: [], - score: 0, - attributes: { - output_id: 'output-test', - is_default: true, - }, - }, - ], + const soClient = getMockedSoClient({ + defaultOutputId: 'output-test', + }); + const defaultId = await outputService.getDefaultDataOutputId(soClient); + + expect(soClient.find).toHaveBeenCalled(); + + expect(defaultId).toEqual('output-test'); + }); + }); + + describe('getDefaultMonitoringOutputOd', () => { + it('work with a predefined id', async () => { + const soClient = getMockedSoClient({ + defaultOutputMonitoringId: 'output-test', }); - const defaultId = await outputService.getDefaultOutputId(soClient); + const defaultId = await outputService.getDefaultMonitoringOutputId(soClient); expect(soClient.find).toHaveBeenCalled(); diff --git a/x-pack/plugins/fleet/server/services/output.ts b/x-pack/plugins/fleet/server/services/output.ts index 5a7ba1e2c1223..e39f70671a232 100644 --- a/x-pack/plugins/fleet/server/services/output.ts +++ b/x-pack/plugins/fleet/server/services/output.ts @@ -10,7 +10,7 @@ import uuid from 'uuid/v5'; import type { NewOutput, Output, OutputSOAttributes } from '../types'; import { DEFAULT_OUTPUT, OUTPUT_SAVED_OBJECT_TYPE } from '../constants'; -import { decodeCloudId, normalizeHostsForAgents } from '../../common'; +import { decodeCloudId, normalizeHostsForAgents, SO_SEARCH_LIMIT } from '../../common'; import { appContextService } from './app_context'; @@ -44,7 +44,7 @@ function outputSavedObjectToOutput(so: SavedObject) { } class OutputService { - private async _getDefaultOutputsSO(soClient: SavedObjectsClientContract) { + private async _getDefaultDataOutputsSO(soClient: SavedObjectsClientContract) { return await soClient.find({ type: OUTPUT_SAVED_OBJECT_TYPE, searchFields: ['is_default'], @@ -52,20 +52,32 @@ class OutputService { }); } + private async _getDefaultMonitoringOutputsSO(soClient: SavedObjectsClientContract) { + return await soClient.find({ + type: OUTPUT_SAVED_OBJECT_TYPE, + searchFields: ['is_default_monitoring'], + search: 'true', + }); + } + public async ensureDefaultOutput(soClient: SavedObjectsClientContract) { - const outputs = await this._getDefaultOutputsSO(soClient); + const outputs = await this.list(soClient); - if (!outputs.saved_objects.length) { + const defaultOutput = outputs.items.find((o) => o.is_default); + const defaultMonitoringOutput = outputs.items.find((o) => o.is_default_monitoring); + + if (!defaultOutput) { const newDefaultOutput = { ...DEFAULT_OUTPUT, hosts: this.getDefaultESHosts(), ca_sha256: appContextService.getConfig()!.agents.elasticsearch.ca_sha256, + is_default_monitoring: !defaultMonitoringOutput, } as NewOutput; return await this.create(soClient, newDefaultOutput); } - return outputSavedObjectToOutput(outputs.saved_objects[0]); + return defaultOutput; } public getDefaultESHosts(): string[] { @@ -82,8 +94,18 @@ class OutputService { return cloudHosts || flagHosts || DEFAULT_ES_HOSTS; } - public async getDefaultOutputId(soClient: SavedObjectsClientContract) { - const outputs = await this._getDefaultOutputsSO(soClient); + public async getDefaultDataOutputId(soClient: SavedObjectsClientContract) { + const outputs = await this._getDefaultDataOutputsSO(soClient); + + if (!outputs.saved_objects.length) { + return null; + } + + return outputSavedObjectToOutput(outputs.saved_objects[0]).id; + } + + public async getDefaultMonitoringOutputId(soClient: SavedObjectsClientContract) { + const outputs = await this._getDefaultMonitoringOutputsSO(soClient); if (!outputs.saved_objects.length) { return null; @@ -95,15 +117,31 @@ class OutputService { public async create( soClient: SavedObjectsClientContract, output: NewOutput, - options?: { id?: string; overwrite?: boolean } + options?: { id?: string; fromPreconfiguration?: boolean } ): Promise { const data: OutputSOAttributes = { ...output }; // ensure only default output exists if (data.is_default) { - const defaultOuput = await this.getDefaultOutputId(soClient); - if (defaultOuput) { - throw new Error(`A default output already exists (${defaultOuput})`); + const defaultDataOuputId = await this.getDefaultDataOutputId(soClient); + if (defaultDataOuputId) { + await this.update( + soClient, + defaultDataOuputId, + { is_default: false }, + { fromPreconfiguration: options?.fromPreconfiguration ?? false } + ); + } + } + if (data.is_default_monitoring) { + const defaultMonitoringOutputId = await this.getDefaultMonitoringOutputId(soClient); + if (defaultMonitoringOutputId) { + await this.update( + soClient, + defaultMonitoringOutputId, + { is_default_monitoring: false }, + { fromPreconfiguration: options?.fromPreconfiguration ?? false } + ); } } @@ -116,7 +154,7 @@ class OutputService { } const newSo = await soClient.create(SAVED_OBJECT_TYPE, data, { - ...options, + overwrite: options?.fromPreconfiguration, id: options?.id ? outputIdToUuid(options.id) : undefined, }); @@ -149,6 +187,21 @@ class OutputService { .filter((output): output is Output => typeof output !== 'undefined'); } + public async list(soClient: SavedObjectsClientContract) { + const outputs = await soClient.find({ + type: SAVED_OBJECT_TYPE, + page: 1, + perPage: SO_SEARCH_LIMIT, + }); + + return { + items: outputs.saved_objects.map(outputSavedObjectToOutput), + total: outputs.total, + page: outputs.page, + perPage: outputs.per_page, + }; + } + public async get(soClient: SavedObjectsClientContract, id: string): Promise { const outputSO = await soClient.get(SAVED_OBJECT_TYPE, outputIdToUuid(id)); @@ -159,13 +212,66 @@ class OutputService { return outputSavedObjectToOutput(outputSO); } - public async delete(soClient: SavedObjectsClientContract, id: string) { + public async delete( + soClient: SavedObjectsClientContract, + id: string, + { fromPreconfiguration = false }: { fromPreconfiguration?: boolean } = { + fromPreconfiguration: false, + } + ) { + const originalOutput = await this.get(soClient, id); + + if (originalOutput.is_preconfigured && !fromPreconfiguration) { + throw new Error( + `Preconfigured output ${id} cannot be deleted outside of kibana config file.` + ); + } return soClient.delete(SAVED_OBJECT_TYPE, outputIdToUuid(id)); } - public async update(soClient: SavedObjectsClientContract, id: string, data: Partial) { + public async update( + soClient: SavedObjectsClientContract, + id: string, + data: Partial, + { fromPreconfiguration = false }: { fromPreconfiguration: boolean } = { + fromPreconfiguration: false, + } + ) { + const originalOutput = await this.get(soClient, id); + + if (originalOutput.is_preconfigured && !fromPreconfiguration) { + throw new Error( + `Preconfigured output ${id} cannot be updated outside of kibana config file.` + ); + } + const updateData = { ...data }; + // ensure only default output exists + if (data.is_default) { + const defaultDataOuputId = await this.getDefaultDataOutputId(soClient); + if (defaultDataOuputId && defaultDataOuputId !== id) { + await this.update( + soClient, + defaultDataOuputId, + { is_default: false }, + { fromPreconfiguration } + ); + } + } + if (data.is_default_monitoring) { + const defaultMonitoringOutputId = await this.getDefaultMonitoringOutputId(soClient); + + if (defaultMonitoringOutputId && defaultMonitoringOutputId !== id) { + await this.update( + soClient, + defaultMonitoringOutputId, + { is_default_monitoring: false }, + { fromPreconfiguration } + ); + } + } + if (updateData.hosts) { updateData.hosts = updateData.hosts.map(normalizeHostsForAgents); } @@ -179,21 +285,6 @@ class OutputService { throw new Error(outputSO.error.message); } } - - public async list(soClient: SavedObjectsClientContract) { - const outputs = await soClient.find({ - type: SAVED_OBJECT_TYPE, - page: 1, - perPage: 1000, - }); - - return { - items: outputs.saved_objects.map(outputSavedObjectToOutput), - total: outputs.total, - page: 1, - perPage: 1000, - }; - } } export const outputService = new OutputService(); diff --git a/x-pack/plugins/fleet/server/services/package_policy.test.ts b/x-pack/plugins/fleet/server/services/package_policy.test.ts index dcc00251e70f4..36976bea4a970 100644 --- a/x-pack/plugins/fleet/server/services/package_policy.test.ts +++ b/x-pack/plugins/fleet/server/services/package_policy.test.ts @@ -134,7 +134,7 @@ jest.mock('./epm/packages/cleanup', () => { }; }); -jest.mock('./upgrade_usage', () => { +jest.mock('./upgrade_sender', () => { return { sendTelemetryEvents: jest.fn(), }; diff --git a/x-pack/plugins/fleet/server/services/package_policy.ts b/x-pack/plugins/fleet/server/services/package_policy.ts index 985351c3e981b..856bf077b33d3 100644 --- a/x-pack/plugins/fleet/server/services/package_policy.ts +++ b/x-pack/plugins/fleet/server/services/package_policy.ts @@ -67,8 +67,8 @@ import { compileTemplate } from './epm/agent/agent'; import { normalizeKuery } from './saved_object'; import { appContextService } from '.'; import { removeOldAssets } from './epm/packages/cleanup'; -import type { PackagePolicyUpgradeUsage } from './upgrade_usage'; -import { sendTelemetryEvents } from './upgrade_usage'; +import type { PackageUpdateEvent, UpdateEventType } from './upgrade_sender'; +import { sendTelemetryEvents } from './upgrade_sender'; export type InputsOverride = Partial & { vars?: Array; @@ -423,12 +423,13 @@ class PackagePolicyService { }); if (packagePolicy.package.version !== currentVersion) { - const upgradeTelemetry: PackagePolicyUpgradeUsage = { - package_name: packagePolicy.package.name, - current_version: currentVersion || 'unknown', - new_version: packagePolicy.package.version, + const upgradeTelemetry: PackageUpdateEvent = { + packageName: packagePolicy.package.name, + currentVersion: currentVersion || 'unknown', + newVersion: packagePolicy.package.version, status: 'success', dryRun: false, + eventType: 'package-policy-upgrade' as UpdateEventType, }; sendTelemetryEvents( appContextService.getLogger(), @@ -620,13 +621,6 @@ class PackagePolicyService { success: true, }); } catch (error) { - // We only want to specifically handle validation errors for the new package policy. If a more severe or - // general error is thrown elsewhere during the upgrade process, we want to surface that directly in - // order to preserve any status code mappings, etc that might be included w/ the particular error type - if (!(error instanceof PackagePolicyValidationError)) { - throw error; - } - result.push({ id, success: false, @@ -675,13 +669,14 @@ class PackagePolicyService { const hasErrors = 'errors' in updatedPackagePolicy; if (packagePolicy.package.version !== packageInfo.version) { - const upgradeTelemetry: PackagePolicyUpgradeUsage = { - package_name: packageInfo.name, - current_version: packagePolicy.package.version, - new_version: packageInfo.version, + const upgradeTelemetry: PackageUpdateEvent = { + packageName: packageInfo.name, + currentVersion: packagePolicy.package.version, + newVersion: packageInfo.version, status: hasErrors ? 'failure' : 'success', error: hasErrors ? updatedPackagePolicy.errors : undefined, dryRun: true, + eventType: 'package-policy-upgrade' as UpdateEventType, }; sendTelemetryEvents( appContextService.getLogger(), @@ -704,10 +699,6 @@ class PackagePolicyService { hasErrors, }; } catch (error) { - if (!(error instanceof PackagePolicyValidationError)) { - throw error; - } - return { hasErrors: true, ...ingestErrorToResponseOptions(error), @@ -727,7 +718,7 @@ class PackagePolicyService { pkgName: pkgInstall.name, pkgVersion: pkgInstall.version, }), - outputService.getDefaultOutputId(soClient), + outputService.getDefaultDataOutputId(soClient), ]); if (packageInfo) { if (!defaultOutputId) { @@ -1204,3 +1195,30 @@ function deepMergeVars(original: any, override: any): any { return result; } + +export async function incrementPackageName( + soClient: SavedObjectsClientContract, + packageName: string +) { + // Fetch all packagePolicies having the package name + const packagePolicyData = await packagePolicyService.list(soClient, { + perPage: 1, + kuery: `${PACKAGE_POLICY_SAVED_OBJECT_TYPE}.package.name: "${packageName}"`, + }); + + // Retrieve highest number appended to package policy name and increment it by one + const pkgPoliciesNamePattern = new RegExp(`${packageName}-(\\d+)`); + + const pkgPoliciesWithMatchingNames = packagePolicyData?.items + ? packagePolicyData.items + .filter((ds) => Boolean(ds.name.match(pkgPoliciesNamePattern))) + .map((ds) => parseInt(ds.name.match(pkgPoliciesNamePattern)![1], 10)) + .sort() + : []; + + return `${packageName}-${ + pkgPoliciesWithMatchingNames.length + ? pkgPoliciesWithMatchingNames[pkgPoliciesWithMatchingNames.length - 1] + 1 + : 1 + }`; +} diff --git a/x-pack/plugins/fleet/server/services/preconfiguration.test.ts b/x-pack/plugins/fleet/server/services/preconfiguration.test.ts index 2899c327e8d2b..6fefc4631239d 100644 --- a/x-pack/plugins/fleet/server/services/preconfiguration.test.ts +++ b/x-pack/plugins/fleet/server/services/preconfiguration.test.ts @@ -40,6 +40,7 @@ const mockConfiguredPolicies = new Map(); const mockDefaultOutput: Output = { id: 'test-id', is_default: true, + is_default_monitoring: false, name: 'default', // @ts-ignore type: 'elasticsearch', @@ -547,17 +548,6 @@ describe('comparePreconfiguredPolicyToCurrent', () => { ); expect(hasChanged).toBe(false); }); - - it('should not return hasChanged when only namespace field changes', () => { - const { hasChanged } = comparePreconfiguredPolicyToCurrent( - { - ...baseConfig, - namespace: 'newnamespace', - }, - basePackagePolicy - ); - expect(hasChanged).toBe(false); - }); }); describe('output preconfiguration', () => { @@ -565,13 +555,14 @@ describe('output preconfiguration', () => { mockedOutputService.create.mockReset(); mockedOutputService.update.mockReset(); mockedOutputService.delete.mockReset(); - mockedOutputService.getDefaultOutputId.mockReset(); + mockedOutputService.getDefaultDataOutputId.mockReset(); mockedOutputService.getDefaultESHosts.mockReturnValue(['http://default-es:9200']); mockedOutputService.bulkGet.mockImplementation(async (soClient, id): Promise => { return [ { id: 'existing-output-1', is_default: false, + is_default_monitoring: false, name: 'Output 1', // @ts-ignore type: 'elasticsearch', @@ -591,6 +582,7 @@ describe('output preconfiguration', () => { name: 'Output 1', type: 'elasticsearch', is_default: false, + is_default_monitoring: false, hosts: ['http://test.fr'], }, ]); @@ -600,26 +592,6 @@ describe('output preconfiguration', () => { expect(spyAgentPolicyServicBumpAllAgentPoliciesForOutput).not.toBeCalled(); }); - it('should delete existing default output if a new preconfigured output is added', async () => { - const soClient = savedObjectsClientMock.create(); - const esClient = elasticsearchServiceMock.createClusterClient().asInternalUser; - mockedOutputService.getDefaultOutputId.mockResolvedValue('default-output-123'); - await ensurePreconfiguredOutputs(soClient, esClient, [ - { - id: 'non-existing-default-output-1', - name: 'Output 1', - type: 'elasticsearch', - is_default: true, - hosts: ['http://test.fr'], - }, - ]); - - expect(mockedOutputService.delete).toBeCalled(); - expect(mockedOutputService.create).toBeCalled(); - expect(mockedOutputService.update).not.toBeCalled(); - expect(spyAgentPolicyServicBumpAllAgentPoliciesForOutput).not.toBeCalled(); - }); - it('should set default hosts if hosts is not set output that does not exists', async () => { const soClient = savedObjectsClientMock.create(); const esClient = elasticsearchServiceMock.createClusterClient().asInternalUser; @@ -629,6 +601,7 @@ describe('output preconfiguration', () => { name: 'Output 1', type: 'elasticsearch', is_default: false, + is_default_monitoring: false, }, ]); @@ -644,6 +617,7 @@ describe('output preconfiguration', () => { { id: 'existing-output-1', is_default: false, + is_default_monitoring: false, name: 'Output 1', type: 'elasticsearch', hosts: ['http://newhostichanged.co:9201'], // field that changed @@ -655,36 +629,16 @@ describe('output preconfiguration', () => { expect(spyAgentPolicyServicBumpAllAgentPoliciesForOutput).toBeCalled(); }); - it('should delete default output if preconfigured output exists and another default output exists', async () => { - const soClient = savedObjectsClientMock.create(); - const esClient = elasticsearchServiceMock.createClusterClient().asInternalUser; - soClient.find.mockResolvedValue({ saved_objects: [], page: 0, per_page: 0, total: 0 }); - mockedOutputService.getDefaultOutputId.mockResolvedValue('default-123'); - await ensurePreconfiguredOutputs(soClient, esClient, [ - { - id: 'existing-output-1', - is_default: true, - name: 'Output 1', - type: 'elasticsearch', - hosts: ['http://newhostichanged.co:9201'], // field that changed - }, - ]); - - expect(mockedOutputService.delete).toBeCalled(); - expect(mockedOutputService.create).not.toBeCalled(); - expect(mockedOutputService.update).toBeCalled(); - expect(spyAgentPolicyServicBumpAllAgentPoliciesForOutput).toBeCalled(); - }); - it('should not delete default output if preconfigured default output exists and changed', async () => { const soClient = savedObjectsClientMock.create(); const esClient = elasticsearchServiceMock.createClusterClient().asInternalUser; soClient.find.mockResolvedValue({ saved_objects: [], page: 0, per_page: 0, total: 0 }); - mockedOutputService.getDefaultOutputId.mockResolvedValue('existing-output-1'); + mockedOutputService.getDefaultDataOutputId.mockResolvedValue('existing-output-1'); await ensurePreconfiguredOutputs(soClient, esClient, [ { id: 'existing-output-1', is_default: true, + is_default_monitoring: false, name: 'Output 1', type: 'elasticsearch', hosts: ['http://newhostichanged.co:9201'], // field that changed @@ -703,6 +657,7 @@ describe('output preconfiguration', () => { data: { id: 'existing-output-1', is_default: false, + is_default_monitoring: false, name: 'Output 1', type: 'elasticsearch', hosts: ['http://es.co:80'], @@ -713,6 +668,7 @@ describe('output preconfiguration', () => { data: { id: 'existing-output-1', is_default: false, + is_default_monitoring: false, name: 'Output 1', type: 'elasticsearch', hosts: ['http://es.co'], @@ -746,6 +702,7 @@ describe('output preconfiguration', () => { { id: 'output1', is_default: false, + is_default_monitoring: false, name: 'Output 1', type: 'elasticsearch', hosts: ['http://es.co:9201'], @@ -753,6 +710,7 @@ describe('output preconfiguration', () => { { id: 'output2', is_default: false, + is_default_monitoring: false, name: 'Output 2', type: 'elasticsearch', hosts: ['http://es.co:9201'], @@ -777,6 +735,7 @@ describe('output preconfiguration', () => { { id: 'output1', is_default: false, + is_default_monitoring: false, name: 'Output 1', type: 'elasticsearch', hosts: ['http://es.co:9201'], diff --git a/x-pack/plugins/fleet/server/services/preconfiguration.ts b/x-pack/plugins/fleet/server/services/preconfiguration.ts index e5fea73815ea7..6cdb3abf24908 100644 --- a/x-pack/plugins/fleet/server/services/preconfiguration.ts +++ b/x-pack/plugins/fleet/server/services/preconfiguration.ts @@ -55,6 +55,7 @@ function isPreconfiguredOutputDifferentFromCurrent( ): boolean { return ( existingOutput.is_default !== preconfiguredOutput.is_default || + existingOutput.is_default_monitoring !== preconfiguredOutput.is_default_monitoring || existingOutput.name !== preconfiguredOutput.name || existingOutput.type !== preconfiguredOutput.type || (preconfiguredOutput.hosts && @@ -103,21 +104,13 @@ export async function ensurePreconfiguredOutputs( const isCreate = !existingOutput; const isUpdateWithNewData = existingOutput && isPreconfiguredOutputDifferentFromCurrent(existingOutput, data); - // If a default output already exists, delete it in favor of the preconfigured one - if (isCreate || isUpdateWithNewData) { - const defaultOutputId = await outputService.getDefaultOutputId(soClient); - - if (defaultOutputId && defaultOutputId !== output.id) { - await outputService.delete(soClient, defaultOutputId); - } - } if (isCreate) { - await outputService.create(soClient, data, { id, overwrite: true }); + await outputService.create(soClient, data, { id, fromPreconfiguration: true }); } else if (isUpdateWithNewData) { - await outputService.update(soClient, id, data); + await outputService.update(soClient, id, data, { fromPreconfiguration: true }); // Bump revision of all policies using that output - if (outputData.is_default) { + if (outputData.is_default || outputData.is_default_monitoring) { await agentPolicyService.bumpAllAgentPolicies(soClient, esClient); } else { await agentPolicyService.bumpAllAgentPoliciesForOutput(soClient, esClient, id); @@ -139,7 +132,7 @@ export async function cleanPreconfiguredOutputs( for (const output of existingPreconfiguredOutput) { if (!outputs.find(({ id }) => output.id === id)) { logger.info(`Deleting preconfigured output ${output.id}`); - await outputService.delete(soClient, output.id); + await outputService.delete(soClient, output.id, { fromPreconfiguration: true }); } } } diff --git a/x-pack/plugins/fleet/server/services/setup.ts b/x-pack/plugins/fleet/server/services/setup.ts index 37d79c1bb691d..7cde9c4c052d6 100644 --- a/x-pack/plugins/fleet/server/services/setup.ts +++ b/x-pack/plugins/fleet/server/services/setup.ts @@ -26,7 +26,6 @@ import { generateEnrollmentAPIKey, hasEnrollementAPIKeysForPolicy } from './api_ import { settingsService } from '.'; import { awaitIfPending } from './setup_utils'; import { ensureFleetServerAgentPoliciesExists } from './agents'; -import { awaitIfFleetServerSetupPending } from './fleet_server'; import { ensureFleetFinalPipelineIsInstalled } from './epm/elasticsearch/ingest_pipeline/install'; import { ensureDefaultComponentTemplate } from './epm/elasticsearch/template/install'; import { getInstallations, installPackage } from './epm/packages'; @@ -68,7 +67,6 @@ async function createSetupSideEffects( const defaultOutput = await outputService.ensureDefaultOutput(soClient); - await awaitIfFleetServerSetupPending(); if (appContextService.getConfig()?.agentIdVerificationEnabled) { await ensureFleetGlobalEsAssets(soClient, esClient); } diff --git a/x-pack/plugins/fleet/server/services/upgrade_sender.test.ts b/x-pack/plugins/fleet/server/services/upgrade_sender.test.ts new file mode 100644 index 0000000000000..c8a64a7172b39 --- /dev/null +++ b/x-pack/plugins/fleet/server/services/upgrade_sender.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 type { Logger } from 'src/core/server'; +import { loggingSystemMock } from 'src/core/server/mocks'; + +import type { TelemetryEventsSender } from '../telemetry/sender'; +import { createMockTelemetryEventsSender } from '../telemetry/__mocks__'; + +import { sendTelemetryEvents, capErrorSize, UpdateEventType } from './upgrade_sender'; +import type { PackageUpdateEvent } from './upgrade_sender'; + +describe('sendTelemetryEvents', () => { + let eventsTelemetryMock: jest.Mocked; + let loggerMock: jest.Mocked; + + beforeEach(() => { + eventsTelemetryMock = createMockTelemetryEventsSender(); + loggerMock = loggingSystemMock.createLogger(); + }); + + it('should queue telemetry events with generic error', () => { + const upgradeMessage: PackageUpdateEvent = { + packageName: 'aws', + currentVersion: '0.6.1', + newVersion: '1.3.0', + status: 'failure', + error: [ + { key: 'queueUrl', message: ['Queue URL is required'] }, + { message: 'Invalid format' }, + ], + dryRun: true, + eventType: UpdateEventType.PACKAGE_POLICY_UPGRADE, + }; + + sendTelemetryEvents(loggerMock, eventsTelemetryMock, upgradeMessage); + + expect(eventsTelemetryMock.queueTelemetryEvents).toHaveBeenCalledWith('fleet-upgrades', [ + { + currentVersion: '0.6.1', + error: [ + { + key: 'queueUrl', + message: ['Queue URL is required'], + }, + { + message: 'Invalid format', + }, + ], + errorMessage: ['Field is required', 'Invalid format'], + newVersion: '1.3.0', + packageName: 'aws', + status: 'failure', + dryRun: true, + eventType: 'package-policy-upgrade', + }, + ]); + }); + + it('should cap error size', () => { + const maxSize = 2; + const errors = [{ message: '1' }, { message: '2' }, { message: '3' }]; + + const result = capErrorSize(errors, maxSize); + + expect(result.length).toEqual(maxSize); + }); +}); diff --git a/x-pack/plugins/fleet/server/services/upgrade_sender.ts b/x-pack/plugins/fleet/server/services/upgrade_sender.ts new file mode 100644 index 0000000000000..9069ab68b55a3 --- /dev/null +++ b/x-pack/plugins/fleet/server/services/upgrade_sender.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 type { Logger } from 'src/core/server'; + +import type { TelemetryEventsSender } from '../telemetry/sender'; +import type { InstallType } from '../types'; + +export interface PackageUpdateEvent { + packageName: string; + currentVersion: string; + newVersion: string; + status: 'success' | 'failure'; + dryRun?: boolean; + errorMessage?: string[] | string; + error?: UpgradeError[]; + eventType: UpdateEventType; + installType?: InstallType; +} + +export enum UpdateEventType { + PACKAGE_POLICY_UPGRADE = 'package-policy-upgrade', + PACKAGE_INSTALL = 'package-install', +} + +export interface UpgradeError { + key?: string; + message: string | string[]; +} + +export const MAX_ERROR_SIZE = 100; +export const FLEET_UPGRADES_CHANNEL_NAME = 'fleet-upgrades'; + +export function sendTelemetryEvents( + logger: Logger, + eventsTelemetry: TelemetryEventsSender | undefined, + upgradeEvent: PackageUpdateEvent +) { + if (eventsTelemetry === undefined) { + return; + } + + try { + const cappedErrors = capErrorSize(upgradeEvent.error || [], MAX_ERROR_SIZE); + eventsTelemetry.queueTelemetryEvents(FLEET_UPGRADES_CHANNEL_NAME, [ + { + ...upgradeEvent, + error: upgradeEvent.error ? cappedErrors : undefined, + errorMessage: upgradeEvent.errorMessage || makeErrorGeneric(cappedErrors), + }, + ]); + } catch (exc) { + logger.error(`queing telemetry events failed ${exc}`); + } +} + +export function capErrorSize(errors: UpgradeError[], maxSize: number): UpgradeError[] { + return errors.length > maxSize ? errors?.slice(0, maxSize) : errors; +} + +function makeErrorGeneric(errors: UpgradeError[]): string[] { + return errors.map((error) => { + if (Array.isArray(error.message)) { + const firstMessage = error.message[0]; + return firstMessage?.indexOf('is required') > -1 ? 'Field is required' : firstMessage; + } + return error.message as string; + }); +} diff --git a/x-pack/plugins/fleet/server/services/upgrade_usage.test.ts b/x-pack/plugins/fleet/server/services/upgrade_usage.test.ts deleted file mode 100644 index 5445ad233eddc..0000000000000 --- a/x-pack/plugins/fleet/server/services/upgrade_usage.test.ts +++ /dev/null @@ -1,70 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import type { Logger } from 'src/core/server'; -import { loggingSystemMock } from 'src/core/server/mocks'; - -import type { TelemetryEventsSender } from '../telemetry/sender'; -import { createMockTelemetryEventsSender } from '../telemetry/__mocks__'; - -import { sendTelemetryEvents, capErrorSize } from './upgrade_usage'; -import type { PackagePolicyUpgradeUsage } from './upgrade_usage'; - -describe('sendTelemetryEvents', () => { - let eventsTelemetryMock: jest.Mocked; - let loggerMock: jest.Mocked; - - beforeEach(() => { - eventsTelemetryMock = createMockTelemetryEventsSender(); - loggerMock = loggingSystemMock.createLogger(); - }); - - it('should queue telemetry events with generic error', () => { - const upgardeMessage: PackagePolicyUpgradeUsage = { - package_name: 'aws', - current_version: '0.6.1', - new_version: '1.3.0', - status: 'failure', - error: [ - { key: 'queueUrl', message: ['Queue URL is required'] }, - { message: 'Invalid format' }, - ], - dryRun: true, - }; - - sendTelemetryEvents(loggerMock, eventsTelemetryMock, upgardeMessage); - - expect(eventsTelemetryMock.queueTelemetryEvents).toHaveBeenCalledWith('fleet-upgrades', [ - { - current_version: '0.6.1', - error: [ - { - key: 'queueUrl', - message: ['Queue URL is required'], - }, - { - message: 'Invalid format', - }, - ], - error_message: ['Field is required', 'Invalid format'], - new_version: '1.3.0', - package_name: 'aws', - status: 'failure', - dryRun: true, - }, - ]); - }); - - it('should cap error size', () => { - const maxSize = 2; - const errors = [{ message: '1' }, { message: '2' }, { message: '3' }]; - - const result = capErrorSize(errors, maxSize); - - expect(result.length).toEqual(maxSize); - }); -}); diff --git a/x-pack/plugins/fleet/server/services/upgrade_usage.ts b/x-pack/plugins/fleet/server/services/upgrade_usage.ts deleted file mode 100644 index 68bb126496e01..0000000000000 --- a/x-pack/plugins/fleet/server/services/upgrade_usage.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; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import type { Logger } from 'src/core/server'; - -import type { TelemetryEventsSender } from '../telemetry/sender'; - -export interface PackagePolicyUpgradeUsage { - package_name: string; - current_version: string; - new_version: string; - status: 'success' | 'failure'; - error?: UpgradeError[]; - dryRun?: boolean; - error_message?: string[]; -} - -export interface UpgradeError { - key?: string; - message: string | string[]; -} - -export const MAX_ERROR_SIZE = 100; -export const FLEET_UPGRADES_CHANNEL_NAME = 'fleet-upgrades'; - -export function sendTelemetryEvents( - logger: Logger, - eventsTelemetry: TelemetryEventsSender | undefined, - upgradeUsage: PackagePolicyUpgradeUsage -) { - if (eventsTelemetry === undefined) { - return; - } - - try { - const cappedErrors = capErrorSize(upgradeUsage.error || [], MAX_ERROR_SIZE); - eventsTelemetry.queueTelemetryEvents(FLEET_UPGRADES_CHANNEL_NAME, [ - { - ...upgradeUsage, - error: upgradeUsage.error ? cappedErrors : undefined, - error_message: makeErrorGeneric(cappedErrors), - }, - ]); - } catch (exc) { - logger.error(`queing telemetry events failed ${exc}`); - } -} - -export function capErrorSize(errors: UpgradeError[], maxSize: number): UpgradeError[] { - return errors.length > maxSize ? errors?.slice(0, maxSize) : errors; -} - -function makeErrorGeneric(errors: UpgradeError[]): string[] { - return errors.map((error) => { - if (Array.isArray(error.message)) { - const firstMessage = error.message[0]; - return firstMessage?.indexOf('is required') > -1 ? 'Field is required' : firstMessage; - } - return error.message as string; - }); -} diff --git a/x-pack/plugins/fleet/server/telemetry/sender.test.ts b/x-pack/plugins/fleet/server/telemetry/sender.test.ts index 8fe4c6e150ff9..a1ba0693bf3f3 100644 --- a/x-pack/plugins/fleet/server/telemetry/sender.test.ts +++ b/x-pack/plugins/fleet/server/telemetry/sender.test.ts @@ -15,6 +15,8 @@ import type { InfoResponse } from '@elastic/elasticsearch/lib/api/types'; import { loggingSystemMock } from 'src/core/server/mocks'; +import { UpdateEventType } from '../services/upgrade_sender'; + import { TelemetryEventsSender } from './sender'; jest.mock('axios', () => { @@ -38,7 +40,13 @@ describe('TelemetryEventsSender', () => { describe('queueTelemetryEvents', () => { it('queues two events', () => { sender.queueTelemetryEvents('fleet-upgrades', [ - { package_name: 'system', current_version: '0.3', new_version: '1.0', status: 'success' }, + { + packageName: 'system', + currentVersion: '0.3', + newVersion: '1.0', + status: 'success', + eventType: UpdateEventType.PACKAGE_POLICY_UPGRADE, + }, ]); expect(sender['queuesPerChannel']['fleet-upgrades']).toBeDefined(); }); @@ -54,7 +62,13 @@ describe('TelemetryEventsSender', () => { }; sender.queueTelemetryEvents('fleet-upgrades', [ - { package_name: 'apache', current_version: '0.3', new_version: '1.0', status: 'success' }, + { + packageName: 'apache', + currentVersion: '0.3', + newVersion: '1.0', + status: 'success', + eventType: UpdateEventType.PACKAGE_POLICY_UPGRADE, + }, ]); sender['sendEvents'] = jest.fn(); @@ -74,7 +88,13 @@ describe('TelemetryEventsSender', () => { sender['telemetryStart'] = telemetryStart; sender.queueTelemetryEvents('fleet-upgrades', [ - { package_name: 'system', current_version: '0.3', new_version: '1.0', status: 'success' }, + { + packageName: 'system', + currentVersion: '0.3', + newVersion: '1.0', + status: 'success', + eventType: UpdateEventType.PACKAGE_POLICY_UPGRADE, + }, ]); sender['sendEvents'] = jest.fn(); diff --git a/x-pack/plugins/fleet/server/telemetry/sender.ts b/x-pack/plugins/fleet/server/telemetry/sender.ts index 3bda17fbd1d79..e7413872b6245 100644 --- a/x-pack/plugins/fleet/server/telemetry/sender.ts +++ b/x-pack/plugins/fleet/server/telemetry/sender.ts @@ -138,7 +138,7 @@ export class TelemetryEventsSender { clusterInfo?.version?.number ); } catch (err) { - this.logger.warn(`Error sending telemetry events data: ${err}`); + this.logger.debug(`Error sending telemetry events data: ${err}`); queue.clearEvents(); } } @@ -175,7 +175,7 @@ export class TelemetryEventsSender { }); this.logger.debug(`Events sent!. Response: ${resp.status} ${JSON.stringify(resp.data)}`); } catch (err) { - this.logger.warn( + this.logger.debug( `Error sending events: ${err.response.status} ${JSON.stringify(err.response.data)}` ); } diff --git a/x-pack/plugins/fleet/server/telemetry/types.ts b/x-pack/plugins/fleet/server/telemetry/types.ts index 4351546ecdf02..3b6478d68fba7 100644 --- a/x-pack/plugins/fleet/server/telemetry/types.ts +++ b/x-pack/plugins/fleet/server/telemetry/types.ts @@ -5,11 +5,11 @@ * 2.0. */ -import type { PackagePolicyUpgradeUsage } from '../services/upgrade_usage'; +import type { PackageUpdateEvent } from '../services/upgrade_sender'; export interface FleetTelemetryChannelEvents { // channel name => event type - 'fleet-upgrades': PackagePolicyUpgradeUsage; + 'fleet-upgrades': PackageUpdateEvent; } export type FleetTelemetryChannel = keyof FleetTelemetryChannelEvents; diff --git a/x-pack/plugins/fleet/server/types/index.tsx b/x-pack/plugins/fleet/server/types/index.tsx index 174aac03d6a3c..9d3e912864785 100644 --- a/x-pack/plugins/fleet/server/types/index.tsx +++ b/x-pack/plugins/fleet/server/types/index.tsx @@ -13,7 +13,6 @@ export type { AgentType, AgentAction, AgentPolicyAction, - AgentPolicyActionV7_9, BaseAgentActionSOAttributes, AgentActionSOAttributes, AgentPolicyActionSOAttributes, diff --git a/x-pack/plugins/fleet/server/types/models/preconfiguration.test.ts b/x-pack/plugins/fleet/server/types/models/preconfiguration.test.ts index eb349e0d0f823..9cf8626f5fed5 100644 --- a/x-pack/plugins/fleet/server/types/models/preconfiguration.test.ts +++ b/x-pack/plugins/fleet/server/types/models/preconfiguration.test.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { PreconfiguredOutputsSchema, PreconfiguredAgentPoliciesSchema } from './preconfiguration'; +import { PreconfiguredOutputsSchema } from './preconfiguration'; describe('Test preconfiguration schema', () => { describe('PreconfiguredOutputsSchema', () => { @@ -25,7 +25,25 @@ describe('Test preconfiguration schema', () => { is_default: true, }, ]); - }).toThrowError('preconfigured outputs need to have only one default output.'); + }).toThrowError('preconfigured outputs can only have one default output.'); + }); + it('should not allow multiple default monitoring output', () => { + expect(() => { + PreconfiguredOutputsSchema.validate([ + { + id: 'output-1', + name: 'Output 1', + type: 'elasticsearch', + is_default_monitoring: true, + }, + { + id: 'output-2', + name: 'Output 2', + type: 'elasticsearch', + is_default_monitoring: true, + }, + ]); + }).toThrowError('preconfigured outputs can only have one default monitoring output.'); }); it('should not allow multiple output with same ids', () => { expect(() => { @@ -60,22 +78,4 @@ describe('Test preconfiguration schema', () => { }).toThrowError('preconfigured outputs need to have unique names.'); }); }); - - describe('PreconfiguredAgentPoliciesSchema', () => { - it('should not allow multiple outputs in one policy', () => { - expect(() => { - PreconfiguredAgentPoliciesSchema.validate([ - { - id: 'policy-1', - name: 'Policy 1', - package_policies: [], - data_output_id: 'test1', - monitoring_output_id: 'test2', - }, - ]); - }).toThrowError( - '[0]: Currently Fleet only support one output per agent policy data_output_id should be the same as monitoring_output_id.' - ); - }); - }); }); diff --git a/x-pack/plugins/fleet/server/types/models/preconfiguration.ts b/x-pack/plugins/fleet/server/types/models/preconfiguration.ts index b65fa122911dc..3ba89f1e526b3 100644 --- a/x-pack/plugins/fleet/server/types/models/preconfiguration.ts +++ b/x-pack/plugins/fleet/server/types/models/preconfiguration.ts @@ -50,7 +50,12 @@ export const PreconfiguredPackagesSchema = schema.arrayOf( ); function validatePreconfiguredOutputs(outputs: PreconfiguredOutput[]) { - const acc = { names: new Set(), ids: new Set(), is_default: false }; + const acc = { + names: new Set(), + ids: new Set(), + is_default_exists: false, + is_default_monitoring_exists: false, + }; for (const output of outputs) { if (acc.names.has(output.name)) { @@ -59,13 +64,17 @@ function validatePreconfiguredOutputs(outputs: PreconfiguredOutput[]) { if (acc.ids.has(output.id)) { return 'preconfigured outputs need to have unique ids.'; } - if (acc.is_default && output.is_default) { - return 'preconfigured outputs need to have only one default output.'; + if (acc.is_default_exists && output.is_default) { + return 'preconfigured outputs can only have one default output.'; + } + if (acc.is_default_monitoring_exists && output.is_default_monitoring) { + return 'preconfigured outputs can only have one default monitoring output.'; } acc.ids.add(output.id); acc.names.add(output.name); - acc.is_default = acc.is_default || output.is_default; + acc.is_default_exists = acc.is_default_exists || output.is_default; + acc.is_default_monitoring_exists = acc.is_default_exists || output.is_default_monitoring; } } @@ -73,6 +82,7 @@ export const PreconfiguredOutputsSchema = schema.arrayOf( schema.object({ id: schema.string(), is_default: schema.boolean({ defaultValue: false }), + is_default_monitoring: schema.boolean({ defaultValue: false }), name: schema.string(), type: schema.oneOf([schema.literal(outputType.Elasticsearch)]), hosts: schema.maybe(schema.arrayOf(schema.uri({ scheme: ['http', 'https'] }))), @@ -86,57 +96,48 @@ export const PreconfiguredOutputsSchema = schema.arrayOf( ); export const PreconfiguredAgentPoliciesSchema = schema.arrayOf( - schema.object( - { - ...AgentPolicyBaseSchema, - namespace: schema.maybe(NamespaceSchema), - id: schema.maybe(schema.oneOf([schema.string(), schema.number()])), - is_default: schema.maybe(schema.boolean()), - is_default_fleet_server: schema.maybe(schema.boolean()), - data_output_id: schema.maybe(schema.string()), - monitoring_output_id: schema.maybe(schema.string()), - package_policies: schema.arrayOf( - schema.object({ + schema.object({ + ...AgentPolicyBaseSchema, + namespace: schema.maybe(NamespaceSchema), + id: schema.maybe(schema.oneOf([schema.string(), schema.number()])), + is_default: schema.maybe(schema.boolean()), + is_default_fleet_server: schema.maybe(schema.boolean()), + data_output_id: schema.maybe(schema.string()), + monitoring_output_id: schema.maybe(schema.string()), + package_policies: schema.arrayOf( + schema.object({ + name: schema.string(), + package: schema.object({ name: schema.string(), - package: schema.object({ - name: schema.string(), - }), - description: schema.maybe(schema.string()), - namespace: schema.maybe(NamespaceSchema), - inputs: schema.maybe( - schema.arrayOf( - schema.object({ - type: schema.string(), - enabled: schema.maybe(schema.boolean()), - keep_enabled: schema.maybe(schema.boolean()), - vars: varsSchema, - streams: schema.maybe( - schema.arrayOf( - schema.object({ - data_stream: schema.object({ - type: schema.maybe(schema.string()), - dataset: schema.string(), - }), - enabled: schema.maybe(schema.boolean()), - keep_enabled: schema.maybe(schema.boolean()), - vars: varsSchema, - }) - ) - ), - }) - ) - ), - }) - ), - }, - { - validate: (policy) => { - if (policy.data_output_id !== policy.monitoring_output_id) { - return 'Currently Fleet only support one output per agent policy data_output_id should be the same as monitoring_output_id.'; - } - }, - } - ), + }), + description: schema.maybe(schema.string()), + namespace: schema.maybe(NamespaceSchema), + inputs: schema.maybe( + schema.arrayOf( + schema.object({ + type: schema.string(), + enabled: schema.maybe(schema.boolean()), + keep_enabled: schema.maybe(schema.boolean()), + vars: varsSchema, + streams: schema.maybe( + schema.arrayOf( + schema.object({ + data_stream: schema.object({ + type: schema.maybe(schema.string()), + dataset: schema.string(), + }), + enabled: schema.maybe(schema.boolean()), + keep_enabled: schema.maybe(schema.boolean()), + vars: varsSchema, + }) + ) + ), + }) + ) + ), + }) + ), + }), { defaultValue: [DEFAULT_AGENT_POLICY, DEFAULT_FLEET_SERVER_AGENT_POLICY], } diff --git a/x-pack/plugins/graph/public/components/field_manager/field_editor.tsx b/x-pack/plugins/graph/public/components/field_manager/field_editor.tsx index d4ccfe66dc7d4..138e4fe5060a4 100644 --- a/x-pack/plugins/graph/public/components/field_manager/field_editor.tsx +++ b/x-pack/plugins/graph/public/components/field_manager/field_editor.tsx @@ -25,11 +25,11 @@ import { EuiIconTip, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; +import { FieldIcon } from '@kbn/react-field/field_icon'; import classNames from 'classnames'; import { WorkspaceField } from '../../types'; import { iconChoices } from '../../helpers/style_choices'; import { LegacyIcon } from '../legacy_icon'; -import { FieldIcon } from '../../../../../../src/plugins/kibana_react/public'; import { UpdateableFieldProperties } from './field_manager'; import { isEqual } from '../helpers'; diff --git a/x-pack/plugins/graph/public/components/field_manager/field_picker.tsx b/x-pack/plugins/graph/public/components/field_manager/field_picker.tsx index a764c0241938b..7728c67f89a82 100644 --- a/x-pack/plugins/graph/public/components/field_manager/field_picker.tsx +++ b/x-pack/plugins/graph/public/components/field_manager/field_picker.tsx @@ -9,8 +9,8 @@ import React, { useState, useEffect, ReactNode } from 'react'; import { EuiPopover, EuiSelectable, EuiBadge } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import classNames from 'classnames'; +import { FieldIcon } from '@kbn/react-field/field_icon'; import { WorkspaceField } from '../../types'; -import { FieldIcon } from '../../../../../../src/plugins/kibana_react/public'; export interface FieldPickerProps { fieldMap: Record; diff --git a/x-pack/plugins/graph/server/routes/search.ts b/x-pack/plugins/graph/server/routes/search.ts index 2f792dd399ccf..92f3d7a02b072 100644 --- a/x-pack/plugins/graph/server/routes/search.ts +++ b/x-pack/plugins/graph/server/routes/search.ts @@ -49,7 +49,7 @@ export function registerSearchRoute({ index: request.body.index, body: request.body.body, track_total_hits: true, - ignore_throttled: !includeFrozen, + ...(includeFrozen ? { ignore_throttled: false } : {}), }) ).body, }, diff --git a/x-pack/plugins/index_management/__jest__/client_integration/home/data_streams_tab.helpers.ts b/x-pack/plugins/index_management/__jest__/client_integration/home/data_streams_tab.helpers.ts index 311acb13d3f06..e3184cadbdc49 100644 --- a/x-pack/plugins/index_management/__jest__/client_integration/home/data_streams_tab.helpers.ts +++ b/x-pack/plugins/index_management/__jest__/client_integration/home/data_streams_tab.helpers.ts @@ -9,7 +9,7 @@ import { act } from 'react-dom/test-utils'; import { ReactWrapper } from 'enzyme'; import { EuiDescriptionListDescription } from '@elastic/eui'; -import { registerTestBed, TestBed, TestBedConfig, findTestSubject } from '@kbn/test/jest'; +import { registerTestBed, TestBed, AsyncTestBedConfig, findTestSubject } from '@kbn/test/jest'; import { DataStream } from '../../../common'; import { IndexManagementHome } from '../../../public/application/sections/home'; import { indexManagementStore } from '../../../public/application/store'; @@ -42,7 +42,7 @@ export interface DataStreamsTabTestBed extends TestBed { } export const setup = async (overridingDependencies: any = {}): Promise => { - const testBedConfig: TestBedConfig = { + const testBedConfig: AsyncTestBedConfig = { store: () => indexManagementStore(services as any), memoryRouter: { initialEntries: [`/indices`], diff --git a/x-pack/plugins/index_management/__jest__/client_integration/home/home.helpers.ts b/x-pack/plugins/index_management/__jest__/client_integration/home/home.helpers.ts index a15e4f2a613d3..ad8aceb7d56b8 100644 --- a/x-pack/plugins/index_management/__jest__/client_integration/home/home.helpers.ts +++ b/x-pack/plugins/index_management/__jest__/client_integration/home/home.helpers.ts @@ -5,12 +5,12 @@ * 2.0. */ -import { registerTestBed, TestBed, TestBedConfig } from '@kbn/test/jest'; +import { registerTestBed, TestBed, AsyncTestBedConfig } from '@kbn/test/jest'; import { IndexManagementHome } from '../../../public/application/sections/home'; import { indexManagementStore } from '../../../public/application/store'; import { WithAppDependencies, services, TestSubjects } from '../helpers'; -const testBedConfig: TestBedConfig = { +const testBedConfig: AsyncTestBedConfig = { store: () => indexManagementStore(services as any), memoryRouter: { initialEntries: [`/indices?includeHidden=true`], diff --git a/x-pack/plugins/index_management/__jest__/client_integration/home/index_templates_tab.helpers.ts b/x-pack/plugins/index_management/__jest__/client_integration/home/index_templates_tab.helpers.ts index 7431686c02bbf..4ddd14562577a 100644 --- a/x-pack/plugins/index_management/__jest__/client_integration/home/index_templates_tab.helpers.ts +++ b/x-pack/plugins/index_management/__jest__/client_integration/home/index_templates_tab.helpers.ts @@ -7,12 +7,12 @@ import { act } from 'react-dom/test-utils'; -import { registerTestBed, TestBed, TestBedConfig, findTestSubject } from '@kbn/test/jest'; +import { registerTestBed, TestBed, AsyncTestBedConfig, findTestSubject } from '@kbn/test/jest'; import { TemplateList } from '../../../public/application/sections/home/template_list'; import { TemplateDeserialized } from '../../../common'; import { WithAppDependencies, TestSubjects } from '../helpers'; -const testBedConfig: TestBedConfig = { +const testBedConfig: AsyncTestBedConfig = { memoryRouter: { initialEntries: [`/templates`], componentRoutePath: `/templates/:templateName?`, diff --git a/x-pack/plugins/index_management/__jest__/client_integration/home/indices_tab.helpers.ts b/x-pack/plugins/index_management/__jest__/client_integration/home/indices_tab.helpers.ts index 2576b5f92b7b2..0e4564163c553 100644 --- a/x-pack/plugins/index_management/__jest__/client_integration/home/indices_tab.helpers.ts +++ b/x-pack/plugins/index_management/__jest__/client_integration/home/indices_tab.helpers.ts @@ -8,12 +8,12 @@ import { act } from 'react-dom/test-utils'; import { ReactWrapper } from 'enzyme'; -import { registerTestBed, TestBed, TestBedConfig, findTestSubject } from '@kbn/test/jest'; +import { registerTestBed, TestBed, AsyncTestBedConfig, findTestSubject } from '@kbn/test/jest'; import { IndexManagementHome } from '../../../public/application/sections/home'; import { indexManagementStore } from '../../../public/application/store'; import { WithAppDependencies, services, TestSubjects } from '../helpers'; -const testBedConfig: TestBedConfig = { +const testBedConfig: AsyncTestBedConfig = { store: () => indexManagementStore(services as any), memoryRouter: { initialEntries: [`/indices?includeHiddenIndices=true`], diff --git a/x-pack/plugins/index_management/__jest__/client_integration/index_template_wizard/template_clone.helpers.ts b/x-pack/plugins/index_management/__jest__/client_integration/index_template_wizard/template_clone.helpers.ts index 222bee28aef4b..dffa6fee19d06 100644 --- a/x-pack/plugins/index_management/__jest__/client_integration/index_template_wizard/template_clone.helpers.ts +++ b/x-pack/plugins/index_management/__jest__/client_integration/index_template_wizard/template_clone.helpers.ts @@ -5,14 +5,14 @@ * 2.0. */ -import { registerTestBed, TestBedConfig } from '@kbn/test/jest'; +import { registerTestBed, AsyncTestBedConfig } from '@kbn/test/jest'; import { TemplateClone } from '../../../public/application/sections/template_clone'; import { WithAppDependencies } from '../helpers'; import { formSetup } from './template_form.helpers'; import { TEMPLATE_NAME } from './constants'; -const testBedConfig: TestBedConfig = { +const testBedConfig: AsyncTestBedConfig = { memoryRouter: { initialEntries: [`/clone_template/${TEMPLATE_NAME}`], componentRoutePath: `/clone_template/:name`, diff --git a/x-pack/plugins/index_management/__jest__/client_integration/index_template_wizard/template_create.helpers.ts b/x-pack/plugins/index_management/__jest__/client_integration/index_template_wizard/template_create.helpers.ts index 7d3b34a6b8238..450d2c524b445 100644 --- a/x-pack/plugins/index_management/__jest__/client_integration/index_template_wizard/template_create.helpers.ts +++ b/x-pack/plugins/index_management/__jest__/client_integration/index_template_wizard/template_create.helpers.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { registerTestBed, TestBedConfig } from '@kbn/test/jest'; +import { registerTestBed, AsyncTestBedConfig } from '@kbn/test/jest'; import { TemplateCreate } from '../../../public/application/sections/template_create'; import { WithAppDependencies } from '../helpers'; @@ -16,7 +16,7 @@ export const setup: any = (isLegacy: boolean = false) => { ? { pathname: '/create_template', search: '?legacy=true' } : { pathname: '/create_template' }; - const testBedConfig: TestBedConfig = { + const testBedConfig: AsyncTestBedConfig = { memoryRouter: { initialEntries: [route], componentRoutePath: route, diff --git a/x-pack/plugins/index_management/__jest__/client_integration/index_template_wizard/template_edit.helpers.ts b/x-pack/plugins/index_management/__jest__/client_integration/index_template_wizard/template_edit.helpers.ts index e087c9432c4c2..6c73da3b3379d 100644 --- a/x-pack/plugins/index_management/__jest__/client_integration/index_template_wizard/template_edit.helpers.ts +++ b/x-pack/plugins/index_management/__jest__/client_integration/index_template_wizard/template_edit.helpers.ts @@ -5,14 +5,14 @@ * 2.0. */ -import { registerTestBed, TestBedConfig } from '@kbn/test/jest'; +import { registerTestBed, AsyncTestBedConfig } from '@kbn/test/jest'; import { TemplateEdit } from '../../../public/application/sections/template_edit'; import { WithAppDependencies } from '../helpers'; import { formSetup, TestSubjects } from './template_form.helpers'; import { TEMPLATE_NAME } from './constants'; -const testBedConfig: TestBedConfig = { +const testBedConfig: AsyncTestBedConfig = { memoryRouter: { initialEntries: [`/edit_template/${TEMPLATE_NAME}`], componentRoutePath: `/edit_template/:name`, diff --git a/x-pack/plugins/index_management/public/application/components/component_templates/__jest__/client_integration/helpers/component_template_create.helpers.ts b/x-pack/plugins/index_management/public/application/components/component_templates/__jest__/client_integration/helpers/component_template_create.helpers.ts index 9d28d57e531cb..06f0036cc5c77 100644 --- a/x-pack/plugins/index_management/public/application/components/component_templates/__jest__/client_integration/helpers/component_template_create.helpers.ts +++ b/x-pack/plugins/index_management/public/application/components/component_templates/__jest__/client_integration/helpers/component_template_create.helpers.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { registerTestBed, TestBed, TestBedConfig } from '@kbn/test/jest'; +import { registerTestBed, TestBed, AsyncTestBedConfig } from '@kbn/test/jest'; import { BASE_PATH } from '../../../../../../../common'; import { ComponentTemplateCreate } from '../../../component_template_wizard'; @@ -19,7 +19,7 @@ export type ComponentTemplateCreateTestBed = TestBed; }; -const testBedConfig: TestBedConfig = { +const testBedConfig: AsyncTestBedConfig = { memoryRouter: { initialEntries: [`${BASE_PATH}/create_component_template`], componentRoutePath: `${BASE_PATH}/create_component_template`, diff --git a/x-pack/plugins/index_management/public/application/components/component_templates/__jest__/client_integration/helpers/component_template_edit.helpers.ts b/x-pack/plugins/index_management/public/application/components/component_templates/__jest__/client_integration/helpers/component_template_edit.helpers.ts index 093a01d8db41c..e7b8df245aaa9 100644 --- a/x-pack/plugins/index_management/public/application/components/component_templates/__jest__/client_integration/helpers/component_template_edit.helpers.ts +++ b/x-pack/plugins/index_management/public/application/components/component_templates/__jest__/client_integration/helpers/component_template_edit.helpers.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { registerTestBed, TestBed, TestBedConfig } from '@kbn/test/jest'; +import { registerTestBed, TestBed, AsyncTestBedConfig } from '@kbn/test/jest'; import { BASE_PATH } from '../../../../../../../common'; import { ComponentTemplateEdit } from '../../../component_template_wizard'; @@ -19,7 +19,7 @@ export type ComponentTemplateEditTestBed = TestBed; }; -const testBedConfig: TestBedConfig = { +const testBedConfig: AsyncTestBedConfig = { memoryRouter: { initialEntries: [`${BASE_PATH}/edit_component_template/comp-1`], componentRoutePath: `${BASE_PATH}/edit_component_template/:name`, diff --git a/x-pack/plugins/index_management/public/application/components/component_templates/__jest__/client_integration/helpers/component_template_list.helpers.ts b/x-pack/plugins/index_management/public/application/components/component_templates/__jest__/client_integration/helpers/component_template_list.helpers.ts index a8d548a9bf2b8..680550d16096b 100644 --- a/x-pack/plugins/index_management/public/application/components/component_templates/__jest__/client_integration/helpers/component_template_list.helpers.ts +++ b/x-pack/plugins/index_management/public/application/components/component_templates/__jest__/client_integration/helpers/component_template_list.helpers.ts @@ -7,12 +7,18 @@ import { act } from 'react-dom/test-utils'; -import { registerTestBed, TestBed, TestBedConfig, findTestSubject, nextTick } from '@kbn/test/jest'; +import { + registerTestBed, + TestBed, + AsyncTestBedConfig, + findTestSubject, + nextTick, +} from '@kbn/test/jest'; import { BASE_PATH } from '../../../../../../../common'; import { WithAppDependencies } from './setup_environment'; import { ComponentTemplateList } from '../../../component_template_list/component_template_list'; -const testBedConfig: TestBedConfig = { +const testBedConfig: AsyncTestBedConfig = { memoryRouter: { initialEntries: [`${BASE_PATH}component_templates`], componentRoutePath: `${BASE_PATH}component_templates`, diff --git a/x-pack/plugins/infra/common/constants.ts b/x-pack/plugins/infra/common/constants.ts index 1c3aa550f2f62..4c70e34c9899f 100644 --- a/x-pack/plugins/infra/common/constants.ts +++ b/x-pack/plugins/infra/common/constants.ts @@ -8,7 +8,6 @@ export const DEFAULT_SOURCE_ID = 'default'; export const METRICS_INDEX_PATTERN = 'metrics-*,metricbeat-*'; export const LOGS_INDEX_PATTERN = 'logs-*,filebeat-*,kibana_sample_data_logs*'; -export const TIMESTAMP_FIELD = '@timestamp'; export const METRICS_APP = 'metrics'; export const LOGS_APP = 'logs'; @@ -16,3 +15,9 @@ export const METRICS_FEATURE_ID = 'infrastructure'; export const LOGS_FEATURE_ID = 'logs'; export type InfraFeatureId = typeof METRICS_FEATURE_ID | typeof LOGS_FEATURE_ID; + +export const TIMESTAMP_FIELD = '@timestamp'; +export const TIEBREAKER_FIELD = '_doc'; +export const HOST_FIELD = 'host.name'; +export const CONTAINER_FIELD = 'container.id'; +export const POD_FIELD = 'kubernetes.pod.uid'; diff --git a/x-pack/plugins/infra/common/http_api/host_details/process_list.ts b/x-pack/plugins/infra/common/http_api/host_details/process_list.ts index 79835a0a78f26..395b1527379a9 100644 --- a/x-pack/plugins/infra/common/http_api/host_details/process_list.ts +++ b/x-pack/plugins/infra/common/http_api/host_details/process_list.ts @@ -14,7 +14,6 @@ const AggValueRT = rt.type({ export const ProcessListAPIRequestRT = rt.type({ hostTerm: rt.record(rt.string, rt.string), - timefield: rt.string, indexPattern: rt.string, to: rt.number, sortBy: rt.type({ @@ -102,7 +101,6 @@ export type ProcessListAPIResponse = rt.TypeOf; export const ProcessListAPIChartRequestRT = rt.type({ hostTerm: rt.record(rt.string, rt.string), - timefield: rt.string, indexPattern: rt.string, to: rt.number, command: rt.string, diff --git a/x-pack/plugins/infra/common/http_api/metrics_api.ts b/x-pack/plugins/infra/common/http_api/metrics_api.ts index c2449707647d7..315a42380397b 100644 --- a/x-pack/plugins/infra/common/http_api/metrics_api.ts +++ b/x-pack/plugins/infra/common/http_api/metrics_api.ts @@ -10,7 +10,6 @@ import { MetricsUIAggregationRT } from '../inventory_models/types'; import { afterKeyObjectRT } from './metrics_explorer'; export const MetricsAPITimerangeRT = rt.type({ - field: rt.string, from: rt.number, to: rt.number, interval: rt.string, diff --git a/x-pack/plugins/infra/common/http_api/metrics_explorer.ts b/x-pack/plugins/infra/common/http_api/metrics_explorer.ts index 5617bd0954f5d..de00d521126e3 100644 --- a/x-pack/plugins/infra/common/http_api/metrics_explorer.ts +++ b/x-pack/plugins/infra/common/http_api/metrics_explorer.ts @@ -41,7 +41,6 @@ export const metricsExplorerMetricRT = rt.intersection([ ]); export const timeRangeRT = rt.type({ - field: rt.string, from: rt.number, to: rt.number, interval: rt.string, diff --git a/x-pack/plugins/infra/common/inventory_models/index.ts b/x-pack/plugins/infra/common/inventory_models/index.ts index 6350e76ca7f29..81f89be8cd6a6 100644 --- a/x-pack/plugins/infra/common/inventory_models/index.ts +++ b/x-pack/plugins/infra/common/inventory_models/index.ts @@ -6,6 +6,7 @@ */ import { i18n } from '@kbn/i18n'; +import { POD_FIELD, HOST_FIELD, CONTAINER_FIELD } from '../constants'; import { host } from './host'; import { pod } from './pod'; import { awsEC2 } from './aws_ec2'; @@ -30,31 +31,23 @@ export const findInventoryModel = (type: InventoryItemType) => { return model; }; -interface InventoryFields { - host: string; - pod: string; - container: string; - timestamp: string; - tiebreaker: string; -} - const LEGACY_TYPES = ['host', 'pod', 'container']; -const getFieldByType = (type: InventoryItemType, fields: InventoryFields) => { +export const getFieldByType = (type: InventoryItemType) => { switch (type) { case 'pod': - return fields.pod; + return POD_FIELD; case 'host': - return fields.host; + return HOST_FIELD; case 'container': - return fields.container; + return CONTAINER_FIELD; } }; -export const findInventoryFields = (type: InventoryItemType, fields?: InventoryFields) => { +export const findInventoryFields = (type: InventoryItemType) => { const inventoryModel = findInventoryModel(type); - if (fields && LEGACY_TYPES.includes(type)) { - const id = getFieldByType(type, fields) || inventoryModel.fields.id; + if (LEGACY_TYPES.includes(type)) { + const id = getFieldByType(type) || inventoryModel.fields.id; return { ...inventoryModel.fields, id, diff --git a/x-pack/plugins/infra/common/log_sources/log_source_configuration.ts b/x-pack/plugins/infra/common/log_sources/log_source_configuration.ts index ab98ad75b8433..5d46ce59457da 100644 --- a/x-pack/plugins/infra/common/log_sources/log_source_configuration.ts +++ b/x-pack/plugins/infra/common/log_sources/log_source_configuration.ts @@ -16,11 +16,6 @@ export const logSourceConfigurationOriginRT = rt.keyof({ export type LogSourceConfigurationOrigin = rt.TypeOf; const logSourceFieldsConfigurationRT = rt.strict({ - container: rt.string, - host: rt.string, - pod: rt.string, - timestamp: rt.string, - tiebreaker: rt.string, message: rt.array(rt.string), }); diff --git a/x-pack/plugins/infra/common/log_sources/resolved_log_source_configuration.ts b/x-pack/plugins/infra/common/log_sources/resolved_log_source_configuration.ts index c6bc10901fcb8..d3459b30a060e 100644 --- a/x-pack/plugins/infra/common/log_sources/resolved_log_source_configuration.ts +++ b/x-pack/plugins/infra/common/log_sources/resolved_log_source_configuration.ts @@ -8,6 +8,7 @@ import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; import { DataView, DataViewsContract } from '../../../../../src/plugins/data_views/common'; import { ObjectEntries } from '../utility_types'; +import { TIMESTAMP_FIELD, TIEBREAKER_FIELD } from '../constants'; import { ResolveLogSourceConfigurationError } from './errors'; import { LogSourceColumnConfiguration, @@ -61,8 +62,8 @@ const resolveLegacyReference = async ( return { indices: sourceConfiguration.logIndices.indexName, - timestampField: sourceConfiguration.fields.timestamp, - tiebreakerField: sourceConfiguration.fields.tiebreaker, + timestampField: TIMESTAMP_FIELD, + tiebreakerField: TIEBREAKER_FIELD, messageField: sourceConfiguration.fields.message, fields, runtimeMappings: {}, @@ -91,8 +92,8 @@ const resolveKibanaIndexPatternReference = async ( return { indices: indexPattern.title, - timestampField: indexPattern.timeFieldName ?? '@timestamp', - tiebreakerField: '_doc', + timestampField: indexPattern.timeFieldName ?? TIMESTAMP_FIELD, + tiebreakerField: TIEBREAKER_FIELD, messageField: ['message'], fields: indexPattern.fields, runtimeMappings: resolveRuntimeMappings(indexPattern), diff --git a/x-pack/plugins/infra/common/metrics_sources/index.ts b/x-pack/plugins/infra/common/metrics_sources/index.ts index a697c65e5a0aa..7fae908707a89 100644 --- a/x-pack/plugins/infra/common/metrics_sources/index.ts +++ b/x-pack/plugins/infra/common/metrics_sources/index.ts @@ -6,7 +6,6 @@ */ import * as rt from 'io-ts'; -import { omit } from 'lodash'; import { SourceConfigurationRT, SourceStatusRuntimeType, @@ -22,7 +21,6 @@ export const metricsSourceConfigurationPropertiesRT = rt.strict({ metricAlias: SourceConfigurationRT.props.metricAlias, inventoryDefaultView: SourceConfigurationRT.props.inventoryDefaultView, metricsExplorerDefaultView: SourceConfigurationRT.props.metricsExplorerDefaultView, - fields: rt.strict(omit(SourceConfigurationRT.props.fields.props, 'message')), anomalyThreshold: rt.number, }); @@ -32,9 +30,6 @@ export type MetricsSourceConfigurationProperties = rt.TypeOf< export const partialMetricsSourceConfigurationPropertiesRT = rt.partial({ ...metricsSourceConfigurationPropertiesRT.type.props, - fields: rt.partial({ - ...metricsSourceConfigurationPropertiesRT.type.props.fields.type.props, - }), }); export type PartialMetricsSourceConfigurationProperties = rt.TypeOf< diff --git a/x-pack/plugins/infra/common/source_configuration/source_configuration.ts b/x-pack/plugins/infra/common/source_configuration/source_configuration.ts index 257cccc86595c..0c30c3d678b2a 100644 --- a/x-pack/plugins/infra/common/source_configuration/source_configuration.ts +++ b/x-pack/plugins/infra/common/source_configuration/source_configuration.ts @@ -50,12 +50,7 @@ export const sourceConfigurationConfigFilePropertiesRT = rt.type({ sources: rt.type({ default: rt.partial({ fields: rt.partial({ - timestamp: rt.string, message: rt.array(rt.string), - tiebreaker: rt.string, - host: rt.string, - container: rt.string, - pod: rt.string, }), }), }), @@ -113,11 +108,6 @@ export type InfraSourceConfigurationColumn = rt.TypeOf = (props) => { const { setAlertParams, alertParams, errors, metadata } = props; - const { http, notifications } = useKibanaContextForPlugin().services; + const { http, notifications, docLinks } = useKibanaContextForPlugin().services; const { source, createDerivedIndexPattern } = useSourceViaHttp({ sourceId: 'default', fetch: http.fetch, @@ -260,6 +261,14 @@ export const Expressions: React.FC = (props) => { [alertParams.groupBy] ); + const disableNoData = useMemo( + () => alertParams.criteria?.every((c) => c.aggType === Aggregators.COUNT), + [alertParams.criteria] + ); + + // Test to see if any of the group fields in groupBy are already filtered down to a single + // group by the filterQuery. If this is the case, then a groupBy is unnecessary, as it would only + // ever produce one group instance const groupByFilterTestPatterns = useMemo(() => { if (!alertParams.groupBy) return null; const groups = !Array.isArray(alertParams.groupBy) @@ -354,6 +363,7 @@ export const Expressions: React.FC = (props) => { > @@ -361,10 +371,13 @@ export const Expressions: React.FC = (props) => { defaultMessage: "Alert me if there's no data", })}{' '} @@ -456,10 +469,20 @@ export const Expressions: React.FC = (props) => { {redundantFilterGroupBy.join(', ')}, groupCount: redundantFilterGroupBy.length, + filteringAndGroupingLink: ( + + {i18n.translate( + 'xpack.infra.metrics.alertFlyout.alertPerRedundantFilterError.docsLink', + { defaultMessage: 'the docs' } + )} + + ), }} /> @@ -474,16 +497,19 @@ export const Expressions: React.FC = (props) => { defaultMessage: 'Alert me if a group stops reporting data', })}{' '} } - disabled={!hasGroupBy} + disabled={disableNoData || !hasGroupBy} checked={Boolean(hasGroupBy && alertParams.alertOnGroupDisappear)} onChange={(e) => setAlertParams('alertOnGroupDisappear', e.target.checked)} /> @@ -492,6 +518,13 @@ export const Expressions: React.FC = (props) => { ); }; +const docCountNoDataDisabledHelpText = i18n.translate( + 'xpack.infra.metrics.alertFlyout.docCountNoDataDisabledHelpText', + { + defaultMessage: '[This setting is not applicable to the Document Count aggregator.]', + } +); + // required for dynamic import // eslint-disable-next-line import/no-default-export export default Expressions; diff --git a/x-pack/plugins/infra/public/alerting/metric_threshold/components/expression_chart.test.tsx b/x-pack/plugins/infra/public/alerting/metric_threshold/components/expression_chart.test.tsx index ec97d01a1cd6f..c2c1fa719bb95 100644 --- a/x-pack/plugins/infra/public/alerting/metric_threshold/components/expression_chart.test.tsx +++ b/x-pack/plugins/infra/public/alerting/metric_threshold/components/expression_chart.test.tsx @@ -54,14 +54,6 @@ describe('ExpressionChart', () => { metricAlias: 'metricbeat-*', inventoryDefaultView: 'host', metricsExplorerDefaultView: 'host', - // @ts-ignore - fields: { - timestamp: '@timestamp', - container: 'container.id', - host: 'host.name', - pod: 'kubernetes.pod.uid', - tiebreaker: '_doc', - }, anomalyThreshold: 20, }, }; diff --git a/x-pack/plugins/infra/public/components/autocomplete_field/autocomplete_field.tsx b/x-pack/plugins/infra/public/components/autocomplete_field/autocomplete_field.tsx index 0e3e2018c963b..4aa0edb406856 100644 --- a/x-pack/plugins/infra/public/components/autocomplete_field/autocomplete_field.tsx +++ b/x-pack/plugins/infra/public/components/autocomplete_field/autocomplete_field.tsx @@ -73,6 +73,7 @@ export class AutocompleteField extends React.Component< placeholder={placeholder} value={value} aria-label={ariaLabel} + data-test-subj="infraSearchField" /> {areSuggestionsVisible && !isLoadingSuggestions && suggestions.length > 0 ? ( diff --git a/x-pack/plugins/infra/public/containers/logs/log_source/log_source.mock.ts b/x-pack/plugins/infra/public/containers/logs/log_source/log_source.mock.ts index 6021c728d32af..204fae7dc0f2b 100644 --- a/x-pack/plugins/infra/public/containers/logs/log_source/log_source.mock.ts +++ b/x-pack/plugins/infra/public/containers/logs/log_source/log_source.mock.ts @@ -73,11 +73,6 @@ export const createBasicSourceConfiguration = (sourceId: string): LogSourceConfi }, logColumns: [], fields: { - container: 'CONTAINER_FIELD', - host: 'HOST_FIELD', - pod: 'POD_FIELD', - tiebreaker: 'TIEBREAKER_FIELD', - timestamp: 'TIMESTAMP_FIELD', message: ['MESSAGE_FIELD'], }, name: sourceId, diff --git a/x-pack/plugins/infra/public/containers/ml/infra_ml_module.tsx b/x-pack/plugins/infra/public/containers/ml/infra_ml_module.tsx index 198a99f394850..22376648ca003 100644 --- a/x-pack/plugins/infra/public/containers/ml/infra_ml_module.tsx +++ b/x-pack/plugins/infra/public/containers/ml/infra_ml_module.tsx @@ -19,7 +19,7 @@ export const useInfraMLModule = ({ moduleDescriptor: ModuleDescriptor; }) => { const { services } = useKibanaContextForPlugin(); - const { spaceId, sourceId, timestampField } = sourceConfiguration; + const { spaceId, sourceId } = sourceConfiguration; const [moduleStatus, dispatchModuleStatus] = useModuleStatus(moduleDescriptor.jobTypes); const [, fetchJobStatus] = useTrackedPromise( @@ -64,7 +64,6 @@ export const useInfraMLModule = ({ indices: selectedIndices, sourceId, spaceId, - timestampField, }, partitionField, }, @@ -91,7 +90,7 @@ export const useInfraMLModule = ({ dispatchModuleStatus({ type: 'failedSetup' }); }, }, - [moduleDescriptor.setUpModule, spaceId, sourceId, timestampField] + [moduleDescriptor.setUpModule, spaceId, sourceId] ); const [cleanUpModuleRequest, cleanUpModule] = useTrackedPromise( diff --git a/x-pack/plugins/infra/public/containers/ml/infra_ml_module_configuration.ts b/x-pack/plugins/infra/public/containers/ml/infra_ml_module_configuration.ts index 4c876c1705364..c258debdddbca 100644 --- a/x-pack/plugins/infra/public/containers/ml/infra_ml_module_configuration.ts +++ b/x-pack/plugins/infra/public/containers/ml/infra_ml_module_configuration.ts @@ -45,8 +45,7 @@ export const isJobConfigurationOutdated = isSubset( new Set(jobConfiguration.indexPattern.split(',')), new Set(currentSourceConfiguration.indices) - ) && - jobConfiguration.timestampField === currentSourceConfiguration.timestampField + ) ); }; diff --git a/x-pack/plugins/infra/public/containers/ml/infra_ml_module_types.ts b/x-pack/plugins/infra/public/containers/ml/infra_ml_module_types.ts index ca655f35f7466..9b172a7c82a98 100644 --- a/x-pack/plugins/infra/public/containers/ml/infra_ml_module_types.ts +++ b/x-pack/plugins/infra/public/containers/ml/infra_ml_module_types.ts @@ -49,12 +49,10 @@ export interface ModuleDescriptor { ) => Promise; validateSetupIndices?: ( indices: string[], - timestampField: string, fetch: HttpHandler ) => Promise; validateSetupDatasets?: ( indices: string[], - timestampField: string, startTime: number, endTime: number, fetch: HttpHandler @@ -65,7 +63,6 @@ export interface ModuleSourceConfiguration { indices: string[]; sourceId: string; spaceId: string; - timestampField: string; } interface ManyCategoriesWarningReason { diff --git a/x-pack/plugins/infra/public/containers/ml/modules/metrics_hosts/module.tsx b/x-pack/plugins/infra/public/containers/ml/modules/metrics_hosts/module.tsx index f892ab62ee3d8..f200ab22c043f 100644 --- a/x-pack/plugins/infra/public/containers/ml/modules/metrics_hosts/module.tsx +++ b/x-pack/plugins/infra/public/containers/ml/modules/metrics_hosts/module.tsx @@ -17,21 +17,18 @@ export const useMetricHostsModule = ({ indexPattern, sourceId, spaceId, - timestampField, }: { indexPattern: string; sourceId: string; spaceId: string; - timestampField: string; }) => { const sourceConfiguration: ModuleSourceConfiguration = useMemo( () => ({ indices: indexPattern.split(','), sourceId, spaceId, - timestampField, }), - [indexPattern, sourceId, spaceId, timestampField] + [indexPattern, sourceId, spaceId] ); const infraMLModule = useInfraMLModule({ diff --git a/x-pack/plugins/infra/public/containers/ml/modules/metrics_hosts/module_descriptor.ts b/x-pack/plugins/infra/public/containers/ml/modules/metrics_hosts/module_descriptor.ts index a7ab948d052aa..f87cd78f4ff34 100644 --- a/x-pack/plugins/infra/public/containers/ml/modules/metrics_hosts/module_descriptor.ts +++ b/x-pack/plugins/infra/public/containers/ml/modules/metrics_hosts/module_descriptor.ts @@ -18,6 +18,7 @@ import { MetricsHostsJobType, bucketSpan, } from '../../../../../common/infra_ml'; +import { TIMESTAMP_FIELD } from '../../../../../common/constants'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths import MemoryJob from '../../../../../../ml/server/models/data_recognizer/modules/metrics_ui_hosts/ml/hosts_memory_usage.json'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths @@ -68,7 +69,7 @@ const setUpModule = async (setUpModuleArgs: SetUpModuleArgs, fetch: HttpHandler) start, end, filter, - moduleSourceConfiguration: { spaceId, sourceId, indices, timestampField }, + moduleSourceConfiguration: { spaceId, sourceId, indices }, partitionField, } = setUpModuleArgs; @@ -93,13 +94,13 @@ const setUpModule = async (setUpModuleArgs: SetUpModuleArgs, fetch: HttpHandler) return { job_id: id, data_description: { - time_field: timestampField, + time_field: TIMESTAMP_FIELD, }, analysis_config, custom_settings: { metrics_source_config: { indexPattern: indexNamePattern, - timestampField, + timestampField: TIMESTAMP_FIELD, bucketSpan, }, }, diff --git a/x-pack/plugins/infra/public/containers/ml/modules/metrics_k8s/module.tsx b/x-pack/plugins/infra/public/containers/ml/modules/metrics_k8s/module.tsx index eadc374434817..08f4f49058dbe 100644 --- a/x-pack/plugins/infra/public/containers/ml/modules/metrics_k8s/module.tsx +++ b/x-pack/plugins/infra/public/containers/ml/modules/metrics_k8s/module.tsx @@ -17,21 +17,18 @@ export const useMetricK8sModule = ({ indexPattern, sourceId, spaceId, - timestampField, }: { indexPattern: string; sourceId: string; spaceId: string; - timestampField: string; }) => { const sourceConfiguration: ModuleSourceConfiguration = useMemo( () => ({ indices: indexPattern.split(','), sourceId, spaceId, - timestampField, }), - [indexPattern, sourceId, spaceId, timestampField] + [indexPattern, sourceId, spaceId] ); const infraMLModule = useInfraMLModule({ diff --git a/x-pack/plugins/infra/public/containers/ml/modules/metrics_k8s/module_descriptor.ts b/x-pack/plugins/infra/public/containers/ml/modules/metrics_k8s/module_descriptor.ts index 4c5eb5fd4bf23..388a7dd0a5656 100644 --- a/x-pack/plugins/infra/public/containers/ml/modules/metrics_k8s/module_descriptor.ts +++ b/x-pack/plugins/infra/public/containers/ml/modules/metrics_k8s/module_descriptor.ts @@ -18,6 +18,7 @@ import { MetricK8sJobType, bucketSpan, } from '../../../../../common/infra_ml'; +import { TIMESTAMP_FIELD } from '../../../../../common/constants'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths import MemoryJob from '../../../../../../ml/server/models/data_recognizer/modules/metrics_ui_k8s/ml/k8s_memory_usage.json'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths @@ -69,7 +70,7 @@ const setUpModule = async (setUpModuleArgs: SetUpModuleArgs, fetch: HttpHandler) start, end, filter, - moduleSourceConfiguration: { spaceId, sourceId, indices, timestampField }, + moduleSourceConfiguration: { spaceId, sourceId, indices }, partitionField, } = setUpModuleArgs; @@ -93,13 +94,13 @@ const setUpModule = async (setUpModuleArgs: SetUpModuleArgs, fetch: HttpHandler) return { job_id: id, data_description: { - time_field: timestampField, + time_field: TIMESTAMP_FIELD, }, analysis_config, custom_settings: { metrics_source_config: { indexPattern: indexNamePattern, - timestampField, + timestampField: TIMESTAMP_FIELD, bucketSpan, }, }, diff --git a/x-pack/plugins/infra/public/lib/lib.ts b/x-pack/plugins/infra/public/lib/lib.ts index 97a3f8eabbe4e..a37a9af7d9320 100644 --- a/x-pack/plugins/infra/public/lib/lib.ts +++ b/x-pack/plugins/infra/public/lib/lib.ts @@ -14,7 +14,6 @@ import { SnapshotNodeMetric, SnapshotNodePath, } from '../../common/http_api/snapshot_api'; -import { MetricsSourceConfigurationProperties } from '../../common/metrics_sources'; import { WaffleSortOption } from '../pages/metrics/inventory_view/hooks/use_waffle_options'; export interface InfraWaffleMapNode { @@ -124,7 +123,6 @@ export enum InfraWaffleMapRuleOperator { } export interface InfraWaffleMapOptions { - fields?: Omit | null; formatter: InfraFormatterType; formatTemplate: string; metric: SnapshotMetricInput; diff --git a/x-pack/plugins/infra/public/pages/link_to/link_to_logs.test.tsx b/x-pack/plugins/infra/public/pages/link_to/link_to_logs.test.tsx index f9c80edd2c199..cfcf8db771b78 100644 --- a/x-pack/plugins/infra/public/pages/link_to/link_to_logs.test.tsx +++ b/x-pack/plugins/infra/public/pages/link_to/link_to_logs.test.tsx @@ -151,7 +151,7 @@ describe('LinkToLogsPage component', () => { const searchParams = new URLSearchParams(history.location.search); expect(searchParams.get('sourceId')).toEqual('default'); expect(searchParams.get('logFilter')).toMatchInlineSnapshot( - `"(language:kuery,query:'HOST_FIELD: HOST_NAME')"` + `"(language:kuery,query:'host.name: HOST_NAME')"` ); expect(searchParams.get('logPosition')).toEqual(null); }); @@ -172,7 +172,7 @@ describe('LinkToLogsPage component', () => { const searchParams = new URLSearchParams(history.location.search); expect(searchParams.get('sourceId')).toEqual('default'); expect(searchParams.get('logFilter')).toMatchInlineSnapshot( - `"(language:kuery,query:'(HOST_FIELD: HOST_NAME) and (FILTER_FIELD:FILTER_VALUE)')"` + `"(language:kuery,query:'(host.name: HOST_NAME) and (FILTER_FIELD:FILTER_VALUE)')"` ); expect(searchParams.get('logPosition')).toMatchInlineSnapshot( `"(end:'2019-02-20T14:58:09.404Z',position:(tiebreaker:0,time:1550671089404),start:'2019-02-20T12:58:09.404Z',streamLive:!f)"` @@ -193,7 +193,7 @@ describe('LinkToLogsPage component', () => { const searchParams = new URLSearchParams(history.location.search); expect(searchParams.get('sourceId')).toEqual('OTHER_SOURCE'); expect(searchParams.get('logFilter')).toMatchInlineSnapshot( - `"(language:kuery,query:'HOST_FIELD: HOST_NAME')"` + `"(language:kuery,query:'host.name: HOST_NAME')"` ); expect(searchParams.get('logPosition')).toEqual(null); }); @@ -229,7 +229,7 @@ describe('LinkToLogsPage component', () => { const searchParams = new URLSearchParams(history.location.search); expect(searchParams.get('sourceId')).toEqual('default'); expect(searchParams.get('logFilter')).toMatchInlineSnapshot( - `"(language:kuery,query:'CONTAINER_FIELD: CONTAINER_ID')"` + `"(language:kuery,query:'container.id: CONTAINER_ID')"` ); expect(searchParams.get('logPosition')).toEqual(null); }); @@ -250,7 +250,7 @@ describe('LinkToLogsPage component', () => { const searchParams = new URLSearchParams(history.location.search); expect(searchParams.get('sourceId')).toEqual('default'); expect(searchParams.get('logFilter')).toMatchInlineSnapshot( - `"(language:kuery,query:'(CONTAINER_FIELD: CONTAINER_ID) and (FILTER_FIELD:FILTER_VALUE)')"` + `"(language:kuery,query:'(container.id: CONTAINER_ID) and (FILTER_FIELD:FILTER_VALUE)')"` ); expect(searchParams.get('logPosition')).toMatchInlineSnapshot( `"(end:'2019-02-20T14:58:09.404Z',position:(tiebreaker:0,time:1550671089404),start:'2019-02-20T12:58:09.404Z',streamLive:!f)"` @@ -287,7 +287,7 @@ describe('LinkToLogsPage component', () => { const searchParams = new URLSearchParams(history.location.search); expect(searchParams.get('sourceId')).toEqual('default'); expect(searchParams.get('logFilter')).toMatchInlineSnapshot( - `"(language:kuery,query:'POD_FIELD: POD_UID')"` + `"(language:kuery,query:'kubernetes.pod.uid: POD_UID')"` ); expect(searchParams.get('logPosition')).toEqual(null); }); @@ -306,7 +306,7 @@ describe('LinkToLogsPage component', () => { const searchParams = new URLSearchParams(history.location.search); expect(searchParams.get('sourceId')).toEqual('default'); expect(searchParams.get('logFilter')).toMatchInlineSnapshot( - `"(language:kuery,query:'(POD_FIELD: POD_UID) and (FILTER_FIELD:FILTER_VALUE)')"` + `"(language:kuery,query:'(kubernetes.pod.uid: POD_UID) and (FILTER_FIELD:FILTER_VALUE)')"` ); expect(searchParams.get('logPosition')).toMatchInlineSnapshot( `"(end:'2019-02-20T14:58:09.404Z',position:(tiebreaker:0,time:1550671089404),start:'2019-02-20T12:58:09.404Z',streamLive:!f)"` diff --git a/x-pack/plugins/infra/public/pages/link_to/redirect_to_node_logs.tsx b/x-pack/plugins/infra/public/pages/link_to/redirect_to_node_logs.tsx index bc8c5699229d8..a8d339cfe979a 100644 --- a/x-pack/plugins/infra/public/pages/link_to/redirect_to_node_logs.tsx +++ b/x-pack/plugins/infra/public/pages/link_to/redirect_to_node_logs.tsx @@ -34,12 +34,11 @@ export const RedirectToNodeLogs = ({ location, }: RedirectToNodeLogsType) => { const { services } = useKibanaContextForPlugin(); - const { isLoading, loadSource, sourceConfiguration } = useLogSource({ + const { isLoading, loadSource } = useLogSource({ fetch: services.http.fetch, sourceId, indexPatternsService: services.data.indexPatterns, }); - const fields = sourceConfiguration?.configuration.fields; useMount(() => { loadSource(); @@ -57,11 +56,9 @@ export const RedirectToNodeLogs = ({ })} /> ); - } else if (fields == null) { - return null; } - const nodeFilter = `${findInventoryFields(nodeType, fields).id}: ${nodeId}`; + const nodeFilter = `${findInventoryFields(nodeType).id}: ${nodeId}`; const userFilter = getFilterFromLocation(location); const filter = userFilter ? `(${nodeFilter}) and (${userFilter})` : nodeFilter; diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/bottom_drawer.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/bottom_drawer.tsx index 31bc09f9d4dd8..3681d740d93d0 100644 --- a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/bottom_drawer.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/bottom_drawer.tsx @@ -52,6 +52,7 @@ export const BottomDrawer: React.FC<{ aria-expanded={isOpen} iconType={isOpen ? 'arrowDown' : 'arrowRight'} onClick={onClick} + data-test-subj="toggleTimelineButton" > {isOpen ? hideHistory : showHistory} diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/layout.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/layout.tsx index de0a56c5be73d..f46a379f52d50 100644 --- a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/layout.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/layout.tsx @@ -18,7 +18,6 @@ import { PageContent } from '../../../../components/page'; import { useWaffleTimeContext } from '../hooks/use_waffle_time'; import { useWaffleFiltersContext } from '../hooks/use_waffle_filters'; import { DEFAULT_LEGEND, useWaffleOptionsContext } from '../hooks/use_waffle_options'; -import { useSourceContext } from '../../../../containers/metrics_source'; import { InfraFormatterType } from '../../../../lib/lib'; import { euiStyled } from '../../../../../../../../src/plugins/kibana_react/common'; import { Toolbar } from './toolbars/toolbar'; @@ -41,7 +40,6 @@ interface Props { export const Layout = React.memo( ({ shouldLoadDefault, currentView, reload, interval, nodes, loading }: Props) => { const [showLoading, setShowLoading] = useState(true); - const { source } = useSourceContext(); const { metric, groupBy, @@ -65,7 +63,6 @@ export const Layout = React.memo( legend: createLegend(legendPalette, legendSteps, legendReverseColors), metric, sort, - fields: source?.configuration?.fields, groupBy, }; diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/ml/anomaly_detection/anomaly_detection_flyout.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/ml/anomaly_detection/anomaly_detection_flyout.tsx index 4e28fb4202bdc..1fcec291fcc29 100644 --- a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/ml/anomaly_detection/anomaly_detection_flyout.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/ml/anomaly_detection/anomaly_detection_flyout.tsx @@ -67,13 +67,11 @@ export const AnomalyDetectionFlyout = () => { indexPattern={source?.configuration.metricAlias ?? ''} sourceId={'default'} spaceId={space.id} - timestampField={source?.configuration.fields.timestamp ?? ''} > {screenName === 'home' && ( diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/node_details/tabs/logs.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/node_details/tabs/logs.tsx index b792078c394e9..8b5224068589c 100644 --- a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/node_details/tabs/logs.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/node_details/tabs/logs.tsx @@ -25,15 +25,13 @@ const TabComponent = (props: TabProps) => { const endTimestamp = props.currentTime; const startTimestamp = endTimestamp - 60 * 60 * 1000; // 60 minutes const { nodeType } = useWaffleOptionsContext(); - const { options, node } = props; + const { node } = props; const throttledTextQuery = useThrottle(textQuery, textQueryThrottleInterval); const filter = useMemo(() => { const query = [ - ...(options.fields != null - ? [`${findInventoryFields(nodeType, options.fields).id}: "${node.id}"`] - : []), + `${findInventoryFields(nodeType).id}: "${node.id}"`, ...(throttledTextQuery !== '' ? [throttledTextQuery] : []), ].join(' and '); @@ -41,7 +39,7 @@ const TabComponent = (props: TabProps) => { language: 'kuery', query, }; - }, [options.fields, nodeType, node.id, throttledTextQuery]); + }, [nodeType, node.id, throttledTextQuery]); const onQueryChange = useCallback((e: React.ChangeEvent) => { setTextQuery(e.target.value); diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/node_details/tabs/metrics/metrics.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/node_details/tabs/metrics/metrics.tsx index fbb8bd469c1e1..7ff4720aec01e 100644 --- a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/node_details/tabs/metrics/metrics.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/node_details/tabs/metrics/metrics.tsx @@ -71,14 +71,12 @@ const TabComponent = (props: TabProps) => { ]); const { sourceId, createDerivedIndexPattern } = useSourceContext(); const { nodeType, accountId, region, customMetrics } = useWaffleOptionsContext(); - const { currentTime, options, node } = props; + const { currentTime, node } = props; const derivedIndexPattern = useMemo( () => createDerivedIndexPattern('metrics'), [createDerivedIndexPattern] ); - let filter = options.fields - ? `${findInventoryFields(nodeType, options.fields).id}: "${node.id}"` - : ''; + let filter = `${findInventoryFields(nodeType).id}: "${node.id}"`; if (filter) { filter = convertKueryToElasticSearchQuery(filter, derivedIndexPattern); diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/node_details/tabs/processes/index.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/node_details/tabs/processes/index.tsx index c227a31edc4ab..2bed7681b8d56 100644 --- a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/node_details/tabs/processes/index.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/node_details/tabs/processes/index.tsx @@ -17,6 +17,7 @@ import { EuiIconTip, Query, } from '@elastic/eui'; +import { getFieldByType } from '../../../../../../../../common/inventory_models'; import { useProcessList, SortBy, @@ -28,7 +29,7 @@ import { SummaryTable } from './summary_table'; import { ProcessesTable } from './processes_table'; import { parseSearchString } from './parse_search_string'; -const TabComponent = ({ currentTime, node, nodeType, options }: TabProps) => { +const TabComponent = ({ currentTime, node, nodeType }: TabProps) => { const [searchBarState, setSearchBarState] = useState(Query.MATCH_ALL); const [searchFilter, setSearchFilter] = useState(''); const [sortBy, setSortBy] = useState({ @@ -36,22 +37,17 @@ const TabComponent = ({ currentTime, node, nodeType, options }: TabProps) => { isAscending: false, }); - const timefield = options.fields!.timestamp; - const hostTerm = useMemo(() => { - const field = - options.fields && Reflect.has(options.fields, nodeType) - ? Reflect.get(options.fields, nodeType) - : nodeType; + const field = getFieldByType(nodeType) ?? nodeType; return { [field]: node.name }; - }, [options, node, nodeType]); + }, [node, nodeType]); const { loading, error, response, makeRequest: reload, - } = useProcessList(hostTerm, timefield, currentTime, sortBy, parseSearchString(searchFilter)); + } = useProcessList(hostTerm, currentTime, sortBy, parseSearchString(searchFilter)); const debouncedSearchOnChange = useMemo( () => debounce<(queryText: string) => void>((queryText) => setSearchFilter(queryText), 500), @@ -73,7 +69,7 @@ const TabComponent = ({ currentTime, node, nodeType, options }: TabProps) => { return ( - + = ({ interval, yAxisFormatter, isVisible } return ( - + diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/custom_field_panel.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/custom_field_panel.tsx index acc6ae7af2727..7d2a327a50826 100644 --- a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/custom_field_panel.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/custom_field_panel.tsx @@ -55,6 +55,7 @@ export class CustomFieldPanel extends React.PureComponent { fullWidth > { /> { - + {group.name} diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/legend_controls.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/legend_controls.tsx index 06b7739e03c54..bd7d0ad2f2a49 100644 --- a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/legend_controls.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/legend_controls.tsx @@ -84,6 +84,7 @@ export const LegendControls = ({ defaultMessage: 'configure legend', })} onClick={() => setPopoverState(true)} + data-test-subj="openLegendControlsButton" /> ); @@ -131,6 +132,7 @@ export const LegendControls = ({ bounds: { min: draftBounds.min / 100, max: draftBounds.max / 100 }, legend: draftLegend, }); + setPopoverState(false); }, [onChange, draftAuto, draftBounds, draftLegend]); const handleCancelClick = useCallback(() => { @@ -179,7 +181,7 @@ export const LegendControls = ({ : []; return ( - + { {valueMode ? ( - - {value} + + + {value} + ) : ( ellipsisMode && ( @@ -124,11 +128,7 @@ export class Node extends React.PureComponent { {isAlertFlyoutVisible && ( = withTheme return { label: host.ip, value: node.ip }; } } else { - if (options.fields) { - const { id } = findInventoryFields(nodeType, options.fields); - return { - label: {id}, - value: node.id, - }; - } + const { id } = findInventoryFields(nodeType); + return { + label: {id}, + value: node.id, + }; } return { label: '', value: '' }; - }, [nodeType, node.ip, node.id, options.fields]); + }, [nodeType, node.ip, node.id]); const nodeLogsMenuItemLinkProps = useLinkProps( getNodeLogsUrl({ @@ -184,11 +182,7 @@ export const NodeContextMenu: React.FC = withTheme {flyoutVisible && ( { {buttonBody} @@ -147,7 +148,11 @@ export class WaffleGroupByControls extends React.PureComponent { panelPaddingSize="none" closePopover={this.handleClose} > - + ); } diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/waffle_sort_controls.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/waffle_sort_controls.tsx index dfa293041d64a..dd5b1857e1e64 100644 --- a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/waffle_sort_controls.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/waffle_sort_controls.tsx @@ -39,6 +39,7 @@ export const WaffleSortControls = ({ sort, onChange }: Props) => { {label} @@ -59,7 +60,8 @@ export const WaffleSortControls = ({ sort, onChange }: Props) => { ...sort, direction: sort.direction === 'asc' ? 'desc' : 'asc', }); - }, [sort, onChange]); + closePopover(); + }, [closePopover, sort, onChange]); const panels = useMemo( () => [ @@ -71,11 +73,13 @@ export const WaffleSortControls = ({ sort, onChange }: Props) => { name: LABELS.name, icon: sort.by === 'name' ? 'check' : 'empty', onClick: selectName, + 'data-test-subj': 'waffleSortByName', }, { name: LABELS.value, icon: sort.by === 'value' ? 'check' : 'empty', onClick: selectValue, + 'data-test-subj': 'waffleSortByValue', }, ], }, @@ -101,6 +105,7 @@ export const WaffleSortControls = ({ sort, onChange }: Props) => { })} checked={sort.direction === 'desc'} onChange={toggleSort} + data-test-subj={'waffleSortByDirection'} /> diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/hooks/use_process_list.ts b/x-pack/plugins/infra/public/pages/metrics/inventory_view/hooks/use_process_list.ts index e74abb2ecc459..c653bb701eda0 100644 --- a/x-pack/plugins/infra/public/pages/metrics/inventory_view/hooks/use_process_list.ts +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/hooks/use_process_list.ts @@ -22,7 +22,6 @@ export interface SortBy { export function useProcessList( hostTerm: Record, - timefield: string, to: number, sortBy: SortBy, searchFilter: object @@ -51,7 +50,6 @@ export function useProcessList( 'POST', JSON.stringify({ hostTerm, - timefield, indexPattern, to, sortBy: parsedSortBy, @@ -75,15 +73,11 @@ export function useProcessList( }; } -function useProcessListParams(props: { - hostTerm: Record; - timefield: string; - to: number; -}) { - const { hostTerm, timefield, to } = props; +function useProcessListParams(props: { hostTerm: Record; to: number }) { + const { hostTerm, to } = props; const { createDerivedIndexPattern } = useSourceContext(); const indexPattern = createDerivedIndexPattern('metrics').title; - return { hostTerm, indexPattern, timefield, to }; + return { hostTerm, indexPattern, to }; } const ProcessListContext = createContainter(useProcessListParams); export const [ProcessListContextProvider, useProcessListContext] = ProcessListContext; diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/hooks/use_process_list_row_chart.ts b/x-pack/plugins/infra/public/pages/metrics/inventory_view/hooks/use_process_list_row_chart.ts index 30d4e5960ba5e..0d718ffbe210c 100644 --- a/x-pack/plugins/infra/public/pages/metrics/inventory_view/hooks/use_process_list_row_chart.ts +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/hooks/use_process_list_row_chart.ts @@ -25,14 +25,13 @@ export function useProcessListRowChart(command: string) { fold(throwErrors(createPlainError), identity) ); }; - const { hostTerm, timefield, indexPattern, to } = useProcessListContext(); + const { hostTerm, indexPattern, to } = useProcessListContext(); const { error, loading, response, makeRequest } = useHTTPRequest( '/api/metrics/process_list/chart', 'POST', JSON.stringify({ hostTerm, - timefield, indexPattern, to, command, diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/lib/create_uptime_link.test.ts b/x-pack/plugins/infra/public/pages/metrics/inventory_view/lib/create_uptime_link.test.ts index dbe45a387891c..af93f6c0d62ce 100644 --- a/x-pack/plugins/infra/public/pages/metrics/inventory_view/lib/create_uptime_link.test.ts +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/lib/create_uptime_link.test.ts @@ -10,13 +10,6 @@ import { InfraWaffleMapOptions, InfraFormatterType } from '../../../../lib/lib'; import { SnapshotMetricType } from '../../../../../common/inventory_models/types'; const options: InfraWaffleMapOptions = { - fields: { - container: 'container.id', - pod: 'kubernetes.pod.uid', - host: 'host.name', - timestamp: '@timestanp', - tiebreaker: '@timestamp', - }, formatter: InfraFormatterType.percent, formatTemplate: '{{value}}', metric: { type: 'cpu' }, diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/lib/create_uptime_link.ts b/x-pack/plugins/infra/public/pages/metrics/inventory_view/lib/create_uptime_link.ts index 5c02893b867de..b6fa4fe4273ab 100644 --- a/x-pack/plugins/infra/public/pages/metrics/inventory_view/lib/create_uptime_link.ts +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/lib/create_uptime_link.ts @@ -5,9 +5,9 @@ * 2.0. */ -import { get } from 'lodash'; import { InfraWaffleMapNode, InfraWaffleMapOptions } from '../../../../lib/lib'; import { InventoryItemType } from '../../../../../common/inventory_models/types'; +import { getFieldByType } from '../../../../../common/inventory_models'; import { LinkDescriptor } from '../../../../hooks/use_link_props'; export const createUptimeLink = ( @@ -24,7 +24,7 @@ export const createUptimeLink = ( }, }; } - const field = get(options, ['fields', nodeType], ''); + const field = getFieldByType(nodeType); return { app: 'uptime', hash: '/', diff --git a/x-pack/plugins/infra/public/pages/metrics/metric_detail/lib/get_filtered_metrics.ts b/x-pack/plugins/infra/public/pages/metrics/metric_detail/lib/get_filtered_metrics.ts index 2339319926da8..d1ba4502f37c3 100644 --- a/x-pack/plugins/infra/public/pages/metrics/metric_detail/lib/get_filtered_metrics.ts +++ b/x-pack/plugins/infra/public/pages/metrics/metric_detail/lib/get_filtered_metrics.ts @@ -8,6 +8,7 @@ import { InfraMetadataFeature } from '../../../../../common/http_api/metadata_api'; import { InventoryMetric } from '../../../../../common/inventory_models/types'; import { metrics } from '../../../../../common/inventory_models/metrics'; +import { TIMESTAMP_FIELD } from '../../../../../common/constants'; export const getFilteredMetrics = ( requiredMetrics: InventoryMetric[], @@ -20,7 +21,7 @@ export const getFilteredMetrics = ( const metricModelCreator = metrics.tsvb[metric]; // We just need to get a dummy version of the model so we can filter // using the `requires` attribute. - const metricModel = metricModelCreator('@timestamp', 'test', '>=1m'); + const metricModel = metricModelCreator(TIMESTAMP_FIELD, 'test', '>=1m'); return metricMetadata.some((m) => m && metricModel.requires.includes(m)); }); }; diff --git a/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/components/chart_context_menu.tsx b/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/components/chart_context_menu.tsx index 005dd5cc8c078..581eec3e824ae 100644 --- a/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/components/chart_context_menu.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/components/chart_context_menu.tsx @@ -27,6 +27,7 @@ import { import { createTSVBLink } from './helpers/create_tsvb_link'; import { getNodeDetailUrl } from '../../../link_to/redirect_to_node_detail'; import { InventoryItemType } from '../../../../../common/inventory_models/types'; +import { HOST_FIELD, POD_FIELD, CONTAINER_FIELD } from '../../../../../common/constants'; import { useLinkProps } from '../../../../hooks/use_link_props'; export interface Props { @@ -44,13 +45,13 @@ const fieldToNodeType = ( groupBy: string | string[] ): InventoryItemType | undefined => { const fields = Array.isArray(groupBy) ? groupBy : [groupBy]; - if (fields.includes(source.fields.host)) { + if (fields.includes(HOST_FIELD)) { return 'host'; } - if (fields.includes(source.fields.pod)) { + if (fields.includes(POD_FIELD)) { return 'pod'; } - if (fields.includes(source.fields.container)) { + if (fields.includes(CONTAINER_FIELD)) { return 'container'; } }; diff --git a/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/components/helpers/create_tsvb_link.test.ts b/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/components/helpers/create_tsvb_link.test.ts index a9e65bc30a3c6..472e86200cba3 100644 --- a/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/components/helpers/create_tsvb_link.test.ts +++ b/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/components/helpers/create_tsvb_link.test.ts @@ -79,7 +79,7 @@ describe('createTSVBLink()', () => { app: 'visualize', hash: '/create', search: { - _a: "(filters:!(),linked:!f,query:(language:kuery,query:''),uiState:(),vis:(aggs:!(),params:(axis_formatter:number,axis_min:0,axis_position:left,axis_scale:normal,default_index_pattern:'my-beats-*',filter:(language:kuery,query:'host.name : \"example-01\"'),id:test-id,index_pattern:'my-beats-*',interval:auto,series:!((axis_position:right,chart_type:line,color:#6092C0,fill:0,formatter:percent,id:test-id,label:'avg(system.cpu.user.pct)',line_width:2,metrics:!((field:system.cpu.user.pct,id:test-id,type:avg)),point_size:0,separate_axis:0,split_mode:everything,stacked:none,value_template:{{value}})),show_grid:1,show_legend:1,time_field:time,type:timeseries),title:example-01,type:metrics))", + _a: "(filters:!(),linked:!f,query:(language:kuery,query:''),uiState:(),vis:(aggs:!(),params:(axis_formatter:number,axis_min:0,axis_position:left,axis_scale:normal,default_index_pattern:'my-beats-*',filter:(language:kuery,query:'host.name : \"example-01\"'),id:test-id,index_pattern:'my-beats-*',interval:auto,series:!((axis_position:right,chart_type:line,color:#6092C0,fill:0,formatter:percent,id:test-id,label:'avg(system.cpu.user.pct)',line_width:2,metrics:!((field:system.cpu.user.pct,id:test-id,type:avg)),point_size:0,separate_axis:0,split_mode:everything,stacked:none,value_template:{{value}})),show_grid:1,show_legend:1,time_field:'@timestamp',type:timeseries),title:example-01,type:metrics))", _g: '(refreshInterval:(pause:!t,value:0),time:(from:now-1h,to:now))', type: 'metrics', }, @@ -97,7 +97,7 @@ describe('createTSVBLink()', () => { app: 'visualize', hash: '/create', search: { - _a: "(filters:!(),linked:!f,query:(language:kuery,query:''),uiState:(),vis:(aggs:!(),params:(axis_formatter:number,axis_min:0,axis_position:left,axis_scale:normal,default_index_pattern:'my-beats-*',filter:(language:kuery,query:'system.network.name:lo* and host.name : \"example-01\"'),id:test-id,index_pattern:'my-beats-*',interval:auto,series:!((axis_position:right,chart_type:line,color:#6092C0,fill:0,formatter:percent,id:test-id,label:'avg(system.cpu.user.pct)',line_width:2,metrics:!((field:system.cpu.user.pct,id:test-id,type:avg)),point_size:0,separate_axis:0,split_mode:everything,stacked:none,value_template:{{value}})),show_grid:1,show_legend:1,time_field:time,type:timeseries),title:example-01,type:metrics))", + _a: "(filters:!(),linked:!f,query:(language:kuery,query:''),uiState:(),vis:(aggs:!(),params:(axis_formatter:number,axis_min:0,axis_position:left,axis_scale:normal,default_index_pattern:'my-beats-*',filter:(language:kuery,query:'system.network.name:lo* and host.name : \"example-01\"'),id:test-id,index_pattern:'my-beats-*',interval:auto,series:!((axis_position:right,chart_type:line,color:#6092C0,fill:0,formatter:percent,id:test-id,label:'avg(system.cpu.user.pct)',line_width:2,metrics:!((field:system.cpu.user.pct,id:test-id,type:avg)),point_size:0,separate_axis:0,split_mode:everything,stacked:none,value_template:{{value}})),show_grid:1,show_legend:1,time_field:'@timestamp',type:timeseries),title:example-01,type:metrics))", _g: '(refreshInterval:(pause:!t,value:0),time:(from:now-1h,to:now))', type: 'metrics', }, @@ -161,7 +161,7 @@ describe('createTSVBLink()', () => { app: 'visualize', hash: '/create', search: { - _a: "(filters:!(),linked:!f,query:(language:kuery,query:''),uiState:(),vis:(aggs:!(),params:(axis_formatter:number,axis_min:0,axis_position:left,axis_scale:normal,default_index_pattern:'metric*',filter:(language:kuery,query:'host.name : \"example-01\"'),id:test-id,index_pattern:'metric*',interval:auto,series:!((axis_position:right,chart_type:line,color:#6092C0,fill:0,formatter:percent,id:test-id,label:'avg(system.cpu.user.pct)',line_width:2,metrics:!((field:system.cpu.user.pct,id:test-id,type:avg)),point_size:0,separate_axis:0,split_mode:everything,stacked:none,value_template:{{value}})),show_grid:1,show_legend:1,time_field:time,type:timeseries),title:example-01,type:metrics))", + _a: "(filters:!(),linked:!f,query:(language:kuery,query:''),uiState:(),vis:(aggs:!(),params:(axis_formatter:number,axis_min:0,axis_position:left,axis_scale:normal,default_index_pattern:'metric*',filter:(language:kuery,query:'host.name : \"example-01\"'),id:test-id,index_pattern:'metric*',interval:auto,series:!((axis_position:right,chart_type:line,color:#6092C0,fill:0,formatter:percent,id:test-id,label:'avg(system.cpu.user.pct)',line_width:2,metrics:!((field:system.cpu.user.pct,id:test-id,type:avg)),point_size:0,separate_axis:0,split_mode:everything,stacked:none,value_template:{{value}})),show_grid:1,show_legend:1,time_field:'@timestamp',type:timeseries),title:example-01,type:metrics))", _g: '(refreshInterval:(pause:!t,value:0),time:(from:now-1h,to:now))', type: 'metrics', }, diff --git a/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/components/helpers/create_tsvb_link.ts b/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/components/helpers/create_tsvb_link.ts index 84d87ee4ad1b7..5d1f9bafdedaf 100644 --- a/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/components/helpers/create_tsvb_link.ts +++ b/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/components/helpers/create_tsvb_link.ts @@ -8,6 +8,7 @@ import { encode } from 'rison-node'; import uuid from 'uuid'; import { set } from '@elastic/safer-lodash-set'; +import { TIMESTAMP_FIELD } from '../../../../../../common/constants'; import { MetricsSourceConfigurationProperties } from '../../../../../../common/metrics_sources'; import { colorTransformer, Color } from '../../../../../../common/color_palette'; import { MetricsExplorerSeries } from '../../../../../../common/http_api/metrics_explorer'; @@ -169,7 +170,7 @@ export const createTSVBLink = ( series: options.metrics.map(mapMetricToSeries(chartOptions)), show_grid: 1, show_legend: 1, - time_field: (source && source.fields.timestamp) || '@timestamp', + time_field: TIMESTAMP_FIELD, type: 'timeseries', filter: createFilterFromOptions(options, series), }, diff --git a/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/hooks/use_metrics_explorer_data.ts b/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/hooks/use_metrics_explorer_data.ts index c0d0b15217df3..788760a0dfe1c 100644 --- a/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/hooks/use_metrics_explorer_data.ts +++ b/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/hooks/use_metrics_explorer_data.ts @@ -84,7 +84,6 @@ export function useMetricsExplorerData( void 0, timerange: { ...timerange, - field: source.fields.timestamp, from: from.valueOf(), to: to.valueOf(), }, diff --git a/x-pack/plugins/infra/public/utils/logs_overview_fetchers.ts b/x-pack/plugins/infra/public/utils/logs_overview_fetchers.ts index 44f65b9e8071a..6843bc631ce27 100644 --- a/x-pack/plugins/infra/public/utils/logs_overview_fetchers.ts +++ b/x-pack/plugins/infra/public/utils/logs_overview_fetchers.ts @@ -8,7 +8,7 @@ import { encode } from 'rison-node'; import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; import { FetchData, FetchDataParams, LogsFetchDataResponse } from '../../../observability/public'; -import { DEFAULT_SOURCE_ID } from '../../common/constants'; +import { DEFAULT_SOURCE_ID, TIMESTAMP_FIELD } from '../../common/constants'; import { callFetchLogSourceConfigurationAPI } from '../containers/logs/log_source/api/fetch_log_source_configuration'; import { callFetchLogSourceStatusAPI } from '../containers/logs/log_source/api/fetch_log_source_status'; import { InfraClientCoreSetup, InfraClientStartDeps } from '../types'; @@ -30,7 +30,6 @@ interface StatsAggregation { interface LogParams { index: string; - timestampField: string; } type StatsAndSeries = Pick; @@ -63,7 +62,6 @@ export function getLogsOverviewDataFetcher( const { stats, series } = await fetchLogsOverview( { index: resolvedLogSourceConfiguration.indices, - timestampField: resolvedLogSourceConfiguration.timestampField, }, params, data @@ -117,7 +115,7 @@ async function fetchLogsOverview( function buildLogOverviewQuery(logParams: LogParams, params: FetchDataParams) { return { range: { - [logParams.timestampField]: { + [TIMESTAMP_FIELD]: { gt: new Date(params.absoluteTime.start).toISOString(), lte: new Date(params.absoluteTime.end).toISOString(), format: 'strict_date_optional_time', @@ -137,7 +135,7 @@ function buildLogOverviewAggregations(logParams: LogParams, params: FetchDataPar aggs: { series: { date_histogram: { - field: logParams.timestampField, + field: TIMESTAMP_FIELD, fixed_interval: params.intervalString, }, }, diff --git a/x-pack/plugins/infra/public/utils/logs_overview_fetches.test.ts b/x-pack/plugins/infra/public/utils/logs_overview_fetches.test.ts index d0349ab20710f..8a1920f534cd6 100644 --- a/x-pack/plugins/infra/public/utils/logs_overview_fetches.test.ts +++ b/x-pack/plugins/infra/public/utils/logs_overview_fetches.test.ts @@ -150,7 +150,6 @@ describe('Logs UI Observability Homepage Functions', () => { type: 'index_pattern', indexPatternId: 'test-index-pattern', }, - fields: { timestamp: '@timestamp', tiebreaker: '_doc' }, }, }, } as GetLogSourceConfigurationSuccessResponsePayload); diff --git a/x-pack/plugins/infra/server/deprecations.ts b/x-pack/plugins/infra/server/deprecations.ts index 4c2f5f6a9b3d1..5e016bc094826 100644 --- a/x-pack/plugins/infra/server/deprecations.ts +++ b/x-pack/plugins/infra/server/deprecations.ts @@ -13,6 +13,13 @@ import { DeprecationsDetails, GetDeprecationsContext, } from 'src/core/server'; +import { + TIMESTAMP_FIELD, + TIEBREAKER_FIELD, + CONTAINER_FIELD, + HOST_FIELD, + POD_FIELD, +} from '../common/constants'; import { InfraSources } from './lib/sources'; const deprecatedFieldMessage = (fieldName: string, defaultValue: string, configNames: string[]) => @@ -28,11 +35,11 @@ const deprecatedFieldMessage = (fieldName: string, defaultValue: string, configN }); const DEFAULT_VALUES = { - timestamp: '@timestamp', - tiebreaker: '_doc', - container: 'container.id', - host: 'host.name', - pod: 'kubernetes.pod.uid', + timestamp: TIMESTAMP_FIELD, + tiebreaker: TIEBREAKER_FIELD, + container: CONTAINER_FIELD, + host: HOST_FIELD, + pod: POD_FIELD, }; const FIELD_DEPRECATION_FACTORIES: Record DeprecationsDetails> = diff --git a/x-pack/plugins/infra/server/lib/adapters/log_entries/kibana_log_entries_adapter.ts b/x-pack/plugins/infra/server/lib/adapters/log_entries/kibana_log_entries_adapter.ts index 75a86ae654d6c..7e8f5ebfd5af4 100644 --- a/x-pack/plugins/infra/server/lib/adapters/log_entries/kibana_log_entries_adapter.ts +++ b/x-pack/plugins/infra/server/lib/adapters/log_entries/kibana_log_entries_adapter.ts @@ -24,6 +24,7 @@ import { import { SortedSearchHit } from '../framework'; import { KibanaFramework } from '../framework/kibana_framework_adapter'; import { ResolvedLogSourceConfiguration } from '../../../../common/log_sources'; +import { TIMESTAMP_FIELD, TIEBREAKER_FIELD } from '../../../../common/constants'; const TIMESTAMP_FORMAT = 'epoch_millis'; @@ -64,8 +65,8 @@ export class InfraKibanaLogEntriesAdapter implements LogEntriesAdapter { : {}; const sort = { - [resolvedLogSourceConfiguration.timestampField]: sortDirection, - [resolvedLogSourceConfiguration.tiebreakerField]: sortDirection, + [TIMESTAMP_FIELD]: sortDirection, + [TIEBREAKER_FIELD]: sortDirection, }; const esQuery = { @@ -83,7 +84,7 @@ export class InfraKibanaLogEntriesAdapter implements LogEntriesAdapter { ...createFilterClauses(query, highlightQuery), { range: { - [resolvedLogSourceConfiguration.timestampField]: { + [TIMESTAMP_FIELD]: { gte: startTimestamp, lte: endTimestamp, format: TIMESTAMP_FORMAT, @@ -146,7 +147,7 @@ export class InfraKibanaLogEntriesAdapter implements LogEntriesAdapter { aggregations: { count_by_date: { date_range: { - field: resolvedLogSourceConfiguration.timestampField, + field: TIMESTAMP_FIELD, format: TIMESTAMP_FORMAT, ranges: bucketIntervalStarts.map((bucketIntervalStart) => ({ from: bucketIntervalStart.getTime(), @@ -157,10 +158,7 @@ export class InfraKibanaLogEntriesAdapter implements LogEntriesAdapter { top_hits_by_key: { top_hits: { size: 1, - sort: [ - { [resolvedLogSourceConfiguration.timestampField]: 'asc' }, - { [resolvedLogSourceConfiguration.tiebreakerField]: 'asc' }, - ], + sort: [{ [TIMESTAMP_FIELD]: 'asc' }, { [TIEBREAKER_FIELD]: 'asc' }], _source: false, }, }, @@ -173,7 +171,7 @@ export class InfraKibanaLogEntriesAdapter implements LogEntriesAdapter { ...createQueryFilterClauses(filterQuery), { range: { - [resolvedLogSourceConfiguration.timestampField]: { + [TIMESTAMP_FIELD]: { gte: startTimestamp, lte: endTimestamp, format: TIMESTAMP_FORMAT, 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 730da9511dc38..e05a5b647ad2b 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 @@ -8,6 +8,7 @@ import { i18n } from '@kbn/i18n'; import { flatten, get } from 'lodash'; import { KibanaRequest } from 'src/core/server'; +import { TIMESTAMP_FIELD } from '../../../../common/constants'; import { NodeDetailsMetricData } from '../../../../common/http_api/node_details_api'; import { KibanaFramework } from '../framework/kibana_framework_adapter'; import { InfraMetricsAdapter, InfraMetricsRequestOptions } from './adapter_types'; @@ -36,7 +37,7 @@ export class KibanaMetricsAdapter implements InfraMetricsAdapter { rawRequest: KibanaRequest ): Promise { const indexPattern = `${options.sourceConfiguration.metricAlias}`; - const fields = findInventoryFields(options.nodeType, options.sourceConfiguration.fields); + const fields = findInventoryFields(options.nodeType); const nodeField = fields.id; const search = (searchOptions: object) => @@ -122,11 +123,7 @@ export class KibanaMetricsAdapter implements InfraMetricsAdapter { max: options.timerange.to, }; - const model = createTSVBModel( - options.sourceConfiguration.fields.timestamp, - indexPattern, - options.timerange.interval - ); + const model = createTSVBModel(TIMESTAMP_FIELD, indexPattern, options.timerange.interval); const client = ( opts: CallWithRequestParams @@ -137,7 +134,6 @@ export class KibanaMetricsAdapter implements InfraMetricsAdapter { client, { indexPattern: `${options.sourceConfiguration.metricAlias}`, - timestampField: options.sourceConfiguration.fields.timestamp, timerange: options.timerange, }, model.requires diff --git a/x-pack/plugins/infra/server/lib/alerting/log_threshold/mocks/index.ts b/x-pack/plugins/infra/server/lib/alerting/log_threshold/mocks/index.ts index 296a540b4a920..f02dac2139097 100644 --- a/x-pack/plugins/infra/server/lib/alerting/log_threshold/mocks/index.ts +++ b/x-pack/plugins/infra/server/lib/alerting/log_threshold/mocks/index.ts @@ -17,7 +17,6 @@ export const libsMock = { type: 'index_pattern', indexPatternId: 'some-id', }, - fields: { timestamp: '@timestamp' }, }, }); }, diff --git a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/lib/evaluate_alert.ts b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/lib/evaluate_alert.ts index 71c18d9f7cf04..8991c884336d3 100644 --- a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/lib/evaluate_alert.ts +++ b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/lib/evaluate_alert.ts @@ -74,7 +74,6 @@ export const evaluateAlert = { timeSize: 1, } as MetricExpressionParams; - const timefield = '@timestamp'; const groupBy = 'host.doggoname'; const timeframe = { start: moment().subtract(5, 'minutes').valueOf(), @@ -25,7 +24,7 @@ describe("The Metric Threshold Alert's getElasticsearchMetricQuery", () => { }; describe('when passed no filterQuery', () => { - const searchBody = getElasticsearchMetricQuery(expressionParams, timefield, timeframe, groupBy); + const searchBody = getElasticsearchMetricQuery(expressionParams, timeframe, groupBy); test('includes a range filter', () => { expect( searchBody.query.bool.filter.find((filter) => filter.hasOwnProperty('range')) @@ -47,7 +46,6 @@ describe("The Metric Threshold Alert's getElasticsearchMetricQuery", () => { const searchBody = getElasticsearchMetricQuery( expressionParams, - timefield, timeframe, groupBy, filterQuery diff --git a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/lib/metric_query.ts b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/lib/metric_query.ts index 59dc398973f8c..588b77250e6a6 100644 --- a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/lib/metric_query.ts +++ b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/lib/metric_query.ts @@ -5,6 +5,7 @@ * 2.0. */ +import { TIMESTAMP_FIELD } from '../../../../../common/constants'; import { networkTraffic } from '../../../../../common/inventory_models/shared/metrics/snapshot/network_traffic'; import { MetricExpressionParams, Aggregators } from '../types'; import { createPercentileAggregation } from './create_percentile_aggregation'; @@ -21,7 +22,6 @@ const getParsedFilterQuery: (filterQuery: string | undefined) => Record { const body = { size: 0, @@ -22,7 +23,7 @@ export const getProcessList = async ( filter: [ { range: { - [timefield]: { + [TIMESTAMP_FIELD]: { gte: to - 60 * 1000, // 1 minute lte: to, }, @@ -47,7 +48,7 @@ export const getProcessList = async ( size: 1, sort: [ { - [timefield]: { + [TIMESTAMP_FIELD]: { order: 'desc', }, }, @@ -93,7 +94,7 @@ export const getProcessList = async ( size: 1, sort: [ { - [timefield]: { + [TIMESTAMP_FIELD]: { order: 'desc', }, }, diff --git a/x-pack/plugins/infra/server/lib/host_details/process_list_chart.ts b/x-pack/plugins/infra/server/lib/host_details/process_list_chart.ts index 413a97cb7a058..7ff66a80e967b 100644 --- a/x-pack/plugins/infra/server/lib/host_details/process_list_chart.ts +++ b/x-pack/plugins/infra/server/lib/host_details/process_list_chart.ts @@ -6,6 +6,7 @@ */ import { first } from 'lodash'; +import { TIMESTAMP_FIELD } from '../../../common/constants'; import { ProcessListAPIChartRequest, ProcessListAPIChartQueryAggregation, @@ -17,7 +18,7 @@ import { CMDLINE_FIELD } from './common'; export const getProcessListChart = async ( search: ESSearchClient, - { hostTerm, timefield, indexPattern, to, command }: ProcessListAPIChartRequest + { hostTerm, indexPattern, to, command }: ProcessListAPIChartRequest ) => { const body = { size: 0, @@ -26,7 +27,7 @@ export const getProcessListChart = async ( filter: [ { range: { - [timefield]: { + [TIMESTAMP_FIELD]: { gte: to - 60 * 1000, // 1 minute lte: to, }, @@ -60,7 +61,7 @@ export const getProcessListChart = async ( aggs: { timeseries: { date_histogram: { - field: timefield, + field: TIMESTAMP_FIELD, fixed_interval: '1m', extended_bounds: { min: to - 60 * 15 * 1000, // 15 minutes, diff --git a/x-pack/plugins/infra/server/lib/infra_ml/queries/metrics_hosts_anomalies.ts b/x-pack/plugins/infra/server/lib/infra_ml/queries/metrics_hosts_anomalies.ts index ab50986c3b3d5..9c0f4313c6bdb 100644 --- a/x-pack/plugins/infra/server/lib/infra_ml/queries/metrics_hosts_anomalies.ts +++ b/x-pack/plugins/infra/server/lib/infra_ml/queries/metrics_hosts_anomalies.ts @@ -6,6 +6,7 @@ */ import * as rt from 'io-ts'; +import { TIEBREAKER_FIELD } from '../../../../common/constants'; import { ANOMALY_THRESHOLD } from '../../../../common/infra_ml'; import { commonSearchSuccessResponseFieldsRT } from '../../../utils/elasticsearch_runtime_types'; import { @@ -20,9 +21,6 @@ import { import { InfluencerFilter } from '../common'; import { Sort, Pagination } from '../../../../common/http_api/infra_ml'; -// TODO: Reassess validity of this against ML docs -const TIEBREAKER_FIELD = '_doc'; - const sortToMlFieldMap = { dataset: 'partition_field_value', anomalyScore: 'record_score', diff --git a/x-pack/plugins/infra/server/lib/infra_ml/queries/metrics_k8s_anomalies.ts b/x-pack/plugins/infra/server/lib/infra_ml/queries/metrics_k8s_anomalies.ts index 8fb8df5eef3d7..23592aad2e322 100644 --- a/x-pack/plugins/infra/server/lib/infra_ml/queries/metrics_k8s_anomalies.ts +++ b/x-pack/plugins/infra/server/lib/infra_ml/queries/metrics_k8s_anomalies.ts @@ -6,6 +6,7 @@ */ import * as rt from 'io-ts'; +import { TIEBREAKER_FIELD } from '../../../../common/constants'; import { ANOMALY_THRESHOLD } from '../../../../common/infra_ml'; import { commonSearchSuccessResponseFieldsRT } from '../../../utils/elasticsearch_runtime_types'; import { @@ -20,9 +21,6 @@ import { import { InfluencerFilter } from '../common'; import { Sort, Pagination } from '../../../../common/http_api/infra_ml'; -// TODO: Reassess validity of this against ML docs -const TIEBREAKER_FIELD = '_doc'; - const sortToMlFieldMap = { dataset: 'partition_field_value', anomalyScore: 'record_score', diff --git a/x-pack/plugins/infra/server/lib/metrics/index.ts b/x-pack/plugins/infra/server/lib/metrics/index.ts index d291dbf88b49a..c4641e265ea55 100644 --- a/x-pack/plugins/infra/server/lib/metrics/index.ts +++ b/x-pack/plugins/infra/server/lib/metrics/index.ts @@ -7,6 +7,7 @@ import { set } from '@elastic/safer-lodash-set'; import { ThrowReporter } from 'io-ts/lib/ThrowReporter'; +import { TIMESTAMP_FIELD } from '../../../common/constants'; import { MetricsAPIRequest, MetricsAPIResponse, afterKeyObjectRT } from '../../../common/http_api'; import { ESSearchClient, @@ -36,7 +37,7 @@ export const query = async ( const filter: Array> = [ { range: { - [options.timerange.field]: { + [TIMESTAMP_FIELD]: { gte: options.timerange.from, lte: options.timerange.to, format: 'epoch_millis', diff --git a/x-pack/plugins/infra/server/lib/metrics/lib/calculate_interval.ts b/x-pack/plugins/infra/server/lib/metrics/lib/calculate_interval.ts index f6bdfb2de0a29..ee309ad449b2d 100644 --- a/x-pack/plugins/infra/server/lib/metrics/lib/calculate_interval.ts +++ b/x-pack/plugins/infra/server/lib/metrics/lib/calculate_interval.ts @@ -21,7 +21,6 @@ export const calculatedInterval = async (search: ESSearchClient, options: Metric search, { indexPattern: options.indexPattern, - timestampField: options.timerange.field, timerange: { from: options.timerange.from, to: options.timerange.to }, }, options.modules diff --git a/x-pack/plugins/infra/server/lib/metrics/lib/convert_histogram_buckets_to_timeseries.test.ts b/x-pack/plugins/infra/server/lib/metrics/lib/convert_histogram_buckets_to_timeseries.test.ts index b49560f8c25f6..8fe22e6f81d71 100644 --- a/x-pack/plugins/infra/server/lib/metrics/lib/convert_histogram_buckets_to_timeseries.test.ts +++ b/x-pack/plugins/infra/server/lib/metrics/lib/convert_histogram_buckets_to_timeseries.test.ts @@ -13,7 +13,6 @@ const keys = ['example-0']; const options: MetricsAPIRequest = { timerange: { - field: '@timestamp', from: moment('2020-01-01T00:00:00Z').valueOf(), to: moment('2020-01-01T00:00:00Z').add(5, 'minute').valueOf(), interval: '1m', diff --git a/x-pack/plugins/infra/server/lib/metrics/lib/create_aggregations.test.ts b/x-pack/plugins/infra/server/lib/metrics/lib/create_aggregations.test.ts index 91bf544b7e48f..9b92793129d44 100644 --- a/x-pack/plugins/infra/server/lib/metrics/lib/create_aggregations.test.ts +++ b/x-pack/plugins/infra/server/lib/metrics/lib/create_aggregations.test.ts @@ -11,7 +11,6 @@ import { MetricsAPIRequest } from '../../../../common/http_api'; const options: MetricsAPIRequest = { timerange: { - field: '@timestamp', from: moment('2020-01-01T00:00:00Z').valueOf(), to: moment('2020-01-01T01:00:00Z').valueOf(), interval: '>=1m', diff --git a/x-pack/plugins/infra/server/lib/metrics/lib/create_aggregations.ts b/x-pack/plugins/infra/server/lib/metrics/lib/create_aggregations.ts index 65cd4ebe2d501..769ccce409e65 100644 --- a/x-pack/plugins/infra/server/lib/metrics/lib/create_aggregations.ts +++ b/x-pack/plugins/infra/server/lib/metrics/lib/create_aggregations.ts @@ -5,6 +5,7 @@ * 2.0. */ +import { TIMESTAMP_FIELD } from '../../../../common/constants'; import { MetricsAPIRequest } from '../../../../common/http_api/metrics_api'; import { calculateDateHistogramOffset } from './calculate_date_histogram_offset'; import { createMetricsAggregations } from './create_metrics_aggregations'; @@ -15,7 +16,7 @@ export const createAggregations = (options: MetricsAPIRequest) => { const histogramAggregation = { histogram: { date_histogram: { - field: options.timerange.field, + field: TIMESTAMP_FIELD, fixed_interval: intervalString, offset: options.alignDataToEnd ? calculateDateHistogramOffset(options.timerange) : '0s', extended_bounds: { diff --git a/x-pack/plugins/infra/server/lib/metrics/lib/create_metrics_aggregations.test.ts b/x-pack/plugins/infra/server/lib/metrics/lib/create_metrics_aggregations.test.ts index 27fe491d3964b..2e2d1736e5925 100644 --- a/x-pack/plugins/infra/server/lib/metrics/lib/create_metrics_aggregations.test.ts +++ b/x-pack/plugins/infra/server/lib/metrics/lib/create_metrics_aggregations.test.ts @@ -11,7 +11,6 @@ import { createMetricsAggregations } from './create_metrics_aggregations'; const options: MetricsAPIRequest = { timerange: { - field: '@timestamp', from: moment('2020-01-01T00:00:00Z').valueOf(), to: moment('2020-01-01T01:00:00Z').valueOf(), interval: '>=1m', diff --git a/x-pack/plugins/infra/server/lib/sources/defaults.ts b/x-pack/plugins/infra/server/lib/sources/defaults.ts index b6139613cfce3..db262a432b3fc 100644 --- a/x-pack/plugins/infra/server/lib/sources/defaults.ts +++ b/x-pack/plugins/infra/server/lib/sources/defaults.ts @@ -5,11 +5,7 @@ * 2.0. */ -import { - METRICS_INDEX_PATTERN, - LOGS_INDEX_PATTERN, - TIMESTAMP_FIELD, -} from '../../../common/constants'; +import { METRICS_INDEX_PATTERN, LOGS_INDEX_PATTERN } from '../../../common/constants'; import { InfraSourceConfiguration } from '../../../common/source_configuration/source_configuration'; export const defaultSourceConfiguration: InfraSourceConfiguration = { @@ -21,12 +17,7 @@ export const defaultSourceConfiguration: InfraSourceConfiguration = { indexName: LOGS_INDEX_PATTERN, }, fields: { - container: 'container.id', - host: 'host.name', message: ['message', '@message'], - pod: 'kubernetes.pod.uid', - tiebreaker: '_doc', - timestamp: TIMESTAMP_FIELD, }, inventoryDefaultView: '0', metricsExplorerDefaultView: '0', diff --git a/x-pack/plugins/infra/server/lib/sources/saved_object_references.test.ts b/x-pack/plugins/infra/server/lib/sources/saved_object_references.test.ts index 9f6f9cd284c67..fb550390e25be 100644 --- a/x-pack/plugins/infra/server/lib/sources/saved_object_references.test.ts +++ b/x-pack/plugins/infra/server/lib/sources/saved_object_references.test.ts @@ -101,12 +101,7 @@ const sourceConfigurationWithIndexPatternReference: InfraSourceConfiguration = { name: 'NAME', description: 'DESCRIPTION', fields: { - container: 'CONTAINER_FIELD', - host: 'HOST_FIELD', message: ['MESSAGE_FIELD'], - pod: 'POD_FIELD', - tiebreaker: 'TIEBREAKER_FIELD', - timestamp: 'TIMESTAMP_FIELD', }, logColumns: [], logIndices: { diff --git a/x-pack/plugins/infra/server/lib/sources/sources.test.ts b/x-pack/plugins/infra/server/lib/sources/sources.test.ts index 904f51d12673f..396d2c22a100f 100644 --- a/x-pack/plugins/infra/server/lib/sources/sources.test.ts +++ b/x-pack/plugins/infra/server/lib/sources/sources.test.ts @@ -24,13 +24,6 @@ describe('the InfraSources lib', () => { attributes: { metricAlias: 'METRIC_ALIAS', logIndices: { type: 'index_pattern', indexPatternId: 'log_index_pattern_0' }, - fields: { - container: 'CONTAINER', - host: 'HOST', - pod: 'POD', - tiebreaker: 'TIEBREAKER', - timestamp: 'TIMESTAMP', - }, }, references: [ { @@ -50,13 +43,6 @@ describe('the InfraSources lib', () => { configuration: { metricAlias: 'METRIC_ALIAS', logIndices: { type: 'index_pattern', indexPatternId: 'LOG_INDEX_PATTERN' }, - fields: { - container: 'CONTAINER', - host: 'HOST', - pod: 'POD', - tiebreaker: 'TIEBREAKER', - timestamp: 'TIMESTAMP', - }, }, }); }); @@ -67,12 +53,6 @@ describe('the InfraSources lib', () => { default: { metricAlias: 'METRIC_ALIAS', logIndices: { type: 'index_pattern', indexPatternId: 'LOG_ALIAS' }, - fields: { - host: 'HOST', - pod: 'POD', - tiebreaker: 'TIEBREAKER', - timestamp: 'TIMESTAMP', - }, }, }), }); @@ -82,11 +62,7 @@ describe('the InfraSources lib', () => { version: 'foo', type: infraSourceConfigurationSavedObjectName, updated_at: '2000-01-01T00:00:00.000Z', - attributes: { - fields: { - container: 'CONTAINER', - }, - }, + attributes: {}, references: [], }); @@ -99,13 +75,6 @@ describe('the InfraSources lib', () => { configuration: { metricAlias: 'METRIC_ALIAS', logIndices: { type: 'index_pattern', indexPatternId: 'LOG_ALIAS' }, - fields: { - container: 'CONTAINER', - host: 'HOST', - pod: 'POD', - tiebreaker: 'TIEBREAKER', - timestamp: 'TIMESTAMP', - }, }, }); }); @@ -133,13 +102,6 @@ describe('the InfraSources lib', () => { configuration: { metricAlias: expect.any(String), logIndices: expect.any(Object), - fields: { - container: expect.any(String), - host: expect.any(String), - pod: expect.any(String), - tiebreaker: expect.any(String), - timestamp: expect.any(String), - }, }, }); }); diff --git a/x-pack/plugins/infra/server/plugin.ts b/x-pack/plugins/infra/server/plugin.ts index 26d9f115405a6..4e655f200d94f 100644 --- a/x-pack/plugins/infra/server/plugin.ts +++ b/x-pack/plugins/infra/server/plugin.ts @@ -53,12 +53,7 @@ export const config: PluginConfigDescriptor = { schema.object({ fields: schema.maybe( schema.object({ - timestamp: schema.maybe(schema.string()), message: schema.maybe(schema.arrayOf(schema.string())), - tiebreaker: schema.maybe(schema.string()), - host: schema.maybe(schema.string()), - container: schema.maybe(schema.string()), - pod: schema.maybe(schema.string()), }) ), }) diff --git a/x-pack/plugins/infra/server/routes/inventory_metadata/lib/get_cloud_metadata.ts b/x-pack/plugins/infra/server/routes/inventory_metadata/lib/get_cloud_metadata.ts index c721ca75ea978..5c4ae1981c5cd 100644 --- a/x-pack/plugins/infra/server/routes/inventory_metadata/lib/get_cloud_metadata.ts +++ b/x-pack/plugins/infra/server/routes/inventory_metadata/lib/get_cloud_metadata.ts @@ -5,6 +5,7 @@ * 2.0. */ +import { TIMESTAMP_FIELD } from '../../../../common/constants'; import { InventoryCloudAccount } from '../../../../common/http_api/inventory_meta_api'; import { InfraMetadataAggregationResponse, @@ -49,7 +50,7 @@ export const getCloudMetadata = async ( must: [ { range: { - [sourceConfiguration.fields.timestamp]: { + [TIMESTAMP_FIELD]: { gte: currentTime - 86400000, // 24 hours ago lte: currentTime, format: 'epoch_millis', diff --git a/x-pack/plugins/infra/server/routes/metadata/lib/get_cloud_metric_metadata.ts b/x-pack/plugins/infra/server/routes/metadata/lib/get_cloud_metric_metadata.ts index d9da7bce2246f..126d1485cb702 100644 --- a/x-pack/plugins/infra/server/routes/metadata/lib/get_cloud_metric_metadata.ts +++ b/x-pack/plugins/infra/server/routes/metadata/lib/get_cloud_metric_metadata.ts @@ -13,6 +13,7 @@ import { import { KibanaFramework } from '../../../lib/adapters/framework/kibana_framework_adapter'; import { InfraSourceConfiguration } from '../../../lib/sources'; import { CLOUD_METRICS_MODULES } from '../../../lib/constants'; +import { TIMESTAMP_FIELD } from '../../../../common/constants'; export interface InfraCloudMetricsAdapterResponse { buckets: InfraMetadataAggregationBucket[]; @@ -36,7 +37,7 @@ export const getCloudMetricsMetadata = async ( { match: { 'cloud.instance.id': instanceId } }, { range: { - [sourceConfiguration.fields.timestamp]: { + [TIMESTAMP_FIELD]: { gte: timeRange.from, lte: timeRange.to, format: 'epoch_millis', diff --git a/x-pack/plugins/infra/server/routes/metadata/lib/get_metric_metadata.ts b/x-pack/plugins/infra/server/routes/metadata/lib/get_metric_metadata.ts index bfa0884bfe199..1962a24f7d4db 100644 --- a/x-pack/plugins/infra/server/routes/metadata/lib/get_metric_metadata.ts +++ b/x-pack/plugins/infra/server/routes/metadata/lib/get_metric_metadata.ts @@ -15,6 +15,7 @@ import { KibanaFramework } from '../../../lib/adapters/framework/kibana_framewor import { InfraSourceConfiguration } from '../../../lib/sources'; import { findInventoryFields } from '../../../../common/inventory_models'; import { InventoryItemType } from '../../../../common/inventory_models/types'; +import { TIMESTAMP_FIELD } from '../../../../common/constants'; export interface InfraMetricsAdapterResponse { id: string; @@ -30,7 +31,7 @@ export const getMetricMetadata = async ( nodeType: InventoryItemType, timeRange: { from: number; to: number } ): Promise => { - const fields = findInventoryFields(nodeType, sourceConfiguration.fields); + const fields = findInventoryFields(nodeType); const metricQuery = { allow_no_indices: true, ignore_unavailable: true, @@ -45,7 +46,7 @@ export const getMetricMetadata = async ( }, { range: { - [sourceConfiguration.fields.timestamp]: { + [TIMESTAMP_FIELD]: { gte: timeRange.from, lte: timeRange.to, format: 'epoch_millis', diff --git a/x-pack/plugins/infra/server/routes/metadata/lib/get_node_info.ts b/x-pack/plugins/infra/server/routes/metadata/lib/get_node_info.ts index 94becdf6d2811..97a0707a4c215 100644 --- a/x-pack/plugins/infra/server/routes/metadata/lib/get_node_info.ts +++ b/x-pack/plugins/infra/server/routes/metadata/lib/get_node_info.ts @@ -15,6 +15,7 @@ import { getPodNodeName } from './get_pod_node_name'; import { CLOUD_METRICS_MODULES } from '../../../lib/constants'; import { findInventoryFields } from '../../../../common/inventory_models'; import { InventoryItemType } from '../../../../common/inventory_models/types'; +import { TIMESTAMP_FIELD } from '../../../../common/constants'; export const getNodeInfo = async ( framework: KibanaFramework, @@ -50,8 +51,7 @@ export const getNodeInfo = async ( } return {}; } - const fields = findInventoryFields(nodeType, sourceConfiguration.fields); - const timestampField = sourceConfiguration.fields.timestamp; + const fields = findInventoryFields(nodeType); const params = { allow_no_indices: true, ignore_unavailable: true, @@ -60,14 +60,14 @@ export const getNodeInfo = async ( body: { size: 1, _source: ['host.*', 'cloud.*', 'agent.*'], - sort: [{ [timestampField]: 'desc' }], + sort: [{ [TIMESTAMP_FIELD]: 'desc' }], query: { bool: { filter: [ { match: { [fields.id]: nodeId } }, { range: { - [timestampField]: { + [TIMESTAMP_FIELD]: { gte: timeRange.from, lte: timeRange.to, format: 'epoch_millis', diff --git a/x-pack/plugins/infra/server/routes/metadata/lib/get_pod_node_name.ts b/x-pack/plugins/infra/server/routes/metadata/lib/get_pod_node_name.ts index 164d94d9f692f..3afb6a8abcb58 100644 --- a/x-pack/plugins/infra/server/routes/metadata/lib/get_pod_node_name.ts +++ b/x-pack/plugins/infra/server/routes/metadata/lib/get_pod_node_name.ts @@ -10,6 +10,7 @@ import { KibanaFramework } from '../../../lib/adapters/framework/kibana_framewor import { InfraSourceConfiguration } from '../../../lib/sources'; import { findInventoryFields } from '../../../../common/inventory_models'; import type { InfraPluginRequestHandlerContext } from '../../../types'; +import { TIMESTAMP_FIELD } from '../../../../common/constants'; export const getPodNodeName = async ( framework: KibanaFramework, @@ -19,8 +20,7 @@ export const getPodNodeName = async ( nodeType: 'host' | 'pod' | 'container', timeRange: { from: number; to: number } ): Promise => { - const fields = findInventoryFields(nodeType, sourceConfiguration.fields); - const timestampField = sourceConfiguration.fields.timestamp; + const fields = findInventoryFields(nodeType); const params = { allow_no_indices: true, ignore_unavailable: true, @@ -29,7 +29,7 @@ export const getPodNodeName = async ( body: { size: 1, _source: ['kubernetes.node.name'], - sort: [{ [timestampField]: 'desc' }], + sort: [{ [TIMESTAMP_FIELD]: 'desc' }], query: { bool: { filter: [ @@ -37,7 +37,7 @@ export const getPodNodeName = async ( { exists: { field: `kubernetes.node.name` } }, { range: { - [timestampField]: { + [TIMESTAMP_FIELD]: { gte: timeRange.from, lte: timeRange.to, format: 'epoch_millis', diff --git a/x-pack/plugins/infra/server/routes/metrics_explorer/lib/convert_request_to_metrics_api_options.test.ts b/x-pack/plugins/infra/server/routes/metrics_explorer/lib/convert_request_to_metrics_api_options.test.ts index 539e9a1fee6ef..a6848e4f7a2dd 100644 --- a/x-pack/plugins/infra/server/routes/metrics_explorer/lib/convert_request_to_metrics_api_options.test.ts +++ b/x-pack/plugins/infra/server/routes/metrics_explorer/lib/convert_request_to_metrics_api_options.test.ts @@ -10,7 +10,6 @@ import { convertRequestToMetricsAPIOptions } from './convert_request_to_metrics_ const BASE_REQUEST: MetricsExplorerRequestBody = { timerange: { - field: '@timestamp', from: new Date('2020-01-01T00:00:00Z').getTime(), to: new Date('2020-01-01T01:00:00Z').getTime(), interval: '1m', @@ -22,7 +21,6 @@ const BASE_REQUEST: MetricsExplorerRequestBody = { const BASE_METRICS_UI_OPTIONS: MetricsAPIRequest = { timerange: { - field: '@timestamp', from: new Date('2020-01-01T00:00:00Z').getTime(), to: new Date('2020-01-01T01:00:00Z').getTime(), interval: '1m', diff --git a/x-pack/plugins/infra/server/routes/metrics_explorer/lib/find_interval_for_metrics.ts b/x-pack/plugins/infra/server/routes/metrics_explorer/lib/find_interval_for_metrics.ts index 9ca8c085eac44..62e99cf8ffd32 100644 --- a/x-pack/plugins/infra/server/routes/metrics_explorer/lib/find_interval_for_metrics.ts +++ b/x-pack/plugins/infra/server/routes/metrics_explorer/lib/find_interval_for_metrics.ts @@ -44,7 +44,6 @@ export const findIntervalForMetrics = async ( client, { indexPattern: options.indexPattern, - timestampField: options.timerange.field, timerange: options.timerange, }, modules.filter(Boolean) as string[] diff --git a/x-pack/plugins/infra/server/routes/metrics_explorer/lib/get_dataset_for_field.ts b/x-pack/plugins/infra/server/routes/metrics_explorer/lib/get_dataset_for_field.ts index 640d62c366726..97154a7361c96 100644 --- a/x-pack/plugins/infra/server/routes/metrics_explorer/lib/get_dataset_for_field.ts +++ b/x-pack/plugins/infra/server/routes/metrics_explorer/lib/get_dataset_for_field.ts @@ -5,6 +5,7 @@ * 2.0. */ +import { TIMESTAMP_FIELD } from '../../../../common/constants'; import { ESSearchClient } from '../../../lib/metrics/types'; interface EventDatasetHit { @@ -19,7 +20,7 @@ export const getDatasetForField = async ( client: ESSearchClient, field: string, indexPattern: string, - timerange: { field: string; to: number; from: number } + timerange: { to: number; from: number } ) => { const params = { allow_no_indices: true, @@ -33,7 +34,7 @@ export const getDatasetForField = async ( { exists: { field } }, { range: { - [timerange.field]: { + [TIMESTAMP_FIELD]: { gte: timerange.from, lte: timerange.to, format: 'epoch_millis', @@ -45,7 +46,7 @@ export const getDatasetForField = async ( }, size: 1, _source: ['event.dataset'], - sort: [{ [timerange.field]: { order: 'desc' } }], + sort: [{ [TIMESTAMP_FIELD]: { order: 'desc' } }], }, }; diff --git a/x-pack/plugins/infra/server/routes/metrics_explorer/lib/query_total_groupings.ts b/x-pack/plugins/infra/server/routes/metrics_explorer/lib/query_total_groupings.ts index a2bf778d5016d..b2e22752609c1 100644 --- a/x-pack/plugins/infra/server/routes/metrics_explorer/lib/query_total_groupings.ts +++ b/x-pack/plugins/infra/server/routes/metrics_explorer/lib/query_total_groupings.ts @@ -6,6 +6,7 @@ */ import { isArray } from 'lodash'; +import { TIMESTAMP_FIELD } from '../../../../common/constants'; import { MetricsAPIRequest } from '../../../../common/http_api'; import { ESSearchClient } from '../../../lib/metrics/types'; @@ -26,7 +27,7 @@ export const queryTotalGroupings = async ( let filters: Array> = [ { range: { - [options.timerange.field]: { + [TIMESTAMP_FIELD]: { gte: options.timerange.from, lte: options.timerange.to, format: 'epoch_millis', diff --git a/x-pack/plugins/infra/server/routes/overview/lib/create_top_nodes_query.ts b/x-pack/plugins/infra/server/routes/overview/lib/create_top_nodes_query.ts index 7533f2801607c..ccead528749cd 100644 --- a/x-pack/plugins/infra/server/routes/overview/lib/create_top_nodes_query.ts +++ b/x-pack/plugins/infra/server/routes/overview/lib/create_top_nodes_query.ts @@ -7,6 +7,7 @@ import { MetricsSourceConfiguration } from '../../../../common/metrics_sources'; import { TopNodesRequest } from '../../../../common/http_api/overview_api'; +import { TIMESTAMP_FIELD } from '../../../../common/constants'; export const createTopNodesQuery = ( options: TopNodesRequest, @@ -22,7 +23,7 @@ export const createTopNodesQuery = ( filter: [ { range: { - [source.configuration.fields.timestamp]: { + [TIMESTAMP_FIELD]: { gte: options.timerange.from, lte: options.timerange.to, }, @@ -49,7 +50,7 @@ export const createTopNodesQuery = ( { field: 'host.name' }, { field: 'cloud.provider' }, ], - sort: { '@timestamp': 'desc' }, + sort: { [TIMESTAMP_FIELD]: 'desc' }, size: 1, }, }, diff --git a/x-pack/plugins/infra/server/routes/snapshot/lib/apply_metadata_to_last_path.ts b/x-pack/plugins/infra/server/routes/snapshot/lib/apply_metadata_to_last_path.ts index 7de63ae59a329..2931555fc06b0 100644 --- a/x-pack/plugins/infra/server/routes/snapshot/lib/apply_metadata_to_last_path.ts +++ b/x-pack/plugins/infra/server/routes/snapshot/lib/apply_metadata_to_last_path.ts @@ -42,10 +42,7 @@ export const applyMetadataToLastPath = ( if (firstMetaDoc && lastPath) { // We will need the inventory fields so we can use the field paths to get // the values from the metadata document - const inventoryFields = findInventoryFields( - snapshotRequest.nodeType, - source.configuration.fields - ); + const inventoryFields = findInventoryFields(snapshotRequest.nodeType); // Set the label as the name and fallback to the id OR path.value lastPath.label = (firstMetaDoc[inventoryFields.name] ?? lastPath.value) as string; // If the inventory fields contain an ip address, we need to try and set that diff --git a/x-pack/plugins/infra/server/routes/snapshot/lib/create_timerange_with_interval.ts b/x-pack/plugins/infra/server/routes/snapshot/lib/create_timerange_with_interval.ts index 7473907b7410b..bf6e51b9fe94f 100644 --- a/x-pack/plugins/infra/server/routes/snapshot/lib/create_timerange_with_interval.ts +++ b/x-pack/plugins/infra/server/routes/snapshot/lib/create_timerange_with_interval.ts @@ -25,7 +25,6 @@ const createInterval = async (client: ESSearchClient, options: InfraSnapshotRequ client, { indexPattern: options.sourceConfiguration.metricAlias, - timestampField: options.sourceConfiguration.fields.timestamp, timerange: { from: timerange.from, to: timerange.to }, }, modules, @@ -81,7 +80,6 @@ const aggregationsToModules = async ( async (field) => await getDatasetForField(client, field as string, options.sourceConfiguration.metricAlias, { ...options.timerange, - field: options.sourceConfiguration.fields.timestamp, }) ) ); diff --git a/x-pack/plugins/infra/server/routes/snapshot/lib/get_nodes.ts b/x-pack/plugins/infra/server/routes/snapshot/lib/get_nodes.ts index f59756e0c5b25..a3ca2cfd683bb 100644 --- a/x-pack/plugins/infra/server/routes/snapshot/lib/get_nodes.ts +++ b/x-pack/plugins/infra/server/routes/snapshot/lib/get_nodes.ts @@ -16,7 +16,6 @@ import { LogQueryFields } from '../../../services/log_queries/get_log_query_fiel export interface SourceOverrides { indexPattern: string; - timestamp: string; } const transformAndQueryData = async ({ diff --git a/x-pack/plugins/infra/server/routes/snapshot/lib/transform_request_to_metrics_api_request.test.ts b/x-pack/plugins/infra/server/routes/snapshot/lib/transform_request_to_metrics_api_request.test.ts index b4e6983a09900..aac5f9e145022 100644 --- a/x-pack/plugins/infra/server/routes/snapshot/lib/transform_request_to_metrics_api_request.test.ts +++ b/x-pack/plugins/infra/server/routes/snapshot/lib/transform_request_to_metrics_api_request.test.ts @@ -47,12 +47,7 @@ const source: InfraSource = { indexPatternId: 'kibana_index_pattern', }, fields: { - container: 'container.id', - host: 'host.name', message: ['message', '@message'], - pod: 'kubernetes.pod.uid', - tiebreaker: '_doc', - timestamp: '@timestamp', }, inventoryDefaultView: '0', metricsExplorerDefaultView: '0', @@ -80,7 +75,7 @@ const snapshotRequest: SnapshotRequest = { const metricsApiRequest = { indexPattern: 'metrics-*,metricbeat-*', - timerange: { field: '@timestamp', from: 1605705900000, to: 1605706200000, interval: '60s' }, + timerange: { from: 1605705900000, to: 1605706200000, interval: '60s' }, metrics: [ { id: 'cpu', diff --git a/x-pack/plugins/infra/server/routes/snapshot/lib/transform_request_to_metrics_api_request.ts b/x-pack/plugins/infra/server/routes/snapshot/lib/transform_request_to_metrics_api_request.ts index 3901c8677ae9b..b7e389cae9126 100644 --- a/x-pack/plugins/infra/server/routes/snapshot/lib/transform_request_to_metrics_api_request.ts +++ b/x-pack/plugins/infra/server/routes/snapshot/lib/transform_request_to_metrics_api_request.ts @@ -5,6 +5,7 @@ * 2.0. */ +import { TIMESTAMP_FIELD } from '../../../../common/constants'; import { findInventoryFields, findInventoryModel } from '../../../../common/inventory_models'; import { MetricsAPIRequest, SnapshotRequest } from '../../../../common/http_api'; import { ESSearchClient } from '../../../lib/metrics/types'; @@ -37,7 +38,6 @@ export const transformRequestToMetricsAPIRequest = async ({ const metricsApiRequest: MetricsAPIRequest = { indexPattern: sourceOverrides?.indexPattern ?? source.configuration.metricAlias, timerange: { - field: sourceOverrides?.timestamp ?? source.configuration.fields.timestamp, from: timeRangeWithIntervalApplied.from, to: timeRangeWithIntervalApplied.to, interval: timeRangeWithIntervalApplied.interval, @@ -69,10 +69,7 @@ export const transformRequestToMetricsAPIRequest = async ({ inventoryModel.nodeFilter?.forEach((f) => filters.push(f)); } - const inventoryFields = findInventoryFields( - snapshotRequest.nodeType, - source.configuration.fields - ); + const inventoryFields = findInventoryFields(snapshotRequest.nodeType); if (snapshotRequest.groupBy) { const groupBy = snapshotRequest.groupBy.map((g) => g.field).filter(Boolean) as string[]; metricsApiRequest.groupBy = [...groupBy, inventoryFields.id]; @@ -86,7 +83,7 @@ export const transformRequestToMetricsAPIRequest = async ({ size: 1, metrics: [{ field: inventoryFields.name }], sort: { - [source.configuration.fields.timestamp]: 'desc', + [TIMESTAMP_FIELD]: 'desc', }, }, }, diff --git a/x-pack/plugins/infra/server/services/log_entries/log_entries_search_strategy.test.ts b/x-pack/plugins/infra/server/services/log_entries/log_entries_search_strategy.test.ts index b0d2eeb987861..e48c990d7822f 100644 --- a/x-pack/plugins/infra/server/services/log_entries/log_entries_search_strategy.test.ts +++ b/x-pack/plugins/infra/server/services/log_entries/log_entries_search_strategy.test.ts @@ -289,12 +289,7 @@ const createSourceConfigurationMock = (): InfraSource => ({ }, ], fields: { - pod: 'POD_FIELD', - host: 'HOST_FIELD', - container: 'CONTAINER_FIELD', message: ['MESSAGE_FIELD'], - timestamp: 'TIMESTAMP_FIELD', - tiebreaker: 'TIEBREAKER_FIELD', }, anomalyThreshold: 20, }, diff --git a/x-pack/plugins/infra/server/services/log_entries/log_entry_search_strategy.test.ts b/x-pack/plugins/infra/server/services/log_entries/log_entry_search_strategy.test.ts index 1f03878ba6feb..685f11cb00a86 100644 --- a/x-pack/plugins/infra/server/services/log_entries/log_entry_search_strategy.test.ts +++ b/x-pack/plugins/infra/server/services/log_entries/log_entry_search_strategy.test.ts @@ -244,12 +244,7 @@ const createSourceConfigurationMock = (): InfraSource => ({ metricsExplorerDefaultView: 'DEFAULT_VIEW', logColumns: [], fields: { - pod: 'POD_FIELD', - host: 'HOST_FIELD', - container: 'CONTAINER_FIELD', message: ['MESSAGE_FIELD'], - timestamp: 'TIMESTAMP_FIELD', - tiebreaker: 'TIEBREAKER_FIELD', }, anomalyThreshold: 20, }, diff --git a/x-pack/plugins/infra/server/services/log_queries/get_log_query_fields.ts b/x-pack/plugins/infra/server/services/log_queries/get_log_query_fields.ts index 55491db97dbd6..db1696854db83 100644 --- a/x-pack/plugins/infra/server/services/log_queries/get_log_query_fields.ts +++ b/x-pack/plugins/infra/server/services/log_queries/get_log_query_fields.ts @@ -12,7 +12,6 @@ import { KibanaFramework } from '../../lib/adapters/framework/kibana_framework_a export interface LogQueryFields { indexPattern: string; - timestamp: string; } export const createGetLogQueryFields = (sources: InfraSources, framework: KibanaFramework) => { @@ -29,7 +28,6 @@ export const createGetLogQueryFields = (sources: InfraSources, framework: Kibana return { indexPattern: resolvedLogSourceConfiguration.indices, - timestamp: resolvedLogSourceConfiguration.timestampField, }; }; }; diff --git a/x-pack/plugins/infra/server/utils/calculate_metric_interval.ts b/x-pack/plugins/infra/server/utils/calculate_metric_interval.ts index 3357b1a842183..cb754153c6615 100644 --- a/x-pack/plugins/infra/server/utils/calculate_metric_interval.ts +++ b/x-pack/plugins/infra/server/utils/calculate_metric_interval.ts @@ -5,6 +5,7 @@ * 2.0. */ +import { TIMESTAMP_FIELD } from '../../common/constants'; import { findInventoryModel } from '../../common/inventory_models'; // import { KibanaFramework } from '../lib/adapters/framework/kibana_framework_adapter'; import { InventoryItemType } from '../../common/inventory_models/types'; @@ -12,7 +13,6 @@ import { ESSearchClient } from '../lib/metrics/types'; interface Options { indexPattern: string; - timestampField: string; timerange: { from: number; to: number; @@ -44,7 +44,7 @@ export const calculateMetricInterval = async ( filter: [ { range: { - [options.timestampField]: { + [TIMESTAMP_FIELD]: { gte: from, lte: options.timerange.to, format: 'epoch_millis', diff --git a/x-pack/plugins/ingest_pipelines/__jest__/client_integration/helpers/pipelines_clone.helpers.ts b/x-pack/plugins/ingest_pipelines/__jest__/client_integration/helpers/pipelines_clone.helpers.ts index cf30870cefbbd..51f6d9bd96bd6 100644 --- a/x-pack/plugins/ingest_pipelines/__jest__/client_integration/helpers/pipelines_clone.helpers.ts +++ b/x-pack/plugins/ingest_pipelines/__jest__/client_integration/helpers/pipelines_clone.helpers.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { registerTestBed, TestBedConfig, TestBed } from '@kbn/test/jest'; +import { registerTestBed, AsyncTestBedConfig, TestBed } from '@kbn/test/jest'; import { PipelinesClone } from '../../../public/application/sections/pipelines_clone'; import { getFormActions, PipelineFormTestSubjects } from './pipeline_form.helpers'; import { WithAppDependencies } from './setup_environment'; @@ -28,7 +28,7 @@ export const PIPELINE_TO_CLONE = { ], }; -const testBedConfig: TestBedConfig = { +const testBedConfig: AsyncTestBedConfig = { memoryRouter: { initialEntries: [getClonePath({ clonedPipelineName: PIPELINE_TO_CLONE.name })], componentRoutePath: ROUTES.clone, diff --git a/x-pack/plugins/ingest_pipelines/__jest__/client_integration/helpers/pipelines_create.helpers.ts b/x-pack/plugins/ingest_pipelines/__jest__/client_integration/helpers/pipelines_create.helpers.ts index 06c880bbceda4..faf1b42042ec1 100644 --- a/x-pack/plugins/ingest_pipelines/__jest__/client_integration/helpers/pipelines_create.helpers.ts +++ b/x-pack/plugins/ingest_pipelines/__jest__/client_integration/helpers/pipelines_create.helpers.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { registerTestBed, TestBedConfig, TestBed } from '@kbn/test/jest'; +import { registerTestBed, AsyncTestBedConfig, TestBed } from '@kbn/test/jest'; import { PipelinesCreate } from '../../../public/application/sections/pipelines_create'; import { getFormActions, PipelineFormTestSubjects } from './pipeline_form.helpers'; import { WithAppDependencies } from './setup_environment'; @@ -15,7 +15,7 @@ export type PipelinesCreateTestBed = TestBed & { actions: ReturnType; }; -const testBedConfig: TestBedConfig = { +const testBedConfig: AsyncTestBedConfig = { memoryRouter: { initialEntries: [getCreatePath()], componentRoutePath: ROUTES.create, diff --git a/x-pack/plugins/ingest_pipelines/__jest__/client_integration/helpers/pipelines_edit.helpers.ts b/x-pack/plugins/ingest_pipelines/__jest__/client_integration/helpers/pipelines_edit.helpers.ts index 913eb1355a6d7..9a3c41196653f 100644 --- a/x-pack/plugins/ingest_pipelines/__jest__/client_integration/helpers/pipelines_edit.helpers.ts +++ b/x-pack/plugins/ingest_pipelines/__jest__/client_integration/helpers/pipelines_edit.helpers.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { registerTestBed, TestBedConfig, TestBed } from '@kbn/test/jest'; +import { registerTestBed, AsyncTestBedConfig, TestBed } from '@kbn/test/jest'; import { PipelinesEdit } from '../../../public/application/sections/pipelines_edit'; import { getFormActions, PipelineFormTestSubjects } from './pipeline_form.helpers'; import { WithAppDependencies } from './setup_environment'; @@ -28,7 +28,7 @@ export const PIPELINE_TO_EDIT = { ], }; -const testBedConfig: TestBedConfig = { +const testBedConfig: AsyncTestBedConfig = { memoryRouter: { initialEntries: [getEditPath({ pipelineName: PIPELINE_TO_EDIT.name })], componentRoutePath: ROUTES.edit, diff --git a/x-pack/plugins/ingest_pipelines/__jest__/client_integration/helpers/pipelines_list.helpers.ts b/x-pack/plugins/ingest_pipelines/__jest__/client_integration/helpers/pipelines_list.helpers.ts index 5f340b645f954..3cd768104203a 100644 --- a/x-pack/plugins/ingest_pipelines/__jest__/client_integration/helpers/pipelines_list.helpers.ts +++ b/x-pack/plugins/ingest_pipelines/__jest__/client_integration/helpers/pipelines_list.helpers.ts @@ -7,12 +7,12 @@ import { act } from 'react-dom/test-utils'; -import { registerTestBed, TestBed, TestBedConfig, findTestSubject } from '@kbn/test/jest'; +import { registerTestBed, TestBed, AsyncTestBedConfig, findTestSubject } from '@kbn/test/jest'; import { PipelinesList } from '../../../public/application/sections/pipelines_list'; import { WithAppDependencies } from './setup_environment'; import { getListPath, ROUTES } from '../../../public/application/services/navigation'; -const testBedConfig: TestBedConfig = { +const testBedConfig: AsyncTestBedConfig = { memoryRouter: { initialEntries: [getListPath()], componentRoutePath: ROUTES.list, diff --git a/x-pack/plugins/lens/common/expressions/datatable/datatable.ts b/x-pack/plugins/lens/common/expressions/datatable/datatable.ts index d9f1f9c1196ff..7a6d68eaa6566 100644 --- a/x-pack/plugins/lens/common/expressions/datatable/datatable.ts +++ b/x-pack/plugins/lens/common/expressions/datatable/datatable.ts @@ -22,6 +22,7 @@ export interface DatatableArgs { columns: ColumnConfigArg[]; sortingColumnId: SortingState['columnId']; sortingDirection: SortingState['direction']; + fitRowToContent?: boolean; } export const getDatatable = ( @@ -57,6 +58,10 @@ export const getDatatable = ( types: ['string'], help: '', }, + fitRowToContent: { + types: ['boolean'], + help: '', + }, }, async fn(...args) { /** Build optimization: prevent adding extra code into initial bundle **/ diff --git a/x-pack/plugins/lens/common/expressions/expression_types/index.ts b/x-pack/plugins/lens/common/expressions/expression_types/index.ts new file mode 100644 index 0000000000000..78821e429fa8f --- /dev/null +++ b/x-pack/plugins/lens/common/expressions/expression_types/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 { lensMultitable } from './lens_multitable'; +export type { LensMultitableExpressionTypeDefinition } from './lens_multitable'; diff --git a/x-pack/plugins/lens/common/expressions/expression_types/lens_multitable.ts b/x-pack/plugins/lens/common/expressions/expression_types/lens_multitable.ts new file mode 100644 index 0000000000000..8817f9315e182 --- /dev/null +++ b/x-pack/plugins/lens/common/expressions/expression_types/lens_multitable.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 { ExpressionTypeDefinition } from '../../../../../../src/plugins/expressions/common'; +import { LensMultiTable } from '../../types'; + +const name = 'lens_multitable'; + +type Input = LensMultiTable; + +export type LensMultitableExpressionTypeDefinition = ExpressionTypeDefinition< + typeof name, + Input, + Input +>; + +export const lensMultitable: LensMultitableExpressionTypeDefinition = { + name, + to: { + datatable: (input: Input) => { + return Object.values(input.tables)[0]; + }, + }, +}; diff --git a/x-pack/plugins/lens/common/expressions/index.ts b/x-pack/plugins/lens/common/expressions/index.ts index 70a85f85938e4..344d22de6461b 100644 --- a/x-pack/plugins/lens/common/expressions/index.ts +++ b/x-pack/plugins/lens/common/expressions/index.ts @@ -15,3 +15,5 @@ export * from './heatmap_chart'; export * from './metric_chart'; export * from './pie_chart'; export * from './xy_chart'; + +export * from './expression_types'; diff --git a/x-pack/plugins/lens/public/datatable_visualization/components/__snapshots__/table_basic.test.tsx.snap b/x-pack/plugins/lens/public/datatable_visualization/components/__snapshots__/table_basic.test.tsx.snap index 7e3c8c3342e4c..bf8497e686e96 100644 --- a/x-pack/plugins/lens/public/datatable_visualization/components/__snapshots__/table_basic.test.tsx.snap +++ b/x-pack/plugins/lens/public/datatable_visualization/components/__snapshots__/table_basic.test.tsx.snap @@ -487,7 +487,7 @@ exports[`DatatableComponent it renders the title and value 1`] = ` `; -exports[`DatatableComponent it should render hide and reset actions on header even when it is in read only mode 1`] = ` +exports[`DatatableComponent it should render hide, reset, and sort actions on header even when it is in read only mode 1`] = ` >, columnConfig: ColumnConfig, DataContext: React.Context, - uiSettings: IUiSettingsClient + uiSettings: IUiSettingsClient, + fitRowToContent?: boolean ) => { // Changing theme requires a full reload of the page, so we can cache here const IS_DARK_THEME = uiSettings.get('theme:darkMode'); @@ -28,6 +30,9 @@ export const createGridCell = ( const content = formatters[columnId]?.convert(rowValue, 'html'); const currentAlignment = alignments && alignments[columnId]; const alignmentClassName = `lnsTableCell--${currentAlignment}`; + const className = classNames(alignmentClassName, { + lnsTableCell: !fitRowToContent, + }); const { colorMode, palette } = columnConfig.columns.find(({ columnId: id }) => id === columnId) || {}; @@ -75,7 +80,7 @@ export const createGridCell = ( */ dangerouslySetInnerHTML={{ __html: content }} // eslint-disable-line react/no-danger data-test-subj="lnsTableCellContent" - className={`lnsTableCell ${alignmentClassName}`} + className={className} /> ); }; diff --git a/x-pack/plugins/lens/public/datatable_visualization/components/columns.tsx b/x-pack/plugins/lens/public/datatable_visualization/components/columns.tsx index 0bc249c783239..a8ba6d553b738 100644 --- a/x-pack/plugins/lens/public/datatable_visualization/components/columns.tsx +++ b/x-pack/plugins/lens/public/datatable_visualization/components/columns.tsx @@ -215,20 +215,16 @@ export const createGridColumns = ( showHide: false, showMoveLeft: false, showMoveRight: false, - showSortAsc: isReadOnly - ? false - : { - label: i18n.translate('xpack.lens.table.sort.ascLabel', { - defaultMessage: 'Sort ascending', - }), - }, - showSortDesc: isReadOnly - ? false - : { - label: i18n.translate('xpack.lens.table.sort.descLabel', { - defaultMessage: 'Sort descending', - }), - }, + showSortAsc: { + label: i18n.translate('xpack.lens.table.sort.ascLabel', { + defaultMessage: 'Sort ascending', + }), + }, + showSortDesc: { + label: i18n.translate('xpack.lens.table.sort.descLabel', { + defaultMessage: 'Sort descending', + }), + }, additional: additionalActions, }, }; diff --git a/x-pack/plugins/lens/public/datatable_visualization/components/table_basic.test.tsx b/x-pack/plugins/lens/public/datatable_visualization/components/table_basic.test.tsx index 312d81e377f32..22407f2b39771 100644 --- a/x-pack/plugins/lens/public/datatable_visualization/components/table_basic.test.tsx +++ b/x-pack/plugins/lens/public/datatable_visualization/components/table_basic.test.tsx @@ -139,7 +139,7 @@ describe('DatatableComponent', () => { ).toMatchSnapshot(); }); - test('it should render hide and reset actions on header even when it is in read only mode', () => { + test('it should render hide, reset, and sort actions on header even when it is in read only mode', () => { const { data, args } = sampleArgs(); expect( diff --git a/x-pack/plugins/lens/public/datatable_visualization/components/table_basic.tsx b/x-pack/plugins/lens/public/datatable_visualization/components/table_basic.tsx index 6be69e5d4d236..ec7a00442f950 100644 --- a/x-pack/plugins/lens/public/datatable_visualization/components/table_basic.tsx +++ b/x-pack/plugins/lens/public/datatable_visualization/components/table_basic.tsx @@ -103,11 +103,9 @@ export const DatatableComponent = (props: DatatableRenderProps) => { const onEditAction = useCallback( (data: LensSortAction['data'] | LensResizeAction['data'] | LensToggleAction['data']) => { - if (renderMode === 'edit') { - dispatchEvent({ name: 'edit', data }); - } + dispatchEvent({ name: 'edit', data }); }, - [dispatchEvent, renderMode] + [dispatchEvent] ); const onRowContextMenuClick = useCallback( (data: LensTableRowContextMenuEvent['data']) => { @@ -264,8 +262,15 @@ export const DatatableComponent = (props: DatatableRenderProps) => { }, [firstTableRef, onRowContextMenuClick, columnConfig, hasAtLeastOneRowClickAction]); const renderCellValue = useMemo( - () => createGridCell(formatters, columnConfig, DataContext, props.uiSettings), - [formatters, columnConfig, props.uiSettings] + () => + createGridCell( + formatters, + columnConfig, + DataContext, + props.uiSettings, + props.args.fitRowToContent + ), + [formatters, columnConfig, props.uiSettings, props.args.fitRowToContent] ); const columnVisibility = useMemo( @@ -351,6 +356,13 @@ export const DatatableComponent = (props: DatatableRenderProps) => { ) { + const { state, setState } = props; + + const onChange = useCallback(() => { + const current = state.fitRowToContent ?? false; + setState({ + ...state, + fitRowToContent: !current, + }); + }, [setState, state]); + + return ( + + + + + + + + ); +} diff --git a/x-pack/plugins/lens/public/datatable_visualization/visualization.tsx b/x-pack/plugins/lens/public/datatable_visualization/visualization.tsx index 807d32a245834..a953da4c380f0 100644 --- a/x-pack/plugins/lens/public/datatable_visualization/visualization.tsx +++ b/x-pack/plugins/lens/public/datatable_visualization/visualization.tsx @@ -24,12 +24,13 @@ import { getStopsForFixedMode } from '../shared_components'; import { LayerType, layerTypes } from '../../common'; import { getDefaultSummaryLabel } from '../../common/expressions'; import type { ColumnState, SortingState } from '../../common/expressions'; - +import { DataTableToolbar } from './components/toolbar'; export interface DatatableVisualizationState { columns: ColumnState[]; layerId: string; layerType: LayerType; sorting?: SortingState; + fitRowToContent?: boolean; } const visualizationLabel = i18n.translate('xpack.lens.datatable.label', { @@ -49,9 +50,9 @@ export const getDatatableVisualization = ({ icon: LensIconChartDatatable, label: visualizationLabel, groupLabel: i18n.translate('xpack.lens.datatable.groupLabel', { - defaultMessage: 'Tabular and single value', + defaultMessage: 'Tabular', }), - sortPriority: 1, + sortPriority: 5, }, ], @@ -389,6 +390,7 @@ export const getDatatableVisualization = ({ }), sortingColumnId: [state.sorting?.columnId || ''], sortingDirection: [state.sorting?.direction || 'none'], + fitRowToContent: [state.fitRowToContent ?? false], }, }, ], @@ -399,6 +401,15 @@ export const getDatatableVisualization = ({ return undefined; }, + renderToolbar(domElement, props) { + render( + + + , + domElement + ); + }, + onEditAction(state, event) { switch (event.data.action) { case 'sort': diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.tsx index bdd5d93c2c2c8..51d880e8f7c1c 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.tsx @@ -125,11 +125,7 @@ export function LayerPanel( dateRange, }; - const { - groups, - supportStaticValue, - supportFieldFormat = true, - } = useMemo( + const { groups } = useMemo( () => activeVisualization.getConfiguration(layerVisualizationConfigProps), // eslint-disable-next-line react-hooks/exhaustive-deps [ @@ -518,7 +514,7 @@ export function LayerPanel( setActiveDimension({ activeGroup: group, activeId: id, - isNew: !supportStaticValue, + isNew: !group.supportStaticValue, }); }} onDrop={onDrop} @@ -575,8 +571,9 @@ export function LayerPanel( toggleFullscreen, isFullscreen, setState: updateDataLayerState, - supportStaticValue: Boolean(supportStaticValue), - supportFieldFormat: Boolean(supportFieldFormat), + supportStaticValue: Boolean(activeGroup.supportStaticValue), + paramEditorCustomProps: activeGroup.paramEditorCustomProps, + supportFieldFormat: activeGroup.supportFieldFormat !== false, layerType: activeVisualization.getLayerType(layerId, visualizationState), }} /> diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/chart_switch.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/chart_switch.tsx index 427306cb54fb9..250359822e068 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/chart_switch.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/chart_switch.tsx @@ -336,6 +336,7 @@ export const ChartSwitch = memo(function ChartSwitch(props: Props) { {v.selection.dataLoss !== 'nothing' ? ( diff --git a/x-pack/plugins/lens/public/editor_frame_service/service.tsx b/x-pack/plugins/lens/public/editor_frame_service/service.tsx index d97cfd3cbca23..b585d03e12f8f 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/service.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/service.tsx @@ -61,16 +61,19 @@ export class EditorFrameService { private readonly datasources: Array Promise)> = []; private readonly visualizations: Array Promise)> = []; + private loadDatasources = () => collectAsyncDefinitions(this.datasources); + public loadVisualizations = () => collectAsyncDefinitions(this.visualizations); + /** * This method takes a Lens saved object as returned from the persistence helper, * initializes datsources and visualization and creates the current expression. - * This is an asynchronous process and should only be triggered once for a saved object. + * This is an asynchronous process. * @param doc parsed Lens saved object */ public documentToExpression = async (doc: Document) => { const [resolvedDatasources, resolvedVisualizations] = await Promise.all([ - collectAsyncDefinitions(this.datasources), - collectAsyncDefinitions(this.visualizations), + this.loadDatasources(), + this.loadVisualizations(), ]); const { persistedStateToExpression } = await import('../async_services'); @@ -92,8 +95,8 @@ export class EditorFrameService { public start(core: CoreStart, plugins: EditorFrameStartPlugins): EditorFrameStart { const createInstance = async (): Promise => { const [resolvedDatasources, resolvedVisualizations] = await Promise.all([ - collectAsyncDefinitions(this.datasources), - collectAsyncDefinitions(this.visualizations), + this.loadDatasources(), + this.loadVisualizations(), ]); const { EditorFrame } = await import('../async_services'); diff --git a/x-pack/plugins/lens/public/embeddable/embeddable.test.tsx b/x-pack/plugins/lens/public/embeddable/embeddable.test.tsx index 4c247c031eac0..59d6325e1c0ce 100644 --- a/x-pack/plugins/lens/public/embeddable/embeddable.test.tsx +++ b/x-pack/plugins/lens/public/embeddable/embeddable.test.tsx @@ -25,6 +25,7 @@ import { LensAttributeService } from '../lens_attribute_service'; import { OnSaveProps } from '../../../../../src/plugins/saved_objects/public/save_modal'; import { act } from 'react-dom/test-utils'; import { inspectorPluginMock } from '../../../../../src/plugins/inspector/public/mocks'; +import { Visualization } from '../types'; jest.mock('../../../../../src/plugins/inspector/public/', () => ({ isAvailable: false, @@ -125,6 +126,7 @@ describe('embeddable', () => { }, inspector: inspectorPluginMock.createStartContract(), getTrigger, + visualizationMap: {}, documentToExpression: () => Promise.resolve({ ast: { @@ -165,6 +167,7 @@ describe('embeddable', () => { inspector: inspectorPluginMock.createStartContract(), capabilities: { canSaveDashboards: true, canSaveVisualizations: true }, getTrigger, + visualizationMap: {}, documentToExpression: () => Promise.resolve({ ast: { @@ -209,6 +212,7 @@ describe('embeddable', () => { canSaveVisualizations: true, }, getTrigger, + visualizationMap: {}, documentToExpression: () => Promise.resolve({ ast: { @@ -255,6 +259,7 @@ describe('embeddable', () => { canSaveVisualizations: true, }, getTrigger, + visualizationMap: {}, documentToExpression: () => Promise.resolve({ ast: { @@ -297,6 +302,7 @@ describe('embeddable', () => { canSaveVisualizations: true, }, getTrigger, + visualizationMap: {}, documentToExpression: () => Promise.resolve({ ast: { @@ -336,6 +342,7 @@ describe('embeddable', () => { canSaveVisualizations: true, }, getTrigger, + visualizationMap: {}, documentToExpression: () => Promise.resolve({ ast: { @@ -378,6 +385,7 @@ describe('embeddable', () => { indexPatternService: {} as IndexPatternsContract, capabilities: { canSaveDashboards: true, canSaveVisualizations: true }, getTrigger, + visualizationMap: {}, documentToExpression: () => Promise.resolve({ ast: { @@ -427,6 +435,7 @@ describe('embeddable', () => { canSaveVisualizations: true, }, getTrigger, + visualizationMap: {}, documentToExpression: () => Promise.resolve({ ast: { @@ -474,6 +483,7 @@ describe('embeddable', () => { canSaveVisualizations: true, }, getTrigger, + visualizationMap: {}, documentToExpression: () => Promise.resolve({ ast: { @@ -528,6 +538,7 @@ describe('embeddable', () => { canSaveVisualizations: true, }, getTrigger, + visualizationMap: {}, documentToExpression: () => Promise.resolve({ ast: { @@ -583,6 +594,7 @@ describe('embeddable', () => { canSaveVisualizations: true, }, getTrigger, + visualizationMap: {}, documentToExpression: () => Promise.resolve({ ast: { @@ -641,6 +653,7 @@ describe('embeddable', () => { canSaveVisualizations: true, }, getTrigger, + visualizationMap: {}, documentToExpression: () => Promise.resolve({ ast: { @@ -683,6 +696,7 @@ describe('embeddable', () => { canSaveVisualizations: true, }, getTrigger, + visualizationMap: {}, documentToExpression: () => Promise.resolve({ ast: { @@ -725,6 +739,7 @@ describe('embeddable', () => { canSaveVisualizations: true, }, getTrigger, + visualizationMap: {}, documentToExpression: () => Promise.resolve({ ast: { @@ -767,6 +782,7 @@ describe('embeddable', () => { canSaveVisualizations: true, }, getTrigger, + visualizationMap: {}, documentToExpression: () => Promise.resolve({ ast: { @@ -824,6 +840,7 @@ describe('embeddable', () => { canSaveVisualizations: true, }, getTrigger, + visualizationMap: {}, documentToExpression: () => Promise.resolve({ ast: { @@ -897,6 +914,7 @@ describe('embeddable', () => { canSaveVisualizations: true, }, getTrigger, + visualizationMap: {}, documentToExpression: () => Promise.resolve({ ast: { @@ -945,6 +963,7 @@ describe('embeddable', () => { canSaveVisualizations: true, }, getTrigger, + visualizationMap: {}, documentToExpression: () => Promise.resolve({ ast: { @@ -993,6 +1012,7 @@ describe('embeddable', () => { canSaveVisualizations: true, }, getTrigger, + visualizationMap: {}, documentToExpression: () => Promise.resolve({ ast: { @@ -1016,4 +1036,82 @@ describe('embeddable', () => { expect(onTableRowClick).toHaveBeenCalledWith({ name: 'test' }); expect(onTableRowClick).toHaveBeenCalledTimes(1); }); + + it('handles edit actions ', async () => { + const editedVisualizationState = { value: 'edited' }; + const onEditActionMock = jest.fn().mockReturnValue(editedVisualizationState); + const documentToExpressionMock = jest.fn().mockImplementation(async (document) => { + const isStateEdited = document.state.visualization.value === 'edited'; + return { + ast: { + type: 'expression', + chain: [ + { + type: 'function', + function: isStateEdited ? 'edited' : 'not_edited', + arguments: {}, + }, + ], + }, + errors: undefined, + }; + }); + + const visDocument: Document = { + state: { + visualization: {}, + datasourceStates: {}, + query: { query: '', language: 'lucene' }, + filters: [], + }, + references: [], + title: 'My title', + visualizationType: 'lensDatatable', + }; + + const embeddable = new Embeddable( + { + timefilter: dataPluginMock.createSetupContract().query.timefilter.timefilter, + attributeService: attributeServiceMockFromSavedVis(visDocument), + expressionRenderer, + basePath, + inspector: inspectorPluginMock.createStartContract(), + indexPatternService: {} as IndexPatternsContract, + capabilities: { + canSaveDashboards: true, + canSaveVisualizations: true, + }, + getTrigger, + visualizationMap: { + [visDocument.visualizationType as string]: { + onEditAction: onEditActionMock, + } as unknown as Visualization, + }, + documentToExpression: documentToExpressionMock, + }, + { id: '123' } as unknown as LensEmbeddableInput + ); + + // SETUP FRESH STATE + await embeddable.initializeSavedVis({ id: '123' } as LensEmbeddableInput); + embeddable.render(mountpoint); + + await new Promise((resolve) => setTimeout(resolve, 0)); + + expect(expressionRenderer).toHaveBeenCalledTimes(1); + expect(expressionRenderer.mock.calls[0][0]!.expression).toBe(`not_edited`); + + // TEST EDIT EVENT + await embeddable.handleEvent({ name: 'edit' }); + + expect(onEditActionMock).toHaveBeenCalledTimes(1); + expect(documentToExpressionMock).toHaveBeenCalled(); + + const docToExpCalls = documentToExpressionMock.mock.calls; + const editedVisDocument = docToExpCalls[docToExpCalls.length - 1][0]; + expect(editedVisDocument.state.visualization).toEqual(editedVisualizationState); + + expect(expressionRenderer).toHaveBeenCalledTimes(2); + expect(expressionRenderer.mock.calls[1][0]!.expression).toBe(`edited`); + }); }); diff --git a/x-pack/plugins/lens/public/embeddable/embeddable.tsx b/x-pack/plugins/lens/public/embeddable/embeddable.tsx index 7faf873cf0b0a..563e10bb03abd 100644 --- a/x-pack/plugins/lens/public/embeddable/embeddable.tsx +++ b/x-pack/plugins/lens/public/embeddable/embeddable.tsx @@ -47,10 +47,13 @@ import { UiActionsStart } from '../../../../../src/plugins/ui_actions/public'; import { isLensBrushEvent, isLensFilterEvent, + isLensEditEvent, isLensTableRowContextMenuClickEvent, LensBrushEvent, LensFilterEvent, LensTableRowContextMenuEvent, + VisualizationMap, + Visualization, } from '../types'; import { IndexPatternsContract } from '../../../../../src/plugins/data/public'; @@ -97,6 +100,7 @@ export interface LensEmbeddableDeps { documentToExpression: ( doc: Document ) => Promise<{ ast: Ast | null; errors: ErrorMessage[] | undefined }>; + visualizationMap: VisualizationMap; indexPatternService: IndexPatternsContract; expressionRenderer: ReactExpressionRendererType; timefilter: TimefilterContract; @@ -109,6 +113,17 @@ export interface LensEmbeddableDeps { spaces?: SpacesPluginStart; } +const getExpressionFromDocument = async ( + document: Document, + documentToExpression: LensEmbeddableDeps['documentToExpression'] +) => { + const { ast, errors } = await documentToExpression(document); + return { + expression: ast ? toExpression(ast) : null, + errors, + }; +}; + export class Embeddable extends AbstractEmbeddable implements ReferenceOrValueEmbeddable @@ -260,6 +275,29 @@ export class Embeddable return this.lensInspector.adapters; } + private maybeAddConflictError( + errors: ErrorMessage[], + sharingSavedObjectProps?: SharingSavedObjectProps + ) { + const ret = [...errors]; + + if (sharingSavedObjectProps?.outcome === 'conflict' && !!this.deps.spaces) { + ret.push({ + shortMessage: i18n.translate('xpack.lens.embeddable.legacyURLConflict.shortMessage', { + defaultMessage: `You've encountered a URL conflict`, + }), + longMessage: ( + + ), + }); + } + + return ret; + } + async initializeSavedVis(input: LensEmbeddableInput) { const attrs: ResolvedLensSavedObjectAttributes | false = await this.deps.attributeService .unwrapAttributes(input) @@ -278,23 +316,14 @@ export class Embeddable type: this.type, savedObjectId: (input as LensByReferenceInput)?.savedObjectId, }; - const { ast, errors } = await this.deps.documentToExpression(this.savedVis); - this.errors = errors; - if (sharingSavedObjectProps?.outcome === 'conflict' && this.deps.spaces) { - const conflictError = { - shortMessage: i18n.translate('xpack.lens.embeddable.legacyURLConflict.shortMessage', { - defaultMessage: `You've encountered a URL conflict`, - }), - longMessage: ( - - ), - }; - this.errors = this.errors ? [...this.errors, conflictError] : [conflictError]; - } - this.expression = ast ? toExpression(ast) : null; + + const { expression, errors } = await getExpressionFromDocument( + this.savedVis, + this.deps.documentToExpression + ); + this.expression = expression; + this.errors = errors && this.maybeAddConflictError(errors, sharingSavedObjectProps); + if (this.errors) { this.logError('validation'); } @@ -432,7 +461,17 @@ export class Embeddable return output; } - handleEvent = (event: ExpressionRendererEvent) => { + private get onEditAction(): Visualization['onEditAction'] { + const visType = this.savedVis?.visualizationType; + + if (!visType) { + return; + } + + return this.deps.visualizationMap[visType].onEditAction; + } + + handleEvent = async (event: ExpressionRendererEvent) => { if (!this.deps.getTrigger || this.input.disableTriggers) { return; } @@ -468,9 +507,29 @@ export class Embeddable this.input.onTableRowClick(event.data as unknown as LensTableRowContextMenuEvent['data']); } } + + // We allow for edit actions in the Embeddable for display purposes only (e.g. changing the datatable sort order). + // No state changes made here with an edit action are persisted. + if (isLensEditEvent(event) && this.onEditAction) { + if (!this.savedVis) return; + + // have to dance since this.savedVis.state is readonly + const newVis = JSON.parse(JSON.stringify(this.savedVis)) as Document; + newVis.state.visualization = this.onEditAction(newVis.state.visualization, event); + this.savedVis = newVis; + + const { expression, errors } = await getExpressionFromDocument( + this.savedVis, + this.deps.documentToExpression + ); + this.expression = expression; + this.errors = errors; + + this.reload(); + } }; - async reload() { + reload() { if (!this.savedVis || !this.isInitialized || this.isDestroyed) { return; } diff --git a/x-pack/plugins/lens/public/embeddable/embeddable_factory.ts b/x-pack/plugins/lens/public/embeddable/embeddable_factory.ts index e51ec4c3e5588..811f391e32f9a 100644 --- a/x-pack/plugins/lens/public/embeddable/embeddable_factory.ts +++ b/x-pack/plugins/lens/public/embeddable/embeddable_factory.ts @@ -25,6 +25,7 @@ import { DOC_TYPE } from '../../common/constants'; import { ErrorMessage } from '../editor_frame_service/types'; import { extract, inject } from '../../common/embeddable_factory'; import type { SpacesPluginStart } from '../../../spaces/public'; +import { VisualizationMap } from '../types'; export interface LensEmbeddableStartServices { timefilter: TimefilterContract; @@ -39,6 +40,7 @@ export interface LensEmbeddableStartServices { documentToExpression: ( doc: Document ) => Promise<{ ast: Ast | null; errors: ErrorMessage[] | undefined }>; + visualizationMap: VisualizationMap; spaces?: SpacesPluginStart; } @@ -85,6 +87,7 @@ export class EmbeddableFactory implements EmbeddableFactoryDefinition { timefilter, expressionRenderer, documentToExpression, + visualizationMap, uiActions, coreHttp, attributeService, @@ -108,6 +111,7 @@ export class EmbeddableFactory implements EmbeddableFactoryDefinition { getTrigger: uiActions?.getTrigger, getTriggerCompatibleActions: uiActions?.getTriggerCompatibleActions, documentToExpression, + visualizationMap, capabilities: { canSaveDashboards: Boolean(capabilities.dashboard?.showWriteControls), canSaveVisualizations: Boolean(capabilities.visualize.save), diff --git a/x-pack/plugins/lens/public/expressions.ts b/x-pack/plugins/lens/public/expressions.ts index 27f3179a2d0c8..9d972d8ed6941 100644 --- a/x-pack/plugins/lens/public/expressions.ts +++ b/x-pack/plugins/lens/public/expressions.ts @@ -33,12 +33,15 @@ import { formatColumn } from '../common/expressions/format_column'; import { counterRate } from '../common/expressions/counter_rate'; import { getTimeScale } from '../common/expressions/time_scale/time_scale'; import { metricChart } from '../common/expressions/metric_chart/metric_chart'; +import { lensMultitable } from '../common/expressions'; export const setupExpressions = ( expressions: ExpressionsSetup, formatFactory: Parameters[0], getTimeZone: Parameters[0] -) => +) => { + [lensMultitable].forEach((expressionType) => expressions.registerType(expressionType)); + [ pie, xyChart, @@ -62,3 +65,4 @@ export const setupExpressions = ( getDatatable(formatFactory), getTimeScale(getTimeZone), ].forEach((expressionFn) => expressions.registerFunction(expressionFn)); +}; diff --git a/x-pack/plugins/lens/public/heatmap_visualization/chart_component.tsx b/x-pack/plugins/lens/public/heatmap_visualization/chart_component.tsx index c999656071ef4..5e4c2f2684062 100644 --- a/x-pack/plugins/lens/public/heatmap_visualization/chart_component.tsx +++ b/x-pack/plugins/lens/public/heatmap_visualization/chart_component.tsx @@ -16,6 +16,8 @@ import { HeatmapSpec, ScaleType, Settings, + ESFixedIntervalUnit, + ESCalendarIntervalUnit, } from '@elastic/charts'; import type { CustomPaletteState } from 'src/plugins/charts/public'; import { VisualizationContainer } from '../visualization_container'; @@ -30,6 +32,7 @@ import { } from '../shared_components'; import { LensIconChartHeatmap } from '../assets/chart_heatmap'; import { DEFAULT_PALETTE_NAME } from './constants'; +import { search } from '../../../../../src/plugins/data/public'; declare global { interface Window { @@ -162,8 +165,30 @@ export const HeatmapComponent: FC = ({ // Fallback to the ordinal scale type when a single row of data is provided. // Related issue https://github.com/elastic/elastic-charts/issues/1184 - const xScaleType = - isTimeBasedSwimLane && chartData.length > 1 ? ScaleType.Time : ScaleType.Ordinal; + + let xScale: HeatmapSpec['xScale'] = { type: ScaleType.Ordinal }; + if (isTimeBasedSwimLane && chartData.length > 1) { + const dateInterval = + search.aggs.getDateHistogramMetaDataByDatatableColumn(xAxisColumn)?.interval; + const esInterval = dateInterval ? search.aggs.parseEsInterval(dateInterval) : undefined; + if (esInterval) { + xScale = { + type: ScaleType.Time, + interval: + esInterval.type === 'fixed' + ? { + type: 'fixed', + unit: esInterval.unit as ESFixedIntervalUnit, + value: esInterval.value, + } + : { + type: 'calendar', + unit: esInterval.unit as ESCalendarIntervalUnit, + value: esInterval.value, + }, + }; + } + } const xValuesFormatter = formatFactory(xAxisMeta.params); const valueFormatter = formatFactory(valueColumn.meta.params); @@ -341,6 +366,10 @@ export const HeatmapComponent: FC = ({ labelOptions: { maxLines: args.legend.shouldTruncate ? args.legend?.maxLines ?? 1 : 0 }, }, }} + xDomain={{ + min: data.dateRange?.fromDate.getTime() ?? NaN, + max: data.dateRange?.toDate.getTime() ?? NaN, + }} onBrushEnd={onBrushEnd as BrushEndListener} /> = ({ yAccessor={args.yAccessor || 'unifiedY'} valueAccessor={args.valueAccessor} valueFormatter={(v: number) => valueFormatter.convert(v)} - xScaleType={xScaleType} + xScale={xScale} ySortPredicate="dataIndex" config={config} xSortPredicate="dataIndex" diff --git a/x-pack/plugins/lens/public/heatmap_visualization/visualization.tsx b/x-pack/plugins/lens/public/heatmap_visualization/visualization.tsx index 674af79db6c90..aa053d4aea06d 100644 --- a/x-pack/plugins/lens/public/heatmap_visualization/visualization.tsx +++ b/x-pack/plugins/lens/public/heatmap_visualization/visualization.tsx @@ -33,8 +33,8 @@ import { getSafePaletteParams } from './utils'; import type { CustomPaletteParams } from '../../common'; import { layerTypes } from '../../common'; -const groupLabelForBar = i18n.translate('xpack.lens.heatmapVisualization.heatmapGroupLabel', { - defaultMessage: 'Heatmap', +const groupLabelForHeatmap = i18n.translate('xpack.lens.heatmapVisualization.heatmapGroupLabel', { + defaultMessage: 'Magnitude', }); interface HeatmapVisualizationDeps { @@ -105,8 +105,9 @@ export const getHeatmapVisualization = ({ label: i18n.translate('xpack.lens.heatmapVisualization.heatmapLabel', { defaultMessage: 'Heatmap', }), - groupLabel: groupLabelForBar, + groupLabel: groupLabelForHeatmap, showExperimentalBadge: true, + sortPriority: 1, }, ], diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_editor.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_editor.tsx index 74628a31ea281..d34430e717e66 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_editor.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_editor.tsx @@ -55,6 +55,7 @@ import { CalloutWarning, LabelInput, getErrorMessage, + DimensionEditorTab, } from './dimensions_editor_helpers'; import type { TemporaryState } from './dimensions_editor_helpers'; @@ -84,6 +85,7 @@ export function DimensionEditor(props: DimensionEditorProps) { supportStaticValue, supportFieldFormat = true, layerType, + paramEditorCustomProps, } = props; const services = { data: props.data, @@ -478,6 +480,7 @@ export function DimensionEditor(props: DimensionEditorProps) { isFullscreen={isFullscreen} toggleFullscreen={toggleFullscreen} setIsCloseable={setIsCloseable} + paramEditorCustomProps={paramEditorCustomProps} {...services} /> ); @@ -559,6 +562,7 @@ export function DimensionEditor(props: DimensionEditorProps) { toggleFullscreen={toggleFullscreen} isFullscreen={isFullscreen} setIsCloseable={setIsCloseable} + paramEditorCustomProps={paramEditorCustomProps} {...services} /> )} @@ -674,6 +678,7 @@ export function DimensionEditor(props: DimensionEditorProps) { toggleFullscreen={toggleFullscreen} isFullscreen={isFullscreen} setIsCloseable={setIsCloseable} + paramEditorCustomProps={paramEditorCustomProps} {...services} /> @@ -700,57 +705,64 @@ export function DimensionEditor(props: DimensionEditorProps) { const hasTabs = !isFullscreen && (hasFormula || supportStaticValue); + const tabs: DimensionEditorTab[] = [ + { + id: staticValueOperationName, + enabled: Boolean(supportStaticValue), + state: showStaticValueFunction, + onClick: () => { + if (selectedColumn?.operationType === formulaOperationName) { + return setTemporaryState(staticValueOperationName); + } + setTemporaryState('none'); + setStateWrapper(addStaticValueColumn()); + return; + }, + label: i18n.translate('xpack.lens.indexPattern.staticValueLabel', { + defaultMessage: 'Static value', + }), + }, + { + id: quickFunctionsName, + enabled: true, + state: showQuickFunctions, + onClick: () => { + if (selectedColumn && !isQuickFunction(selectedColumn.operationType)) { + setTemporaryState(quickFunctionsName); + return; + } + }, + label: i18n.translate('xpack.lens.indexPattern.quickFunctionsLabel', { + defaultMessage: 'Quick functions', + }), + }, + { + id: formulaOperationName, + enabled: hasFormula, + state: temporaryState === 'none' && selectedColumn?.operationType === formulaOperationName, + onClick: () => { + setTemporaryState('none'); + if (selectedColumn?.operationType !== formulaOperationName) { + const newLayer = insertOrReplaceColumn({ + layer: props.state.layers[props.layerId], + indexPattern: currentIndexPattern, + columnId, + op: formulaOperationName, + visualizationGroups: dimensionGroups, + }); + setStateWrapper(newLayer); + trackUiEvent(`indexpattern_dimension_operation_formula`); + } + }, + label: i18n.translate('xpack.lens.indexPattern.formulaLabel', { + defaultMessage: 'Formula', + }), + }, + ]; + return (
    - {hasTabs ? ( - { - if (tabClicked === 'quickFunctions') { - if (selectedColumn && !isQuickFunction(selectedColumn.operationType)) { - setTemporaryState(quickFunctionsName); - return; - } - } - - if (tabClicked === 'static_value') { - // when coming from a formula, set a temporary state - if (selectedColumn?.operationType === formulaOperationName) { - return setTemporaryState(staticValueOperationName); - } - setTemporaryState('none'); - setStateWrapper(addStaticValueColumn()); - return; - } - - if (tabClicked === 'formula') { - setTemporaryState('none'); - if (selectedColumn?.operationType !== formulaOperationName) { - const newLayer = insertOrReplaceColumn({ - layer: props.state.layers[props.layerId], - indexPattern: currentIndexPattern, - columnId, - op: formulaOperationName, - visualizationGroups: dimensionGroups, - }); - setStateWrapper(newLayer); - trackUiEvent(`indexpattern_dimension_operation_formula`); - } - } - }} - /> - ) : null} - + {hasTabs ? : null} void; + id: typeof quickFunctionsName | typeof staticValueOperationName | typeof formulaOperationName; + label: string; +} -export const DimensionEditorTabs = ({ - tabsEnabled, - tabsState, - onClick, -}: { - tabsEnabled: Record; - tabsState: Record; - onClick: (tabClicked: DimensionEditorTabsType) => void; -}) => { +export const DimensionEditorTabs = ({ tabs }: { tabs: DimensionEditorTab[] }) => { return ( - {tabsEnabled.static_value ? ( - onClick(staticValueOperationName)} - > - {i18n.translate('xpack.lens.indexPattern.staticValueLabel', { - defaultMessage: 'Static value', - })} - - ) : null} - onClick(quickFunctionsName)} - > - {i18n.translate('xpack.lens.indexPattern.quickFunctionsLabel', { - defaultMessage: 'Quick functions', - })} - - {tabsEnabled.formula ? ( - onClick(formulaOperationName)} - > - {i18n.translate('xpack.lens.indexPattern.formulaLabel', { - defaultMessage: 'Formula', - })} - - ) : null} + {tabs.map(({ id, enabled, state, onClick, label }) => { + return enabled ? ( + + {label} + + ) : null; + })} ); }; diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/reference_editor.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/reference_editor.tsx index af8c8d7d1bf28..6fa1912effc2a 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/reference_editor.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/reference_editor.tsx @@ -34,7 +34,7 @@ import { FieldSelect } from './field_select'; import { hasField } from '../utils'; import type { IndexPattern, IndexPatternLayer, IndexPatternPrivateState } from '../types'; import { trackUiEvent } from '../../lens_ui_telemetry'; -import { VisualizationDimensionGroupConfig } from '../../types'; +import { ParamEditorCustomProps, VisualizationDimensionGroupConfig } from '../../types'; import { IndexPatternDimensionEditorProps } from './dimension_panel'; const operationPanels = getOperationDisplay(); @@ -65,6 +65,7 @@ export interface ReferenceEditorProps { savedObjectsClient: SavedObjectsClientContract; http: HttpSetup; data: DataPublicPluginStart; + paramEditorCustomProps?: ParamEditorCustomProps; } export function ReferenceEditor(props: ReferenceEditorProps) { @@ -84,6 +85,7 @@ export function ReferenceEditor(props: ReferenceEditorProps) { isFullscreen, toggleFullscreen, setIsCloseable, + paramEditorCustomProps, ...services } = props; @@ -364,6 +366,7 @@ export function ReferenceEditor(props: ReferenceEditorProps) { isFullscreen={isFullscreen} toggleFullscreen={toggleFullscreen} setIsCloseable={setIsCloseable} + paramEditorCustomProps={paramEditorCustomProps} {...services} /> diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/field_item.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/field_item.tsx index 5d8ba778e30d1..f84650a183f31 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/field_item.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/field_item.tsx @@ -36,6 +36,7 @@ import { TooltipType, } from '@elastic/charts'; import { i18n } from '@kbn/i18n'; +import { FieldButton } from '@kbn/react-field/field_button'; import type { FieldFormatsStart } from 'src/plugins/field_formats/public'; import { EuiHighlight } from '@elastic/eui'; import { @@ -45,7 +46,6 @@ import { Filter, esQuery, } from '../../../../../src/plugins/data/public'; -import { FieldButton } from '../../../../../src/plugins/kibana_react/public'; import { ChartsPluginSetup } from '../../../../../src/plugins/charts/public'; import { DragDrop, DragDropIdentifier } from '../drag_drop'; import { DatasourceDataPanelProps, DataType } from '../types'; 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 1dfc7d40f6f3e..8f180d4a021e0 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.test.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.test.ts @@ -1345,8 +1345,11 @@ describe('IndexPattern Data Source', () => { }); describe('#getWarningMessages', () => { - it('should return mismatched time shifts', () => { - const state: IndexPatternPrivateState = { + let state: IndexPatternPrivateState; + let framePublicAPI: FramePublicAPI; + + beforeEach(() => { + state = { indexPatternRefs: [], existingFields: {}, isFirstExistenceFetch: false, @@ -1410,7 +1413,8 @@ describe('IndexPattern Data Source', () => { }, currentIndexPatternId: '1', }; - const warnings = indexPatternDatasource.getWarningMessages!(state, { + + framePublicAPI = { activeData: { first: { type: 'datatable', @@ -1433,20 +1437,39 @@ describe('IndexPattern Data Source', () => { ], }, }, - } as unknown as FramePublicAPI); - expect(warnings!.length).toBe(2); - expect((warnings![0] as React.ReactElement).props.id).toEqual( - 'xpack.lens.indexPattern.timeShiftSmallWarning' - ); - expect((warnings![1] as React.ReactElement).props.id).toEqual( - 'xpack.lens.indexPattern.timeShiftMultipleWarning' - ); + } as unknown as FramePublicAPI; + }); + + it('should return mismatched time shifts', () => { + const warnings = indexPatternDatasource.getWarningMessages!(state, framePublicAPI); + + expect(warnings!.map((item) => (item as React.ReactElement).props.id)).toMatchInlineSnapshot(` + Array [ + "xpack.lens.indexPattern.timeShiftSmallWarning", + "xpack.lens.indexPattern.timeShiftMultipleWarning", + ] + `); + }); + + it('should show different types of warning messages', () => { + framePublicAPI.activeData!.first.columns[0].meta.sourceParams!.hasPrecisionError = true; + + const warnings = indexPatternDatasource.getWarningMessages!(state, framePublicAPI); + + expect(warnings!.map((item) => (item as React.ReactElement).props.id)).toMatchInlineSnapshot(` + Array [ + "xpack.lens.indexPattern.timeShiftSmallWarning", + "xpack.lens.indexPattern.timeShiftMultipleWarning", + "xpack.lens.indexPattern.precisionErrorWarning", + ] + `); }); it('should prepend each error with its layer number on multi-layer chart', () => { (getErrorMessages as jest.Mock).mockClear(); (getErrorMessages as jest.Mock).mockReturnValueOnce(['error 1', 'error 2']); - const state: IndexPatternPrivateState = { + + state = { indexPatternRefs: [], existingFields: {}, isFirstExistenceFetch: false, @@ -1465,6 +1488,7 @@ describe('IndexPattern Data Source', () => { }, currentIndexPatternId: '1', }; + expect(indexPatternDatasource.getErrorMessages(state)).toEqual([ { longMessage: 'Layer 1 error: error 1', shortMessage: '' }, { longMessage: 'Layer 1 error: error 2', shortMessage: '' }, @@ -1696,7 +1720,7 @@ describe('IndexPattern Data Source', () => { isBucketed: false, label: 'Static value: 0', operationType: 'static_value', - params: { value: 0 }, + params: { value: '0' }, references: [], scale: 'ratio', }, diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.tsx index bdcc0e621cc36..b970ad092c7f4 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.tsx @@ -57,7 +57,7 @@ import { UiActionsStart } from '../../../../../src/plugins/ui_actions/public'; import { GeoFieldWorkspacePanel } from '../editor_frame_service/editor_frame/workspace_panel/geo_field_workspace_panel'; import { DraggingIdentifier } from '../drag_drop'; import { getStateTimeShiftWarningMessages } from './time_shift_utils'; - +import { getPrecisionErrorWarningMessages } from './utils'; export type { OperationType, IndexPatternColumn } from './operations'; export { deleteColumn } from './operations'; @@ -502,7 +502,12 @@ export function getIndexPatternDatasource({ }); return messages.length ? messages : undefined; }, - getWarningMessages: getStateTimeShiftWarningMessages, + getWarningMessages: (state, frame) => { + return [ + ...(getStateTimeShiftWarningMessages(state, frame) || []), + ...getPrecisionErrorWarningMessages(state, frame, core.docLinks), + ]; + }, checkIntegrity: (state) => { const ids = Object.values(state.layers || {}).map(({ indexPatternId }) => indexPatternId); return ids.filter((id) => !state.indexPatterns[id]); diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/lens_field_icon.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/lens_field_icon.tsx index edb6957fcf0b2..fa4d3e5e1513d 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/lens_field_icon.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/lens_field_icon.tsx @@ -6,7 +6,7 @@ */ import React from 'react'; -import { FieldIcon, FieldIconProps } from '../../../../../src/plugins/kibana_react/public'; +import { FieldIcon, FieldIconProps } from '@kbn/react-field/field_icon'; import { DataType } from '../types'; import { normalizeOperationDataType } from './utils'; 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 392b2b135ca22..5898cfc26d88c 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 @@ -51,7 +51,7 @@ import { } from './formula'; import { staticValueOperation, StaticValueIndexPatternColumn } from './static_value'; import { lastValueOperation, LastValueIndexPatternColumn } from './last_value'; -import { FrameDatasourceAPI, OperationMetadata } from '../../../types'; +import { FrameDatasourceAPI, OperationMetadata, ParamEditorCustomProps } from '../../../types'; import type { BaseIndexPatternColumn, ReferenceBasedIndexPatternColumn } from './column_types'; import { IndexPattern, IndexPatternField, IndexPatternLayer } from '../../types'; import { DateRange, LayerType } from '../../../../common'; @@ -197,6 +197,7 @@ export interface ParamEditorProps { data: DataPublicPluginStart; activeData?: IndexPatternDimensionEditorProps['activeData']; operationDefinitionMap: Record; + paramEditorCustomProps?: ParamEditorCustomProps; } export interface HelpProps { diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/static_value.test.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/static_value.test.tsx index 1c574fe69611c..816324f9f8fb5 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/static_value.test.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/static_value.test.tsx @@ -338,6 +338,36 @@ describe('static_value', () => { expect(input.prop('value')).toEqual('23'); }); + it('should allow 0 as initial value', () => { + const updateLayerSpy = jest.fn(); + const zeroLayer = { + ...layer, + columns: { + ...layer.columns, + col2: { + ...layer.columns.col2, + operationType: 'static_value', + references: [], + params: { + value: '0', + }, + }, + }, + } as IndexPatternLayer; + const instance = shallow( + + ); + + const input = instance.find('[data-test-subj="lns-indexPattern-static_value-input"]'); + expect(input.prop('value')).toEqual('0'); + }); + it('should update state on change', async () => { const updateLayerSpy = jest.fn(); const instance = mount( diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/static_value.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/static_value.tsx index 26be4e7b114da..b66092e8a48c3 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/static_value.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/static_value.tsx @@ -95,7 +95,7 @@ export const staticValueOperation: OperationDefinition< arguments: { id: [columnId], name: [label || defaultLabel], - expression: [isValidNumber(params.value) ? params.value! : String(defaultValue)], + expression: [String(isValidNumber(params.value) ? params.value! : defaultValue)], }, }, ]; @@ -118,7 +118,7 @@ export const staticValueOperation: OperationDefinition< operationType: 'static_value', isBucketed: false, scale: 'ratio', - params: { ...previousParams, value: previousParams.value ?? String(defaultValue) }, + params: { ...previousParams, value: String(previousParams.value ?? defaultValue) }, references: [], }; }, @@ -137,13 +137,12 @@ export const staticValueOperation: OperationDefinition< }, paramEditor: function StaticValueEditor({ - layer, updateLayer, currentColumn, columnId, activeData, layerId, - indexPattern, + paramEditorCustomProps, }) { const onChange = useCallback( (newValue) => { @@ -201,11 +200,7 @@ export const staticValueOperation: OperationDefinition< return (
    - - {i18n.translate('xpack.lens.indexPattern.staticValue.label', { - defaultMessage: 'Reference line value', - })} - + {paramEditorCustomProps?.label || defaultLabel} { + describe('getPrecisionErrorWarningMessages', () => { + let state: IndexPatternPrivateState; + let framePublicAPI: FramePublicAPI; + let docLinks: DocLinksStart; + + beforeEach(() => { + state = {} as IndexPatternPrivateState; + framePublicAPI = { + activeData: { + id: { + columns: [ + { + meta: { + sourceParams: { + hasPrecisionError: false, + }, + }, + }, + ], + }, + }, + } as unknown as FramePublicAPI; + + docLinks = { + links: { + aggs: { + terms_doc_count_error: 'http://terms_doc_count_error', + }, + }, + } as DocLinksStart; + }); + test('should not show precisionError if hasPrecisionError is false', () => { + expect(getPrecisionErrorWarningMessages(state, framePublicAPI, docLinks)).toHaveLength(0); + }); + + test('should not show precisionError if hasPrecisionError is not defined', () => { + delete framePublicAPI.activeData!.id.columns[0].meta.sourceParams!.hasPrecisionError; + + expect(getPrecisionErrorWarningMessages(state, framePublicAPI, docLinks)).toHaveLength(0); + }); + + test('should show precisionError if hasPrecisionError is true', () => { + framePublicAPI.activeData!.id.columns[0].meta.sourceParams!.hasPrecisionError = true; + + expect(getPrecisionErrorWarningMessages(state, framePublicAPI, docLinks)).toHaveLength(1); + }); + }); +}); diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/utils.ts b/x-pack/plugins/lens/public/indexpattern_datasource/utils.ts deleted file mode 100644 index a4e36367cef47..0000000000000 --- a/x-pack/plugins/lens/public/indexpattern_datasource/utils.ts +++ /dev/null @@ -1,103 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { DataType } from '../types'; -import { IndexPattern, IndexPatternLayer, DraggedField } from './types'; -import type { - BaseIndexPatternColumn, - FieldBasedIndexPatternColumn, - ReferenceBasedIndexPatternColumn, -} from './operations/definitions/column_types'; -import { operationDefinitionMap, IndexPatternColumn } from './operations'; - -import { getInvalidFieldMessage } from './operations/definitions/helpers'; -import { isQueryValid } from './operations/definitions/filters'; - -/** - * Normalizes the specified operation type. (e.g. document operations - * produce 'number') - */ -export function normalizeOperationDataType(type: DataType) { - if (type === 'histogram') return 'number'; - return type === 'document' ? 'number' : type; -} - -export function hasField(column: BaseIndexPatternColumn): column is FieldBasedIndexPatternColumn { - return 'sourceField' in column; -} - -export function sortByField(columns: C[]) { - return [...columns].sort((column1, column2) => { - if (hasField(column1) && hasField(column2)) { - return column1.sourceField.localeCompare(column2.sourceField); - } - return column1.operationType.localeCompare(column2.operationType); - }); -} - -export function isDraggedField(fieldCandidate: unknown): fieldCandidate is DraggedField { - return ( - typeof fieldCandidate === 'object' && - fieldCandidate !== null && - ['id', 'field', 'indexPatternId'].every((prop) => prop in fieldCandidate) - ); -} - -export function isColumnInvalid( - layer: IndexPatternLayer, - columnId: string, - indexPattern: IndexPattern -) { - const column: IndexPatternColumn | undefined = layer.columns[columnId]; - if (!column || !indexPattern) return; - - const operationDefinition = column.operationType && operationDefinitionMap[column.operationType]; - // check also references for errors - const referencesHaveErrors = - true && - 'references' in column && - Boolean(getReferencesErrors(layer, column, indexPattern).filter(Boolean).length); - - const operationErrorMessages = operationDefinition.getErrorMessage?.( - layer, - columnId, - indexPattern, - operationDefinitionMap - ); - - const filterHasError = column.filter ? !isQueryValid(column.filter, indexPattern) : false; - - return ( - (operationErrorMessages && operationErrorMessages.length > 0) || - referencesHaveErrors || - filterHasError - ); -} - -function getReferencesErrors( - layer: IndexPatternLayer, - column: ReferenceBasedIndexPatternColumn, - indexPattern: IndexPattern -) { - return column.references?.map((referenceId: string) => { - const referencedOperation = layer.columns[referenceId]?.operationType; - const referencedDefinition = operationDefinitionMap[referencedOperation]; - return referencedDefinition?.getErrorMessage?.( - layer, - referenceId, - indexPattern, - operationDefinitionMap - ); - }); -} - -export function fieldIsInvalid(column: IndexPatternColumn | undefined, indexPattern: IndexPattern) { - if (!column || !hasField(column)) { - return false; - } - return !!getInvalidFieldMessage(column, indexPattern)?.length; -} diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/utils.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/utils.tsx new file mode 100644 index 0000000000000..6d3f75a403dd7 --- /dev/null +++ b/x-pack/plugins/lens/public/indexpattern_datasource/utils.tsx @@ -0,0 +1,173 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { FormattedMessage } from '@kbn/i18n/react'; +import type { DocLinksStart } from 'kibana/public'; +import { EuiLink, EuiTextColor } from '@elastic/eui'; + +import { DatatableColumn } from 'src/plugins/expressions'; +import type { DataType, FramePublicAPI } from '../types'; +import type { + IndexPattern, + IndexPatternLayer, + DraggedField, + IndexPatternPrivateState, +} from './types'; +import type { + BaseIndexPatternColumn, + FieldBasedIndexPatternColumn, + ReferenceBasedIndexPatternColumn, +} from './operations/definitions/column_types'; + +import { operationDefinitionMap, IndexPatternColumn } from './operations'; + +import { getInvalidFieldMessage } from './operations/definitions/helpers'; +import { isQueryValid } from './operations/definitions/filters'; +import { checkColumnForPrecisionError } from '../../../../../src/plugins/data/common'; + +/** + * Normalizes the specified operation type. (e.g. document operations + * produce 'number') + */ +export function normalizeOperationDataType(type: DataType) { + if (type === 'histogram') return 'number'; + return type === 'document' ? 'number' : type; +} + +export function hasField(column: BaseIndexPatternColumn): column is FieldBasedIndexPatternColumn { + return 'sourceField' in column; +} + +export function sortByField(columns: C[]) { + return [...columns].sort((column1, column2) => { + if (hasField(column1) && hasField(column2)) { + return column1.sourceField.localeCompare(column2.sourceField); + } + return column1.operationType.localeCompare(column2.operationType); + }); +} + +export function isDraggedField(fieldCandidate: unknown): fieldCandidate is DraggedField { + return ( + typeof fieldCandidate === 'object' && + fieldCandidate !== null && + ['id', 'field', 'indexPatternId'].every((prop) => prop in fieldCandidate) + ); +} + +export function isColumnInvalid( + layer: IndexPatternLayer, + columnId: string, + indexPattern: IndexPattern +) { + const column: IndexPatternColumn | undefined = layer.columns[columnId]; + if (!column || !indexPattern) return; + + const operationDefinition = column.operationType && operationDefinitionMap[column.operationType]; + // check also references for errors + const referencesHaveErrors = + true && + 'references' in column && + Boolean(getReferencesErrors(layer, column, indexPattern).filter(Boolean).length); + + const operationErrorMessages = operationDefinition.getErrorMessage?.( + layer, + columnId, + indexPattern, + operationDefinitionMap + ); + + const filterHasError = column.filter ? !isQueryValid(column.filter, indexPattern) : false; + + return ( + (operationErrorMessages && operationErrorMessages.length > 0) || + referencesHaveErrors || + filterHasError + ); +} + +function getReferencesErrors( + layer: IndexPatternLayer, + column: ReferenceBasedIndexPatternColumn, + indexPattern: IndexPattern +) { + return column.references?.map((referenceId: string) => { + const referencedOperation = layer.columns[referenceId]?.operationType; + const referencedDefinition = operationDefinitionMap[referencedOperation]; + return referencedDefinition?.getErrorMessage?.( + layer, + referenceId, + indexPattern, + operationDefinitionMap + ); + }); +} + +export function fieldIsInvalid(column: IndexPatternColumn | undefined, indexPattern: IndexPattern) { + if (!column || !hasField(column)) { + return false; + } + return !!getInvalidFieldMessage(column, indexPattern)?.length; +} + +export function getPrecisionErrorWarningMessages( + state: IndexPatternPrivateState, + { activeData }: FramePublicAPI, + docLinks: DocLinksStart +) { + const warningMessages: React.ReactNode[] = []; + + if (state && activeData) { + Object.values(activeData) + .reduce((acc: DatatableColumn[], { columns }) => [...acc, ...columns], []) + .forEach((column) => { + if (checkColumnForPrecisionError(column)) { + warningMessages.push( + {column.name}, + topValues: ( + + + + ), + filters: ( + + + + ), + link: ( + + + + ), + }} + /> + ); + } + }); + } + + return warningMessages; +} diff --git a/x-pack/plugins/lens/public/metric_visualization/visualization.tsx b/x-pack/plugins/lens/public/metric_visualization/visualization.tsx index d832848db06f6..1b30e6c7fd932 100644 --- a/x-pack/plugins/lens/public/metric_visualization/visualization.tsx +++ b/x-pack/plugins/lens/public/metric_visualization/visualization.tsx @@ -54,9 +54,9 @@ export const metricVisualization: Visualization = { defaultMessage: 'Metric', }), groupLabel: i18n.translate('xpack.lens.metric.groupLabel', { - defaultMessage: 'Tabular and single value', + defaultMessage: 'Single value', }), - sortPriority: 1, + sortPriority: 3, }, ], diff --git a/x-pack/plugins/lens/public/pie_visualization/render_function.test.tsx b/x-pack/plugins/lens/public/pie_visualization/render_function.test.tsx index ad4e30cd6e89f..55b621498bb10 100644 --- a/x-pack/plugins/lens/public/pie_visualization/render_function.test.tsx +++ b/x-pack/plugins/lens/public/pie_visualization/render_function.test.tsx @@ -376,5 +376,50 @@ describe('PieVisualization component', () => { expect(component.find(VisualizationContainer)).toHaveLength(1); expect(component.find(EmptyPlaceholder).prop('icon')).toEqual(LensIconChartDonut); }); + + test('it should dynamically shrink the chart area to when some small slices are detected', () => { + const defaultData = getDefaultArgs().data; + const emptyData: LensMultiTable = { + ...defaultData, + tables: { + first: { + ...defaultData.tables.first, + rows: [ + { a: 60, b: 'I', c: 200, d: 'Row 1' }, + { a: 1, b: 'J', c: 0.1, d: 'Row 2' }, + ], + }, + }, + }; + + const component = shallow( + + ); + expect(component.find(Partition).prop('config')?.outerSizeRatio).toBeCloseTo(1 / 1.05); + }); + + test('it should bound the shrink the chart area to ~20% when some small slices are detected', () => { + const defaultData = getDefaultArgs().data; + const emptyData: LensMultiTable = { + ...defaultData, + tables: { + first: { + ...defaultData.tables.first, + rows: [ + { a: 60, b: 'I', c: 200, d: 'Row 1' }, + { a: 1, b: 'J', c: 0.1, d: 'Row 2' }, + { a: 1, b: 'K', c: 0.1, d: 'Row 3' }, + { a: 1, b: 'G', c: 0.1, d: 'Row 4' }, + { a: 1, b: 'H', c: 0.1, d: 'Row 5' }, + ], + }, + }, + }; + + const component = shallow( + + ); + expect(component.find(Partition).prop('config')?.outerSizeRatio).toBeCloseTo(1 / 1.2); + }); }); }); 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 449b152523881..070448978f4ef 100644 --- a/x-pack/plugins/lens/public/pie_visualization/render_function.tsx +++ b/x-pack/plugins/lens/public/pie_visualization/render_function.tsx @@ -204,6 +204,16 @@ export function PieComponent( } else if (categoryDisplay === 'inside') { // Prevent links from showing config.linkLabel = { maxCount: 0 }; + } else { + // if it contains any slice below 2% reduce the ratio + // first step: sum it up the overall sum + const overallSum = firstTable.rows.reduce((sum, row) => sum + row[metric!], 0); + const slices = firstTable.rows.map((row) => row[metric!] / overallSum); + const smallSlices = slices.filter((value) => value < 0.02).length; + if (smallSlices) { + // shrink up to 20% to give some room for the linked values + config.outerSizeRatio = 1 / (1 + Math.min(smallSlices * 0.05, 0.2)); + } } } const metricColumn = firstTable.columns.find((c) => c.id === metric)!; diff --git a/x-pack/plugins/lens/public/plugin.ts b/x-pack/plugins/lens/public/plugin.ts index 1532b2b099104..fb0a922c7e9a2 100644 --- a/x-pack/plugins/lens/public/plugin.ts +++ b/x-pack/plugins/lens/public/plugin.ts @@ -201,6 +201,8 @@ export class LensPlugin { plugins.fieldFormats.deserialize ); + const visualizationMap = await this.editorFrameService!.loadVisualizations(); + return { attributeService: getLensAttributeService(coreStart, plugins), capabilities: coreStart.application.capabilities, @@ -208,6 +210,7 @@ export class LensPlugin { timefilter: plugins.data.query.timefilter.timefilter, expressionRenderer: plugins.expressions.ReactExpressionRenderer, documentToExpression: this.editorFrameService!.documentToExpression, + visualizationMap, indexPatternService: plugins.data.indexPatterns, uiActions: plugins.uiActions, usageCollection, diff --git a/x-pack/plugins/lens/public/shared_components/legend_action_popover.tsx b/x-pack/plugins/lens/public/shared_components/legend_action_popover.tsx index 5027629ef6ae5..fa7e12083435c 100644 --- a/x-pack/plugins/lens/public/shared_components/legend_action_popover.tsx +++ b/x-pack/plugins/lens/public/shared_components/legend_action_popover.tsx @@ -8,6 +8,7 @@ import React, { useState } from 'react'; import { i18n } from '@kbn/i18n'; import { EuiContextMenuPanelDescriptor, EuiIcon, EuiPopover, EuiContextMenu } from '@elastic/eui'; +import { useLegendAction } from '@elastic/charts'; import type { LensFilterEvent } from '../types'; export interface LegendActionPopoverProps { @@ -31,6 +32,7 @@ export const LegendActionPopover: React.FunctionComponent { const [popoverOpen, setPopoverOpen] = useState(false); + const [ref, onClose] = useLegendAction(); const panels: EuiContextMenuPanelDescriptor[] = [ { id: 'main', @@ -65,6 +67,7 @@ export const LegendActionPopover: React.FunctionComponent setPopoverOpen(false)} + closePopover={() => { + setPopoverOpen(false); + onClose(); + }} panelPaddingSize="none" anchorPosition="upLeft" title={i18n.translate('xpack.lens.shared.legend.filterOptionsLegend', { diff --git a/x-pack/plugins/lens/public/shared_components/toolbar_popover.scss b/x-pack/plugins/lens/public/shared_components/toolbar_popover.scss new file mode 100644 index 0000000000000..a11e3373df467 --- /dev/null +++ b/x-pack/plugins/lens/public/shared_components/toolbar_popover.scss @@ -0,0 +1,3 @@ +.lnsVisToolbar__popover { + width: 365px; +} diff --git a/x-pack/plugins/lens/public/shared_components/toolbar_popover.tsx b/x-pack/plugins/lens/public/shared_components/toolbar_popover.tsx index 18c73a01cf784..e6bb2fcdc0825 100644 --- a/x-pack/plugins/lens/public/shared_components/toolbar_popover.tsx +++ b/x-pack/plugins/lens/public/shared_components/toolbar_popover.tsx @@ -5,6 +5,7 @@ * 2.0. */ +import './toolbar_popover.scss'; import React, { useState } from 'react'; import { EuiFlexItem, EuiPopover, EuiIcon, EuiPopoverTitle, IconType } from '@elastic/eui'; import { EuiIconLegend } from '../assets/legend'; @@ -36,6 +37,8 @@ export interface ToolbarPopoverProps { */ groupPosition?: ToolbarButtonProps['groupPosition']; buttonDataTestSubj?: string; + panelClassName?: string; + handleClose?: () => void; } export const ToolbarPopover: React.FunctionComponent = ({ @@ -45,6 +48,8 @@ export const ToolbarPopover: React.FunctionComponent = ({ isDisabled = false, groupPosition, buttonDataTestSubj, + panelClassName = 'lnsVisToolbar__popover', + handleClose, }) => { const [open, setOpen] = useState(false); @@ -53,7 +58,7 @@ export const ToolbarPopover: React.FunctionComponent = ({ return ( = ({ isOpen={open} closePopover={() => { setOpen(false); + handleClose?.(); }} anchorPosition="downRight" > diff --git a/x-pack/plugins/lens/public/types.ts b/x-pack/plugins/lens/public/types.ts index 975e44f703959..a9a9539064659 100644 --- a/x-pack/plugins/lens/public/types.ts +++ b/x-pack/plugins/lens/public/types.ts @@ -338,7 +338,7 @@ export type DatasourceDimensionProps = SharedDimensionProps & { invalid?: boolean; invalidMessage?: string; }; - +export type ParamEditorCustomProps = Record & { label?: string }; // The only way a visualization has to restrict the query building export type DatasourceDimensionEditorProps = DatasourceDimensionProps & { // Not a StateSetter because we have this unique use case of determining valid columns @@ -356,6 +356,7 @@ export type DatasourceDimensionEditorProps = DatasourceDimensionPro isFullscreen: boolean; layerType: LayerType | undefined; supportStaticValue: boolean; + paramEditorCustomProps?: ParamEditorCustomProps; supportFieldFormat?: boolean; }; @@ -485,6 +486,9 @@ export type VisualizationDimensionGroupConfig = SharedDimensionProps & { invalidMessage?: string; // need a special flag to know when to pass the previous column on duplicating requiresPreviousColumnOnDuplicate?: boolean; + supportStaticValue?: boolean; + paramEditorCustomProps?: ParamEditorCustomProps; + supportFieldFormat?: boolean; }; interface VisualizationDimensionChangeProps { @@ -673,8 +677,6 @@ export interface Visualization { */ getConfiguration: (props: VisualizationConfigProps) => { groups: VisualizationDimensionGroupConfig[]; - supportStaticValue?: boolean; - supportFieldFormat?: boolean; }; /** diff --git a/x-pack/plugins/lens/public/xy_visualization/expression_reference_lines.test.tsx b/x-pack/plugins/lens/public/xy_visualization/expression_reference_lines.test.tsx new file mode 100644 index 0000000000000..85d5dd362a431 --- /dev/null +++ b/x-pack/plugins/lens/public/xy_visualization/expression_reference_lines.test.tsx @@ -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 { LineAnnotation, RectAnnotation } from '@elastic/charts'; +import { shallow } from 'enzyme'; +import React from 'react'; +import { PaletteOutput } from 'src/plugins/charts/common'; +import { chartPluginMock } from 'src/plugins/charts/public/mocks'; +import { FieldFormat } from 'src/plugins/field_formats/common'; +import { layerTypes, LensMultiTable } from '../../common'; +import { LayerArgs, YConfig } from '../../common/expressions'; +import { + ReferenceLineAnnotations, + ReferenceLineAnnotationsProps, +} from './expression_reference_lines'; + +const paletteService = chartPluginMock.createPaletteRegistry(); + +const mockPaletteOutput: PaletteOutput = { + type: 'palette', + name: 'mock', + params: {}, +}; + +const row: Record = { + xAccessorFirstId: 1, + xAccessorSecondId: 2, + yAccessorLeftFirstId: 5, + yAccessorLeftSecondId: 10, + yAccessorRightFirstId: 5, + yAccessorRightSecondId: 10, +}; + +const histogramData: LensMultiTable = { + type: 'lens_multitable', + tables: { + firstLayer: { + type: 'datatable', + rows: [row], + columns: Object.keys(row).map((id) => ({ + id, + name: `Static value: ${row[id]}`, + meta: { + type: 'number', + params: { id: 'number' }, + }, + })), + }, + }, + dateRange: { + fromDate: new Date('2020-04-01T16:14:16.246Z'), + toDate: new Date('2020-04-01T17:15:41.263Z'), + }, +}; + +function createLayers(yConfigs: LayerArgs['yConfig']): LayerArgs[] { + return [ + { + layerId: 'firstLayer', + layerType: layerTypes.REFERENCE_LINE, + hide: false, + yScaleType: 'linear', + xScaleType: 'linear', + isHistogram: false, + seriesType: 'bar_stacked', + accessors: (yConfigs || []).map(({ forAccessor }) => forAccessor), + palette: mockPaletteOutput, + yConfig: yConfigs, + }, + ]; +} + +interface YCoords { + y0: number | undefined; + y1: number | undefined; +} +interface XCoords { + x0: number | undefined; + x1: number | undefined; +} + +function getAxisFromId(layerPrefix: string): YConfig['axisMode'] { + return /left/i.test(layerPrefix) ? 'left' : /right/i.test(layerPrefix) ? 'right' : 'bottom'; +} + +const emptyCoords = { x0: undefined, x1: undefined, y0: undefined, y1: undefined }; + +describe('ReferenceLineAnnotations', () => { + describe('with fill', () => { + let formatters: Record<'left' | 'right' | 'bottom', FieldFormat | undefined>; + let defaultProps: Omit; + + beforeEach(() => { + formatters = { + left: { convert: jest.fn((x) => x) } as unknown as FieldFormat, + right: { convert: jest.fn((x) => x) } as unknown as FieldFormat, + bottom: { convert: jest.fn((x) => x) } as unknown as FieldFormat, + }; + + defaultProps = { + formatters, + paletteService, + syncColors: false, + isHorizontal: false, + axesMap: { left: true, right: false }, + paddingMap: {}, + }; + }); + + it.each([ + ['yAccessorLeft', 'above'], + ['yAccessorLeft', 'below'], + ['yAccessorRight', 'above'], + ['yAccessorRight', 'below'], + ] as Array<[string, YConfig['fill']]>)( + 'should render a RectAnnotation for a reference line with fill set: %s %s', + (layerPrefix, fill) => { + const axisMode = getAxisFromId(layerPrefix); + const wrapper = shallow( + + ); + + const y0 = fill === 'above' ? 5 : undefined; + const y1 = fill === 'above' ? undefined : 5; + + expect(wrapper.find(LineAnnotation).exists()).toBe(true); + expect(wrapper.find(RectAnnotation).exists()).toBe(true); + expect(wrapper.find(RectAnnotation).prop('dataValues')).toEqual( + expect.arrayContaining([ + { + coordinates: { x0: undefined, x1: undefined, y0, y1 }, + details: y0 ?? y1, + header: undefined, + }, + ]) + ); + } + ); + + it.each([ + ['xAccessor', 'above'], + ['xAccessor', 'below'], + ] as Array<[string, YConfig['fill']]>)( + 'should render a RectAnnotation for a reference line with fill set: %s %s', + (layerPrefix, fill) => { + const wrapper = shallow( + + ); + + const x0 = fill === 'above' ? 1 : undefined; + const x1 = fill === 'above' ? undefined : 1; + + expect(wrapper.find(LineAnnotation).exists()).toBe(true); + expect(wrapper.find(RectAnnotation).exists()).toBe(true); + expect(wrapper.find(RectAnnotation).prop('dataValues')).toEqual( + expect.arrayContaining([ + { + coordinates: { ...emptyCoords, x0, x1 }, + details: x0 ?? x1, + header: undefined, + }, + ]) + ); + } + ); + + it.each([ + ['yAccessorLeft', 'above', { y0: 5, y1: 10 }, { y0: 10, y1: undefined }], + ['yAccessorLeft', 'below', { y0: undefined, y1: 5 }, { y0: 5, y1: 10 }], + ['yAccessorRight', 'above', { y0: 5, y1: 10 }, { y0: 10, y1: undefined }], + ['yAccessorRight', 'below', { y0: undefined, y1: 5 }, { y0: 5, y1: 10 }], + ] as Array<[string, YConfig['fill'], YCoords, YCoords]>)( + 'should avoid overlap between two reference lines with fill in the same direction: 2 x %s %s', + (layerPrefix, fill, coordsA, coordsB) => { + const axisMode = getAxisFromId(layerPrefix); + const wrapper = shallow( + + ); + + expect(wrapper.find(RectAnnotation).first().prop('dataValues')).toEqual( + expect.arrayContaining([ + { + coordinates: { ...emptyCoords, ...coordsA }, + details: coordsA.y0 ?? coordsA.y1, + header: undefined, + }, + ]) + ); + expect(wrapper.find(RectAnnotation).last().prop('dataValues')).toEqual( + expect.arrayContaining([ + { + coordinates: { ...emptyCoords, ...coordsB }, + details: coordsB.y1 ?? coordsB.y0, + header: undefined, + }, + ]) + ); + } + ); + + it.each([ + ['xAccessor', 'above', { x0: 1, x1: 2 }, { x0: 2, x1: undefined }], + ['xAccessor', 'below', { x0: undefined, x1: 1 }, { x0: 1, x1: 2 }], + ] as Array<[string, YConfig['fill'], XCoords, XCoords]>)( + 'should avoid overlap between two reference lines with fill in the same direction: 2 x %s %s', + (layerPrefix, fill, coordsA, coordsB) => { + const wrapper = shallow( + + ); + + expect(wrapper.find(RectAnnotation).first().prop('dataValues')).toEqual( + expect.arrayContaining([ + { + coordinates: { ...emptyCoords, ...coordsA }, + details: coordsA.x0 ?? coordsA.x1, + header: undefined, + }, + ]) + ); + expect(wrapper.find(RectAnnotation).last().prop('dataValues')).toEqual( + expect.arrayContaining([ + { + coordinates: { ...emptyCoords, ...coordsB }, + details: coordsB.x1 ?? coordsB.x0, + header: undefined, + }, + ]) + ); + } + ); + + it.each(['yAccessorLeft', 'yAccessorRight', 'xAccessor'])( + 'should let areas in different directions overlap: %s', + (layerPrefix) => { + const axisMode = getAxisFromId(layerPrefix); + + const wrapper = shallow( + + ); + + expect(wrapper.find(RectAnnotation).first().prop('dataValues')).toEqual( + expect.arrayContaining([ + { + coordinates: { ...emptyCoords, ...(axisMode === 'bottom' ? { x0: 1 } : { y0: 5 }) }, + details: axisMode === 'bottom' ? 1 : 5, + header: undefined, + }, + ]) + ); + expect(wrapper.find(RectAnnotation).last().prop('dataValues')).toEqual( + expect.arrayContaining([ + { + coordinates: { ...emptyCoords, ...(axisMode === 'bottom' ? { x1: 2 } : { y1: 10 }) }, + details: axisMode === 'bottom' ? 2 : 10, + header: undefined, + }, + ]) + ); + } + ); + + it.each([ + ['above', { y0: 5, y1: 10 }, { y0: 10, y1: undefined }], + ['below', { y0: undefined, y1: 5 }, { y0: 5, y1: 10 }], + ] as Array<[YConfig['fill'], YCoords, YCoords]>)( + 'should be robust and works also for different axes when on same direction: 1x Left + 1x Right both %s', + (fill, coordsA, coordsB) => { + const wrapper = shallow( + + ); + + expect(wrapper.find(RectAnnotation).first().prop('dataValues')).toEqual( + expect.arrayContaining([ + { + coordinates: { ...emptyCoords, ...coordsA }, + details: coordsA.y0 ?? coordsA.y1, + header: undefined, + }, + ]) + ); + expect(wrapper.find(RectAnnotation).last().prop('dataValues')).toEqual( + expect.arrayContaining([ + { + coordinates: { ...emptyCoords, ...coordsB }, + details: coordsB.y1 ?? coordsB.y0, + header: undefined, + }, + ]) + ); + } + ); + }); +}); diff --git a/x-pack/plugins/lens/public/xy_visualization/expression_reference_lines.tsx b/x-pack/plugins/lens/public/xy_visualization/expression_reference_lines.tsx index 42e02871026df..d41baff0bc1dc 100644 --- a/x-pack/plugins/lens/public/xy_visualization/expression_reference_lines.tsx +++ b/x-pack/plugins/lens/public/xy_visualization/expression_reference_lines.tsx @@ -17,7 +17,7 @@ import type { LayerArgs, YConfig } from '../../common/expressions'; import type { LensMultiTable } from '../../common/types'; import { hasIcon } from './xy_config_panel/reference_line_panel'; -const REFERENCE_LINE_MARKER_SIZE = 20; +export const REFERENCE_LINE_MARKER_SIZE = 20; export const computeChartMargins = ( referenceLinePaddings: Partial>, @@ -180,6 +180,17 @@ function getMarkerToShow( } } +export interface ReferenceLineAnnotationsProps { + layers: LayerArgs[]; + data: LensMultiTable; + formatters: Record<'left' | 'right' | 'bottom', FieldFormat | undefined>; + paletteService: PaletteRegistry; + syncColors: boolean; + axesMap: Record<'left' | 'right', boolean>; + isHorizontal: boolean; + paddingMap: Partial>; +} + export const ReferenceLineAnnotations = ({ layers, data, @@ -189,16 +200,7 @@ export const ReferenceLineAnnotations = ({ axesMap, isHorizontal, paddingMap, -}: { - layers: LayerArgs[]; - data: LensMultiTable; - formatters: Record<'left' | 'right' | 'bottom', FieldFormat | undefined>; - paletteService: PaletteRegistry; - syncColors: boolean; - axesMap: Record<'left' | 'right', boolean>; - isHorizontal: boolean; - paddingMap: Partial>; -}) => { +}: ReferenceLineAnnotationsProps) => { return ( <> {layers.flatMap((layer) => { @@ -317,10 +319,9 @@ export const ReferenceLineAnnotations = ({ id={`${layerId}-${yConfig.forAccessor}-rect`} key={`${layerId}-${yConfig.forAccessor}-rect`} dataValues={table.rows.map(() => { - const nextValue = - !isFillAbove && shouldCheckNextReferenceLine - ? row[groupedByDirection[yConfig.fill!][indexFromSameType + 1].forAccessor] - : undefined; + const nextValue = shouldCheckNextReferenceLine + ? row[groupedByDirection[yConfig.fill!][indexFromSameType + 1].forAccessor] + : undefined; if (yConfig.axisMode === 'bottom') { return { coordinates: { diff --git a/x-pack/plugins/lens/public/xy_visualization/reference_line_helpers.test.ts b/x-pack/plugins/lens/public/xy_visualization/reference_line_helpers.test.ts index 9dacc12c68d65..9f48b8c8c36e4 100644 --- a/x-pack/plugins/lens/public/xy_visualization/reference_line_helpers.test.ts +++ b/x-pack/plugins/lens/public/xy_visualization/reference_line_helpers.test.ts @@ -120,6 +120,32 @@ describe('reference_line helpers', () => { ).toBe(100); }); + it('should return 0 as result of calculation', () => { + expect( + getStaticValue( + [ + { + layerId: 'id-a', + seriesType: 'area', + layerType: 'data', + accessors: ['a'], + yConfig: [{ forAccessor: 'a', axisMode: 'right' }], + } as XYLayerConfig, + ], + 'yRight', + { + activeData: getActiveData([ + { + id: 'id-a', + rows: [{ a: -30 }, { a: 10 }], + }, + ]), + }, + hasAllNumberHistogram + ) + ).toBe(0); + }); + it('should work for no yConfig defined and fallback to left axis', () => { expect( getStaticValue( @@ -459,6 +485,34 @@ describe('reference_line helpers', () => { ).toEqual({ min: 0, max: 375 }); }); + it('should compute the correct value for a histogram on stacked chart for the xAccessor', () => { + for (const seriesType of ['bar_stacked', 'bar_horizontal_stacked', 'area_stacked']) + expect( + computeOverallDataDomain( + [ + { layerId: 'id-a', seriesType, accessors: ['c'] }, + { layerId: 'id-b', seriesType, accessors: ['f'] }, + ] as XYLayerConfig[], + ['c', 'f'], + getActiveData([ + { + id: 'id-a', + rows: Array(3) + .fill(1) + .map((_, i) => ({ a: 50 * i, b: 100 * i, c: i })), + }, + { + id: 'id-b', + rows: Array(3) + .fill(1) + .map((_, i) => ({ d: 25 * (i + 1), e: i % 2 ? 100 : null, f: i })), + }, + ]), + false // this will avoid the stacking behaviour + ) + ).toEqual({ min: 0, max: 2 }); + }); + it('should compute the correct value for a histogram non-stacked chart', () => { for (const seriesType of ['bar', 'bar_horizontal', 'line', 'area']) expect( diff --git a/x-pack/plugins/lens/public/xy_visualization/reference_line_helpers.tsx b/x-pack/plugins/lens/public/xy_visualization/reference_line_helpers.tsx index 71ce2d0ea2082..127bf02b81f89 100644 --- a/x-pack/plugins/lens/public/xy_visualization/reference_line_helpers.tsx +++ b/x-pack/plugins/lens/public/xy_visualization/reference_line_helpers.tsx @@ -104,14 +104,14 @@ export function getStaticValue( ) { return fallbackValue; } - return ( - computeStaticValueForGroup( - filteredLayers, - accessors, - activeData, - groupId !== 'x' // histogram axis should compute the min based on the current data - ) || fallbackValue + const computedValue = computeStaticValueForGroup( + filteredLayers, + accessors, + activeData, + groupId !== 'x', // histogram axis should compute the min based on the current data + groupId !== 'x' ); + return computedValue ?? fallbackValue; } function getAccessorCriteriaForGroup( @@ -152,13 +152,15 @@ function getAccessorCriteriaForGroup( export function computeOverallDataDomain( dataLayers: Array>, accessorIds: string[], - activeData: NonNullable + activeData: NonNullable, + allowStacking: boolean = true ) { const accessorMap = new Set(accessorIds); let min: number | undefined; let max: number | undefined; - const [stacked, unstacked] = partition(dataLayers, ({ seriesType }) => - isStackedChart(seriesType) + const [stacked, unstacked] = partition( + dataLayers, + ({ seriesType }) => isStackedChart(seriesType) && allowStacking ); for (const { layerId, accessors } of unstacked) { const table = activeData[layerId]; @@ -215,7 +217,8 @@ function computeStaticValueForGroup( dataLayers: Array>, accessorIds: string[], activeData: NonNullable, - minZeroOrNegativeBase: boolean = true + minZeroOrNegativeBase: boolean = true, + allowStacking: boolean = true ) { const defaultReferenceLineFactor = 3 / 4; @@ -224,7 +227,12 @@ function computeStaticValueForGroup( return defaultReferenceLineFactor; } - const { min, max } = computeOverallDataDomain(dataLayers, accessorIds, activeData); + const { min, max } = computeOverallDataDomain( + dataLayers, + accessorIds, + activeData, + allowStacking + ); if (min != null && max != null && isFinite(min) && isFinite(max)) { // Custom axis bounds can go below 0, so consider also lower values than 0 diff --git a/x-pack/plugins/lens/public/xy_visualization/types.ts b/x-pack/plugins/lens/public/xy_visualization/types.ts index 475571b2965f6..75e80782c5d38 100644 --- a/x-pack/plugins/lens/public/xy_visualization/types.ts +++ b/x-pack/plugins/lens/public/xy_visualization/types.ts @@ -69,6 +69,7 @@ export const visualizationTypes: VisualizationType[] = [ defaultMessage: 'Bar vertical', }), groupLabel: groupLabelForBar, + sortPriority: 4, }, { id: 'bar_horizontal', @@ -153,5 +154,6 @@ export const visualizationTypes: VisualizationType[] = [ defaultMessage: 'Line', }), groupLabel: groupLabelForLineAndArea, + sortPriority: 2, }, ]; diff --git a/x-pack/plugins/lens/public/xy_visualization/visualization.test.ts b/x-pack/plugins/lens/public/xy_visualization/visualization.test.ts index 973501816bc3e..0c3fa21708263 100644 --- a/x-pack/plugins/lens/public/xy_visualization/visualization.test.ts +++ b/x-pack/plugins/lens/public/xy_visualization/visualization.test.ts @@ -781,13 +781,12 @@ describe('xy_visualization', () => { const state = getStateWithBaseReferenceLine(); state.layers[0].accessors = []; state.layers[1].yConfig = undefined; - expect( xyVisualization.getConfiguration({ state: getStateWithBaseReferenceLine(), frame, layerId: 'referenceLine', - }).supportStaticValue + }).groups[0].supportStaticValue ).toBeTruthy(); }); diff --git a/x-pack/plugins/lens/public/xy_visualization/visualization.tsx b/x-pack/plugins/lens/public/xy_visualization/visualization.tsx index c23eccb196744..2f3ec7e2723d4 100644 --- a/x-pack/plugins/lens/public/xy_visualization/visualization.tsx +++ b/x-pack/plugins/lens/public/xy_visualization/visualization.tsx @@ -274,7 +274,7 @@ export const getXyVisualization = ({ getConfiguration({ state, frame, layerId }) { const layer = state.layers.find((l) => l.layerId === layerId); if (!layer) { - return { groups: [], supportStaticValue: true }; + return { groups: [] }; } const datasource = frame.datasourceLayers[layer.layerId]; @@ -345,8 +345,6 @@ export const getXyVisualization = ({ frame?.activeData ); return { - supportFieldFormat: false, - supportStaticValue: true, // Each reference lines layer panel will have sections for each available axis // (horizontal axis, vertical axis left, vertical axis right). // Only axes that support numeric reference lines should be shown @@ -362,6 +360,13 @@ export const getXyVisualization = ({ supportsMoreColumns: true, required: false, enableDimensionEditor: true, + supportStaticValue: true, + paramEditorCustomProps: { + label: i18n.translate('xpack.lens.indexPattern.staticValue.label', { + defaultMessage: 'Reference line value', + }), + }, + supportFieldFormat: false, dataTestSubj, invalid: !valid, invalidMessage: diff --git a/x-pack/plugins/lens/public/xy_visualization/xy_config_panel/color_picker.tsx b/x-pack/plugins/lens/public/xy_visualization/xy_config_panel/color_picker.tsx index e3e53126015eb..517f4bd378591 100644 --- a/x-pack/plugins/lens/public/xy_visualization/xy_config_panel/color_picker.tsx +++ b/x-pack/plugins/lens/public/xy_visualization/xy_config_panel/color_picker.tsx @@ -5,7 +5,6 @@ * 2.0. */ -import './xy_config_panel.scss'; import React, { useMemo, useState } from 'react'; import { i18n } from '@kbn/i18n'; import { debounce } from 'lodash'; diff --git a/x-pack/plugins/lens/public/xy_visualization/xy_config_panel/index.tsx b/x-pack/plugins/lens/public/xy_visualization/xy_config_panel/index.tsx index e18ea18c30fb0..cef4a5f01ce8a 100644 --- a/x-pack/plugins/lens/public/xy_visualization/xy_config_panel/index.tsx +++ b/x-pack/plugins/lens/public/xy_visualization/xy_config_panel/index.tsx @@ -5,7 +5,6 @@ * 2.0. */ -import './xy_config_panel.scss'; import React, { memo, useCallback } from 'react'; import { i18n } from '@kbn/i18n'; import { Position, ScaleType, VerticalAlignment, HorizontalAlignment } from '@elastic/charts'; diff --git a/x-pack/plugins/lens/public/xy_visualization/xy_config_panel/layer_header.tsx b/x-pack/plugins/lens/public/xy_visualization/xy_config_panel/layer_header.tsx index d81979f603943..5de54cecd2101 100644 --- a/x-pack/plugins/lens/public/xy_visualization/xy_config_panel/layer_header.tsx +++ b/x-pack/plugins/lens/public/xy_visualization/xy_config_panel/layer_header.tsx @@ -5,7 +5,6 @@ * 2.0. */ -import './xy_config_panel.scss'; import React, { useState } from 'react'; import { i18n } from '@kbn/i18n'; import { EuiIcon, EuiPopover, EuiSelectable, EuiText, EuiPopoverTitle } from '@elastic/eui'; diff --git a/x-pack/plugins/lens/public/xy_visualization/xy_config_panel/reference_line_panel.tsx b/x-pack/plugins/lens/public/xy_visualization/xy_config_panel/reference_line_panel.tsx index 7b9fd01e540fe..f35bcae6ffb9f 100644 --- a/x-pack/plugins/lens/public/xy_visualization/xy_config_panel/reference_line_panel.tsx +++ b/x-pack/plugins/lens/public/xy_visualization/xy_config_panel/reference_line_panel.tsx @@ -5,7 +5,6 @@ * 2.0. */ -import './xy_config_panel.scss'; import React, { useCallback, useState } from 'react'; import { i18n } from '@kbn/i18n'; import { diff --git a/x-pack/plugins/lens/public/xy_visualization/xy_config_panel/xy_config_panel.scss b/x-pack/plugins/lens/public/xy_visualization/xy_config_panel/xy_config_panel.scss deleted file mode 100644 index a2caeb93477fa..0000000000000 --- a/x-pack/plugins/lens/public/xy_visualization/xy_config_panel/xy_config_panel.scss +++ /dev/null @@ -1,3 +0,0 @@ -.lnsXyToolbar__popover { - width: 365px; -} diff --git a/x-pack/plugins/lens/server/expressions/expressions.ts b/x-pack/plugins/lens/server/expressions/expressions.ts index 882b8b938717b..a04ad27d1a276 100644 --- a/x-pack/plugins/lens/server/expressions/expressions.ts +++ b/x-pack/plugins/lens/server/expressions/expressions.ts @@ -22,6 +22,7 @@ import { axisTitlesVisibilityConfig, getTimeScale, getDatatable, + lensMultitable, } from '../../common/expressions'; import { getFormatFactory, getTimeZoneFactory } from './utils'; @@ -32,6 +33,8 @@ export const setupExpressions = ( core: CoreSetup, expressions: ExpressionsServerSetup ) => { + [lensMultitable].forEach((expressionType) => expressions.registerType(expressionType)); + [ pie, xyChart, diff --git a/x-pack/plugins/maps/common/constants.ts b/x-pack/plugins/maps/common/constants.ts index 5de21099c9340..0f2ce2c917738 100644 --- a/x-pack/plugins/maps/common/constants.ts +++ b/x-pack/plugins/maps/common/constants.ts @@ -85,7 +85,6 @@ export const SOURCE_DATA_REQUEST_ID = 'source'; export const SOURCE_META_DATA_REQUEST_ID = `${SOURCE_DATA_REQUEST_ID}_${META_DATA_REQUEST_ID_SUFFIX}`; export const SOURCE_FORMATTERS_DATA_REQUEST_ID = `${SOURCE_DATA_REQUEST_ID}_${FORMATTERS_DATA_REQUEST_ID_SUFFIX}`; export const SOURCE_BOUNDS_DATA_REQUEST_ID = `${SOURCE_DATA_REQUEST_ID}_bounds`; -export const SUPPORTS_FEATURE_EDITING_REQUEST_ID = 'SUPPORTS_FEATURE_EDITING_REQUEST_ID'; export const MIN_ZOOM = 0; export const MAX_ZOOM = 24; diff --git a/x-pack/plugins/maps/common/migrations/add_field_meta_options.js b/x-pack/plugins/maps/common/migrations/add_field_meta_options.js index 8143c05913f7b..33a98c7dbf33c 100644 --- a/x-pack/plugins/maps/common/migrations/add_field_meta_options.js +++ b/x-pack/plugins/maps/common/migrations/add_field_meta_options.js @@ -18,7 +18,13 @@ export function addFieldMetaOptions({ attributes }) { return attributes; } - const layerList = JSON.parse(attributes.layerListJSON); + let layerList = []; + try { + layerList = JSON.parse(attributes.layerListJSON); + } catch (e) { + throw new Error('Unable to parse attribute layerListJSON'); + } + layerList.forEach((layerDescriptor) => { if (isVectorLayer(layerDescriptor) && _.has(layerDescriptor, 'style.properties')) { Object.values(layerDescriptor.style.properties).forEach((stylePropertyDescriptor) => { diff --git a/x-pack/plugins/maps/common/migrations/add_type_to_termjoin.ts b/x-pack/plugins/maps/common/migrations/add_type_to_termjoin.ts index e46bf6a1a6e7f..b43b8979094bb 100644 --- a/x-pack/plugins/maps/common/migrations/add_type_to_termjoin.ts +++ b/x-pack/plugins/maps/common/migrations/add_type_to_termjoin.ts @@ -21,7 +21,12 @@ export function addTypeToTermJoin({ return attributes; } - const layerList: LayerDescriptor[] = JSON.parse(attributes.layerListJSON); + let layerList: LayerDescriptor[] = []; + try { + layerList = JSON.parse(attributes.layerListJSON); + } catch (e) { + throw new Error('Unable to parse attribute layerListJSON'); + } layerList.forEach((layer: LayerDescriptor) => { if (!('joins' in layer)) { diff --git a/x-pack/plugins/maps/common/migrations/ems_raster_tile_to_ems_vector_tile.js b/x-pack/plugins/maps/common/migrations/ems_raster_tile_to_ems_vector_tile.js index 4e52e88bcc1cd..2945b9efed958 100644 --- a/x-pack/plugins/maps/common/migrations/ems_raster_tile_to_ems_vector_tile.js +++ b/x-pack/plugins/maps/common/migrations/ems_raster_tile_to_ems_vector_tile.js @@ -23,7 +23,13 @@ export function emsRasterTileToEmsVectorTile({ attributes }) { return attributes; } - const layerList = JSON.parse(attributes.layerListJSON); + let layerList = []; + try { + layerList = JSON.parse(attributes.layerListJSON); + } catch (e) { + throw new Error('Unable to parse attribute layerListJSON'); + } + layerList.forEach((layer) => { if (isTileLayer(layer) && isEmsTileSource(layer)) { // Just need to switch layer type to migrate TILE layer to VECTOR_TILE layer diff --git a/x-pack/plugins/maps/common/migrations/join_agg_key.ts b/x-pack/plugins/maps/common/migrations/join_agg_key.ts index e3e5a2fac34f4..726855783be63 100644 --- a/x-pack/plugins/maps/common/migrations/join_agg_key.ts +++ b/x-pack/plugins/maps/common/migrations/join_agg_key.ts @@ -62,7 +62,13 @@ export function migrateJoinAggKey({ return attributes; } - const layerList: LayerDescriptor[] = JSON.parse(attributes.layerListJSON); + let layerList = []; + try { + layerList = JSON.parse(attributes.layerListJSON); + } catch (e) { + throw new Error('Unable to parse attribute layerListJSON'); + } + layerList.forEach((layerDescriptor: LayerDescriptor) => { if ( layerDescriptor.type === LAYER_TYPE.VECTOR || diff --git a/x-pack/plugins/maps/common/migrations/migrate_symbol_style_descriptor.js b/x-pack/plugins/maps/common/migrations/migrate_symbol_style_descriptor.js index 3d06c60d23df7..6dab8595663ed 100644 --- a/x-pack/plugins/maps/common/migrations/migrate_symbol_style_descriptor.js +++ b/x-pack/plugins/maps/common/migrations/migrate_symbol_style_descriptor.js @@ -18,7 +18,13 @@ export function migrateSymbolStyleDescriptor({ attributes }) { return attributes; } - const layerList = JSON.parse(attributes.layerListJSON); + let layerList = []; + try { + layerList = JSON.parse(attributes.layerListJSON); + } catch (e) { + throw new Error('Unable to parse attribute layerListJSON'); + } + layerList.forEach((layerDescriptor) => { if (!isVectorLayer(layerDescriptor) || !_.has(layerDescriptor, 'style.properties')) { return; diff --git a/x-pack/plugins/maps/common/migrations/move_apply_global_query.js b/x-pack/plugins/maps/common/migrations/move_apply_global_query.js index 2d485400db9ca..b0c7d2031ffa7 100644 --- a/x-pack/plugins/maps/common/migrations/move_apply_global_query.js +++ b/x-pack/plugins/maps/common/migrations/move_apply_global_query.js @@ -22,7 +22,13 @@ export function moveApplyGlobalQueryToSources({ attributes }) { return attributes; } - const layerList = JSON.parse(attributes.layerListJSON); + let layerList = []; + try { + layerList = JSON.parse(attributes.layerListJSON); + } catch (e) { + throw new Error('Unable to parse attribute layerListJSON'); + } + layerList.forEach((layerDescriptor) => { const applyGlobalQuery = _.get(layerDescriptor, 'applyGlobalQuery', true); delete layerDescriptor.applyGlobalQuery; diff --git a/x-pack/plugins/maps/common/migrations/move_attribution.ts b/x-pack/plugins/maps/common/migrations/move_attribution.ts index 74258e815439e..6ab5fb93ca981 100644 --- a/x-pack/plugins/maps/common/migrations/move_attribution.ts +++ b/x-pack/plugins/maps/common/migrations/move_attribution.ts @@ -18,7 +18,12 @@ export function moveAttribution({ return attributes; } - const layerList: LayerDescriptor[] = JSON.parse(attributes.layerListJSON); + let layerList: LayerDescriptor[] = []; + try { + layerList = JSON.parse(attributes.layerListJSON); + } catch (e) { + throw new Error('Unable to parse attribute layerListJSON'); + } layerList.forEach((layer: LayerDescriptor) => { const sourceDescriptor = layer.sourceDescriptor as { diff --git a/x-pack/plugins/maps/common/migrations/references.ts b/x-pack/plugins/maps/common/migrations/references.ts index 41d9dc063fe47..1ced7e06c59cc 100644 --- a/x-pack/plugins/maps/common/migrations/references.ts +++ b/x-pack/plugins/maps/common/migrations/references.ts @@ -29,7 +29,13 @@ export function extractReferences({ const extractedReferences: SavedObjectReference[] = []; - const layerList: LayerDescriptor[] = JSON.parse(attributes.layerListJSON); + let layerList: LayerDescriptor[] = []; + try { + layerList = JSON.parse(attributes.layerListJSON); + } catch (e) { + throw new Error('Unable to parse attribute layerListJSON'); + } + layerList.forEach((layer, layerIndex) => { // Extract index-pattern references from source descriptor if (layer.sourceDescriptor && 'indexPatternId' in layer.sourceDescriptor) { @@ -92,7 +98,13 @@ export function injectReferences({ return { attributes }; } - const layerList: LayerDescriptor[] = JSON.parse(attributes.layerListJSON); + let layerList: LayerDescriptor[] = []; + try { + layerList = JSON.parse(attributes.layerListJSON); + } catch (e) { + throw new Error('Unable to parse attribute layerListJSON'); + } + layerList.forEach((layer) => { // Inject index-pattern references into source descriptor if (layer.sourceDescriptor && 'indexPatternRefName' in layer.sourceDescriptor) { diff --git a/x-pack/plugins/maps/common/migrations/scaling_type.ts b/x-pack/plugins/maps/common/migrations/scaling_type.ts index 1744b3627b1d6..5106784a16d0a 100644 --- a/x-pack/plugins/maps/common/migrations/scaling_type.ts +++ b/x-pack/plugins/maps/common/migrations/scaling_type.ts @@ -24,7 +24,13 @@ export function migrateUseTopHitsToScalingType({ return attributes; } - const layerList: LayerDescriptor[] = JSON.parse(attributes.layerListJSON); + let layerList: LayerDescriptor[] = []; + try { + layerList = JSON.parse(attributes.layerListJSON); + } catch (e) { + throw new Error('Unable to parse attribute layerListJSON'); + } + layerList.forEach((layerDescriptor: LayerDescriptor) => { if (isEsDocumentSource(layerDescriptor)) { const sourceDescriptor = layerDescriptor.sourceDescriptor as ESSearchSourceDescriptor; diff --git a/x-pack/plugins/maps/common/migrations/set_default_auto_fit_to_bounds.ts b/x-pack/plugins/maps/common/migrations/set_default_auto_fit_to_bounds.ts index 36fd3cf8da7e2..af86b62165683 100644 --- a/x-pack/plugins/maps/common/migrations/set_default_auto_fit_to_bounds.ts +++ b/x-pack/plugins/maps/common/migrations/set_default_auto_fit_to_bounds.ts @@ -16,10 +16,14 @@ export function setDefaultAutoFitToBounds({ return attributes; } - // MapState type is defined in public, no need to bring all of that to common for this migration - const mapState: { settings?: { autoFitToDataBounds: boolean } } = JSON.parse( - attributes.mapStateJSON - ); + // MapState type is defined in public, no need to pull type definition into common for this migration + let mapState: { settings?: { autoFitToDataBounds: boolean } } = {}; + try { + mapState = JSON.parse(attributes.mapStateJSON); + } catch (e) { + throw new Error('Unable to parse attribute mapStateJSON'); + } + if ('settings' in mapState) { mapState.settings!.autoFitToDataBounds = false; } else { diff --git a/x-pack/plugins/maps/common/migrations/set_ems_tms_default_modes.ts b/x-pack/plugins/maps/common/migrations/set_ems_tms_default_modes.ts index 94dbd8741add7..aab0d6b345428 100644 --- a/x-pack/plugins/maps/common/migrations/set_ems_tms_default_modes.ts +++ b/x-pack/plugins/maps/common/migrations/set_ems_tms_default_modes.ts @@ -22,7 +22,13 @@ export function setEmsTmsDefaultModes({ return attributes; } - const layerList: LayerDescriptor[] = JSON.parse(attributes.layerListJSON); + let layerList: LayerDescriptor[] = []; + try { + layerList = JSON.parse(attributes.layerListJSON); + } catch (e) { + throw new Error('Unable to parse attribute layerListJSON'); + } + layerList.forEach((layerDescriptor: LayerDescriptor) => { if (layerDescriptor.sourceDescriptor?.type === SOURCE_TYPES.EMS_TMS) { const sourceDescriptor = layerDescriptor.sourceDescriptor as EMSTMSSourceDescriptor; diff --git a/x-pack/plugins/maps/common/migrations/top_hits_time_to_sort.js b/x-pack/plugins/maps/common/migrations/top_hits_time_to_sort.js index ea7ea6cf91c66..fa5b5111ff797 100644 --- a/x-pack/plugins/maps/common/migrations/top_hits_time_to_sort.js +++ b/x-pack/plugins/maps/common/migrations/top_hits_time_to_sort.js @@ -19,7 +19,13 @@ export function topHitsTimeToSort({ attributes }) { return attributes; } - const layerList = JSON.parse(attributes.layerListJSON); + let layerList = []; + try { + layerList = JSON.parse(attributes.layerListJSON); + } catch (e) { + throw new Error('Unable to parse attribute layerListJSON'); + } + layerList.forEach((layerDescriptor) => { if (isEsDocumentSource(layerDescriptor)) { if (_.has(layerDescriptor, 'sourceDescriptor.topHitsTimeField')) { diff --git a/x-pack/plugins/maps/public/actions/map_actions.ts b/x-pack/plugins/maps/public/actions/map_actions.ts index d921f9748f65c..9ec9a42986fbb 100644 --- a/x-pack/plugins/maps/public/actions/map_actions.ts +++ b/x-pack/plugins/maps/public/actions/map_actions.ts @@ -61,7 +61,7 @@ import { MapSettings } from '../reducers/map'; import { DrawState, MapCenterAndZoom, MapExtent, Timeslice } from '../../common/descriptor_types'; import { INITIAL_LOCATION } from '../../common/constants'; import { updateTooltipStateForLayer } from './tooltip_actions'; -import { VectorLayer } from '../classes/layers/vector_layer'; +import { isVectorLayer, IVectorLayer } from '../classes/layers/vector_layer'; import { SET_DRAW_MODE } from './ui_actions'; import { expandToTileBoundaries } from '../classes/util/geo_tile_utils'; import { getToasts } from '../kibana_services'; @@ -357,12 +357,12 @@ export function addNewFeatureToIndex(geometry: Geometry | Position[]) { return; } const layer = getLayerById(layerId, getState()); - if (!layer || !(layer instanceof VectorLayer)) { + if (!layer || !isVectorLayer(layer)) { return; } try { - await layer.addFeature(geometry); + await (layer as IVectorLayer).addFeature(geometry); await dispatch(syncDataForLayerDueToDrawing(layer)); } catch (e) { getToasts().addError(e, { @@ -385,11 +385,11 @@ export function deleteFeatureFromIndex(featureId: string) { return; } const layer = getLayerById(layerId, getState()); - if (!layer || !(layer instanceof VectorLayer)) { + if (!layer || !isVectorLayer(layer)) { return; } try { - await layer.deleteFeature(featureId); + await (layer as IVectorLayer).deleteFeature(featureId); await dispatch(syncDataForLayerDueToDrawing(layer)); } catch (e) { getToasts().addError(e, { diff --git a/x-pack/plugins/maps/public/classes/layers/choropleth_layer_wizard/create_choropleth_layer_descriptor.ts b/x-pack/plugins/maps/public/classes/layers/choropleth_layer_wizard/create_choropleth_layer_descriptor.ts index 6b91e4812a1d6..ad507aa171631 100644 --- a/x-pack/plugins/maps/public/classes/layers/choropleth_layer_wizard/create_choropleth_layer_descriptor.ts +++ b/x-pack/plugins/maps/public/classes/layers/choropleth_layer_wizard/create_choropleth_layer_descriptor.ts @@ -23,7 +23,7 @@ import { ESSearchSourceDescriptor, } from '../../../../common/descriptor_types'; import { VectorStyle } from '../../styles/vector/vector_style'; -import { VectorLayer } from '../vector_layer'; +import { GeoJsonVectorLayer } from '../vector_layer'; import { EMSFileSource } from '../../sources/ems_file_source'; // @ts-ignore import { ESSearchSource } from '../../sources/es_search_source'; @@ -51,7 +51,7 @@ function createChoroplethLayerDescriptor({ aggFieldName: '', rightSourceId: joinId, }); - return VectorLayer.createDescriptor({ + return GeoJsonVectorLayer.createDescriptor({ joins: [ { leftField, diff --git a/x-pack/plugins/maps/public/classes/layers/create_region_map_layer_descriptor.ts b/x-pack/plugins/maps/public/classes/layers/create_region_map_layer_descriptor.ts index 408460de28aeb..19d9567a3480a 100644 --- a/x-pack/plugins/maps/public/classes/layers/create_region_map_layer_descriptor.ts +++ b/x-pack/plugins/maps/public/classes/layers/create_region_map_layer_descriptor.ts @@ -22,7 +22,7 @@ import { } from '../../../common/constants'; import { VectorStyle } from '../styles/vector/vector_style'; import { EMSFileSource } from '../sources/ems_file_source'; -import { VectorLayer } from './vector_layer'; +import { GeoJsonVectorLayer } from './vector_layer'; import { getDefaultDynamicProperties } from '../styles/vector/vector_style_defaults'; import { NUMERICAL_COLOR_PALETTES } from '../styles/color_palettes'; import { getJoinAggKey } from '../../../common/get_agg_key'; @@ -97,7 +97,7 @@ export function createRegionMapLayerDescriptor({ if (termsSize !== undefined) { termSourceDescriptor.size = termsSize; } - return VectorLayer.createDescriptor({ + return GeoJsonVectorLayer.createDescriptor({ label, joins: [ { diff --git a/x-pack/plugins/maps/public/classes/layers/create_tile_map_layer_descriptor.ts b/x-pack/plugins/maps/public/classes/layers/create_tile_map_layer_descriptor.ts index 98217a5f28ad8..676ba4e8c88b1 100644 --- a/x-pack/plugins/maps/public/classes/layers/create_tile_map_layer_descriptor.ts +++ b/x-pack/plugins/maps/public/classes/layers/create_tile_map_layer_descriptor.ts @@ -24,7 +24,7 @@ import { } from '../../../common/constants'; import { VectorStyle } from '../styles/vector/vector_style'; import { ESGeoGridSource } from '../sources/es_geo_grid_source'; -import { VectorLayer } from './vector_layer'; +import { GeoJsonVectorLayer } from './vector_layer'; import { HeatmapLayer } from './heatmap_layer'; import { getDefaultDynamicProperties } from '../styles/vector/vector_style_defaults'; import { NUMERICAL_COLOR_PALETTES } from '../styles/color_palettes'; @@ -162,7 +162,7 @@ export function createTileMapLayerDescriptor({ }; } - return VectorLayer.createDescriptor({ + return GeoJsonVectorLayer.createDescriptor({ label, sourceDescriptor: geoGridSourceDescriptor, style: VectorStyle.createDescriptor(styleProperties), diff --git a/x-pack/plugins/maps/public/classes/layers/file_upload_wizard/wizard.tsx b/x-pack/plugins/maps/public/classes/layers/file_upload_wizard/wizard.tsx index 87747d915af4a..f29a4c3a55e28 100644 --- a/x-pack/plugins/maps/public/classes/layers/file_upload_wizard/wizard.tsx +++ b/x-pack/plugins/maps/public/classes/layers/file_upload_wizard/wizard.tsx @@ -12,7 +12,7 @@ import { FeatureCollection } from 'geojson'; import { EuiPanel } from '@elastic/eui'; import { DEFAULT_MAX_RESULT_WINDOW, SCALING_TYPES } from '../../../../common/constants'; import { GeoJsonFileSource } from '../../sources/geojson_file_source'; -import { VectorLayer } from '../../layers/vector_layer'; +import { GeoJsonVectorLayer } from '../../layers/vector_layer'; import { createDefaultLayerDescriptor } from '../../sources/es_search_source'; import { RenderWizardArguments } from '../../layers/layer_wizard_registry'; import { FileUploadGeoResults } from '../../../../../file_upload/public'; @@ -113,7 +113,7 @@ export class ClientFileCreateSourceEditor extends Component) { const heatmapLayerDescriptor = super.createDescriptor(options); - heatmapLayerDescriptor.type = HeatmapLayer.type; + heatmapLayerDescriptor.type = LAYER_TYPE.HEATMAP; heatmapLayerDescriptor.style = HeatmapStyle.createDescriptor(); return heatmapLayerDescriptor; } diff --git a/x-pack/plugins/maps/public/classes/layers/layer.tsx b/x-pack/plugins/maps/public/classes/layers/layer.tsx index 051115a072608..21fee2e3dfdce 100644 --- a/x-pack/plugins/maps/public/classes/layers/layer.tsx +++ b/x-pack/plugins/maps/public/classes/layers/layer.tsx @@ -466,7 +466,7 @@ export class AbstractLayer implements ILayer { return null; } - isBasemap(): boolean { + isBasemap(order: number): boolean { return false; } } diff --git a/x-pack/plugins/maps/public/classes/layers/new_vector_layer_wizard/wizard.tsx b/x-pack/plugins/maps/public/classes/layers/new_vector_layer_wizard/wizard.tsx index 100e9dfa45c1d..d26853850c387 100644 --- a/x-pack/plugins/maps/public/classes/layers/new_vector_layer_wizard/wizard.tsx +++ b/x-pack/plugins/maps/public/classes/layers/new_vector_layer_wizard/wizard.tsx @@ -10,7 +10,7 @@ import { EuiPanel, EuiCallOut } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { createNewIndexAndPattern } from './create_new_index_pattern'; import { RenderWizardArguments } from '../layer_wizard_registry'; -import { VectorLayer } from '../vector_layer'; +import { GeoJsonVectorLayer } from '../vector_layer'; import { ESSearchSource } from '../../sources/es_search_source'; import { ADD_LAYER_STEP_ID } from '../../../connected_components/add_layer_panel/view'; import { getFileUpload, getIndexNameFormComponent } from '../../../kibana_services'; @@ -127,7 +127,7 @@ export class NewVectorLayerEditor extends Component) { + const tileLayerDescriptor = super.createDescriptor(options); + tileLayerDescriptor.type = LAYER_TYPE.TILE; + tileLayerDescriptor.alpha = _.get(options, 'alpha', 1); + tileLayerDescriptor.style = { type: LAYER_STYLE_TYPE.TILE }; + return tileLayerDescriptor; + } + + private readonly _style: TileStyle; + + constructor({ source, layerDescriptor }: ITileLayerArguments) { + super({ source, layerDescriptor }); + this._style = new TileStyle(); + } + + getSource(): ITMSSource { + return super.getSource() as ITMSSource; + } + + getStyleForEditing() { + return this._style; + } + + getStyle() { + return this._style; + } + + getCurrentStyle() { + return this._style; + } + + async syncData({ startLoading, stopLoading, onLoadError, dataFilters }: DataRequestContext) { + const sourceDataRequest = this.getSourceDataRequest(); + if (sourceDataRequest) { + // data is immmutable + return; + } + const requestToken = Symbol(`layer-source-refresh:${this.getId()} - source`); + startLoading(SOURCE_DATA_REQUEST_ID, requestToken, dataFilters); + try { + const url = await this.getSource().getUrlTemplate(); + stopLoading(SOURCE_DATA_REQUEST_ID, requestToken, { url }, {}); + } catch (error) { + onLoadError(SOURCE_DATA_REQUEST_ID, requestToken, error.message); + } + } + + _getMbLayerId() { + return this.makeMbLayerId('raster'); + } + + getMbLayerIds() { + return [this._getMbLayerId()]; + } + + ownsMbLayerId(mbLayerId: string) { + return this._getMbLayerId() === mbLayerId; + } + + ownsMbSourceId(mbSourceId: string) { + return this.getId() === mbSourceId; + } + + syncLayerWithMB(mbMap: MbMap) { + const source = mbMap.getSource(this.getId()); + const mbLayerId = this._getMbLayerId(); + + if (!source) { + const sourceDataRequest = this.getSourceDataRequest(); + if (!sourceDataRequest) { + // this is possible if the layer was invisible at startup. + // the actions will not perform any data=syncing as an optimization when a layer is invisible + // when turning the layer back into visible, it's possible the url has not been resovled yet. + return; + } + + const tmsSourceData = sourceDataRequest.getData() as { url?: string }; + if (!tmsSourceData || !tmsSourceData.url) { + return; + } + + const mbSourceId = this._getMbSourceId(); + mbMap.addSource(mbSourceId, { + type: 'raster', + tiles: [tmsSourceData.url], + tileSize: 256, + scheme: 'xyz', + }); + + mbMap.addLayer({ + id: mbLayerId, + type: 'raster', + source: mbSourceId, + minzoom: this._descriptor.minZoom, + maxzoom: this._descriptor.maxZoom, + }); + } + + this._setTileLayerProperties(mbMap, mbLayerId); + } + + _setTileLayerProperties(mbMap: MbMap, mbLayerId: string) { + this.syncVisibilityWithMb(mbMap, mbLayerId); + mbMap.setLayerZoomRange(mbLayerId, this.getMinZoom(), this.getMaxZoom()); + mbMap.setPaintProperty(mbLayerId, 'raster-opacity', this.getAlpha()); + } + + getLayerTypeIconName() { + return 'grid'; + } + + isBasemap(order: number) { + return order === 0; + } +} diff --git a/x-pack/plugins/maps/public/classes/layers/tiled_vector_layer/tiled_vector_layer.test.tsx b/x-pack/plugins/maps/public/classes/layers/tiled_vector_layer/tiled_vector_layer.test.tsx deleted file mode 100644 index fd78ea2ebde59..0000000000000 --- a/x-pack/plugins/maps/public/classes/layers/tiled_vector_layer/tiled_vector_layer.test.tsx +++ /dev/null @@ -1,241 +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 { MockSyncContext } from '../__fixtures__/mock_sync_context'; -import sinon from 'sinon'; -import url from 'url'; - -jest.mock('../../../kibana_services', () => { - return { - getIsDarkMode() { - return false; - }, - }; -}); - -import { shallow } from 'enzyme'; - -import { Feature } from 'geojson'; -import { MVTSingleLayerVectorSource } from '../../sources/mvt_single_layer_vector_source'; -import { - DataRequestDescriptor, - TiledSingleLayerVectorSourceDescriptor, - VectorLayerDescriptor, -} from '../../../../common/descriptor_types'; -import { SOURCE_TYPES } from '../../../../common/constants'; -import { TiledVectorLayer } from './tiled_vector_layer'; - -const defaultConfig = { - urlTemplate: 'https://example.com/{x}/{y}/{z}.pbf', - layerName: 'foobar', - minSourceZoom: 4, - maxSourceZoom: 14, -}; - -function createLayer( - layerOptions: Partial = {}, - sourceOptions: Partial = {}, - isTimeAware: boolean = false, - includeToken: boolean = false -): TiledVectorLayer { - const sourceDescriptor: TiledSingleLayerVectorSourceDescriptor = { - type: SOURCE_TYPES.MVT_SINGLE_LAYER, - ...defaultConfig, - fields: [], - tooltipProperties: [], - ...sourceOptions, - }; - const mvtSource = new MVTSingleLayerVectorSource(sourceDescriptor); - if (isTimeAware) { - mvtSource.isTimeAware = async () => { - return true; - }; - mvtSource.getApplyGlobalTime = () => { - return true; - }; - } - - if (includeToken) { - mvtSource.getUrlTemplateWithMeta = async (...args) => { - const superReturn = await MVTSingleLayerVectorSource.prototype.getUrlTemplateWithMeta.call( - mvtSource, - ...args - ); - return { - ...superReturn, - refreshTokenParamName: 'token', - }; - }; - } - - const defaultLayerOptions = { - ...layerOptions, - sourceDescriptor, - }; - const layerDescriptor = TiledVectorLayer.createDescriptor(defaultLayerOptions); - return new TiledVectorLayer({ layerDescriptor, source: mvtSource }); -} - -describe('visiblity', () => { - it('should get minzoom from source', async () => { - const layer: TiledVectorLayer = createLayer({}, {}); - expect(layer.getMinZoom()).toEqual(4); - }); - it('should get maxzoom from default', async () => { - const layer: TiledVectorLayer = createLayer({}, {}); - expect(layer.getMaxZoom()).toEqual(24); - }); - it('should get maxzoom from layer options', async () => { - const layer: TiledVectorLayer = createLayer({ maxZoom: 10 }, {}); - expect(layer.getMaxZoom()).toEqual(10); - }); -}); - -describe('getCustomIconAndTooltipContent', () => { - it('Layers with non-elasticsearch sources should display icon', async () => { - const layer: TiledVectorLayer = createLayer({}, {}); - - const iconAndTooltipContent = layer.getCustomIconAndTooltipContent(); - const component = shallow(iconAndTooltipContent.icon); - expect(component).toMatchSnapshot(); - }); -}); - -describe('getFeatureById', () => { - it('should return null feature', async () => { - const layer: TiledVectorLayer = createLayer({}, {}); - const feature = layer.getFeatureById('foobar') as Feature; - expect(feature).toEqual(null); - }); -}); - -describe('syncData', () => { - it('Should sync with source-params', async () => { - const layer: TiledVectorLayer = createLayer({}, {}); - - const syncContext = new MockSyncContext({ dataFilters: {} }); - - await layer.syncData(syncContext); - // @ts-expect-error - sinon.assert.calledOnce(syncContext.startLoading); - // @ts-expect-error - sinon.assert.calledOnce(syncContext.stopLoading); - - // @ts-expect-error - const call = syncContext.stopLoading.getCall(0); - expect(call.args[2]!.minSourceZoom).toEqual(defaultConfig.minSourceZoom); - expect(call.args[2]!.maxSourceZoom).toEqual(defaultConfig.maxSourceZoom); - expect(call.args[2]!.layerName).toEqual(defaultConfig.layerName); - expect(call.args[2]!.urlTemplate).toEqual(defaultConfig.urlTemplate); - }); - - it('Should not resync when no changes to source params', async () => { - const dataRequestDescriptor: DataRequestDescriptor = { - data: { ...defaultConfig }, - dataId: 'source', - }; - const layer: TiledVectorLayer = createLayer( - { - __dataRequests: [dataRequestDescriptor], - }, - {} - ); - const syncContext = new MockSyncContext({ dataFilters: {} }); - await layer.syncData(syncContext); - // @ts-expect-error - sinon.assert.notCalled(syncContext.startLoading); - // @ts-expect-error - sinon.assert.notCalled(syncContext.stopLoading); - }); - - it('Should resync when changes to syncContext', async () => { - const dataRequestDescriptor: DataRequestDescriptor = { - data: { ...defaultConfig }, - dataId: 'source', - }; - const layer: TiledVectorLayer = createLayer( - { - __dataRequests: [dataRequestDescriptor], - }, - {}, - true - ); - const syncContext = new MockSyncContext({ - dataFilters: { - timeFilters: { - from: 'now', - to: '30m', - mode: 'relative', - }, - }, - }); - await layer.syncData(syncContext); - // @ts-expect-error - sinon.assert.calledOnce(syncContext.startLoading); - // @ts-expect-error - sinon.assert.calledOnce(syncContext.stopLoading); - }); - - describe('Should resync when changes to source params: ', () => { - [{ layerName: 'barfoo' }, { minSourceZoom: 1 }, { maxSourceZoom: 12 }].forEach((changes) => { - it(`change in ${Object.keys(changes).join(',')}`, async () => { - const dataRequestDescriptor: DataRequestDescriptor = { - data: defaultConfig, - dataId: 'source', - }; - const layer: TiledVectorLayer = createLayer( - { - __dataRequests: [dataRequestDescriptor], - }, - changes - ); - const syncContext = new MockSyncContext({ dataFilters: {} }); - await layer.syncData(syncContext); - - // @ts-expect-error - sinon.assert.calledOnce(syncContext.startLoading); - // @ts-expect-error - sinon.assert.calledOnce(syncContext.stopLoading); - - // @ts-expect-error - const call = syncContext.stopLoading.getCall(0); - - const newMeta = { ...defaultConfig, ...changes }; - expect(call.args[2]!.minSourceZoom).toEqual(newMeta.minSourceZoom); - expect(call.args[2]!.maxSourceZoom).toEqual(newMeta.maxSourceZoom); - expect(call.args[2]!.layerName).toEqual(newMeta.layerName); - expect(call.args[2]!.urlTemplate).toEqual(newMeta.urlTemplate); - }); - }); - }); - - describe('refresh token', () => { - const uuidRegex = /\b[0-9a-f]{8}\b-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-\b[0-9a-f]{12}\b/; - - it(`should add token in url`, async () => { - const layer: TiledVectorLayer = createLayer({}, {}, false, true); - - const syncContext = new MockSyncContext({ dataFilters: {} }); - - await layer.syncData(syncContext); - // @ts-expect-error - sinon.assert.calledOnce(syncContext.startLoading); - // @ts-expect-error - sinon.assert.calledOnce(syncContext.stopLoading); - - // @ts-expect-error - const call = syncContext.stopLoading.getCall(0); - expect(call.args[2]!.minSourceZoom).toEqual(defaultConfig.minSourceZoom); - expect(call.args[2]!.maxSourceZoom).toEqual(defaultConfig.maxSourceZoom); - expect(call.args[2]!.layerName).toEqual(defaultConfig.layerName); - expect(call.args[2]!.urlTemplate.startsWith(defaultConfig.urlTemplate)).toBe(true); - - const parsedUrl = url.parse(call.args[2]!.urlTemplate, true); - expect(!!(parsedUrl.query.token! as string).match(uuidRegex)).toBe(true); - }); - }); -}); diff --git a/x-pack/plugins/maps/public/classes/layers/tiled_vector_layer/tiled_vector_layer.tsx b/x-pack/plugins/maps/public/classes/layers/tiled_vector_layer/tiled_vector_layer.tsx deleted file mode 100644 index 4b881228f79b5..0000000000000 --- a/x-pack/plugins/maps/public/classes/layers/tiled_vector_layer/tiled_vector_layer.tsx +++ /dev/null @@ -1,503 +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 type { - Map as MbMap, - AnyLayer as MbLayer, - GeoJSONSource as MbGeoJSONSource, - VectorSource as MbVectorSource, -} from '@kbn/mapbox-gl'; -import { Feature } from 'geojson'; -import { i18n } from '@kbn/i18n'; -import uuid from 'uuid/v4'; -import { parse as parseUrl } from 'url'; -import { euiThemeVars } from '@kbn/ui-shared-deps-src/theme'; -import { IVectorStyle, VectorStyle } from '../../styles/vector/vector_style'; -import { LAYER_TYPE, SOURCE_DATA_REQUEST_ID, SOURCE_TYPES } from '../../../../common/constants'; -import { - NO_RESULTS_ICON_AND_TOOLTIPCONTENT, - VectorLayer, - VectorLayerArguments, -} from '../vector_layer'; -import { ITiledSingleLayerVectorSource } from '../../sources/tiled_single_layer_vector_source'; -import { DataRequestContext } from '../../../actions'; -import { - StyleMetaDescriptor, - TileMetaFeature, - Timeslice, - VectorLayerDescriptor, - VectorSourceRequestMeta, -} from '../../../../common/descriptor_types'; -import { MVTSingleLayerVectorSourceConfig } from '../../sources/mvt_single_layer_vector_source/types'; -import { ESSearchSource } from '../../sources/es_search_source'; -import { canSkipSourceUpdate } from '../../util/can_skip_fetch'; -import { CustomIconAndTooltipContent } from '../layer'; - -const ES_MVT_META_LAYER_NAME = 'meta'; -const ES_MVT_HITS_TOTAL_RELATION = 'hits.total.relation'; -const ES_MVT_HITS_TOTAL_VALUE = 'hits.total.value'; -const MAX_RESULT_WINDOW_DATA_REQUEST_ID = 'maxResultWindow'; - -/* - * MVT vector layer - */ -export class TiledVectorLayer extends VectorLayer { - static type = LAYER_TYPE.TILED_VECTOR; - - static createDescriptor( - descriptor: Partial, - mapColors?: string[] - ): VectorLayerDescriptor { - const layerDescriptor = super.createDescriptor(descriptor, mapColors); - layerDescriptor.type = LAYER_TYPE.TILED_VECTOR; - - if (!layerDescriptor.style) { - const styleProperties = VectorStyle.createDefaultStyleProperties(mapColors ? mapColors : []); - layerDescriptor.style = VectorStyle.createDescriptor(styleProperties); - } - - return layerDescriptor; - } - - readonly _source: ITiledSingleLayerVectorSource; // downcast to the more specific type - - constructor({ layerDescriptor, source }: VectorLayerArguments) { - super({ layerDescriptor, source }); - this._source = source as ITiledSingleLayerVectorSource; - } - - getFeatureId(feature: Feature): string | number | undefined { - if (!this.getSource().isESSource()) { - return feature.id; - } - - return this.getSource().getType() === SOURCE_TYPES.ES_SEARCH - ? feature.properties?._id - : feature.properties?._key; - } - - _getMetaFromTiles(): TileMetaFeature[] { - return this._descriptor.__metaFromTiles || []; - } - - getCustomIconAndTooltipContent(): CustomIconAndTooltipContent { - const icon = this.getCurrentStyle().getIcon(); - if (!this.getSource().isESSource()) { - // Only ES-sources can have a special meta-tile, not 3rd party vector tile sources - return { - icon, - tooltipContent: null, - areResultsTrimmed: false, - }; - } - - // - // TODO ES MVT specific - move to es_tiled_vector_layer implementation - // - - const tileMetaFeatures = this._getMetaFromTiles(); - if (!tileMetaFeatures.length) { - return NO_RESULTS_ICON_AND_TOOLTIPCONTENT; - } - - if (this.getSource().getType() !== SOURCE_TYPES.ES_SEARCH) { - // aggregation ES sources are never trimmed - return { - icon, - tooltipContent: null, - areResultsTrimmed: false, - }; - } - - const maxResultWindow = this._getMaxResultWindow(); - if (maxResultWindow === undefined) { - return { - icon, - tooltipContent: null, - areResultsTrimmed: false, - }; - } - - const totalFeaturesCount: number = tileMetaFeatures.reduce((acc: number, tileMeta: Feature) => { - const count = - tileMeta && tileMeta.properties ? tileMeta.properties[ES_MVT_HITS_TOTAL_VALUE] : 0; - return count + acc; - }, 0); - - if (totalFeaturesCount === 0) { - return NO_RESULTS_ICON_AND_TOOLTIPCONTENT; - } - - const isIncomplete: boolean = tileMetaFeatures.some((tileMeta: TileMetaFeature) => { - if (tileMeta?.properties?.[ES_MVT_HITS_TOTAL_RELATION] === 'gte') { - return tileMeta?.properties?.[ES_MVT_HITS_TOTAL_VALUE] >= maxResultWindow + 1; - } else { - return false; - } - }); - - return { - icon, - tooltipContent: isIncomplete - ? i18n.translate('xpack.maps.tiles.resultsTrimmedMsg', { - defaultMessage: `Results limited to {count} documents.`, - values: { - count: totalFeaturesCount.toLocaleString(), - }, - }) - : i18n.translate('xpack.maps.tiles.resultsCompleteMsg', { - defaultMessage: `Found {count} documents.`, - values: { - count: totalFeaturesCount.toLocaleString(), - }, - }), - areResultsTrimmed: isIncomplete, - }; - } - - _getMaxResultWindow(): number | undefined { - const dataRequest = this.getDataRequest(MAX_RESULT_WINDOW_DATA_REQUEST_ID); - if (!dataRequest) { - return; - } - const data = dataRequest.getData() as { maxResultWindow: number } | undefined; - return data ? data.maxResultWindow : undefined; - } - - async _syncMaxResultWindow({ startLoading, stopLoading }: DataRequestContext) { - const prevDataRequest = this.getDataRequest(MAX_RESULT_WINDOW_DATA_REQUEST_ID); - if (prevDataRequest) { - return; - } - - const requestToken = Symbol(`${this.getId()}-${MAX_RESULT_WINDOW_DATA_REQUEST_ID}`); - startLoading(MAX_RESULT_WINDOW_DATA_REQUEST_ID, requestToken); - const maxResultWindow = await (this.getSource() as ESSearchSource).getMaxResultWindow(); - stopLoading(MAX_RESULT_WINDOW_DATA_REQUEST_ID, requestToken, { maxResultWindow }); - } - - async _syncMVTUrlTemplate({ - startLoading, - stopLoading, - onLoadError, - dataFilters, - isForceRefresh, - }: DataRequestContext) { - const requestToken: symbol = Symbol(`layer-${this.getId()}-${SOURCE_DATA_REQUEST_ID}`); - const requestMeta: VectorSourceRequestMeta = await this._getVectorSourceRequestMeta( - isForceRefresh, - dataFilters, - this.getSource(), - this._style as IVectorStyle - ); - const prevDataRequest = this.getSourceDataRequest(); - if (prevDataRequest) { - const data: MVTSingleLayerVectorSourceConfig = - prevDataRequest.getData() as MVTSingleLayerVectorSourceConfig; - if (data) { - const noChangesInSourceState: boolean = - data.layerName === this._source.getLayerName() && - data.minSourceZoom === this._source.getMinZoom() && - data.maxSourceZoom === this._source.getMaxZoom(); - const noChangesInSearchState: boolean = await canSkipSourceUpdate({ - extentAware: false, // spatial extent knowledge is already fully automated by tile-loading based on pan-zooming - source: this.getSource(), - prevDataRequest, - nextRequestMeta: requestMeta, - getUpdateDueToTimeslice: (timeslice?: Timeslice) => { - // TODO use meta features to determine if tiles already contain features for timeslice. - return true; - }, - }); - const canSkip = noChangesInSourceState && noChangesInSearchState; - - if (canSkip) { - return null; - } - } - } - - startLoading(SOURCE_DATA_REQUEST_ID, requestToken, requestMeta); - try { - const prevData = prevDataRequest - ? (prevDataRequest.getData() as MVTSingleLayerVectorSourceConfig) - : undefined; - const urlToken = - !prevData || (requestMeta.isForceRefresh && requestMeta.applyForceRefresh) - ? uuid() - : prevData.urlToken; - - const newUrlTemplateAndMeta = await this._source.getUrlTemplateWithMeta(requestMeta); - - let urlTemplate; - if (newUrlTemplateAndMeta.refreshTokenParamName) { - const parsedUrl = parseUrl(newUrlTemplateAndMeta.urlTemplate, true); - const separator = !parsedUrl.query || Object.keys(parsedUrl.query).length === 0 ? '?' : '&'; - urlTemplate = `${newUrlTemplateAndMeta.urlTemplate}${separator}${newUrlTemplateAndMeta.refreshTokenParamName}=${urlToken}`; - } else { - urlTemplate = newUrlTemplateAndMeta.urlTemplate; - } - - const urlTemplateAndMetaWithToken = { - ...newUrlTemplateAndMeta, - urlToken, - urlTemplate, - }; - stopLoading(SOURCE_DATA_REQUEST_ID, requestToken, urlTemplateAndMetaWithToken, {}); - } catch (error) { - onLoadError(SOURCE_DATA_REQUEST_ID, requestToken, error.message); - } - } - - async syncData(syncContext: DataRequestContext) { - if (this.getSource().getType() === SOURCE_TYPES.ES_SEARCH) { - await this._syncMaxResultWindow(syncContext); - } - await this._syncSourceStyleMeta(syncContext, this._source, this._style as IVectorStyle); - await this._syncSourceFormatters(syncContext, this._source, this._style as IVectorStyle); - await this._syncMVTUrlTemplate(syncContext); - } - - _syncSourceBindingWithMb(mbMap: MbMap) { - const mbSource = mbMap.getSource(this._getMbSourceId()); - if (mbSource) { - return; - } - const sourceDataRequest = this.getSourceDataRequest(); - if (!sourceDataRequest) { - // this is possible if the layer was invisible at startup. - // the actions will not perform any data=syncing as an optimization when a layer is invisible - // when turning the layer back into visible, it's possible the url had not been resolved yet. - return; - } - - const sourceMeta: MVTSingleLayerVectorSourceConfig | null = - sourceDataRequest.getData() as MVTSingleLayerVectorSourceConfig; - if (!sourceMeta) { - return; - } - - const mbSourceId = this._getMbSourceId(); - mbMap.addSource(mbSourceId, { - type: 'vector', - tiles: [sourceMeta.urlTemplate], - minzoom: sourceMeta.minSourceZoom, - maxzoom: sourceMeta.maxSourceZoom, - }); - } - - getMbLayerIds() { - return [...super.getMbLayerIds(), this._getMbTooManyFeaturesLayerId()]; - } - - ownsMbSourceId(mbSourceId: string): boolean { - return this._getMbSourceId() === mbSourceId; - } - - _getMbTooManyFeaturesLayerId() { - return this.makeMbLayerId('toomanyfeatures'); - } - - _syncStylePropertiesWithMb(mbMap: MbMap) { - // @ts-ignore - const mbSource = mbMap.getSource(this._getMbSourceId()); - if (!mbSource) { - return; - } - - const sourceDataRequest = this.getSourceDataRequest(); - if (!sourceDataRequest) { - return; - } - const sourceMeta: MVTSingleLayerVectorSourceConfig = - sourceDataRequest.getData() as MVTSingleLayerVectorSourceConfig; - if (sourceMeta.layerName === '') { - return; - } - - this._setMbLabelProperties(mbMap, sourceMeta.layerName); - this._setMbPointsProperties(mbMap, sourceMeta.layerName); - this._setMbLinePolygonProperties(mbMap, sourceMeta.layerName); - this._syncTooManyFeaturesProperties(mbMap); - } - - // TODO ES MVT specific - move to es_tiled_vector_layer implementation - _syncTooManyFeaturesProperties(mbMap: MbMap) { - if (this.getSource().getType() !== SOURCE_TYPES.ES_SEARCH) { - return; - } - - const maxResultWindow = this._getMaxResultWindow(); - if (maxResultWindow === undefined) { - return; - } - - const tooManyFeaturesLayerId = this._getMbTooManyFeaturesLayerId(); - - if (!mbMap.getLayer(tooManyFeaturesLayerId)) { - const mbTooManyFeaturesLayer: MbLayer = { - id: tooManyFeaturesLayerId, - type: 'line', - source: this.getId(), - paint: {}, - }; - mbTooManyFeaturesLayer['source-layer'] = ES_MVT_META_LAYER_NAME; - mbMap.addLayer(mbTooManyFeaturesLayer); - mbMap.setFilter(tooManyFeaturesLayerId, [ - 'all', - ['==', ['get', ES_MVT_HITS_TOTAL_RELATION], 'gte'], - ['>=', ['get', ES_MVT_HITS_TOTAL_VALUE], maxResultWindow + 1], - ]); - mbMap.setPaintProperty(tooManyFeaturesLayerId, 'line-color', euiThemeVars.euiColorWarning); - mbMap.setPaintProperty(tooManyFeaturesLayerId, 'line-width', 3); - mbMap.setPaintProperty(tooManyFeaturesLayerId, 'line-dasharray', [2, 1]); - mbMap.setPaintProperty(tooManyFeaturesLayerId, 'line-opacity', this.getAlpha()); - } - - this.syncVisibilityWithMb(mbMap, tooManyFeaturesLayerId); - mbMap.setLayerZoomRange(tooManyFeaturesLayerId, this.getMinZoom(), this.getMaxZoom()); - } - - queryTileMetaFeatures(mbMap: MbMap): TileMetaFeature[] | null { - if (!this.getSource().isESSource()) { - return null; - } - - // @ts-ignore - const mbSource = mbMap.getSource(this._getMbSourceId()); - if (!mbSource) { - return null; - } - - const sourceDataRequest = this.getSourceDataRequest(); - if (!sourceDataRequest) { - return null; - } - const sourceMeta: MVTSingleLayerVectorSourceConfig = - sourceDataRequest.getData() as MVTSingleLayerVectorSourceConfig; - if (sourceMeta.layerName === '') { - return null; - } - - // querySourceFeatures can return duplicated features when features cross tile boundaries. - // Tile meta will never have duplicated features since by there nature, tile meta is a feature contained within a single tile - const mbFeatures = mbMap.querySourceFeatures(this._getMbSourceId(), { - sourceLayer: ES_MVT_META_LAYER_NAME, - }); - - const metaFeatures: Array = ( - mbFeatures as unknown as TileMetaFeature[] - ).map((mbFeature: TileMetaFeature | null) => { - const parsedProperties: Record = {}; - for (const key in mbFeature?.properties) { - if (mbFeature?.properties.hasOwnProperty(key)) { - parsedProperties[key] = - typeof mbFeature.properties[key] === 'string' || - typeof mbFeature.properties[key] === 'number' || - typeof mbFeature.properties[key] === 'boolean' - ? mbFeature.properties[key] - : JSON.parse(mbFeature.properties[key]); // mvt properties cannot be nested geojson - } - } - - try { - return { - type: 'Feature', - id: mbFeature?.id, - geometry: mbFeature?.geometry, // this getter might throw with non-conforming geometries - properties: parsedProperties, - } as TileMetaFeature; - } catch (e) { - return null; - } - }); - - const filtered = metaFeatures.filter((f) => f !== null); - return filtered as TileMetaFeature[]; - } - - _requiresPrevSourceCleanup(mbMap: MbMap): boolean { - const mbSource = mbMap.getSource(this._getMbSourceId()) as MbVectorSource | MbGeoJSONSource; - if (!mbSource) { - return false; - } - if (!('tiles' in mbSource)) { - // Expected source is not compatible, so remove. - return true; - } - const mbTileSource = mbSource as MbVectorSource; - - const dataRequest = this.getSourceDataRequest(); - if (!dataRequest) { - return false; - } - const tiledSourceMeta: MVTSingleLayerVectorSourceConfig | null = - dataRequest.getData() as MVTSingleLayerVectorSourceConfig; - - if (!tiledSourceMeta) { - return false; - } - - const isSourceDifferent = - mbTileSource.tiles?.[0] !== tiledSourceMeta.urlTemplate || - mbTileSource.minzoom !== tiledSourceMeta.minSourceZoom || - mbTileSource.maxzoom !== tiledSourceMeta.maxSourceZoom; - - if (isSourceDifferent) { - return true; - } - - const layerIds = this.getMbLayerIds(); - for (let i = 0; i < layerIds.length; i++) { - const mbLayer = mbMap.getLayer(layerIds[i]); - // The mapbox type in the spec is specified with `source-layer` - // but the programmable JS-object uses camelcase `sourceLayer` - if ( - mbLayer && - // @ts-expect-error - mbLayer.sourceLayer !== tiledSourceMeta.layerName && - // @ts-expect-error - mbLayer.sourceLayer !== ES_MVT_META_LAYER_NAME - ) { - // If the source-pointer of one of the layers is stale, they will all be stale. - // In this case, all the mb-layers need to be removed and re-added. - return true; - } - } - - return false; - } - - syncLayerWithMB(mbMap: MbMap) { - this._removeStaleMbSourcesAndLayers(mbMap); - this._syncSourceBindingWithMb(mbMap); - this._syncStylePropertiesWithMb(mbMap); - } - - getJoins() { - return []; - } - - getMinZoom() { - // higher resolution vector tiles cannot be displayed at lower-res - return Math.max(this._source.getMinZoom(), super.getMinZoom()); - } - - getFeatureById(id: string | number): Feature | null { - return null; - } - - async getStyleMetaDescriptorFromLocalFeatures(): Promise { - const style = this.getCurrentStyle(); - if (!style) { - return null; - } - - const metaFromTiles = this._getMetaFromTiles(); - return await style.pluckStyleMetaFromTileMeta(metaFromTiles); - } -} diff --git a/x-pack/plugins/maps/public/classes/layers/blended_vector_layer/blended_vector_layer.test.tsx b/x-pack/plugins/maps/public/classes/layers/vector_layer/blended_vector_layer/blended_vector_layer.test.tsx similarity index 95% rename from x-pack/plugins/maps/public/classes/layers/blended_vector_layer/blended_vector_layer.test.tsx rename to x-pack/plugins/maps/public/classes/layers/vector_layer/blended_vector_layer/blended_vector_layer.test.tsx index d4138ccfaf319..ee97f4c243491 100644 --- a/x-pack/plugins/maps/public/classes/layers/blended_vector_layer/blended_vector_layer.test.tsx +++ b/x-pack/plugins/maps/public/classes/layers/vector_layer/blended_vector_layer/blended_vector_layer.test.tsx @@ -5,15 +5,15 @@ * 2.0. */ -import { SCALING_TYPES, SOURCE_TYPES } from '../../../../common/constants'; +import { SCALING_TYPES, SOURCE_TYPES } from '../../../../../common/constants'; import { BlendedVectorLayer } from './blended_vector_layer'; -import { ESSearchSource } from '../../sources/es_search_source'; +import { ESSearchSource } from '../../../sources/es_search_source'; import { AbstractESSourceDescriptor, ESGeoGridSourceDescriptor, -} from '../../../../common/descriptor_types'; +} from '../../../../../common/descriptor_types'; -jest.mock('../../../kibana_services', () => { +jest.mock('../../../../kibana_services', () => { return { getIsDarkMode() { return false; diff --git a/x-pack/plugins/maps/public/classes/layers/blended_vector_layer/blended_vector_layer.ts b/x-pack/plugins/maps/public/classes/layers/vector_layer/blended_vector_layer/blended_vector_layer.ts similarity index 88% rename from x-pack/plugins/maps/public/classes/layers/blended_vector_layer/blended_vector_layer.ts rename to x-pack/plugins/maps/public/classes/layers/vector_layer/blended_vector_layer/blended_vector_layer.ts index a158892be9d09..e4c0ccdca09a4 100644 --- a/x-pack/plugins/maps/public/classes/layers/blended_vector_layer/blended_vector_layer.ts +++ b/x-pack/plugins/maps/public/classes/layers/vector_layer/blended_vector_layer/blended_vector_layer.ts @@ -6,11 +6,12 @@ */ import { i18n } from '@kbn/i18n'; -import { IVectorLayer, VectorLayer } from '../vector_layer'; -import { IVectorStyle, VectorStyle } from '../../styles/vector/vector_style'; -import { getDefaultDynamicProperties } from '../../styles/vector/vector_style_defaults'; -import { IDynamicStyleProperty } from '../../styles/vector/properties/dynamic_style_property'; -import { IStyleProperty } from '../../styles/vector/properties/style_property'; +import { IVectorLayer } from '../vector_layer'; +import { GeoJsonVectorLayer } from '../geojson_vector_layer'; +import { IVectorStyle, VectorStyle } from '../../../styles/vector/vector_style'; +import { getDefaultDynamicProperties } from '../../../styles/vector/vector_style_defaults'; +import { IDynamicStyleProperty } from '../../../styles/vector/properties/dynamic_style_property'; +import { IStyleProperty } from '../../../styles/vector/properties/style_property'; import { COUNT_PROP_LABEL, COUNT_PROP_NAME, @@ -21,13 +22,13 @@ import { VECTOR_STYLES, LAYER_STYLE_TYPE, FIELD_ORIGIN, -} from '../../../../common/constants'; -import { ESGeoGridSource } from '../../sources/es_geo_grid_source/es_geo_grid_source'; -import { canSkipSourceUpdate } from '../../util/can_skip_fetch'; -import { IESSource } from '../../sources/es_source'; -import { ISource } from '../../sources/source'; -import { DataRequestContext } from '../../../actions'; -import { DataRequestAbortError } from '../../util/data_request'; +} from '../../../../../common/constants'; +import { ESGeoGridSource } from '../../../sources/es_geo_grid_source/es_geo_grid_source'; +import { canSkipSourceUpdate } from '../../../util/can_skip_fetch'; +import { IESSource } from '../../../sources/es_source'; +import { ISource } from '../../../sources/source'; +import { DataRequestContext } from '../../../../actions'; +import { DataRequestAbortError } from '../../../util/data_request'; import { VectorStyleDescriptor, SizeDynamicOptions, @@ -37,11 +38,11 @@ import { VectorLayerDescriptor, VectorSourceRequestMeta, VectorStylePropertiesDescriptor, -} from '../../../../common/descriptor_types'; -import { IVectorSource } from '../../sources/vector_source'; -import { LICENSED_FEATURES } from '../../../licensed_features'; -import { ESSearchSource } from '../../sources/es_search_source/es_search_source'; -import { isSearchSourceAbortError } from '../../sources/es_source/es_source'; +} from '../../../../../common/descriptor_types'; +import { IVectorSource } from '../../../sources/vector_source'; +import { LICENSED_FEATURES } from '../../../../licensed_features'; +import { ESSearchSource } from '../../../sources/es_search_source/es_search_source'; +import { isSearchSourceAbortError } from '../../../sources/es_source/es_source'; const ACTIVE_COUNT_DATA_ID = 'ACTIVE_COUNT_DATA_ID'; @@ -170,14 +171,12 @@ export interface BlendedVectorLayerArguments { layerDescriptor: VectorLayerDescriptor; } -export class BlendedVectorLayer extends VectorLayer implements IVectorLayer { - static type = LAYER_TYPE.BLENDED_VECTOR; - +export class BlendedVectorLayer extends GeoJsonVectorLayer implements IVectorLayer { static createDescriptor( options: Partial, mapColors: string[] ): VectorLayerDescriptor { - const layerDescriptor = VectorLayer.createDescriptor(options, mapColors); + const layerDescriptor = GeoJsonVectorLayer.createDescriptor(options, mapColors); layerDescriptor.type = LAYER_TYPE.BLENDED_VECTOR; return layerDescriptor; } diff --git a/x-pack/plugins/maps/public/classes/layers/vector_layer/blended_vector_layer/index.ts b/x-pack/plugins/maps/public/classes/layers/vector_layer/blended_vector_layer/index.ts new file mode 100644 index 0000000000000..a8079e06358d5 --- /dev/null +++ b/x-pack/plugins/maps/public/classes/layers/vector_layer/blended_vector_layer/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 { BlendedVectorLayer } from './blended_vector_layer'; diff --git a/x-pack/plugins/maps/public/classes/layers/vector_layer/assign_feature_ids.test.ts b/x-pack/plugins/maps/public/classes/layers/vector_layer/geojson_vector_layer/assign_feature_ids.test.ts similarity index 100% rename from x-pack/plugins/maps/public/classes/layers/vector_layer/assign_feature_ids.test.ts rename to x-pack/plugins/maps/public/classes/layers/vector_layer/geojson_vector_layer/assign_feature_ids.test.ts diff --git a/x-pack/plugins/maps/public/classes/layers/vector_layer/assign_feature_ids.ts b/x-pack/plugins/maps/public/classes/layers/vector_layer/geojson_vector_layer/assign_feature_ids.ts similarity index 100% rename from x-pack/plugins/maps/public/classes/layers/vector_layer/assign_feature_ids.ts rename to x-pack/plugins/maps/public/classes/layers/vector_layer/geojson_vector_layer/assign_feature_ids.ts diff --git a/x-pack/plugins/maps/public/classes/layers/vector_layer/geojson_vector_layer/geojson_vector_layer.tsx b/x-pack/plugins/maps/public/classes/layers/vector_layer/geojson_vector_layer/geojson_vector_layer.tsx new file mode 100644 index 0000000000000..80da6ceecf3a6 --- /dev/null +++ b/x-pack/plugins/maps/public/classes/layers/vector_layer/geojson_vector_layer/geojson_vector_layer.tsx @@ -0,0 +1,405 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license 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 { EuiIcon } from '@elastic/eui'; +import { Feature, FeatureCollection } from 'geojson'; +import type { Map as MbMap, GeoJSONSource as MbGeoJSONSource } from '@kbn/mapbox-gl'; +import { + EMPTY_FEATURE_COLLECTION, + FEATURE_VISIBLE_PROPERTY_NAME, + FIELD_ORIGIN, + LAYER_TYPE, +} from '../../../../../common/constants'; +import { + StyleMetaDescriptor, + Timeslice, + VectorJoinSourceRequestMeta, + VectorLayerDescriptor, +} from '../../../../../common/descriptor_types'; +import { PropertiesMap } from '../../../../../common/elasticsearch_util'; +import { TimesliceMaskConfig } from '../../../util/mb_filter_expressions'; +import { DataRequestContext } from '../../../../actions'; +import { IVectorStyle, VectorStyle } from '../../../styles/vector/vector_style'; +import { ISource } from '../../../sources/source'; +import { IVectorSource } from '../../../sources/vector_source'; +import { AbstractLayer, CustomIconAndTooltipContent } from '../../layer'; +import { InnerJoin } from '../../../joins/inner_join'; +import { + AbstractVectorLayer, + noResultsIcon, + NO_RESULTS_ICON_AND_TOOLTIPCONTENT, +} from '../vector_layer'; +import { DataRequestAbortError } from '../../../util/data_request'; +import { canSkipSourceUpdate } from '../../../util/can_skip_fetch'; +import { getFeatureCollectionBounds } from '../../../util/get_feature_collection_bounds'; +import { GEOJSON_FEATURE_ID_PROPERTY_NAME } from './assign_feature_ids'; +import { addGeoJsonMbSource, syncVectorSource } from './utils'; +import { JoinState, performInnerJoins } from './perform_inner_joins'; +import { buildVectorRequestMeta } from '../../build_vector_request_meta'; + +export const SUPPORTS_FEATURE_EDITING_REQUEST_ID = 'SUPPORTS_FEATURE_EDITING_REQUEST_ID'; + +export class GeoJsonVectorLayer extends AbstractVectorLayer { + static createDescriptor( + options: Partial, + mapColors?: string[] + ): VectorLayerDescriptor { + const layerDescriptor = super.createDescriptor(options) as VectorLayerDescriptor; + layerDescriptor.type = LAYER_TYPE.VECTOR; + + if (!options.style) { + const styleProperties = VectorStyle.createDefaultStyleProperties(mapColors ? mapColors : []); + layerDescriptor.style = VectorStyle.createDescriptor(styleProperties); + } + + if (!options.joins) { + layerDescriptor.joins = []; + } + + return layerDescriptor; + } + + supportsFeatureEditing(): boolean { + const dataRequest = this.getDataRequest(SUPPORTS_FEATURE_EDITING_REQUEST_ID); + const data = dataRequest?.getData() as { supportsFeatureEditing: boolean } | undefined; + return data ? data.supportsFeatureEditing : false; + } + + async getBounds(syncContext: DataRequestContext) { + const isStaticLayer = !this.getSource().isBoundsAware(); + return isStaticLayer || this.hasJoins() + ? getFeatureCollectionBounds(this._getSourceFeatureCollection(), this.hasJoins()) + : super.getBounds(syncContext); + } + + getCustomIconAndTooltipContent(): CustomIconAndTooltipContent { + const featureCollection = this._getSourceFeatureCollection(); + + if (!featureCollection || featureCollection.features.length === 0) { + return NO_RESULTS_ICON_AND_TOOLTIPCONTENT; + } + + if ( + this.getJoins().length && + !featureCollection.features.some( + (feature) => feature.properties?.[FEATURE_VISIBLE_PROPERTY_NAME] + ) + ) { + return { + icon: noResultsIcon, + tooltipContent: i18n.translate('xpack.maps.vectorLayer.noResultsFoundInJoinTooltip', { + defaultMessage: `No matching results found in term joins`, + }), + }; + } + + const sourceDataRequest = this.getSourceDataRequest(); + const { tooltipContent, areResultsTrimmed, isDeprecated } = + this.getSource().getSourceTooltipContent(sourceDataRequest); + return { + icon: isDeprecated ? ( + + ) : ( + this.getCurrentStyle().getIcon() + ), + tooltipContent, + areResultsTrimmed, + }; + } + + getFeatureId(feature: Feature): string | number | undefined { + return feature.properties?.[GEOJSON_FEATURE_ID_PROPERTY_NAME]; + } + + getFeatureById(id: string | number) { + const featureCollection = this._getSourceFeatureCollection(); + if (!featureCollection) { + return null; + } + + const targetFeature = featureCollection.features.find((feature) => { + return this.getFeatureId(feature) === id; + }); + return targetFeature ? targetFeature : null; + } + + async getStyleMetaDescriptorFromLocalFeatures(): Promise { + const sourceDataRequest = this.getSourceDataRequest(); + const style = this.getCurrentStyle(); + if (!style || !sourceDataRequest) { + return null; + } + return await style.pluckStyleMetaFromSourceDataRequest(sourceDataRequest); + } + + syncLayerWithMB(mbMap: MbMap, timeslice?: Timeslice) { + addGeoJsonMbSource(this._getMbSourceId(), this.getMbLayerIds(), mbMap); + + this._syncFeatureCollectionWithMb(mbMap); + + const timesliceMaskConfig = this._getTimesliceMaskConfig(timeslice); + this._setMbLabelProperties(mbMap, undefined, timesliceMaskConfig); + this._setMbPointsProperties(mbMap, undefined, timesliceMaskConfig); + this._setMbLinePolygonProperties(mbMap, undefined, timesliceMaskConfig); + } + + _syncFeatureCollectionWithMb(mbMap: MbMap) { + const mbGeoJSONSource = mbMap.getSource(this.getId()) as MbGeoJSONSource; + const featureCollection = this._getSourceFeatureCollection(); + const featureCollectionOnMap = AbstractLayer.getBoundDataForSource(mbMap, this.getId()); + + if (!featureCollection) { + if (featureCollectionOnMap) { + this.getCurrentStyle().clearFeatureState(featureCollectionOnMap, mbMap, this.getId()); + } + mbGeoJSONSource.setData(EMPTY_FEATURE_COLLECTION); + return; + } + + // "feature-state" data expressions are not supported with layout properties. + // To work around this limitation, + // scaled layout properties (like icon-size) must fall back to geojson property values :( + const hasGeoJsonProperties = this.getCurrentStyle().setFeatureStateAndStyleProps( + featureCollection, + mbMap, + this.getId() + ); + if (featureCollection !== featureCollectionOnMap || hasGeoJsonProperties) { + mbGeoJSONSource.setData(featureCollection); + } + } + + _getTimesliceMaskConfig(timeslice?: Timeslice): TimesliceMaskConfig | undefined { + if (!timeslice || this.hasJoins()) { + return; + } + + const prevMeta = this.getSourceDataRequest()?.getMeta(); + return prevMeta !== undefined && prevMeta.timesliceMaskField !== undefined + ? { + timesliceMaskField: prevMeta.timesliceMaskField, + timeslice, + } + : undefined; + } + + async syncData(syncContext: DataRequestContext) { + await this._syncData(syncContext, this.getSource(), this.getCurrentStyle()); + } + + // TLDR: Do not call getSource or getCurrentStyle in syncData flow. Use 'source' and 'style' arguments instead. + // + // 1) State is contained in the redux store. Layer instance state is readonly. + // 2) Even though data request descriptor updates trigger new instances for rendering, + // syncing data executes on a single object instance. Syncing data can not use updated redux store state. + // + // Blended layer data syncing branches on the source/style depending on whether clustering is used or not. + // Given 1 above, which source/style to use can not be stored in Layer instance state. + // Given 2 above, which source/style to use can not be pulled from data request state. + // Therefore, source and style are provided as arugments and must be used instead of calling getSource or getCurrentStyle. + async _syncData(syncContext: DataRequestContext, source: IVectorSource, style: IVectorStyle) { + if (this.isLoadingBounds()) { + return; + } + + try { + await this._syncSourceStyleMeta(syncContext, source, style); + await this._syncSourceFormatters(syncContext, source, style); + const sourceResult = await syncVectorSource({ + layerId: this.getId(), + layerName: await this.getDisplayName(source), + prevDataRequest: this.getSourceDataRequest(), + requestMeta: await this._getVectorSourceRequestMeta( + syncContext.isForceRefresh, + syncContext.dataFilters, + source, + style + ), + syncContext, + source, + getUpdateDueToTimeslice: (timeslice?: Timeslice) => { + return this._getUpdateDueToTimesliceFromSourceRequestMeta(source, timeslice); + }, + }); + await this._syncSupportsFeatureEditing({ syncContext, source }); + if ( + !sourceResult.featureCollection || + !sourceResult.featureCollection.features.length || + !this.hasJoins() + ) { + return; + } + + const joinStates = await this._syncJoins(syncContext, style); + performInnerJoins( + sourceResult, + joinStates, + syncContext.updateSourceData, + syncContext.onJoinError + ); + } catch (error) { + if (!(error instanceof DataRequestAbortError)) { + throw error; + } + } + } + + async _syncJoin({ + join, + startLoading, + stopLoading, + onLoadError, + registerCancelCallback, + dataFilters, + isForceRefresh, + }: { join: InnerJoin } & DataRequestContext): Promise { + const joinSource = join.getRightJoinSource(); + const sourceDataId = join.getSourceDataRequestId(); + const requestToken = Symbol(`layer-join-refresh:${this.getId()} - ${sourceDataId}`); + + const joinRequestMeta: VectorJoinSourceRequestMeta = buildVectorRequestMeta( + joinSource, + joinSource.getFieldNames(), + dataFilters, + joinSource.getWhereQuery(), + isForceRefresh + ) as VectorJoinSourceRequestMeta; + + const prevDataRequest = this.getDataRequest(sourceDataId); + const canSkipFetch = await canSkipSourceUpdate({ + source: joinSource, + prevDataRequest, + nextRequestMeta: joinRequestMeta, + extentAware: false, // join-sources are term-aggs that are spatially unaware (e.g. ESTermSource/TableSource). + getUpdateDueToTimeslice: () => { + return true; + }, + }); + + if (canSkipFetch) { + return { + dataHasChanged: false, + join, + propertiesMap: prevDataRequest?.getData() as PropertiesMap, + }; + } + + try { + startLoading(sourceDataId, requestToken, joinRequestMeta); + const leftSourceName = await this._source.getDisplayName(); + const propertiesMap = await joinSource.getPropertiesMap( + joinRequestMeta, + leftSourceName, + join.getLeftField().getName(), + registerCancelCallback.bind(null, requestToken) + ); + stopLoading(sourceDataId, requestToken, propertiesMap); + return { + dataHasChanged: true, + join, + propertiesMap, + }; + } catch (error) { + if (!(error instanceof DataRequestAbortError)) { + onLoadError(sourceDataId, requestToken, `Join error: ${error.message}`); + } + throw error; + } + } + + async _syncJoins(syncContext: DataRequestContext, style: IVectorStyle) { + const joinSyncs = this.getValidJoins().map(async (join) => { + await this._syncJoinStyleMeta(syncContext, join, style); + await this._syncJoinFormatters(syncContext, join, style); + return this._syncJoin({ join, ...syncContext }); + }); + + return await Promise.all(joinSyncs); + } + + async _syncJoinStyleMeta(syncContext: DataRequestContext, join: InnerJoin, style: IVectorStyle) { + const joinSource = join.getRightJoinSource(); + return this._syncStyleMeta({ + source: joinSource, + style, + sourceQuery: joinSource.getWhereQuery(), + dataRequestId: join.getSourceMetaDataRequestId(), + dynamicStyleProps: this.getCurrentStyle() + .getDynamicPropertiesArray() + .filter((dynamicStyleProp) => { + const matchingField = joinSource.getFieldByName(dynamicStyleProp.getFieldName()); + return ( + dynamicStyleProp.getFieldOrigin() === FIELD_ORIGIN.JOIN && + !!matchingField && + dynamicStyleProp.isFieldMetaEnabled() + ); + }), + ...syncContext, + }); + } + + async _syncJoinFormatters(syncContext: DataRequestContext, join: InnerJoin, style: IVectorStyle) { + const joinSource = join.getRightJoinSource(); + return this._syncFormatters({ + source: joinSource, + dataRequestId: join.getSourceFormattersDataRequestId(), + fields: style + .getDynamicPropertiesArray() + .filter((dynamicStyleProp) => { + const matchingField = joinSource.getFieldByName(dynamicStyleProp.getFieldName()); + return dynamicStyleProp.getFieldOrigin() === FIELD_ORIGIN.JOIN && !!matchingField; + }) + .map((dynamicStyleProp) => { + return dynamicStyleProp.getField()!; + }), + ...syncContext, + }); + } + + async _syncSupportsFeatureEditing({ + syncContext, + source, + }: { + syncContext: DataRequestContext; + source: IVectorSource; + }) { + if (syncContext.dataFilters.isReadOnly) { + return; + } + const { startLoading, stopLoading, onLoadError } = syncContext; + const dataRequestId = SUPPORTS_FEATURE_EDITING_REQUEST_ID; + const requestToken = Symbol(`layer-${this.getId()}-${dataRequestId}`); + const prevDataRequest = this.getDataRequest(dataRequestId); + if (prevDataRequest) { + return; + } + try { + startLoading(dataRequestId, requestToken); + const supportsFeatureEditing = await source.supportsFeatureEditing(); + stopLoading(dataRequestId, requestToken, { supportsFeatureEditing }); + } catch (error) { + onLoadError(dataRequestId, requestToken, error.message); + throw error; + } + } + + _getSourceFeatureCollection() { + const sourceDataRequest = this.getSourceDataRequest(); + return sourceDataRequest ? (sourceDataRequest.getData() as FeatureCollection) : null; + } + + _getUpdateDueToTimesliceFromSourceRequestMeta(source: ISource, timeslice?: Timeslice) { + const prevDataRequest = this.getSourceDataRequest(); + const prevMeta = prevDataRequest?.getMeta(); + if (!prevMeta) { + return true; + } + return source.getUpdateDueToTimeslice(prevMeta, timeslice); + } +} diff --git a/x-pack/plugins/maps/public/classes/layers/vector_layer/get_centroid_features.test.ts b/x-pack/plugins/maps/public/classes/layers/vector_layer/geojson_vector_layer/get_centroid_features.test.ts similarity index 100% rename from x-pack/plugins/maps/public/classes/layers/vector_layer/get_centroid_features.test.ts rename to x-pack/plugins/maps/public/classes/layers/vector_layer/geojson_vector_layer/get_centroid_features.test.ts diff --git a/x-pack/plugins/maps/public/classes/layers/vector_layer/get_centroid_features.ts b/x-pack/plugins/maps/public/classes/layers/vector_layer/geojson_vector_layer/get_centroid_features.ts similarity index 99% rename from x-pack/plugins/maps/public/classes/layers/vector_layer/get_centroid_features.ts rename to x-pack/plugins/maps/public/classes/layers/vector_layer/geojson_vector_layer/get_centroid_features.ts index 6afe61f8a16b9..48df2661d269b 100644 --- a/x-pack/plugins/maps/public/classes/layers/vector_layer/get_centroid_features.ts +++ b/x-pack/plugins/maps/public/classes/layers/vector_layer/geojson_vector_layer/get_centroid_features.ts @@ -21,7 +21,7 @@ import turfArea from '@turf/area'; import turfCenterOfMass from '@turf/center-of-mass'; import turfLength from '@turf/length'; import { lineString, polygon } from '@turf/helpers'; -import { GEO_JSON_TYPE, KBN_IS_CENTROID_FEATURE } from '../../../../common/constants'; +import { GEO_JSON_TYPE, KBN_IS_CENTROID_FEATURE } from '../../../../../common/constants'; export function getCentroidFeatures(featureCollection: FeatureCollection): Feature[] { const centroids = []; diff --git a/x-pack/plugins/maps/public/classes/layers/vector_layer/geojson_vector_layer/index.ts b/x-pack/plugins/maps/public/classes/layers/vector_layer/geojson_vector_layer/index.ts new file mode 100644 index 0000000000000..36566ba6c54ab --- /dev/null +++ b/x-pack/plugins/maps/public/classes/layers/vector_layer/geojson_vector_layer/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 { GeoJsonVectorLayer } from './geojson_vector_layer'; diff --git a/x-pack/plugins/maps/public/classes/layers/vector_layer/perform_inner_joins.test.ts b/x-pack/plugins/maps/public/classes/layers/vector_layer/geojson_vector_layer/perform_inner_joins.test.ts similarity index 95% rename from x-pack/plugins/maps/public/classes/layers/vector_layer/perform_inner_joins.test.ts rename to x-pack/plugins/maps/public/classes/layers/vector_layer/geojson_vector_layer/perform_inner_joins.test.ts index 9346bb1621e44..1049c4373c933 100644 --- a/x-pack/plugins/maps/public/classes/layers/vector_layer/perform_inner_joins.test.ts +++ b/x-pack/plugins/maps/public/classes/layers/vector_layer/geojson_vector_layer/perform_inner_joins.test.ts @@ -8,16 +8,16 @@ import sinon from 'sinon'; import _ from 'lodash'; import { FeatureCollection } from 'geojson'; -import { ESTermSourceDescriptor } from '../../../../common/descriptor_types'; +import { ESTermSourceDescriptor } from '../../../../../common/descriptor_types'; import { AGG_TYPE, FEATURE_VISIBLE_PROPERTY_NAME, SOURCE_TYPES, -} from '../../../../common/constants'; +} from '../../../../../common/constants'; import { performInnerJoins } from './perform_inner_joins'; -import { InnerJoin } from '../../joins/inner_join'; -import { IVectorSource } from '../../sources/vector_source'; -import { IField } from '../../fields/field'; +import { InnerJoin } from '../../../joins/inner_join'; +import { IVectorSource } from '../../../sources/vector_source'; +import { IField } from '../../../fields/field'; const LEFT_FIELD = 'leftKey'; const COUNT_PROPERTY_NAME = '__kbnjoin__count__d3625663-5b34-4d50-a784-0d743f676a0c'; diff --git a/x-pack/plugins/maps/public/classes/layers/vector_layer/perform_inner_joins.ts b/x-pack/plugins/maps/public/classes/layers/vector_layer/geojson_vector_layer/perform_inner_joins.ts similarity index 94% rename from x-pack/plugins/maps/public/classes/layers/vector_layer/perform_inner_joins.ts rename to x-pack/plugins/maps/public/classes/layers/vector_layer/geojson_vector_layer/perform_inner_joins.ts index 23c6527d3e818..3dd2a5ddb377e 100644 --- a/x-pack/plugins/maps/public/classes/layers/vector_layer/perform_inner_joins.ts +++ b/x-pack/plugins/maps/public/classes/layers/vector_layer/geojson_vector_layer/perform_inner_joins.ts @@ -7,10 +7,10 @@ import { FeatureCollection } from 'geojson'; import { i18n } from '@kbn/i18n'; -import { FEATURE_VISIBLE_PROPERTY_NAME } from '../../../../common/constants'; -import { DataRequestContext } from '../../../actions'; -import { InnerJoin } from '../../joins/inner_join'; -import { PropertiesMap } from '../../../../common/elasticsearch_util'; +import { FEATURE_VISIBLE_PROPERTY_NAME } from '../../../../../common/constants'; +import { DataRequestContext } from '../../../../actions'; +import { InnerJoin } from '../../../joins/inner_join'; +import { PropertiesMap } from '../../../../../common/elasticsearch_util'; interface SourceResult { refreshed: boolean; diff --git a/x-pack/plugins/maps/public/classes/layers/vector_layer/geojson_vector_layer/utils.tsx b/x-pack/plugins/maps/public/classes/layers/vector_layer/geojson_vector_layer/utils.tsx new file mode 100644 index 0000000000000..4385adbd4de65 --- /dev/null +++ b/x-pack/plugins/maps/public/classes/layers/vector_layer/geojson_vector_layer/utils.tsx @@ -0,0 +1,173 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { FeatureCollection } from 'geojson'; +import type { Map as MbMap } from '@kbn/mapbox-gl'; +import type { Query } from 'src/plugins/data/common'; +import { + EMPTY_FEATURE_COLLECTION, + SOURCE_BOUNDS_DATA_REQUEST_ID, + SOURCE_DATA_REQUEST_ID, + VECTOR_SHAPE_TYPE, +} from '../../../../../common/constants'; +import { + DataRequestMeta, + MapExtent, + Timeslice, + VectorSourceRequestMeta, +} from '../../../../../common/descriptor_types'; +import { DataRequestContext } from '../../../../actions'; +import { IVectorSource } from '../../../sources/vector_source'; +import { DataRequestAbortError } from '../../../util/data_request'; +import { DataRequest } from '../../../util/data_request'; +import { getCentroidFeatures } from './get_centroid_features'; +import { canSkipSourceUpdate } from '../../../util/can_skip_fetch'; +import { assignFeatureIds } from './assign_feature_ids'; + +export function addGeoJsonMbSource(mbSourceId: string, mbLayerIds: string[], mbMap: MbMap) { + const mbSource = mbMap.getSource(mbSourceId); + if (!mbSource) { + mbMap.addSource(mbSourceId, { + type: 'geojson', + data: EMPTY_FEATURE_COLLECTION, + }); + } else if (mbSource.type !== 'geojson') { + // Recreate source when existing source is not geojson. This can occur when layer changes from tile layer to vector layer. + mbLayerIds.forEach((mbLayerId) => { + if (mbMap.getLayer(mbLayerId)) { + mbMap.removeLayer(mbLayerId); + } + }); + + mbMap.removeSource(mbSourceId); + mbMap.addSource(mbSourceId, { + type: 'geojson', + data: EMPTY_FEATURE_COLLECTION, + }); + } +} + +export async function syncVectorSource({ + layerId, + layerName, + prevDataRequest, + requestMeta, + syncContext, + source, + getUpdateDueToTimeslice, +}: { + layerId: string; + layerName: string; + prevDataRequest: DataRequest | undefined; + requestMeta: VectorSourceRequestMeta; + syncContext: DataRequestContext; + source: IVectorSource; + getUpdateDueToTimeslice: (timeslice?: Timeslice) => boolean; +}): Promise<{ refreshed: boolean; featureCollection: FeatureCollection }> { + const { startLoading, stopLoading, onLoadError, registerCancelCallback, isRequestStillActive } = + syncContext; + const dataRequestId = SOURCE_DATA_REQUEST_ID; + const requestToken = Symbol(`${layerId}-${dataRequestId}`); + + const canSkipFetch = syncContext.forceRefreshDueToDrawing + ? false + : await canSkipSourceUpdate({ + source, + prevDataRequest, + nextRequestMeta: requestMeta, + extentAware: source.isFilterByMapBounds(), + getUpdateDueToTimeslice, + }); + + if (canSkipFetch) { + return { + refreshed: false, + featureCollection: prevDataRequest + ? (prevDataRequest.getData() as FeatureCollection) + : EMPTY_FEATURE_COLLECTION, + }; + } + + try { + startLoading(dataRequestId, requestToken, requestMeta); + const { data: sourceFeatureCollection, meta } = await source.getGeoJsonWithMeta( + layerName, + requestMeta, + registerCancelCallback.bind(null, requestToken), + () => { + return isRequestStillActive(dataRequestId, requestToken); + } + ); + const layerFeatureCollection = assignFeatureIds(sourceFeatureCollection); + const supportedShapes = await source.getSupportedShapeTypes(); + if ( + supportedShapes.includes(VECTOR_SHAPE_TYPE.LINE) || + supportedShapes.includes(VECTOR_SHAPE_TYPE.POLYGON) + ) { + layerFeatureCollection.features.push(...getCentroidFeatures(layerFeatureCollection)); + } + const responseMeta: DataRequestMeta = meta ? { ...meta } : {}; + if (requestMeta.applyGlobalTime && (await source.isTimeAware())) { + const timesliceMaskField = await source.getTimesliceMaskFieldName(); + if (timesliceMaskField) { + responseMeta.timesliceMaskField = timesliceMaskField; + } + } + stopLoading(dataRequestId, requestToken, layerFeatureCollection, responseMeta); + return { + refreshed: true, + featureCollection: layerFeatureCollection, + }; + } catch (error) { + if (!(error instanceof DataRequestAbortError)) { + onLoadError(dataRequestId, requestToken, error.message); + } + throw error; + } +} + +export async function getVectorSourceBounds({ + layerId, + syncContext, + source, + sourceQuery, +}: { + layerId: string; + syncContext: DataRequestContext; + source: IVectorSource; + sourceQuery: Query | null; +}): Promise { + const { startLoading, stopLoading, registerCancelCallback, dataFilters } = syncContext; + + const requestToken = Symbol(`${SOURCE_BOUNDS_DATA_REQUEST_ID}-${layerId}`); + + // Do not pass all searchFilters to source.getBoundsForFilters(). + // For example, do not want to filter bounds request by extent and buffer. + const boundsFilters = { + sourceQuery: sourceQuery ? sourceQuery : undefined, + query: dataFilters.query, + timeFilters: dataFilters.timeFilters, + timeslice: dataFilters.timeslice, + filters: dataFilters.filters, + applyGlobalQuery: source.getApplyGlobalQuery(), + applyGlobalTime: source.getApplyGlobalTime(), + }; + + let bounds = null; + try { + startLoading(SOURCE_BOUNDS_DATA_REQUEST_ID, requestToken, boundsFilters); + bounds = await source.getBoundsForFilters( + boundsFilters, + registerCancelCallback.bind(null, requestToken) + ); + } finally { + // Use stopLoading callback instead of onLoadError callback. + // Function is loading bounds and not feature data. + stopLoading(SOURCE_BOUNDS_DATA_REQUEST_ID, requestToken, bounds ? bounds : {}); + } + return bounds; +} diff --git a/x-pack/plugins/maps/public/classes/layers/vector_layer/index.ts b/x-pack/plugins/maps/public/classes/layers/vector_layer/index.ts index 2b14b78f92946..b3d7c47fbc71f 100644 --- a/x-pack/plugins/maps/public/classes/layers/vector_layer/index.ts +++ b/x-pack/plugins/maps/public/classes/layers/vector_layer/index.ts @@ -5,6 +5,14 @@ * 2.0. */ -export { addGeoJsonMbSource, getVectorSourceBounds, syncVectorSource } from './utils'; +export { + addGeoJsonMbSource, + getVectorSourceBounds, + syncVectorSource, +} from './geojson_vector_layer/utils'; export type { IVectorLayer, VectorLayerArguments } from './vector_layer'; -export { isVectorLayer, VectorLayer, NO_RESULTS_ICON_AND_TOOLTIPCONTENT } from './vector_layer'; +export { isVectorLayer, NO_RESULTS_ICON_AND_TOOLTIPCONTENT } from './vector_layer'; + +export { BlendedVectorLayer } from './blended_vector_layer'; +export { GeoJsonVectorLayer } from './geojson_vector_layer'; +export { MvtVectorLayer } from './mvt_vector_layer'; diff --git a/x-pack/plugins/maps/public/classes/layers/tiled_vector_layer/__snapshots__/tiled_vector_layer.test.tsx.snap b/x-pack/plugins/maps/public/classes/layers/vector_layer/mvt_vector_layer/__snapshots__/mvt_vector_layer.test.tsx.snap similarity index 100% rename from x-pack/plugins/maps/public/classes/layers/tiled_vector_layer/__snapshots__/tiled_vector_layer.test.tsx.snap rename to x-pack/plugins/maps/public/classes/layers/vector_layer/mvt_vector_layer/__snapshots__/mvt_vector_layer.test.tsx.snap diff --git a/x-pack/plugins/maps/public/classes/layers/vector_layer/mvt_vector_layer/index.ts b/x-pack/plugins/maps/public/classes/layers/vector_layer/mvt_vector_layer/index.ts new file mode 100644 index 0000000000000..85ff76f716a7b --- /dev/null +++ b/x-pack/plugins/maps/public/classes/layers/vector_layer/mvt_vector_layer/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 { MvtVectorLayer } from './mvt_vector_layer'; diff --git a/x-pack/plugins/maps/public/classes/layers/vector_layer/mvt_vector_layer/mvt_vector_layer.test.tsx b/x-pack/plugins/maps/public/classes/layers/vector_layer/mvt_vector_layer/mvt_vector_layer.test.tsx new file mode 100644 index 0000000000000..60001cb9e8b1d --- /dev/null +++ b/x-pack/plugins/maps/public/classes/layers/vector_layer/mvt_vector_layer/mvt_vector_layer.test.tsx @@ -0,0 +1,241 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { MockSyncContext } from '../../__fixtures__/mock_sync_context'; +import sinon from 'sinon'; +import url from 'url'; + +jest.mock('../../../../kibana_services', () => { + return { + getIsDarkMode() { + return false; + }, + }; +}); + +import { shallow } from 'enzyme'; + +import { Feature } from 'geojson'; +import { MVTSingleLayerVectorSource } from '../../../sources/mvt_single_layer_vector_source'; +import { + DataRequestDescriptor, + TiledSingleLayerVectorSourceDescriptor, + VectorLayerDescriptor, +} from '../../../../../common/descriptor_types'; +import { SOURCE_TYPES } from '../../../../../common/constants'; +import { MvtVectorLayer } from './mvt_vector_layer'; + +const defaultConfig = { + urlTemplate: 'https://example.com/{x}/{y}/{z}.pbf', + layerName: 'foobar', + minSourceZoom: 4, + maxSourceZoom: 14, +}; + +function createLayer( + layerOptions: Partial = {}, + sourceOptions: Partial = {}, + isTimeAware: boolean = false, + includeToken: boolean = false +): MvtVectorLayer { + const sourceDescriptor: TiledSingleLayerVectorSourceDescriptor = { + type: SOURCE_TYPES.MVT_SINGLE_LAYER, + ...defaultConfig, + fields: [], + tooltipProperties: [], + ...sourceOptions, + }; + const mvtSource = new MVTSingleLayerVectorSource(sourceDescriptor); + if (isTimeAware) { + mvtSource.isTimeAware = async () => { + return true; + }; + mvtSource.getApplyGlobalTime = () => { + return true; + }; + } + + if (includeToken) { + mvtSource.getUrlTemplateWithMeta = async (...args) => { + const superReturn = await MVTSingleLayerVectorSource.prototype.getUrlTemplateWithMeta.call( + mvtSource, + ...args + ); + return { + ...superReturn, + refreshTokenParamName: 'token', + }; + }; + } + + const defaultLayerOptions = { + ...layerOptions, + sourceDescriptor, + }; + const layerDescriptor = MvtVectorLayer.createDescriptor(defaultLayerOptions); + return new MvtVectorLayer({ layerDescriptor, source: mvtSource }); +} + +describe('visiblity', () => { + it('should get minzoom from source', async () => { + const layer: MvtVectorLayer = createLayer({}, {}); + expect(layer.getMinZoom()).toEqual(4); + }); + it('should get maxzoom from default', async () => { + const layer: MvtVectorLayer = createLayer({}, {}); + expect(layer.getMaxZoom()).toEqual(24); + }); + it('should get maxzoom from layer options', async () => { + const layer: MvtVectorLayer = createLayer({ maxZoom: 10 }, {}); + expect(layer.getMaxZoom()).toEqual(10); + }); +}); + +describe('getCustomIconAndTooltipContent', () => { + it('Layers with non-elasticsearch sources should display icon', async () => { + const layer: MvtVectorLayer = createLayer({}, {}); + + const iconAndTooltipContent = layer.getCustomIconAndTooltipContent(); + const component = shallow(iconAndTooltipContent.icon); + expect(component).toMatchSnapshot(); + }); +}); + +describe('getFeatureById', () => { + it('should return null feature', async () => { + const layer: MvtVectorLayer = createLayer({}, {}); + const feature = layer.getFeatureById('foobar') as Feature; + expect(feature).toEqual(null); + }); +}); + +describe('syncData', () => { + it('Should sync with source-params', async () => { + const layer: MvtVectorLayer = createLayer({}, {}); + + const syncContext = new MockSyncContext({ dataFilters: {} }); + + await layer.syncData(syncContext); + // @ts-expect-error + sinon.assert.calledOnce(syncContext.startLoading); + // @ts-expect-error + sinon.assert.calledOnce(syncContext.stopLoading); + + // @ts-expect-error + const call = syncContext.stopLoading.getCall(0); + expect(call.args[2]!.minSourceZoom).toEqual(defaultConfig.minSourceZoom); + expect(call.args[2]!.maxSourceZoom).toEqual(defaultConfig.maxSourceZoom); + expect(call.args[2]!.layerName).toEqual(defaultConfig.layerName); + expect(call.args[2]!.urlTemplate).toEqual(defaultConfig.urlTemplate); + }); + + it('Should not resync when no changes to source params', async () => { + const dataRequestDescriptor: DataRequestDescriptor = { + data: { ...defaultConfig }, + dataId: 'source', + }; + const layer: MvtVectorLayer = createLayer( + { + __dataRequests: [dataRequestDescriptor], + }, + {} + ); + const syncContext = new MockSyncContext({ dataFilters: {} }); + await layer.syncData(syncContext); + // @ts-expect-error + sinon.assert.notCalled(syncContext.startLoading); + // @ts-expect-error + sinon.assert.notCalled(syncContext.stopLoading); + }); + + it('Should resync when changes to syncContext', async () => { + const dataRequestDescriptor: DataRequestDescriptor = { + data: { ...defaultConfig }, + dataId: 'source', + }; + const layer: MvtVectorLayer = createLayer( + { + __dataRequests: [dataRequestDescriptor], + }, + {}, + true + ); + const syncContext = new MockSyncContext({ + dataFilters: { + timeFilters: { + from: 'now', + to: '30m', + mode: 'relative', + }, + }, + }); + await layer.syncData(syncContext); + // @ts-expect-error + sinon.assert.calledOnce(syncContext.startLoading); + // @ts-expect-error + sinon.assert.calledOnce(syncContext.stopLoading); + }); + + describe('Should resync when changes to source params: ', () => { + [{ layerName: 'barfoo' }, { minSourceZoom: 1 }, { maxSourceZoom: 12 }].forEach((changes) => { + it(`change in ${Object.keys(changes).join(',')}`, async () => { + const dataRequestDescriptor: DataRequestDescriptor = { + data: defaultConfig, + dataId: 'source', + }; + const layer: MvtVectorLayer = createLayer( + { + __dataRequests: [dataRequestDescriptor], + }, + changes + ); + const syncContext = new MockSyncContext({ dataFilters: {} }); + await layer.syncData(syncContext); + + // @ts-expect-error + sinon.assert.calledOnce(syncContext.startLoading); + // @ts-expect-error + sinon.assert.calledOnce(syncContext.stopLoading); + + // @ts-expect-error + const call = syncContext.stopLoading.getCall(0); + + const newMeta = { ...defaultConfig, ...changes }; + expect(call.args[2]!.minSourceZoom).toEqual(newMeta.minSourceZoom); + expect(call.args[2]!.maxSourceZoom).toEqual(newMeta.maxSourceZoom); + expect(call.args[2]!.layerName).toEqual(newMeta.layerName); + expect(call.args[2]!.urlTemplate).toEqual(newMeta.urlTemplate); + }); + }); + }); + + describe('refresh token', () => { + const uuidRegex = /\b[0-9a-f]{8}\b-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-\b[0-9a-f]{12}\b/; + + it(`should add token in url`, async () => { + const layer: MvtVectorLayer = createLayer({}, {}, false, true); + + const syncContext = new MockSyncContext({ dataFilters: {} }); + + await layer.syncData(syncContext); + // @ts-expect-error + sinon.assert.calledOnce(syncContext.startLoading); + // @ts-expect-error + sinon.assert.calledOnce(syncContext.stopLoading); + + // @ts-expect-error + const call = syncContext.stopLoading.getCall(0); + expect(call.args[2]!.minSourceZoom).toEqual(defaultConfig.minSourceZoom); + expect(call.args[2]!.maxSourceZoom).toEqual(defaultConfig.maxSourceZoom); + expect(call.args[2]!.layerName).toEqual(defaultConfig.layerName); + expect(call.args[2]!.urlTemplate.startsWith(defaultConfig.urlTemplate)).toBe(true); + + const parsedUrl = url.parse(call.args[2]!.urlTemplate, true); + expect(!!(parsedUrl.query.token! as string).match(uuidRegex)).toBe(true); + }); + }); +}); diff --git a/x-pack/plugins/maps/public/classes/layers/vector_layer/mvt_vector_layer/mvt_vector_layer.tsx b/x-pack/plugins/maps/public/classes/layers/vector_layer/mvt_vector_layer/mvt_vector_layer.tsx new file mode 100644 index 0000000000000..237bab80ce758 --- /dev/null +++ b/x-pack/plugins/maps/public/classes/layers/vector_layer/mvt_vector_layer/mvt_vector_layer.tsx @@ -0,0 +1,498 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license 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 { + Map as MbMap, + AnyLayer as MbLayer, + GeoJSONSource as MbGeoJSONSource, + VectorSource as MbVectorSource, +} from '@kbn/mapbox-gl'; +import { Feature } from 'geojson'; +import { i18n } from '@kbn/i18n'; +import uuid from 'uuid/v4'; +import { parse as parseUrl } from 'url'; +import { euiThemeVars } from '@kbn/ui-shared-deps-src/theme'; +import { IVectorStyle, VectorStyle } from '../../../styles/vector/vector_style'; +import { LAYER_TYPE, SOURCE_DATA_REQUEST_ID, SOURCE_TYPES } from '../../../../../common/constants'; +import { + NO_RESULTS_ICON_AND_TOOLTIPCONTENT, + AbstractVectorLayer, + VectorLayerArguments, +} from '../vector_layer'; +import { ITiledSingleLayerVectorSource } from '../../../sources/tiled_single_layer_vector_source'; +import { DataRequestContext } from '../../../../actions'; +import { + StyleMetaDescriptor, + TileMetaFeature, + Timeslice, + VectorLayerDescriptor, + VectorSourceRequestMeta, +} from '../../../../../common/descriptor_types'; +import { MVTSingleLayerVectorSourceConfig } from '../../../sources/mvt_single_layer_vector_source/types'; +import { ESSearchSource } from '../../../sources/es_search_source'; +import { canSkipSourceUpdate } from '../../../util/can_skip_fetch'; +import { CustomIconAndTooltipContent } from '../../layer'; + +const ES_MVT_META_LAYER_NAME = 'meta'; +const ES_MVT_HITS_TOTAL_RELATION = 'hits.total.relation'; +const ES_MVT_HITS_TOTAL_VALUE = 'hits.total.value'; +const MAX_RESULT_WINDOW_DATA_REQUEST_ID = 'maxResultWindow'; + +export class MvtVectorLayer extends AbstractVectorLayer { + static createDescriptor( + descriptor: Partial, + mapColors?: string[] + ): VectorLayerDescriptor { + const layerDescriptor = super.createDescriptor(descriptor, mapColors); + layerDescriptor.type = LAYER_TYPE.TILED_VECTOR; + + if (!layerDescriptor.style) { + const styleProperties = VectorStyle.createDefaultStyleProperties(mapColors ? mapColors : []); + layerDescriptor.style = VectorStyle.createDescriptor(styleProperties); + } + + return layerDescriptor; + } + + readonly _source: ITiledSingleLayerVectorSource; // downcast to the more specific type + + constructor({ layerDescriptor, source }: VectorLayerArguments) { + super({ layerDescriptor, source }); + this._source = source as ITiledSingleLayerVectorSource; + } + + getFeatureId(feature: Feature): string | number | undefined { + if (!this.getSource().isESSource()) { + return feature.id; + } + + return this.getSource().getType() === SOURCE_TYPES.ES_SEARCH + ? feature.properties?._id + : feature.properties?._key; + } + + _getMetaFromTiles(): TileMetaFeature[] { + return this._descriptor.__metaFromTiles || []; + } + + getCustomIconAndTooltipContent(): CustomIconAndTooltipContent { + const icon = this.getCurrentStyle().getIcon(); + if (!this.getSource().isESSource()) { + // Only ES-sources can have a special meta-tile, not 3rd party vector tile sources + return { + icon, + tooltipContent: null, + areResultsTrimmed: false, + }; + } + + // + // TODO ES MVT specific - move to es_tiled_vector_layer implementation + // + + const tileMetaFeatures = this._getMetaFromTiles(); + if (!tileMetaFeatures.length) { + return NO_RESULTS_ICON_AND_TOOLTIPCONTENT; + } + + if (this.getSource().getType() !== SOURCE_TYPES.ES_SEARCH) { + // aggregation ES sources are never trimmed + return { + icon, + tooltipContent: null, + areResultsTrimmed: false, + }; + } + + const maxResultWindow = this._getMaxResultWindow(); + if (maxResultWindow === undefined) { + return { + icon, + tooltipContent: null, + areResultsTrimmed: false, + }; + } + + const totalFeaturesCount: number = tileMetaFeatures.reduce((acc: number, tileMeta: Feature) => { + const count = + tileMeta && tileMeta.properties ? tileMeta.properties[ES_MVT_HITS_TOTAL_VALUE] : 0; + return count + acc; + }, 0); + + if (totalFeaturesCount === 0) { + return NO_RESULTS_ICON_AND_TOOLTIPCONTENT; + } + + const isIncomplete: boolean = tileMetaFeatures.some((tileMeta: TileMetaFeature) => { + if (tileMeta?.properties?.[ES_MVT_HITS_TOTAL_RELATION] === 'gte') { + return tileMeta?.properties?.[ES_MVT_HITS_TOTAL_VALUE] >= maxResultWindow + 1; + } else { + return false; + } + }); + + return { + icon, + tooltipContent: isIncomplete + ? i18n.translate('xpack.maps.tiles.resultsTrimmedMsg', { + defaultMessage: `Results limited to {count} documents.`, + values: { + count: totalFeaturesCount.toLocaleString(), + }, + }) + : i18n.translate('xpack.maps.tiles.resultsCompleteMsg', { + defaultMessage: `Found {count} documents.`, + values: { + count: totalFeaturesCount.toLocaleString(), + }, + }), + areResultsTrimmed: isIncomplete, + }; + } + + _getMaxResultWindow(): number | undefined { + const dataRequest = this.getDataRequest(MAX_RESULT_WINDOW_DATA_REQUEST_ID); + if (!dataRequest) { + return; + } + const data = dataRequest.getData() as { maxResultWindow: number } | undefined; + return data ? data.maxResultWindow : undefined; + } + + async _syncMaxResultWindow({ startLoading, stopLoading }: DataRequestContext) { + const prevDataRequest = this.getDataRequest(MAX_RESULT_WINDOW_DATA_REQUEST_ID); + if (prevDataRequest) { + return; + } + + const requestToken = Symbol(`${this.getId()}-${MAX_RESULT_WINDOW_DATA_REQUEST_ID}`); + startLoading(MAX_RESULT_WINDOW_DATA_REQUEST_ID, requestToken); + const maxResultWindow = await (this.getSource() as ESSearchSource).getMaxResultWindow(); + stopLoading(MAX_RESULT_WINDOW_DATA_REQUEST_ID, requestToken, { maxResultWindow }); + } + + async _syncMVTUrlTemplate({ + startLoading, + stopLoading, + onLoadError, + dataFilters, + isForceRefresh, + }: DataRequestContext) { + const requestToken: symbol = Symbol(`layer-${this.getId()}-${SOURCE_DATA_REQUEST_ID}`); + const requestMeta: VectorSourceRequestMeta = await this._getVectorSourceRequestMeta( + isForceRefresh, + dataFilters, + this.getSource(), + this._style as IVectorStyle + ); + const prevDataRequest = this.getSourceDataRequest(); + if (prevDataRequest) { + const data: MVTSingleLayerVectorSourceConfig = + prevDataRequest.getData() as MVTSingleLayerVectorSourceConfig; + if (data) { + const noChangesInSourceState: boolean = + data.layerName === this._source.getLayerName() && + data.minSourceZoom === this._source.getMinZoom() && + data.maxSourceZoom === this._source.getMaxZoom(); + const noChangesInSearchState: boolean = await canSkipSourceUpdate({ + extentAware: false, // spatial extent knowledge is already fully automated by tile-loading based on pan-zooming + source: this.getSource(), + prevDataRequest, + nextRequestMeta: requestMeta, + getUpdateDueToTimeslice: (timeslice?: Timeslice) => { + // TODO use meta features to determine if tiles already contain features for timeslice. + return true; + }, + }); + const canSkip = noChangesInSourceState && noChangesInSearchState; + + if (canSkip) { + return null; + } + } + } + + startLoading(SOURCE_DATA_REQUEST_ID, requestToken, requestMeta); + try { + const prevData = prevDataRequest + ? (prevDataRequest.getData() as MVTSingleLayerVectorSourceConfig) + : undefined; + const urlToken = + !prevData || (requestMeta.isForceRefresh && requestMeta.applyForceRefresh) + ? uuid() + : prevData.urlToken; + + const newUrlTemplateAndMeta = await this._source.getUrlTemplateWithMeta(requestMeta); + + let urlTemplate; + if (newUrlTemplateAndMeta.refreshTokenParamName) { + const parsedUrl = parseUrl(newUrlTemplateAndMeta.urlTemplate, true); + const separator = !parsedUrl.query || Object.keys(parsedUrl.query).length === 0 ? '?' : '&'; + urlTemplate = `${newUrlTemplateAndMeta.urlTemplate}${separator}${newUrlTemplateAndMeta.refreshTokenParamName}=${urlToken}`; + } else { + urlTemplate = newUrlTemplateAndMeta.urlTemplate; + } + + const urlTemplateAndMetaWithToken = { + ...newUrlTemplateAndMeta, + urlToken, + urlTemplate, + }; + stopLoading(SOURCE_DATA_REQUEST_ID, requestToken, urlTemplateAndMetaWithToken, {}); + } catch (error) { + onLoadError(SOURCE_DATA_REQUEST_ID, requestToken, error.message); + } + } + + async syncData(syncContext: DataRequestContext) { + if (this.getSource().getType() === SOURCE_TYPES.ES_SEARCH) { + await this._syncMaxResultWindow(syncContext); + } + await this._syncSourceStyleMeta(syncContext, this._source, this._style as IVectorStyle); + await this._syncSourceFormatters(syncContext, this._source, this._style as IVectorStyle); + await this._syncMVTUrlTemplate(syncContext); + } + + _syncSourceBindingWithMb(mbMap: MbMap) { + const mbSource = mbMap.getSource(this._getMbSourceId()); + if (mbSource) { + return; + } + const sourceDataRequest = this.getSourceDataRequest(); + if (!sourceDataRequest) { + // this is possible if the layer was invisible at startup. + // the actions will not perform any data=syncing as an optimization when a layer is invisible + // when turning the layer back into visible, it's possible the url had not been resolved yet. + return; + } + + const sourceMeta: MVTSingleLayerVectorSourceConfig | null = + sourceDataRequest.getData() as MVTSingleLayerVectorSourceConfig; + if (!sourceMeta) { + return; + } + + const mbSourceId = this._getMbSourceId(); + mbMap.addSource(mbSourceId, { + type: 'vector', + tiles: [sourceMeta.urlTemplate], + minzoom: sourceMeta.minSourceZoom, + maxzoom: sourceMeta.maxSourceZoom, + }); + } + + getMbLayerIds() { + return [...super.getMbLayerIds(), this._getMbTooManyFeaturesLayerId()]; + } + + ownsMbSourceId(mbSourceId: string): boolean { + return this._getMbSourceId() === mbSourceId; + } + + _getMbTooManyFeaturesLayerId() { + return this.makeMbLayerId('toomanyfeatures'); + } + + _syncStylePropertiesWithMb(mbMap: MbMap) { + // @ts-ignore + const mbSource = mbMap.getSource(this._getMbSourceId()); + if (!mbSource) { + return; + } + + const sourceDataRequest = this.getSourceDataRequest(); + if (!sourceDataRequest) { + return; + } + const sourceMeta: MVTSingleLayerVectorSourceConfig = + sourceDataRequest.getData() as MVTSingleLayerVectorSourceConfig; + if (sourceMeta.layerName === '') { + return; + } + + this._setMbLabelProperties(mbMap, sourceMeta.layerName); + this._setMbPointsProperties(mbMap, sourceMeta.layerName); + this._setMbLinePolygonProperties(mbMap, sourceMeta.layerName); + this._syncTooManyFeaturesProperties(mbMap); + } + + // TODO ES MVT specific - move to es_tiled_vector_layer implementation + _syncTooManyFeaturesProperties(mbMap: MbMap) { + if (this.getSource().getType() !== SOURCE_TYPES.ES_SEARCH) { + return; + } + + const maxResultWindow = this._getMaxResultWindow(); + if (maxResultWindow === undefined) { + return; + } + + const tooManyFeaturesLayerId = this._getMbTooManyFeaturesLayerId(); + + if (!mbMap.getLayer(tooManyFeaturesLayerId)) { + const mbTooManyFeaturesLayer: MbLayer = { + id: tooManyFeaturesLayerId, + type: 'line', + source: this.getId(), + paint: {}, + }; + mbTooManyFeaturesLayer['source-layer'] = ES_MVT_META_LAYER_NAME; + mbMap.addLayer(mbTooManyFeaturesLayer); + mbMap.setFilter(tooManyFeaturesLayerId, [ + 'all', + ['==', ['get', ES_MVT_HITS_TOTAL_RELATION], 'gte'], + ['>=', ['get', ES_MVT_HITS_TOTAL_VALUE], maxResultWindow + 1], + ]); + mbMap.setPaintProperty(tooManyFeaturesLayerId, 'line-color', euiThemeVars.euiColorWarning); + mbMap.setPaintProperty(tooManyFeaturesLayerId, 'line-width', 3); + mbMap.setPaintProperty(tooManyFeaturesLayerId, 'line-dasharray', [2, 1]); + mbMap.setPaintProperty(tooManyFeaturesLayerId, 'line-opacity', this.getAlpha()); + } + + this.syncVisibilityWithMb(mbMap, tooManyFeaturesLayerId); + mbMap.setLayerZoomRange(tooManyFeaturesLayerId, this.getMinZoom(), this.getMaxZoom()); + } + + queryTileMetaFeatures(mbMap: MbMap): TileMetaFeature[] | null { + if (!this.getSource().isESSource()) { + return null; + } + + // @ts-ignore + const mbSource = mbMap.getSource(this._getMbSourceId()); + if (!mbSource) { + return null; + } + + const sourceDataRequest = this.getSourceDataRequest(); + if (!sourceDataRequest) { + return null; + } + const sourceMeta: MVTSingleLayerVectorSourceConfig = + sourceDataRequest.getData() as MVTSingleLayerVectorSourceConfig; + if (sourceMeta.layerName === '') { + return null; + } + + // querySourceFeatures can return duplicated features when features cross tile boundaries. + // Tile meta will never have duplicated features since by there nature, tile meta is a feature contained within a single tile + const mbFeatures = mbMap.querySourceFeatures(this._getMbSourceId(), { + sourceLayer: ES_MVT_META_LAYER_NAME, + }); + + const metaFeatures: Array = ( + mbFeatures as unknown as TileMetaFeature[] + ).map((mbFeature: TileMetaFeature | null) => { + const parsedProperties: Record = {}; + for (const key in mbFeature?.properties) { + if (mbFeature?.properties.hasOwnProperty(key)) { + parsedProperties[key] = + typeof mbFeature.properties[key] === 'string' || + typeof mbFeature.properties[key] === 'number' || + typeof mbFeature.properties[key] === 'boolean' + ? mbFeature.properties[key] + : JSON.parse(mbFeature.properties[key]); // mvt properties cannot be nested geojson + } + } + + try { + return { + type: 'Feature', + id: mbFeature?.id, + geometry: mbFeature?.geometry, // this getter might throw with non-conforming geometries + properties: parsedProperties, + } as TileMetaFeature; + } catch (e) { + return null; + } + }); + + const filtered = metaFeatures.filter((f) => f !== null); + return filtered as TileMetaFeature[]; + } + + _requiresPrevSourceCleanup(mbMap: MbMap): boolean { + const mbSource = mbMap.getSource(this._getMbSourceId()) as MbVectorSource | MbGeoJSONSource; + if (!mbSource) { + return false; + } + if (!('tiles' in mbSource)) { + // Expected source is not compatible, so remove. + return true; + } + const mbTileSource = mbSource as MbVectorSource; + + const dataRequest = this.getSourceDataRequest(); + if (!dataRequest) { + return false; + } + const tiledSourceMeta: MVTSingleLayerVectorSourceConfig | null = + dataRequest.getData() as MVTSingleLayerVectorSourceConfig; + + if (!tiledSourceMeta) { + return false; + } + + const isSourceDifferent = + mbTileSource.tiles?.[0] !== tiledSourceMeta.urlTemplate || + mbTileSource.minzoom !== tiledSourceMeta.minSourceZoom || + mbTileSource.maxzoom !== tiledSourceMeta.maxSourceZoom; + + if (isSourceDifferent) { + return true; + } + + const layerIds = this.getMbLayerIds(); + for (let i = 0; i < layerIds.length; i++) { + const mbLayer = mbMap.getLayer(layerIds[i]); + // The mapbox type in the spec is specified with `source-layer` + // but the programmable JS-object uses camelcase `sourceLayer` + if ( + mbLayer && + // @ts-expect-error + mbLayer.sourceLayer !== tiledSourceMeta.layerName && + // @ts-expect-error + mbLayer.sourceLayer !== ES_MVT_META_LAYER_NAME + ) { + // If the source-pointer of one of the layers is stale, they will all be stale. + // In this case, all the mb-layers need to be removed and re-added. + return true; + } + } + + return false; + } + + syncLayerWithMB(mbMap: MbMap) { + this._removeStaleMbSourcesAndLayers(mbMap); + this._syncSourceBindingWithMb(mbMap); + this._syncStylePropertiesWithMb(mbMap); + } + + getJoins() { + return []; + } + + getMinZoom() { + // higher resolution vector tiles cannot be displayed at lower-res + return Math.max(this._source.getMinZoom(), super.getMinZoom()); + } + + getFeatureById(id: string | number): Feature | null { + return null; + } + + async getStyleMetaDescriptorFromLocalFeatures(): Promise { + const style = this.getCurrentStyle(); + if (!style) { + return null; + } + + const metaFromTiles = this._getMetaFromTiles(); + return await style.pluckStyleMetaFromTileMeta(metaFromTiles); + } +} diff --git a/x-pack/plugins/maps/public/classes/layers/vector_layer/utils.tsx b/x-pack/plugins/maps/public/classes/layers/vector_layer/utils.tsx deleted file mode 100644 index cc30f30fe9898..0000000000000 --- a/x-pack/plugins/maps/public/classes/layers/vector_layer/utils.tsx +++ /dev/null @@ -1,173 +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 { FeatureCollection } from 'geojson'; -import type { Map as MbMap } from '@kbn/mapbox-gl'; -import type { Query } from 'src/plugins/data/common'; -import { - EMPTY_FEATURE_COLLECTION, - SOURCE_BOUNDS_DATA_REQUEST_ID, - SOURCE_DATA_REQUEST_ID, - VECTOR_SHAPE_TYPE, -} from '../../../../common/constants'; -import { - DataRequestMeta, - MapExtent, - Timeslice, - VectorSourceRequestMeta, -} from '../../../../common/descriptor_types'; -import { DataRequestContext } from '../../../actions'; -import { IVectorSource } from '../../sources/vector_source'; -import { DataRequestAbortError } from '../../util/data_request'; -import { DataRequest } from '../../util/data_request'; -import { getCentroidFeatures } from './get_centroid_features'; -import { canSkipSourceUpdate } from '../../util/can_skip_fetch'; -import { assignFeatureIds } from './assign_feature_ids'; - -export function addGeoJsonMbSource(mbSourceId: string, mbLayerIds: string[], mbMap: MbMap) { - const mbSource = mbMap.getSource(mbSourceId); - if (!mbSource) { - mbMap.addSource(mbSourceId, { - type: 'geojson', - data: EMPTY_FEATURE_COLLECTION, - }); - } else if (mbSource.type !== 'geojson') { - // Recreate source when existing source is not geojson. This can occur when layer changes from tile layer to vector layer. - mbLayerIds.forEach((mbLayerId) => { - if (mbMap.getLayer(mbLayerId)) { - mbMap.removeLayer(mbLayerId); - } - }); - - mbMap.removeSource(mbSourceId); - mbMap.addSource(mbSourceId, { - type: 'geojson', - data: EMPTY_FEATURE_COLLECTION, - }); - } -} - -export async function syncVectorSource({ - layerId, - layerName, - prevDataRequest, - requestMeta, - syncContext, - source, - getUpdateDueToTimeslice, -}: { - layerId: string; - layerName: string; - prevDataRequest: DataRequest | undefined; - requestMeta: VectorSourceRequestMeta; - syncContext: DataRequestContext; - source: IVectorSource; - getUpdateDueToTimeslice: (timeslice?: Timeslice) => boolean; -}): Promise<{ refreshed: boolean; featureCollection: FeatureCollection }> { - const { startLoading, stopLoading, onLoadError, registerCancelCallback, isRequestStillActive } = - syncContext; - const dataRequestId = SOURCE_DATA_REQUEST_ID; - const requestToken = Symbol(`${layerId}-${dataRequestId}`); - - const canSkipFetch = syncContext.forceRefreshDueToDrawing - ? false - : await canSkipSourceUpdate({ - source, - prevDataRequest, - nextRequestMeta: requestMeta, - extentAware: source.isFilterByMapBounds(), - getUpdateDueToTimeslice, - }); - - if (canSkipFetch) { - return { - refreshed: false, - featureCollection: prevDataRequest - ? (prevDataRequest.getData() as FeatureCollection) - : EMPTY_FEATURE_COLLECTION, - }; - } - - try { - startLoading(dataRequestId, requestToken, requestMeta); - const { data: sourceFeatureCollection, meta } = await source.getGeoJsonWithMeta( - layerName, - requestMeta, - registerCancelCallback.bind(null, requestToken), - () => { - return isRequestStillActive(dataRequestId, requestToken); - } - ); - const layerFeatureCollection = assignFeatureIds(sourceFeatureCollection); - const supportedShapes = await source.getSupportedShapeTypes(); - if ( - supportedShapes.includes(VECTOR_SHAPE_TYPE.LINE) || - supportedShapes.includes(VECTOR_SHAPE_TYPE.POLYGON) - ) { - layerFeatureCollection.features.push(...getCentroidFeatures(layerFeatureCollection)); - } - const responseMeta: DataRequestMeta = meta ? { ...meta } : {}; - if (requestMeta.applyGlobalTime && (await source.isTimeAware())) { - const timesliceMaskField = await source.getTimesliceMaskFieldName(); - if (timesliceMaskField) { - responseMeta.timesliceMaskField = timesliceMaskField; - } - } - stopLoading(dataRequestId, requestToken, layerFeatureCollection, responseMeta); - return { - refreshed: true, - featureCollection: layerFeatureCollection, - }; - } catch (error) { - if (!(error instanceof DataRequestAbortError)) { - onLoadError(dataRequestId, requestToken, error.message); - } - throw error; - } -} - -export async function getVectorSourceBounds({ - layerId, - syncContext, - source, - sourceQuery, -}: { - layerId: string; - syncContext: DataRequestContext; - source: IVectorSource; - sourceQuery: Query | null; -}): Promise { - const { startLoading, stopLoading, registerCancelCallback, dataFilters } = syncContext; - - const requestToken = Symbol(`${SOURCE_BOUNDS_DATA_REQUEST_ID}-${layerId}`); - - // Do not pass all searchFilters to source.getBoundsForFilters(). - // For example, do not want to filter bounds request by extent and buffer. - const boundsFilters = { - sourceQuery: sourceQuery ? sourceQuery : undefined, - query: dataFilters.query, - timeFilters: dataFilters.timeFilters, - timeslice: dataFilters.timeslice, - filters: dataFilters.filters, - applyGlobalQuery: source.getApplyGlobalQuery(), - applyGlobalTime: source.getApplyGlobalTime(), - }; - - let bounds = null; - try { - startLoading(SOURCE_BOUNDS_DATA_REQUEST_ID, requestToken, boundsFilters); - bounds = await source.getBoundsForFilters( - boundsFilters, - registerCancelCallback.bind(null, requestToken) - ); - } finally { - // Use stopLoading callback instead of onLoadError callback. - // Function is loading bounds and not feature data. - stopLoading(SOURCE_BOUNDS_DATA_REQUEST_ID, requestToken, bounds ? bounds : {}); - } - return bounds; -} diff --git a/x-pack/plugins/maps/public/classes/layers/vector_layer/vector_layer.test.tsx b/x-pack/plugins/maps/public/classes/layers/vector_layer/vector_layer.test.tsx index 618be0b21cd73..bd2c8a036bf59 100644 --- a/x-pack/plugins/maps/public/classes/layers/vector_layer/vector_layer.test.tsx +++ b/x-pack/plugins/maps/public/classes/layers/vector_layer/vector_layer.test.tsx @@ -27,7 +27,7 @@ import { import { ESTermSourceDescriptor, VectorStyleDescriptor } from '../../../../common/descriptor_types'; import { getDefaultDynamicProperties } from '../../styles/vector/vector_style_defaults'; import { IVectorSource } from '../../sources/vector_source'; -import { VectorLayer } from './vector_layer'; +import { AbstractVectorLayer } from './vector_layer'; class MockSource { cloneDescriptor() { @@ -64,7 +64,7 @@ describe('cloneDescriptor', () => { }; test('Should update data driven styling properties using join fields', async () => { - const layerDescriptor = VectorLayer.createDescriptor({ + const layerDescriptor = AbstractVectorLayer.createDescriptor({ style: styleDescriptor, joins: [ { @@ -83,7 +83,7 @@ describe('cloneDescriptor', () => { }, ], }); - const layer = new VectorLayer({ + const layer = new AbstractVectorLayer({ layerDescriptor, source: new MockSource() as unknown as IVectorSource, }); @@ -105,7 +105,7 @@ describe('cloneDescriptor', () => { }); test('Should update data driven styling properties using join fields when metrics are not provided', async () => { - const layerDescriptor = VectorLayer.createDescriptor({ + const layerDescriptor = AbstractVectorLayer.createDescriptor({ style: styleDescriptor, joins: [ { @@ -120,7 +120,7 @@ describe('cloneDescriptor', () => { }, ], }); - const layer = new VectorLayer({ + const layer = new AbstractVectorLayer({ layerDescriptor, source: new MockSource() as unknown as IVectorSource, }); diff --git a/x-pack/plugins/maps/public/classes/layers/vector_layer/vector_layer.tsx b/x-pack/plugins/maps/public/classes/layers/vector_layer/vector_layer.tsx index 434743ef7ac9e..59078c076433e 100644 --- a/x-pack/plugins/maps/public/classes/layers/vector_layer/vector_layer.tsx +++ b/x-pack/plugins/maps/public/classes/layers/vector_layer/vector_layer.tsx @@ -7,13 +7,9 @@ import React from 'react'; import uuid from 'uuid/v4'; -import type { - Map as MbMap, - AnyLayer as MbLayer, - GeoJSONSource as MbGeoJSONSource, -} from '@kbn/mapbox-gl'; +import type { Map as MbMap, AnyLayer as MbLayer } from '@kbn/mapbox-gl'; import type { Query } from 'src/plugins/data/common'; -import { Feature, FeatureCollection, GeoJsonProperties, Geometry, Position } from 'geojson'; +import { Feature, GeoJsonProperties, Geometry, Position } from 'geojson'; import _ from 'lodash'; import { EuiIcon } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; @@ -23,24 +19,16 @@ import { AGG_TYPE, SOURCE_META_DATA_REQUEST_ID, SOURCE_FORMATTERS_DATA_REQUEST_ID, - FEATURE_VISIBLE_PROPERTY_NAME, - EMPTY_FEATURE_COLLECTION, LAYER_TYPE, FIELD_ORIGIN, FieldFormatter, SOURCE_TYPES, STYLE_TYPE, - SUPPORTS_FEATURE_EDITING_REQUEST_ID, VECTOR_STYLES, } from '../../../../common/constants'; import { JoinTooltipProperty } from '../../tooltips/join_tooltip_property'; import { DataRequestAbortError } from '../../util/data_request'; -import { - canSkipSourceUpdate, - canSkipStyleMetaUpdate, - canSkipFormattersUpdate, -} from '../../util/can_skip_fetch'; -import { getFeatureCollectionBounds } from '../../util/get_feature_collection_bounds'; +import { canSkipStyleMetaUpdate, canSkipFormattersUpdate } from '../../util/can_skip_fetch'; import { getLabelFilterExpression, getFillFilterExpression, @@ -55,13 +43,10 @@ import { ESTermSourceDescriptor, JoinDescriptor, StyleMetaDescriptor, - Timeslice, VectorLayerDescriptor, VectorSourceRequestMeta, VectorStyleRequestMeta, - VectorJoinSourceRequestMeta, } from '../../../../common/descriptor_types'; -import { ISource } from '../../sources/source'; import { IVectorSource } from '../../sources/vector_source'; import { CustomIconAndTooltipContent, ILayer } from '../layer'; import { InnerJoin } from '../../joins/inner_join'; @@ -70,13 +55,10 @@ import { DataRequestContext } from '../../../actions'; import { ITooltipProperty } from '../../tooltips/tooltip_property'; import { IDynamicStyleProperty } from '../../styles/vector/properties/dynamic_style_property'; import { IESSource } from '../../sources/es_source'; -import { PropertiesMap } from '../../../../common/elasticsearch_util'; import { ITermJoinSource } from '../../sources/term_join_source'; -import { addGeoJsonMbSource, getVectorSourceBounds, syncVectorSource } from './utils'; -import { JoinState, performInnerJoins } from './perform_inner_joins'; import { buildVectorRequestMeta } from '../build_vector_request_meta'; import { getJoinAggKey } from '../../../../common/get_agg_key'; -import { GEOJSON_FEATURE_ID_PROPERTY_NAME } from './assign_feature_ids'; +import { getVectorSourceBounds } from './geojson_vector_layer/utils'; export function isVectorLayer(layer: ILayer) { return (layer as IVectorLayer).canShowTooltip !== undefined; @@ -114,7 +96,7 @@ export interface IVectorLayer extends ILayer { deleteFeature(featureId: string): Promise; } -const noResultsIcon = ; +export const noResultsIcon = ; export const NO_RESULTS_ICON_AND_TOOLTIPCONTENT = { icon: noResultsIcon, tooltipContent: i18n.translate('xpack.maps.vectorLayer.noResultsFoundTooltip', { @@ -122,12 +104,7 @@ export const NO_RESULTS_ICON_AND_TOOLTIPCONTENT = { }), }; -/* - * Geojson vector layer - */ -export class VectorLayer extends AbstractLayer implements IVectorLayer { - static type = LAYER_TYPE.VECTOR; - +export class AbstractVectorLayer extends AbstractLayer implements IVectorLayer { protected readonly _style: VectorStyle; private readonly _joins: InnerJoin[]; @@ -265,9 +242,7 @@ export class VectorLayer extends AbstractLayer implements IVectorLayer { } supportsFeatureEditing(): boolean { - const dataRequest = this.getDataRequest(SUPPORTS_FEATURE_EDITING_REQUEST_ID); - const data = dataRequest?.getData() as { supportsFeatureEditing: boolean } | undefined; - return data ? data.supportsFeatureEditing : false; + return false; } hasJoins() { @@ -296,38 +271,7 @@ export class VectorLayer extends AbstractLayer implements IVectorLayer { } getCustomIconAndTooltipContent(): CustomIconAndTooltipContent { - const featureCollection = this._getSourceFeatureCollection(); - - if (!featureCollection || featureCollection.features.length === 0) { - return NO_RESULTS_ICON_AND_TOOLTIPCONTENT; - } - - if ( - this.getJoins().length && - !featureCollection.features.some( - (feature) => feature.properties?.[FEATURE_VISIBLE_PROPERTY_NAME] - ) - ) { - return { - icon: noResultsIcon, - tooltipContent: i18n.translate('xpack.maps.vectorLayer.noResultsFoundInJoinTooltip', { - defaultMessage: `No matching results found in term joins`, - }), - }; - } - - const sourceDataRequest = this.getSourceDataRequest(); - const { tooltipContent, areResultsTrimmed, isDeprecated } = - this.getSource().getSourceTooltipContent(sourceDataRequest); - return { - icon: isDeprecated ? ( - - ) : ( - this.getCurrentStyle().getIcon() - ), - tooltipContent, - areResultsTrimmed, - }; + throw new Error('Should implement AbstractVectorLayer#getCustomIconAndTooltipContent'); } getLayerTypeIconName() { @@ -343,15 +287,12 @@ export class VectorLayer extends AbstractLayer implements IVectorLayer { } async getBounds(syncContext: DataRequestContext) { - const isStaticLayer = !this.getSource().isBoundsAware(); - return isStaticLayer || this.hasJoins() - ? getFeatureCollectionBounds(this._getSourceFeatureCollection(), this.hasJoins()) - : getVectorSourceBounds({ - layerId: this.getId(), - syncContext, - source: this.getSource(), - sourceQuery: this.getQuery(), - }); + return getVectorSourceBounds({ + layerId: this.getId(), + syncContext, + source: this.getSource(), + sourceQuery: this.getQuery(), + }); } async getLeftJoinFields() { @@ -409,79 +350,6 @@ export class VectorLayer extends AbstractLayer implements IVectorLayer { }); } - async _syncJoin({ - join, - startLoading, - stopLoading, - onLoadError, - registerCancelCallback, - dataFilters, - isForceRefresh, - }: { join: InnerJoin } & DataRequestContext): Promise { - const joinSource = join.getRightJoinSource(); - const sourceDataId = join.getSourceDataRequestId(); - const requestToken = Symbol(`layer-join-refresh:${this.getId()} - ${sourceDataId}`); - - const joinRequestMeta: VectorJoinSourceRequestMeta = buildVectorRequestMeta( - joinSource, - joinSource.getFieldNames(), - dataFilters, - joinSource.getWhereQuery(), - isForceRefresh - ) as VectorJoinSourceRequestMeta; - - const prevDataRequest = this.getDataRequest(sourceDataId); - const canSkipFetch = await canSkipSourceUpdate({ - source: joinSource, - prevDataRequest, - nextRequestMeta: joinRequestMeta, - extentAware: false, // join-sources are term-aggs that are spatially unaware (e.g. ESTermSource/TableSource). - getUpdateDueToTimeslice: () => { - return true; - }, - }); - - if (canSkipFetch) { - return { - dataHasChanged: false, - join, - propertiesMap: prevDataRequest?.getData() as PropertiesMap, - }; - } - - try { - startLoading(sourceDataId, requestToken, joinRequestMeta); - const leftSourceName = await this._source.getDisplayName(); - const propertiesMap = await joinSource.getPropertiesMap( - joinRequestMeta, - leftSourceName, - join.getLeftField().getName(), - registerCancelCallback.bind(null, requestToken) - ); - stopLoading(sourceDataId, requestToken, propertiesMap); - return { - dataHasChanged: true, - join, - propertiesMap, - }; - } catch (error) { - if (!(error instanceof DataRequestAbortError)) { - onLoadError(sourceDataId, requestToken, `Join error: ${error.message}`); - } - throw error; - } - } - - async _syncJoins(syncContext: DataRequestContext, style: IVectorStyle) { - const joinSyncs = this.getValidJoins().map(async (join) => { - await this._syncJoinStyleMeta(syncContext, join, style); - await this._syncJoinFormatters(syncContext, join, style); - return this._syncJoin({ join, ...syncContext }); - }); - - return await Promise.all(joinSyncs); - } - async _getVectorSourceRequestMeta( isForceRefresh: boolean, dataFilters: DataFilters, @@ -522,27 +390,6 @@ export class VectorLayer extends AbstractLayer implements IVectorLayer { }); } - async _syncJoinStyleMeta(syncContext: DataRequestContext, join: InnerJoin, style: IVectorStyle) { - const joinSource = join.getRightJoinSource(); - return this._syncStyleMeta({ - source: joinSource, - style, - sourceQuery: joinSource.getWhereQuery(), - dataRequestId: join.getSourceMetaDataRequestId(), - dynamicStyleProps: this.getCurrentStyle() - .getDynamicPropertiesArray() - .filter((dynamicStyleProp) => { - const matchingField = joinSource.getFieldByName(dynamicStyleProp.getFieldName()); - return ( - dynamicStyleProp.getFieldOrigin() === FIELD_ORIGIN.JOIN && - !!matchingField && - dynamicStyleProp.isFieldMetaEnabled() - ); - }), - ...syncContext, - }); - } - async _syncStyleMeta({ source, style, @@ -626,24 +473,6 @@ export class VectorLayer extends AbstractLayer implements IVectorLayer { }); } - async _syncJoinFormatters(syncContext: DataRequestContext, join: InnerJoin, style: IVectorStyle) { - const joinSource = join.getRightJoinSource(); - return this._syncFormatters({ - source: joinSource, - dataRequestId: join.getSourceFormattersDataRequestId(), - fields: style - .getDynamicPropertiesArray() - .filter((dynamicStyleProp) => { - const matchingField = joinSource.getFieldByName(dynamicStyleProp.getFieldName()); - return dynamicStyleProp.getFieldOrigin() === FIELD_ORIGIN.JOIN && !!matchingField; - }) - .map((dynamicStyleProp) => { - return dynamicStyleProp.getField()!; - }), - ...syncContext, - }); - } - async _syncFormatters({ source, dataRequestId, @@ -693,128 +522,6 @@ export class VectorLayer extends AbstractLayer implements IVectorLayer { } } - async syncData(syncContext: DataRequestContext) { - await this._syncData(syncContext, this.getSource(), this.getCurrentStyle()); - } - - // TLDR: Do not call getSource or getCurrentStyle in syncData flow. Use 'source' and 'style' arguments instead. - // - // 1) State is contained in the redux store. Layer instance state is readonly. - // 2) Even though data request descriptor updates trigger new instances for rendering, - // syncing data executes on a single object instance. Syncing data can not use updated redux store state. - // - // Blended layer data syncing branches on the source/style depending on whether clustering is used or not. - // Given 1 above, which source/style to use can not be stored in Layer instance state. - // Given 2 above, which source/style to use can not be pulled from data request state. - // Therefore, source and style are provided as arugments and must be used instead of calling getSource or getCurrentStyle. - async _syncData(syncContext: DataRequestContext, source: IVectorSource, style: IVectorStyle) { - if (this.isLoadingBounds()) { - return; - } - - try { - await this._syncSourceStyleMeta(syncContext, source, style); - await this._syncSourceFormatters(syncContext, source, style); - const sourceResult = await syncVectorSource({ - layerId: this.getId(), - layerName: await this.getDisplayName(source), - prevDataRequest: this.getSourceDataRequest(), - requestMeta: await this._getVectorSourceRequestMeta( - syncContext.isForceRefresh, - syncContext.dataFilters, - source, - style - ), - syncContext, - source, - getUpdateDueToTimeslice: (timeslice?: Timeslice) => { - return this._getUpdateDueToTimesliceFromSourceRequestMeta(source, timeslice); - }, - }); - await this._syncSupportsFeatureEditing({ syncContext, source }); - if ( - !sourceResult.featureCollection || - !sourceResult.featureCollection.features.length || - !this.hasJoins() - ) { - return; - } - - const joinStates = await this._syncJoins(syncContext, style); - performInnerJoins( - sourceResult, - joinStates, - syncContext.updateSourceData, - syncContext.onJoinError - ); - } catch (error) { - if (!(error instanceof DataRequestAbortError)) { - throw error; - } - } - } - - async _syncSupportsFeatureEditing({ - syncContext, - source, - }: { - syncContext: DataRequestContext; - source: IVectorSource; - }) { - if (syncContext.dataFilters.isReadOnly) { - return; - } - const { startLoading, stopLoading, onLoadError } = syncContext; - const dataRequestId = SUPPORTS_FEATURE_EDITING_REQUEST_ID; - const requestToken = Symbol(`layer-${this.getId()}-${dataRequestId}`); - const prevDataRequest = this.getDataRequest(dataRequestId); - if (prevDataRequest) { - return; - } - try { - startLoading(dataRequestId, requestToken); - const supportsFeatureEditing = await source.supportsFeatureEditing(); - stopLoading(dataRequestId, requestToken, { supportsFeatureEditing }); - } catch (error) { - onLoadError(dataRequestId, requestToken, error.message); - throw error; - } - } - - _getSourceFeatureCollection() { - if (this.getSource().isMvt()) { - return null; - } - const sourceDataRequest = this.getSourceDataRequest(); - return sourceDataRequest ? (sourceDataRequest.getData() as FeatureCollection) : null; - } - - _syncFeatureCollectionWithMb(mbMap: MbMap) { - const mbGeoJSONSource = mbMap.getSource(this.getId()) as MbGeoJSONSource; - const featureCollection = this._getSourceFeatureCollection(); - const featureCollectionOnMap = AbstractLayer.getBoundDataForSource(mbMap, this.getId()); - - if (!featureCollection) { - if (featureCollectionOnMap) { - this.getCurrentStyle().clearFeatureState(featureCollectionOnMap, mbMap, this.getId()); - } - mbGeoJSONSource.setData(EMPTY_FEATURE_COLLECTION); - return; - } - - // "feature-state" data expressions are not supported with layout properties. - // To work around this limitation, - // scaled layout properties (like icon-size) must fall back to geojson property values :( - const hasGeoJsonProperties = this.getCurrentStyle().setFeatureStateAndStyleProps( - featureCollection, - mbMap, - this.getId() - ); - if (featureCollection !== featureCollectionOnMap || hasGeoJsonProperties) { - mbGeoJSONSource.setData(featureCollection); - } - } - _setMbPointsProperties( mbMap: MbMap, mvtSourceLayer?: string, @@ -989,33 +696,6 @@ export class VectorLayer extends AbstractLayer implements IVectorLayer { mbMap.setLayerZoomRange(labelLayerId, this.getMinZoom(), this.getMaxZoom()); } - _syncStylePropertiesWithMb(mbMap: MbMap, timeslice?: Timeslice) { - const timesliceMaskConfig = this._getTimesliceMaskConfig(timeslice); - this._setMbLabelProperties(mbMap, undefined, timesliceMaskConfig); - this._setMbPointsProperties(mbMap, undefined, timesliceMaskConfig); - this._setMbLinePolygonProperties(mbMap, undefined, timesliceMaskConfig); - } - - _getTimesliceMaskConfig(timeslice?: Timeslice): TimesliceMaskConfig | undefined { - if (!timeslice || this.hasJoins()) { - return; - } - - const prevMeta = this.getSourceDataRequest()?.getMeta(); - return prevMeta !== undefined && prevMeta.timesliceMaskField !== undefined - ? { - timesliceMaskField: prevMeta.timesliceMaskField, - timeslice, - } - : undefined; - } - - syncLayerWithMB(mbMap: MbMap, timeslice?: Timeslice) { - addGeoJsonMbSource(this._getMbSourceId(), this.getMbLayerIds(), mbMap); - this._syncFeatureCollectionWithMb(mbMap); - this._syncStylePropertiesWithMb(mbMap, timeslice); - } - _getMbPointLayerId() { return this.makeMbLayerId('circle'); } @@ -1090,34 +770,17 @@ export class VectorLayer extends AbstractLayer implements IVectorLayer { } getFeatureId(feature: Feature): string | number | undefined { - return feature.properties?.[GEOJSON_FEATURE_ID_PROPERTY_NAME]; + throw new Error('Should implement AbstractVectorLayer#getFeatureId'); } - getFeatureById(id: string | number) { - const featureCollection = this._getSourceFeatureCollection(); - if (!featureCollection) { - return null; - } - - const targetFeature = featureCollection.features.find((feature) => { - return this.getFeatureId(feature) === id; - }); - return targetFeature ? targetFeature : null; + getFeatureById(id: string | number): Feature | null { + throw new Error('Should implement AbstractVectorLayer#getFeatureById'); } async getLicensedFeatures() { return await this._source.getLicensedFeatures(); } - _getUpdateDueToTimesliceFromSourceRequestMeta(source: ISource, timeslice?: Timeslice) { - const prevDataRequest = this.getSourceDataRequest(); - const prevMeta = prevDataRequest?.getMeta(); - if (!prevMeta) { - return true; - } - return source.getUpdateDueToTimeslice(prevMeta, timeslice); - } - async addFeature(geometry: Geometry | Position[]) { const layerSource = this.getSource(); const defaultFields = await layerSource.getDefaultFields(); @@ -1130,11 +793,6 @@ export class VectorLayer extends AbstractLayer implements IVectorLayer { } async getStyleMetaDescriptorFromLocalFeatures(): Promise { - const sourceDataRequest = this.getSourceDataRequest(); - const style = this.getCurrentStyle(); - if (!style || !sourceDataRequest) { - return null; - } - return await style.pluckStyleMetaFromSourceDataRequest(sourceDataRequest); + throw new Error('Should implement AbstractVectorLayer#getStyleMetaDescriptorFromLocalFeatures'); } } diff --git a/x-pack/plugins/maps/public/classes/layers/vector_tile_layer/vector_tile_layer.d.ts b/x-pack/plugins/maps/public/classes/layers/vector_tile_layer/vector_tile_layer.d.ts deleted file mode 100644 index a1f49f0e1d0b3..0000000000000 --- a/x-pack/plugins/maps/public/classes/layers/vector_tile_layer/vector_tile_layer.d.ts +++ /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; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { ITileLayerArguments, TileLayer } from '../tile_layer/tile_layer'; - -export class VectorTileLayer extends TileLayer { - static type: string; - constructor(args: ITileLayerArguments); -} diff --git a/x-pack/plugins/maps/public/classes/layers/vector_tile_layer/vector_tile_layer.js b/x-pack/plugins/maps/public/classes/layers/vector_tile_layer/vector_tile_layer.js deleted file mode 100644 index 5096e5e29bf23..0000000000000 --- a/x-pack/plugins/maps/public/classes/layers/vector_tile_layer/vector_tile_layer.js +++ /dev/null @@ -1,293 +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 { TileLayer } from '../tile_layer/tile_layer'; -import _ from 'lodash'; -import { SOURCE_DATA_REQUEST_ID, LAYER_TYPE, LAYER_STYLE_TYPE } from '../../../../common/constants'; -import { isRetina } from '../../../util'; -import { - addSpriteSheetToMapFromImageData, - loadSpriteSheetImageData, -} from '../../../connected_components/mb_map/utils'; - -const MB_STYLE_TYPE_TO_OPACITY = { - fill: ['fill-opacity'], - line: ['line-opacity'], - circle: ['circle-opacity'], - background: ['background-opacity'], - symbol: ['icon-opacity', 'text-opacity'], -}; - -export class VectorTileLayer extends TileLayer { - static type = LAYER_TYPE.VECTOR_TILE; - - static createDescriptor(options) { - const tileLayerDescriptor = super.createDescriptor(options); - tileLayerDescriptor.type = VectorTileLayer.type; - tileLayerDescriptor.alpha = _.get(options, 'alpha', 1); - tileLayerDescriptor.style = { type: LAYER_STYLE_TYPE.TILE }; - return tileLayerDescriptor; - } - - _canSkipSync({ prevDataRequest, nextMeta }) { - if (!prevDataRequest) { - return false; - } - const prevMeta = prevDataRequest.getMeta(); - if (!prevMeta) { - return false; - } - - return prevMeta.tileLayerId === nextMeta.tileLayerId; - } - - async syncData({ startLoading, stopLoading, onLoadError }) { - const nextMeta = { tileLayerId: this.getSource().getTileLayerId() }; - const canSkipSync = this._canSkipSync({ - prevDataRequest: this.getSourceDataRequest(), - nextMeta, - }); - if (canSkipSync) { - return; - } - - const requestToken = Symbol(`layer-source-refresh:${this.getId()} - source`); - try { - startLoading(SOURCE_DATA_REQUEST_ID, requestToken, nextMeta); - const styleAndSprites = await this.getSource().getVectorStyleSheetAndSpriteMeta(isRetina()); - const spriteSheetImageData = await loadSpriteSheetImageData(styleAndSprites.spriteMeta.png); - const data = { - ...styleAndSprites, - spriteSheetImageData, - }; - stopLoading(SOURCE_DATA_REQUEST_ID, requestToken, data); - } catch (error) { - onLoadError(SOURCE_DATA_REQUEST_ID, requestToken, error.message); - } - } - - _generateMbId(name) { - return `${this.getId()}_${name}`; - } - - _generateMbSourceIdPrefix() { - const DELIMITTER = '___'; - return `${this.getId()}${DELIMITTER}${this.getSource().getTileLayerId()}${DELIMITTER}`; - } - - _generateMbSourceId(name) { - return `${this._generateMbSourceIdPrefix()}${name}`; - } - - _getVectorStyle() { - const sourceDataRequest = this.getSourceDataRequest(); - if (!sourceDataRequest) { - return null; - } - const vectorStyleAndSprites = sourceDataRequest.getData(); - if (!vectorStyleAndSprites) { - return null; - } - return vectorStyleAndSprites.vectorStyleSheet; - } - - _getSpriteMeta() { - const sourceDataRequest = this.getSourceDataRequest(); - if (!sourceDataRequest) { - return null; - } - const vectorStyleAndSprites = sourceDataRequest.getData(); - return vectorStyleAndSprites.spriteMeta; - } - - _getSpriteImageData() { - const sourceDataRequest = this.getSourceDataRequest(); - if (!sourceDataRequest) { - return null; - } - const vectorStyleAndSprites = sourceDataRequest.getData(); - return vectorStyleAndSprites.spriteSheetImageData; - } - - getMbLayerIds() { - const vectorStyle = this._getVectorStyle(); - if (!vectorStyle) { - return []; - } - return vectorStyle.layers.map((layer) => this._generateMbId(layer.id)); - } - - getMbSourceIds() { - const vectorStyle = this._getVectorStyle(); - if (!vectorStyle) { - return []; - } - const sourceIds = Object.keys(vectorStyle.sources); - return sourceIds.map((sourceId) => this._generateMbSourceId(sourceId)); - } - - ownsMbLayerId(mbLayerId) { - return mbLayerId.startsWith(this.getId()); - } - - ownsMbSourceId(mbSourceId) { - return mbSourceId.startsWith(this.getId()); - } - - _makeNamespacedImageId(imageId) { - const prefix = this.getSource().getSpriteNamespacePrefix() + '/'; - return prefix + imageId; - } - - _requiresPrevSourceCleanup(mbMap) { - const sourceIdPrefix = this._generateMbSourceIdPrefix(); - const mbStyle = mbMap.getStyle(); - return Object.keys(mbStyle.sources).some((mbSourceId) => { - const doesMbSourceBelongToLayer = this.ownsMbSourceId(mbSourceId); - const doesMbSourceBelongToSource = mbSourceId.startsWith(sourceIdPrefix); - return doesMbSourceBelongToLayer && !doesMbSourceBelongToSource; - }); - } - - syncLayerWithMB(mbMap) { - const vectorStyle = this._getVectorStyle(); - if (!vectorStyle) { - return; - } - - this._removeStaleMbSourcesAndLayers(mbMap); - - let initialBootstrapCompleted = false; - const sourceIds = Object.keys(vectorStyle.sources); - sourceIds.forEach((sourceId) => { - if (initialBootstrapCompleted) { - return; - } - const mbSourceId = this._generateMbSourceId(sourceId); - const mbSource = mbMap.getSource(mbSourceId); - if (mbSource) { - //if a single source is present, the layer already has bootstrapped with the mbMap - initialBootstrapCompleted = true; - return; - } - mbMap.addSource(mbSourceId, vectorStyle.sources[sourceId]); - }); - - if (!initialBootstrapCompleted) { - //sync spritesheet - const spriteMeta = this._getSpriteMeta(); - if (!spriteMeta) { - return; - } - const newJson = {}; - for (const imageId in spriteMeta.json) { - if (spriteMeta.json.hasOwnProperty(imageId)) { - const namespacedImageId = this._makeNamespacedImageId(imageId); - newJson[namespacedImageId] = spriteMeta.json[imageId]; - } - } - - const imageData = this._getSpriteImageData(); - if (!imageData) { - return; - } - addSpriteSheetToMapFromImageData(newJson, imageData, mbMap); - - //sync layers - vectorStyle.layers.forEach((layer) => { - const mbLayerId = this._generateMbId(layer.id); - const mbLayer = mbMap.getLayer(mbLayerId); - if (mbLayer) { - return; - } - const newLayerObject = { - ...layer, - source: this._generateMbSourceId(layer.source), - id: mbLayerId, - }; - - if ( - newLayerObject.type === 'symbol' && - newLayerObject.layout && - typeof newLayerObject.layout['icon-image'] === 'string' - ) { - newLayerObject.layout['icon-image'] = this._makeNamespacedImageId( - newLayerObject.layout['icon-image'] - ); - } - - if ( - newLayerObject.type === 'fill' && - newLayerObject.paint && - typeof newLayerObject.paint['fill-pattern'] === 'string' - ) { - newLayerObject.paint['fill-pattern'] = this._makeNamespacedImageId( - newLayerObject.paint['fill-pattern'] - ); - } - - mbMap.addLayer(newLayerObject); - }); - } - - this._setTileLayerProperties(mbMap); - } - - _setOpacityForType(mbMap, mbLayer, mbLayerId) { - const opacityProps = MB_STYLE_TYPE_TO_OPACITY[mbLayer.type]; - if (!opacityProps) { - return; - } - - opacityProps.forEach((opacityProp) => { - if (mbLayer.paint && typeof mbLayer.paint[opacityProp] === 'number') { - const newOpacity = mbLayer.paint[opacityProp] * this.getAlpha(); - mbMap.setPaintProperty(mbLayerId, opacityProp, newOpacity); - } else { - mbMap.setPaintProperty(mbLayerId, opacityProp, this.getAlpha()); - } - }); - } - - _setLayerZoomRange(mbMap, mbLayer, mbLayerId) { - let minZoom = this._descriptor.minZoom; - if (typeof mbLayer.minzoom === 'number') { - minZoom = Math.max(minZoom, mbLayer.minzoom); - } - let maxZoom = this._descriptor.maxZoom; - if (typeof mbLayer.maxzoom === 'number') { - maxZoom = Math.min(maxZoom, mbLayer.maxzoom); - } - mbMap.setLayerZoomRange(mbLayerId, minZoom, maxZoom); - } - - _setTileLayerProperties(mbMap) { - const vectorStyle = this._getVectorStyle(); - if (!vectorStyle) { - return; - } - - vectorStyle.layers.forEach((mbLayer) => { - const mbLayerId = this._generateMbId(mbLayer.id); - this.syncVisibilityWithMb(mbMap, mbLayerId); - this._setLayerZoomRange(mbMap, mbLayer, mbLayerId); - this._setOpacityForType(mbMap, mbLayer, mbLayerId); - }); - } - - areLabelsOnTop() { - return !!this._descriptor.areLabelsOnTop; - } - - supportsLabelsOnTop() { - return true; - } - - async getLicensedFeatures() { - return this._source.getLicensedFeatures(); - } -} diff --git a/x-pack/plugins/maps/public/classes/layers/vector_tile_layer/vector_tile_layer.tsx b/x-pack/plugins/maps/public/classes/layers/vector_tile_layer/vector_tile_layer.tsx new file mode 100644 index 0000000000000..2b36ecc160954 --- /dev/null +++ b/x-pack/plugins/maps/public/classes/layers/vector_tile_layer/vector_tile_layer.tsx @@ -0,0 +1,351 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license 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 { Map as MbMap, Layer as MbLayer, Style as MbStyle } from '@kbn/mapbox-gl'; +import _ from 'lodash'; +import { TileLayer } from '../tile_layer/tile_layer'; +import { SOURCE_DATA_REQUEST_ID, LAYER_TYPE, LAYER_STYLE_TYPE } from '../../../../common/constants'; +import { LayerDescriptor } from '../../../../common/descriptor_types'; +import { DataRequest } from '../../util/data_request'; +import { isRetina } from '../../../util'; +import { + addSpriteSheetToMapFromImageData, + loadSpriteSheetImageData, + // @ts-expect-error +} from '../../../connected_components/mb_map/utils'; +import { DataRequestContext } from '../../../actions'; +import { EMSTMSSource } from '../../sources/ems_tms_source'; + +interface SourceRequestMeta { + tileLayerId: string; +} + +// TODO remove once ems_client exports EmsSpriteSheet and EmsSprite type +interface EmsSprite { + height: number; + pixelRatio: number; + width: number; + x: number; + y: number; +} + +interface EmsSpriteSheet { + [spriteName: string]: EmsSprite; +} + +interface SourceRequestData { + spriteSheetImageData?: ImageData; + vectorStyleSheet?: MbStyle; + spriteMeta?: { + png: string; + json: EmsSpriteSheet; + }; +} + +// TODO - rename to EmsVectorTileLayer +export class VectorTileLayer extends TileLayer { + static type = LAYER_TYPE.VECTOR_TILE; + + static createDescriptor(options: Partial) { + const tileLayerDescriptor = super.createDescriptor(options); + tileLayerDescriptor.type = VectorTileLayer.type; + tileLayerDescriptor.alpha = _.get(options, 'alpha', 1); + tileLayerDescriptor.style = { type: LAYER_STYLE_TYPE.TILE }; + return tileLayerDescriptor; + } + + getSource(): EMSTMSSource { + return super.getSource() as EMSTMSSource; + } + + _canSkipSync({ + prevDataRequest, + nextMeta, + }: { + prevDataRequest?: DataRequest; + nextMeta: SourceRequestMeta; + }) { + if (!prevDataRequest) { + return false; + } + const prevMeta = prevDataRequest.getMeta() as SourceRequestMeta; + if (!prevMeta) { + return false; + } + + return prevMeta.tileLayerId === nextMeta.tileLayerId; + } + + async syncData({ startLoading, stopLoading, onLoadError }: DataRequestContext) { + const nextMeta = { tileLayerId: this.getSource().getTileLayerId() }; + const canSkipSync = this._canSkipSync({ + prevDataRequest: this.getSourceDataRequest(), + nextMeta, + }); + if (canSkipSync) { + return; + } + + const requestToken = Symbol(`layer-source-refresh:${this.getId()} - source`); + try { + startLoading(SOURCE_DATA_REQUEST_ID, requestToken, nextMeta); + const styleAndSprites = await this.getSource().getVectorStyleSheetAndSpriteMeta(isRetina()); + const spriteSheetImageData = styleAndSprites.spriteMeta + ? await loadSpriteSheetImageData(styleAndSprites.spriteMeta.png) + : undefined; + const data = { + ...styleAndSprites, + spriteSheetImageData, + }; + stopLoading(SOURCE_DATA_REQUEST_ID, requestToken, data); + } catch (error) { + onLoadError(SOURCE_DATA_REQUEST_ID, requestToken, error.message); + } + } + + _generateMbId(name: string) { + return `${this.getId()}_${name}`; + } + + _generateMbSourceIdPrefix() { + const DELIMITTER = '___'; + return `${this.getId()}${DELIMITTER}${this.getSource().getTileLayerId()}${DELIMITTER}`; + } + + _generateMbSourceId(name: string) { + return `${this._generateMbSourceIdPrefix()}${name}`; + } + + _getVectorStyle() { + const sourceDataRequest = this.getSourceDataRequest(); + if (!sourceDataRequest) { + return null; + } + return (sourceDataRequest.getData() as SourceRequestData)?.vectorStyleSheet; + } + + _getSpriteMeta() { + const sourceDataRequest = this.getSourceDataRequest(); + if (!sourceDataRequest) { + return null; + } + return (sourceDataRequest.getData() as SourceRequestData)?.spriteMeta; + } + + _getSpriteImageData() { + const sourceDataRequest = this.getSourceDataRequest(); + if (!sourceDataRequest) { + return null; + } + return (sourceDataRequest.getData() as SourceRequestData)?.spriteSheetImageData; + } + + getMbLayerIds() { + const vectorStyle = this._getVectorStyle(); + if (!vectorStyle || !vectorStyle.layers) { + return []; + } + return vectorStyle.layers.map((layer) => this._generateMbId(layer.id)); + } + + getMbSourceIds() { + const vectorStyle = this._getVectorStyle(); + if (!vectorStyle || !vectorStyle.sources) { + return []; + } + const sourceIds = Object.keys(vectorStyle.sources); + return sourceIds.map((sourceId) => this._generateMbSourceId(sourceId)); + } + + ownsMbLayerId(mbLayerId: string) { + return mbLayerId.startsWith(this.getId()); + } + + ownsMbSourceId(mbSourceId: string) { + return mbSourceId.startsWith(this.getId()); + } + + _makeNamespacedImageId(imageId: string) { + const prefix = this.getSource().getSpriteNamespacePrefix() + '/'; + return prefix + imageId; + } + + _requiresPrevSourceCleanup(mbMap: MbMap) { + const sourceIdPrefix = this._generateMbSourceIdPrefix(); + const mbStyle = mbMap.getStyle(); + if (!mbStyle.sources) { + return false; + } + return Object.keys(mbStyle.sources).some((mbSourceId) => { + const doesMbSourceBelongToLayer = this.ownsMbSourceId(mbSourceId); + const doesMbSourceBelongToSource = mbSourceId.startsWith(sourceIdPrefix); + return doesMbSourceBelongToLayer && !doesMbSourceBelongToSource; + }); + } + + syncLayerWithMB(mbMap: MbMap) { + const vectorStyle = this._getVectorStyle(); + if (!vectorStyle) { + return; + } + + this._removeStaleMbSourcesAndLayers(mbMap); + + let initialBootstrapCompleted = false; + const sourceIds = vectorStyle.sources ? Object.keys(vectorStyle.sources) : []; + sourceIds.forEach((sourceId) => { + if (initialBootstrapCompleted) { + return; + } + const mbSourceId = this._generateMbSourceId(sourceId); + const mbSource = mbMap.getSource(mbSourceId); + if (mbSource) { + // if a single source is present, the layer already has bootstrapped with the mbMap + initialBootstrapCompleted = true; + return; + } + mbMap.addSource(mbSourceId, vectorStyle.sources![sourceId]); + }); + + if (!initialBootstrapCompleted) { + // sync spritesheet + const spriteMeta = this._getSpriteMeta(); + if (!spriteMeta) { + return; + } + const newJson: EmsSpriteSheet = {}; + for (const imageId in spriteMeta.json) { + if (spriteMeta.json.hasOwnProperty(imageId)) { + const namespacedImageId = this._makeNamespacedImageId(imageId); + newJson[namespacedImageId] = spriteMeta.json[imageId]; + } + } + + const imageData = this._getSpriteImageData(); + if (!imageData) { + return; + } + addSpriteSheetToMapFromImageData(newJson, imageData, mbMap); + + // sync layers + const layers = vectorStyle.layers ? vectorStyle.layers : []; + layers.forEach((layer) => { + const mbLayerId = this._generateMbId(layer.id); + const mbLayer = mbMap.getLayer(mbLayerId); + if (mbLayer) { + return; + } + const newLayerObject = { + ...layer, + source: + typeof (layer as MbLayer).source === 'string' + ? this._generateMbSourceId((layer as MbLayer).source as string) + : undefined, + id: mbLayerId, + }; + + if ( + newLayerObject.type === 'symbol' && + newLayerObject.layout && + typeof newLayerObject.layout['icon-image'] === 'string' + ) { + newLayerObject.layout['icon-image'] = this._makeNamespacedImageId( + newLayerObject.layout['icon-image'] + ); + } + + if ( + newLayerObject.type === 'fill' && + newLayerObject.paint && + typeof newLayerObject.paint['fill-pattern'] === 'string' + ) { + newLayerObject.paint['fill-pattern'] = this._makeNamespacedImageId( + newLayerObject.paint['fill-pattern'] + ); + } + + mbMap.addLayer(newLayerObject); + }); + } + + this._setTileLayerProperties(mbMap); + } + + _getOpacityProps(layerType: string): string[] { + if (layerType === 'fill') { + return ['fill-opacity']; + } + + if (layerType === 'line') { + return ['line-opacity']; + } + + if (layerType === 'circle') { + return ['circle-opacity']; + } + + if (layerType === 'background') { + return ['background-opacity']; + } + + if (layerType === 'symbol') { + return ['icon-opacity', 'text-opacity']; + } + + return []; + } + + _setOpacityForType(mbMap: MbMap, mbLayer: MbLayer, mbLayerId: string) { + this._getOpacityProps(mbLayer.type).forEach((opacityProp) => { + const mbPaint = mbLayer.paint as { [key: string]: unknown } | undefined; + if (mbPaint && typeof mbPaint[opacityProp] === 'number') { + const newOpacity = (mbPaint[opacityProp] as number) * this.getAlpha(); + mbMap.setPaintProperty(mbLayerId, opacityProp, newOpacity); + } else { + mbMap.setPaintProperty(mbLayerId, opacityProp, this.getAlpha()); + } + }); + } + + _setLayerZoomRange(mbMap: MbMap, mbLayer: MbLayer, mbLayerId: string) { + let minZoom = this.getMinZoom(); + if (typeof mbLayer.minzoom === 'number') { + minZoom = Math.max(minZoom, mbLayer.minzoom); + } + let maxZoom = this.getMaxZoom(); + if (typeof mbLayer.maxzoom === 'number') { + maxZoom = Math.min(maxZoom, mbLayer.maxzoom); + } + mbMap.setLayerZoomRange(mbLayerId, minZoom, maxZoom); + } + + _setTileLayerProperties(mbMap: MbMap) { + const vectorStyle = this._getVectorStyle(); + if (!vectorStyle || !vectorStyle.layers) { + return; + } + + vectorStyle.layers.forEach((mbLayer) => { + const mbLayerId = this._generateMbId(mbLayer.id); + this.syncVisibilityWithMb(mbMap, mbLayerId); + this._setLayerZoomRange(mbMap, mbLayer, mbLayerId); + this._setOpacityForType(mbMap, mbLayer, mbLayerId); + }); + } + + areLabelsOnTop() { + return !!this._descriptor.areLabelsOnTop; + } + + supportsLabelsOnTop() { + return true; + } + + async getLicensedFeatures() { + return this._source.getLicensedFeatures(); + } +} diff --git a/x-pack/plugins/maps/public/classes/sources/ems_file_source/ems_boundaries_layer_wizard.tsx b/x-pack/plugins/maps/public/classes/sources/ems_file_source/ems_boundaries_layer_wizard.tsx index d4cf4dbee7943..dd2317506e5f9 100644 --- a/x-pack/plugins/maps/public/classes/sources/ems_file_source/ems_boundaries_layer_wizard.tsx +++ b/x-pack/plugins/maps/public/classes/sources/ems_file_source/ems_boundaries_layer_wizard.tsx @@ -7,7 +7,7 @@ import React from 'react'; import { i18n } from '@kbn/i18n'; -import { VectorLayer } from '../../layers/vector_layer'; +import { GeoJsonVectorLayer } from '../../layers/vector_layer'; import { LayerWizard, RenderWizardArguments } from '../../layers/layer_wizard_registry'; import { EMSFileCreateSourceEditor } from './create_source_editor'; import { EMSFileSource, getSourceTitle } from './ems_file_source'; @@ -46,7 +46,7 @@ export const emsBoundariesLayerWizardConfig: LayerWizard = { renderWizard: ({ previewLayers, mapColors }: RenderWizardArguments) => { const onSourceConfigChange = (sourceConfig: Partial) => { const sourceDescriptor = EMSFileSource.createDescriptor(sourceConfig); - const layerDescriptor = VectorLayer.createDescriptor({ sourceDescriptor }, mapColors); + const layerDescriptor = GeoJsonVectorLayer.createDescriptor({ sourceDescriptor }, mapColors); previewLayers([layerDescriptor]); }; return ; diff --git a/x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/clusters_layer_wizard.tsx b/x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/clusters_layer_wizard.tsx index 36dd28cb5bbf1..ad046eeb02d47 100644 --- a/x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/clusters_layer_wizard.tsx +++ b/x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/clusters_layer_wizard.tsx @@ -11,7 +11,7 @@ import React from 'react'; import { CreateSourceEditor } from './create_source_editor'; import { ESGeoGridSource, clustersTitle } from './es_geo_grid_source'; import { LayerWizard, RenderWizardArguments } from '../../layers/layer_wizard_registry'; -import { VectorLayer } from '../../layers/vector_layer'; +import { GeoJsonVectorLayer } from '../../layers/vector_layer'; import { ESGeoGridSourceDescriptor, ColorDynamicOptions, @@ -45,7 +45,7 @@ export const clustersLayerWizardConfig: LayerWizard = { } const defaultDynamicProperties = getDefaultDynamicProperties(); - const layerDescriptor = VectorLayer.createDescriptor({ + const layerDescriptor = GeoJsonVectorLayer.createDescriptor({ sourceDescriptor: ESGeoGridSource.createDescriptor(sourceConfig), style: VectorStyle.createDescriptor({ // @ts-ignore diff --git a/x-pack/plugins/maps/public/classes/sources/es_geo_line_source/layer_wizard.tsx b/x-pack/plugins/maps/public/classes/sources/es_geo_line_source/layer_wizard.tsx index 8da7037a5a34c..ba1c3c4eece4b 100644 --- a/x-pack/plugins/maps/public/classes/sources/es_geo_line_source/layer_wizard.tsx +++ b/x-pack/plugins/maps/public/classes/sources/es_geo_line_source/layer_wizard.tsx @@ -12,7 +12,7 @@ import { ESGeoLineSource, geoLineTitle, REQUIRES_GOLD_LICENSE_MSG } from './es_g import { LayerWizard, RenderWizardArguments } from '../../layers/layer_wizard_registry'; import { LAYER_WIZARD_CATEGORY, STYLE_TYPE, VECTOR_STYLES } from '../../../../common/constants'; import { VectorStyle } from '../../styles/vector/vector_style'; -import { VectorLayer } from '../../layers/vector_layer'; +import { GeoJsonVectorLayer } from '../../layers/vector_layer'; import { getIsGoldPlus } from '../../../licensed_features'; import { TracksLayerIcon } from '../../layers/icons/tracks_layer_icon'; @@ -40,7 +40,7 @@ export const geoLineLayerWizardConfig: LayerWizard = { return; } - const layerDescriptor = VectorLayer.createDescriptor({ + const layerDescriptor = GeoJsonVectorLayer.createDescriptor({ sourceDescriptor: ESGeoLineSource.createDescriptor(sourceConfig), style: VectorStyle.createDescriptor({ [VECTOR_STYLES.LINE_WIDTH]: { diff --git a/x-pack/plugins/maps/public/classes/sources/es_pew_pew_source/point_2_point_layer_wizard.tsx b/x-pack/plugins/maps/public/classes/sources/es_pew_pew_source/point_2_point_layer_wizard.tsx index c94c7859a85e7..84dea15daf48f 100644 --- a/x-pack/plugins/maps/public/classes/sources/es_pew_pew_source/point_2_point_layer_wizard.tsx +++ b/x-pack/plugins/maps/public/classes/sources/es_pew_pew_source/point_2_point_layer_wizard.tsx @@ -8,7 +8,7 @@ import React from 'react'; import { i18n } from '@kbn/i18n'; import { getDefaultDynamicProperties } from '../../styles/vector/vector_style_defaults'; -import { VectorLayer } from '../../layers/vector_layer'; +import { GeoJsonVectorLayer } from '../../layers/vector_layer'; // @ts-ignore import { ESPewPewSource, sourceTitle } from './es_pew_pew_source'; import { VectorStyle } from '../../styles/vector/vector_style'; @@ -40,7 +40,7 @@ export const point2PointLayerWizardConfig: LayerWizard = { } const defaultDynamicProperties = getDefaultDynamicProperties(); - const layerDescriptor = VectorLayer.createDescriptor({ + const layerDescriptor = GeoJsonVectorLayer.createDescriptor({ sourceDescriptor: ESPewPewSource.createDescriptor(sourceConfig), style: VectorStyle.createDescriptor({ [VECTOR_STYLES.LINE_COLOR]: { diff --git a/x-pack/plugins/maps/public/classes/sources/es_search_source/create_layer_descriptor.ts b/x-pack/plugins/maps/public/classes/sources/es_search_source/create_layer_descriptor.ts index 41b4e8d7a318a..5553e925258e9 100644 --- a/x-pack/plugins/maps/public/classes/sources/es_search_source/create_layer_descriptor.ts +++ b/x-pack/plugins/maps/public/classes/sources/es_search_source/create_layer_descriptor.ts @@ -9,7 +9,7 @@ import { Query } from 'src/plugins/data/public'; import { LayerDescriptor } from '../../../../common/descriptor_types'; import { ES_GEO_FIELD_TYPE, SCALING_TYPES } from '../../../../common/constants'; import { ESSearchSource } from './es_search_source'; -import { VectorLayer } from '../../layers/vector_layer'; +import { GeoJsonVectorLayer } from '../../layers/vector_layer'; import { getIsGoldPlus } from '../../../licensed_features'; export interface CreateLayerDescriptorParams { @@ -37,5 +37,5 @@ export function createLayerDescriptor({ scalingType, }); - return VectorLayer.createDescriptor({ sourceDescriptor, query }); + return GeoJsonVectorLayer.createDescriptor({ sourceDescriptor, query }); } 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 26771c1bed023..601fcee50ab2a 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 @@ -11,10 +11,8 @@ import React from 'react'; import { CreateSourceEditor } from './create_source_editor'; import { LayerWizard, RenderWizardArguments } from '../../layers/layer_wizard_registry'; import { ESSearchSource, sourceTitle } from './es_search_source'; -import { BlendedVectorLayer } from '../../layers/blended_vector_layer/blended_vector_layer'; -import { VectorLayer } from '../../layers/vector_layer'; +import { BlendedVectorLayer, GeoJsonVectorLayer, MvtVectorLayer } from '../../layers/vector_layer'; import { LAYER_WIZARD_CATEGORY, SCALING_TYPES } from '../../../../common/constants'; -import { TiledVectorLayer } from '../../layers/tiled_vector_layer/tiled_vector_layer'; import { DocumentsLayerIcon } from '../../layers/icons/documents_layer_icon'; import { ESSearchSourceDescriptor, @@ -30,9 +28,9 @@ export function createDefaultLayerDescriptor( if (sourceDescriptor.scalingType === SCALING_TYPES.CLUSTERS) { return BlendedVectorLayer.createDescriptor({ sourceDescriptor }, mapColors); } else if (sourceDescriptor.scalingType === SCALING_TYPES.MVT) { - return TiledVectorLayer.createDescriptor({ sourceDescriptor }, mapColors); + return MvtVectorLayer.createDescriptor({ sourceDescriptor }, mapColors); } else { - return VectorLayer.createDescriptor({ sourceDescriptor }, mapColors); + return GeoJsonVectorLayer.createDescriptor({ sourceDescriptor }, mapColors); } } 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 index e02ada305ecff..b4339eb20b1fd 100644 --- 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 @@ -9,7 +9,7 @@ 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 { GeoJsonVectorLayer } 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'; @@ -30,7 +30,7 @@ export const esTopHitsLayerWizardConfig: LayerWizard = { } const sourceDescriptor = ESSearchSource.createDescriptor(sourceConfig); - const layerDescriptor = VectorLayer.createDescriptor({ sourceDescriptor }, mapColors); + const layerDescriptor = GeoJsonVectorLayer.createDescriptor({ sourceDescriptor }, mapColors); previewLayers([layerDescriptor]); }; return ; diff --git a/x-pack/plugins/maps/public/classes/sources/mvt_single_layer_vector_source/layer_wizard.tsx b/x-pack/plugins/maps/public/classes/sources/mvt_single_layer_vector_source/layer_wizard.tsx index e5e1877c1ccd1..a3f7ceafd54ef 100644 --- a/x-pack/plugins/maps/public/classes/sources/mvt_single_layer_vector_source/layer_wizard.tsx +++ b/x-pack/plugins/maps/public/classes/sources/mvt_single_layer_vector_source/layer_wizard.tsx @@ -10,7 +10,7 @@ import React from 'react'; import { MVTSingleLayerVectorSourceEditor } from './mvt_single_layer_vector_source_editor'; import { MVTSingleLayerVectorSource, sourceTitle } from './mvt_single_layer_vector_source'; import { LayerWizard, RenderWizardArguments } from '../../layers/layer_wizard_registry'; -import { TiledVectorLayer } from '../../layers/tiled_vector_layer/tiled_vector_layer'; +import { MvtVectorLayer } from '../../layers/vector_layer'; import { LAYER_WIZARD_CATEGORY } from '../../../../common/constants'; import { TiledSingleLayerVectorSourceSettings } from '../../../../common/descriptor_types'; import { VectorTileLayerIcon } from '../../layers/icons/vector_tile_layer_icon'; @@ -24,7 +24,7 @@ export const mvtVectorSourceWizardConfig: LayerWizard = { renderWizard: ({ previewLayers, mapColors }: RenderWizardArguments) => { const onSourceConfigChange = (sourceConfig: TiledSingleLayerVectorSourceSettings) => { const sourceDescriptor = MVTSingleLayerVectorSource.createDescriptor(sourceConfig); - const layerDescriptor = TiledVectorLayer.createDescriptor({ sourceDescriptor }, mapColors); + const layerDescriptor = MvtVectorLayer.createDescriptor({ sourceDescriptor }, mapColors); previewLayers([layerDescriptor]); }; diff --git a/x-pack/plugins/maps/public/classes/sources/mvt_single_layer_vector_source/mvt_field_config_editor.tsx b/x-pack/plugins/maps/public/classes/sources/mvt_single_layer_vector_source/mvt_field_config_editor.tsx index 8924e346c94dc..3038d4b1df51f 100644 --- a/x-pack/plugins/maps/public/classes/sources/mvt_single_layer_vector_source/mvt_field_config_editor.tsx +++ b/x-pack/plugins/maps/public/classes/sources/mvt_single_layer_vector_source/mvt_field_config_editor.tsx @@ -16,9 +16,9 @@ import { EuiSpacer, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; +import { FieldIcon } from '@kbn/react-field/field_icon'; import _ from 'lodash'; import { MVTFieldDescriptor } from '../../../../common/descriptor_types'; -import { FieldIcon } from '../../../../../../../src/plugins/kibana_react/public'; import { MVT_FIELD_TYPE } from '../../../../common/constants'; function makeOption({ diff --git a/x-pack/plugins/maps/public/classes/styles/vector/components/field_select.tsx b/x-pack/plugins/maps/public/classes/styles/vector/components/field_select.tsx index 74269f39bc36c..e90bf5b2339d0 100644 --- a/x-pack/plugins/maps/public/classes/styles/vector/components/field_select.tsx +++ b/x-pack/plugins/maps/public/classes/styles/vector/components/field_select.tsx @@ -16,8 +16,8 @@ import { EuiFlexItem, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; +import { FieldIcon } from '@kbn/react-field/field_icon'; import { FIELD_ORIGIN, VECTOR_STYLES } from '../../../../../common/constants'; -import { FieldIcon } from '../../../../../../../../src/plugins/kibana_react/public'; import { StyleField } from '../style_fields_helper'; function renderOption( diff --git a/x-pack/plugins/maps/public/components/single_field_select.tsx b/x-pack/plugins/maps/public/components/single_field_select.tsx index 67594db11eb37..9b7f5d12725a5 100644 --- a/x-pack/plugins/maps/public/components/single_field_select.tsx +++ b/x-pack/plugins/maps/public/components/single_field_select.tsx @@ -17,8 +17,8 @@ import { EuiFlexItem, EuiToolTip, } from '@elastic/eui'; +import { FieldIcon } from '@kbn/react-field/field_icon'; import { IndexPatternField } from 'src/plugins/data/public'; -import { FieldIcon } from '../../../../../src/plugins/kibana_react/public'; function fieldsToOptions( fields?: IndexPatternField[], 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 3804e3873b47e..3c702b1334272 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 @@ -21,7 +21,7 @@ import { } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; import { i18n } from '@kbn/i18n'; -import { FieldIcon } from '../../../../../../src/plugins/kibana_react/public'; +import { FieldIcon } from '@kbn/react-field/field_icon'; export type FieldProps = { label: string; 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 93dfebecd1c34..eb0196ea156aa 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 @@ -50,7 +50,7 @@ import { ResizeChecker } from '../../../../../../src/plugins/kibana_utils/public import { RenderToolTipContent } from '../../classes/tooltips/tooltip_property'; import { TileStatusTracker } from './tile_status_tracker'; import { DrawFeatureControl } from './draw_control/draw_feature_control'; -import { TiledVectorLayer } from '../../classes/layers/tiled_vector_layer/tiled_vector_layer'; +import { MvtVectorLayer } from '../../classes/layers/vector_layer'; import type { MapExtentState } from '../../reducers/map/types'; export interface Props { @@ -127,7 +127,7 @@ export class MbMap extends Component { // This keeps track of the latest update calls, per layerId _queryForMeta = (layer: ILayer) => { if (this.state.mbMap && layer.isVisible() && layer.getType() === LAYER_TYPE.TILED_VECTOR) { - const mbFeatures = (layer as TiledVectorLayer).queryTileMetaFeatures(this.state.mbMap); + const mbFeatures = (layer as MvtVectorLayer).queryTileMetaFeatures(this.state.mbMap); if (mbFeatures !== null) { this.props.updateMetaFromTiles(layer.getId(), mbFeatures); } diff --git a/x-pack/plugins/maps/public/connected_components/right_side_controls/layer_control/layer_toc/toc_entry/toc_entry_actions_popover/toc_entry_actions_popover.tsx b/x-pack/plugins/maps/public/connected_components/right_side_controls/layer_control/layer_toc/toc_entry/toc_entry_actions_popover/toc_entry_actions_popover.tsx index 51acab6453921..db903c6a02593 100644 --- a/x-pack/plugins/maps/public/connected_components/right_side_controls/layer_control/layer_toc/toc_entry/toc_entry_actions_popover/toc_entry_actions_popover.tsx +++ b/x-pack/plugins/maps/public/connected_components/right_side_controls/layer_control/layer_toc/toc_entry/toc_entry_actions_popover/toc_entry_actions_popover.tsx @@ -18,7 +18,7 @@ import { getVisibilityToggleLabel, } from '../action_labels'; import { ESSearchSource } from '../../../../../../classes/sources/es_search_source'; -import { VectorLayer } from '../../../../../../classes/layers/vector_layer'; +import { isVectorLayer, IVectorLayer } from '../../../../../../classes/layers/vector_layer'; import { SCALING_TYPES, VECTOR_SHAPE_TYPE } from '../../../../../../../common/constants'; export interface Props { @@ -67,10 +67,10 @@ export class TOCEntryActionsPopover extends Component { } async _loadFeatureEditing() { - if (!(this.props.layer instanceof VectorLayer)) { + if (!isVectorLayer(this.props.layer)) { return; } - const supportsFeatureEditing = this.props.layer.supportsFeatureEditing(); + const supportsFeatureEditing = (this.props.layer as IVectorLayer).supportsFeatureEditing(); const isFeatureEditingEnabled = await this._getIsFeatureEditingEnabled(); if ( !this._isMounted || @@ -83,7 +83,7 @@ export class TOCEntryActionsPopover extends Component { } async _getIsFeatureEditingEnabled(): Promise { - const vectorLayer = this.props.layer as VectorLayer; + const vectorLayer = this.props.layer as IVectorLayer; const layerSource = this.props.layer.getSource(); if (!(layerSource instanceof ESSearchSource)) { return false; diff --git a/x-pack/plugins/maps/public/routes/map_page/map_app/map_app.tsx b/x-pack/plugins/maps/public/routes/map_page/map_app/map_app.tsx index 8fc2d97c4862a..7765b3467a805 100644 --- a/x-pack/plugins/maps/public/routes/map_page/map_app/map_app.tsx +++ b/x-pack/plugins/maps/public/routes/map_page/map_app/map_app.tsx @@ -40,7 +40,6 @@ import { MapContainer } from '../../../connected_components/map_container'; import { getIndexPatternsFromIds } from '../../../index_pattern_util'; import { getTopNavConfig } from '../top_nav_config'; import { goToSpecifiedPath } from '../../../render_app'; -import { MapSavedObjectAttributes } from '../../../../common/map_saved_object_type'; import { getEditPath, getFullPath, APP_ID } from '../../../../common/constants'; import { getMapEmbeddableDisplayName } from '../../../../common/i18n_getters'; import { @@ -52,11 +51,7 @@ import { unsavedChangesWarning, } from '../saved_map'; import { waitUntilTimeLayersLoad$ } from './wait_until_time_layers_load'; - -interface MapRefreshConfig { - isPaused: boolean; - interval: number; -} +import { RefreshConfig as MapRefreshConfig, SerializedMapState } from '../saved_map'; export interface Props { savedMap: SavedMap; @@ -212,12 +207,10 @@ export class MapApp extends React.Component { filters, query, time, - forceRefresh = false, }: { filters?: Filter[]; query?: Query; time?: TimeRange; - forceRefresh?: boolean; }) => { const { filterManager } = getData().query; @@ -226,7 +219,7 @@ export class MapApp extends React.Component { } this.props.setQuery({ - forceRefresh, + forceRefresh: false, filters: filterManager.getFilters(), query, timeFilters: time, @@ -248,20 +241,14 @@ export class MapApp extends React.Component { updateGlobalState(updatedGlobalState, !this.state.initialized); }; - _initMapAndLayerSettings(mapSavedObjectAttributes: MapSavedObjectAttributes) { + _initMapAndLayerSettings(serializedMapState?: SerializedMapState) { const globalState: MapsGlobalState = getGlobalState(); - let savedObjectFilters = []; - if (mapSavedObjectAttributes.mapStateJSON) { - const mapState = JSON.parse(mapSavedObjectAttributes.mapStateJSON); - if (mapState.filters) { - savedObjectFilters = mapState.filters; - } - } + const savedObjectFilters = serializedMapState?.filters ? serializedMapState.filters : []; const appFilters = this._appStateManager.getFilters() || []; const query = getInitialQuery({ - mapStateJSON: mapSavedObjectAttributes.mapStateJSON, + serializedMapState, appState: this._appStateManager.getAppState(), }); if (query) { @@ -272,14 +259,14 @@ export class MapApp extends React.Component { filters: [..._.get(globalState, 'filters', []), ...appFilters, ...savedObjectFilters], query, time: getInitialTimeFilters({ - mapStateJSON: mapSavedObjectAttributes.mapStateJSON, + serializedMapState, globalState, }), }); this._onRefreshConfigChange( getInitialRefreshConfig({ - mapStateJSON: mapSavedObjectAttributes.mapStateJSON, + serializedMapState, globalState, }) ); @@ -371,7 +358,16 @@ export class MapApp extends React.Component { ); } - this._initMapAndLayerSettings(this.props.savedMap.getAttributes()); + let serializedMapState: SerializedMapState | undefined; + try { + const attributes = this.props.savedMap.getAttributes(); + if (attributes.mapStateJSON) { + serializedMapState = JSON.parse(attributes.mapStateJSON); + } + } catch (e) { + // ignore malformed mapStateJSON, not a critical error for viewing map - map will just use defaults + } + this._initMapAndLayerSettings(serializedMapState); this.setState({ initialized: true }); } @@ -400,11 +396,16 @@ export class MapApp extends React.Component { filters={this.props.filters} query={this.props.query} onQuerySubmit={({ dateRange, query }) => { - this._onQueryChange({ - query, - time: dateRange, - forceRefresh: true, - }); + const isUpdate = + !_.isEqual(dateRange, this.props.timeFilters) || !_.isEqual(query, this.props.query); + if (isUpdate) { + this._onQueryChange({ + query, + time: dateRange, + }); + } else { + this.props.setQuery({ forceRefresh: true }); + } }} onFiltersUpdated={this._onFiltersChange} dateRangeFrom={this.props.timeFilters.from} diff --git a/x-pack/plugins/maps/public/routes/map_page/saved_map/get_initial_query.ts b/x-pack/plugins/maps/public/routes/map_page/saved_map/get_initial_query.ts index 276e89f78ebac..1a57c09672c04 100644 --- a/x-pack/plugins/maps/public/routes/map_page/saved_map/get_initial_query.ts +++ b/x-pack/plugins/maps/public/routes/map_page/saved_map/get_initial_query.ts @@ -7,23 +7,21 @@ import { getData } from '../../../kibana_services'; import { MapsAppState } from '../url_state'; +import { SerializedMapState } from './types'; export function getInitialQuery({ - mapStateJSON, + serializedMapState, appState = {}, }: { - mapStateJSON?: string; + serializedMapState?: SerializedMapState; appState: MapsAppState; }) { if (appState.query) { return appState.query; } - if (mapStateJSON) { - const mapState = JSON.parse(mapStateJSON); - if (mapState.query) { - return mapState.query; - } + if (serializedMapState?.query) { + return serializedMapState.query; } return getData().query.queryString.getDefaultQuery(); diff --git a/x-pack/plugins/maps/public/routes/map_page/saved_map/get_initial_refresh_config.ts b/x-pack/plugins/maps/public/routes/map_page/saved_map/get_initial_refresh_config.ts index b343323cdf7ab..ad5a56dcd4c26 100644 --- a/x-pack/plugins/maps/public/routes/map_page/saved_map/get_initial_refresh_config.ts +++ b/x-pack/plugins/maps/public/routes/map_page/saved_map/get_initial_refresh_config.ts @@ -8,21 +8,19 @@ import { QueryState } from 'src/plugins/data/public'; import { getUiSettings } from '../../../kibana_services'; import { UI_SETTINGS } from '../../../../../../../src/plugins/data/public'; +import { SerializedMapState } from './types'; export function getInitialRefreshConfig({ - mapStateJSON, + serializedMapState, globalState = {}, }: { - mapStateJSON?: string; + serializedMapState?: SerializedMapState; globalState: QueryState; }) { const uiSettings = getUiSettings(); - if (mapStateJSON) { - const mapState = JSON.parse(mapStateJSON); - if (mapState.refreshConfig) { - return mapState.refreshConfig; - } + if (serializedMapState?.refreshConfig) { + return serializedMapState.refreshConfig; } const defaultRefreshConfig = uiSettings.get(UI_SETTINGS.TIMEPICKER_REFRESH_INTERVAL_DEFAULTS); diff --git a/x-pack/plugins/maps/public/routes/map_page/saved_map/get_initial_time_filters.ts b/x-pack/plugins/maps/public/routes/map_page/saved_map/get_initial_time_filters.ts index 80c5d70ebacf2..9cb67cefde547 100644 --- a/x-pack/plugins/maps/public/routes/map_page/saved_map/get_initial_time_filters.ts +++ b/x-pack/plugins/maps/public/routes/map_page/saved_map/get_initial_time_filters.ts @@ -7,19 +7,17 @@ import { QueryState } from 'src/plugins/data/public'; import { getUiSettings } from '../../../kibana_services'; +import { SerializedMapState } from './types'; export function getInitialTimeFilters({ - mapStateJSON, + serializedMapState, globalState, }: { - mapStateJSON?: string; + serializedMapState?: SerializedMapState; globalState: QueryState; }) { - if (mapStateJSON) { - const mapState = JSON.parse(mapStateJSON); - if (mapState.timeFilters) { - return mapState.timeFilters; - } + if (serializedMapState?.timeFilters) { + return serializedMapState.timeFilters; } const defaultTime = getUiSettings().get('timepicker:timeDefaults'); diff --git a/x-pack/plugins/maps/public/routes/map_page/saved_map/index.ts b/x-pack/plugins/maps/public/routes/map_page/saved_map/index.ts index 549c9949b9027..a3e8ef96160bb 100644 --- a/x-pack/plugins/maps/public/routes/map_page/saved_map/index.ts +++ b/x-pack/plugins/maps/public/routes/map_page/saved_map/index.ts @@ -5,6 +5,7 @@ * 2.0. */ +export type { RefreshConfig, SerializedMapState, SerializedUiState } from './types'; export { SavedMap } from './saved_map'; export { getInitialLayersFromUrlParam } from './get_initial_layers_from_url_param'; export { getInitialQuery } from './get_initial_query'; diff --git a/x-pack/plugins/maps/public/routes/map_page/saved_map/saved_map.ts b/x-pack/plugins/maps/public/routes/map_page/saved_map/saved_map.ts index 004b88a242623..3cff8d9713830 100644 --- a/x-pack/plugins/maps/public/routes/map_page/saved_map/saved_map.ts +++ b/x-pack/plugins/maps/public/routes/map_page/saved_map/saved_map.ts @@ -47,6 +47,7 @@ import { getBreadcrumbs } from './get_breadcrumbs'; import { DEFAULT_IS_LAYER_TOC_OPEN } from '../../../reducers/ui'; import { createBasemapLayerDescriptor } from '../../../classes/layers/create_basemap_layer_descriptor'; import { whenLicenseInitialized } from '../../../licensed_features'; +import { SerializedMapState, SerializedUiState } from './types'; export class SavedMap { private _attributes: MapSavedObjectAttributes | null = null; @@ -113,9 +114,13 @@ export class SavedMap { if (this._mapEmbeddableInput && this._mapEmbeddableInput.mapSettings !== undefined) { this._store.dispatch(setMapSettings(this._mapEmbeddableInput.mapSettings)); } else if (this._attributes?.mapStateJSON) { - const mapState = JSON.parse(this._attributes.mapStateJSON); - if (mapState.settings) { - this._store.dispatch(setMapSettings(mapState.settings)); + try { + const mapState = JSON.parse(this._attributes.mapStateJSON) as SerializedMapState; + if (mapState.settings) { + this._store.dispatch(setMapSettings(mapState.settings)); + } + } catch (e) { + // ignore malformed mapStateJSON, not a critical error for viewing map - map will just use defaults } } @@ -123,20 +128,28 @@ export class SavedMap { if (this._mapEmbeddableInput && this._mapEmbeddableInput.isLayerTOCOpen !== undefined) { isLayerTOCOpen = this._mapEmbeddableInput.isLayerTOCOpen; } else if (this._attributes?.uiStateJSON) { - const uiState = JSON.parse(this._attributes.uiStateJSON); - if ('isLayerTOCOpen' in uiState) { - isLayerTOCOpen = uiState.isLayerTOCOpen; + try { + const uiState = JSON.parse(this._attributes.uiStateJSON) as SerializedUiState; + if ('isLayerTOCOpen' in uiState) { + isLayerTOCOpen = uiState.isLayerTOCOpen; + } + } catch (e) { + // ignore malformed uiStateJSON, not a critical error for viewing map - map will just use defaults } } this._store.dispatch(setIsLayerTOCOpen(isLayerTOCOpen)); - let openTOCDetails = []; + let openTOCDetails: string[] = []; if (this._mapEmbeddableInput && this._mapEmbeddableInput.openTOCDetails !== undefined) { openTOCDetails = this._mapEmbeddableInput.openTOCDetails; } else if (this._attributes?.uiStateJSON) { - const uiState = JSON.parse(this._attributes.uiStateJSON); - if ('openTOCDetails' in uiState) { - openTOCDetails = uiState.openTOCDetails; + try { + const uiState = JSON.parse(this._attributes.uiStateJSON) as SerializedUiState; + if ('openTOCDetails' in uiState) { + openTOCDetails = uiState.openTOCDetails; + } + } catch (e) { + // ignore malformed uiStateJSON, not a critical error for viewing map - map will just use defaults } } this._store.dispatch(setOpenTOCDetails(openTOCDetails)); @@ -150,19 +163,27 @@ export class SavedMap { }) ); } else if (this._attributes?.mapStateJSON) { - const mapState = JSON.parse(this._attributes.mapStateJSON); - this._store.dispatch( - setGotoWithCenter({ - lat: mapState.center.lat, - lon: mapState.center.lon, - zoom: mapState.zoom, - }) - ); + try { + const mapState = JSON.parse(this._attributes.mapStateJSON) as SerializedMapState; + this._store.dispatch( + setGotoWithCenter({ + lat: mapState.center.lat, + lon: mapState.center.lon, + zoom: mapState.zoom, + }) + ); + } catch (e) { + // ignore malformed mapStateJSON, not a critical error for viewing map - map will just use defaults + } } let layerList: LayerDescriptor[] = []; if (this._attributes.layerListJSON) { - layerList = JSON.parse(this._attributes.layerListJSON); + try { + layerList = JSON.parse(this._attributes.layerListJSON) as LayerDescriptor[]; + } catch (e) { + throw new Error('Malformed saved object: unable to parse layerListJSON'); + } } else { const basemapLayerDescriptor = createBasemapLayerDescriptor(); if (basemapLayerDescriptor) { @@ -413,11 +434,11 @@ export class SavedMap { query: getQuery(state), filters: getFilters(state), settings: getMapSettings(state), - }); + } as SerializedMapState); this._attributes!.uiStateJSON = JSON.stringify({ isLayerTOCOpen: getIsLayerTOCOpen(state), openTOCDetails: getOpenTOCDetails(state), - }); + } as SerializedUiState); } } diff --git a/x-pack/plugins/maps/public/routes/map_page/saved_map/types.ts b/x-pack/plugins/maps/public/routes/map_page/saved_map/types.ts new file mode 100644 index 0000000000000..808007c075533 --- /dev/null +++ b/x-pack/plugins/maps/public/routes/map_page/saved_map/types.ts @@ -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 type { Query } from 'src/plugins/data/common'; +import { Filter, TimeRange } from '../../../../../../../src/plugins/data/public'; +import { MapCenter } from '../../../../common/descriptor_types'; +import { MapSettings } from '../../../reducers/map'; + +export interface RefreshConfig { + isPaused: boolean; + interval: number; +} + +// parsed contents of mapStateJSON +export interface SerializedMapState { + zoom: number; + center: MapCenter; + timeFilters?: TimeRange; + refreshConfig: RefreshConfig; + query?: Query; + filters: Filter[]; + settings: MapSettings; +} + +// parsed contents of uiStateJSON +export interface SerializedUiState { + isLayerTOCOpen: boolean; + openTOCDetails: string[]; +} diff --git a/x-pack/plugins/maps/public/selectors/map_selectors.test.ts b/x-pack/plugins/maps/public/selectors/map_selectors.test.ts index dc3c6dca46237..4f336d9a8ad27 100644 --- a/x-pack/plugins/maps/public/selectors/map_selectors.test.ts +++ b/x-pack/plugins/maps/public/selectors/map_selectors.test.ts @@ -7,8 +7,6 @@ import { LAYER_STYLE_TYPE, LAYER_TYPE, SOURCE_TYPES } from '../../common/constants'; -jest.mock('../classes/layers/tiled_vector_layer/tiled_vector_layer', () => {}); -jest.mock('../classes/layers/blended_vector_layer/blended_vector_layer', () => {}); jest.mock('../classes/layers/heatmap_layer', () => {}); jest.mock('../classes/layers/vector_tile_layer/vector_tile_layer', () => {}); jest.mock('../classes/joins/inner_join', () => {}); diff --git a/x-pack/plugins/maps/public/selectors/map_selectors.ts b/x-pack/plugins/maps/public/selectors/map_selectors.ts index 5ca297bdff020..f58525ea6f974 100644 --- a/x-pack/plugins/maps/public/selectors/map_selectors.ts +++ b/x-pack/plugins/maps/public/selectors/map_selectors.ts @@ -13,21 +13,25 @@ import type { Query } from 'src/plugins/data/common'; import { TileLayer } from '../classes/layers/tile_layer/tile_layer'; // @ts-ignore import { VectorTileLayer } from '../classes/layers/vector_tile_layer/vector_tile_layer'; -import { IVectorLayer, VectorLayer } from '../classes/layers/vector_layer'; +import { + BlendedVectorLayer, + IVectorLayer, + MvtVectorLayer, + GeoJsonVectorLayer, +} from '../classes/layers/vector_layer'; import { VectorStyle } from '../classes/styles/vector/vector_style'; import { HeatmapLayer } from '../classes/layers/heatmap_layer'; -import { BlendedVectorLayer } from '../classes/layers/blended_vector_layer/blended_vector_layer'; import { getTimeFilter } from '../kibana_services'; import { getChartsPaletteServiceGetColor, getInspectorAdapters, } from '../reducers/non_serializable_instances'; -import { TiledVectorLayer } from '../classes/layers/tiled_vector_layer/tiled_vector_layer'; import { copyPersistentState, TRACKED_LAYER_DESCRIPTOR } from '../reducers/copy_persistent_state'; import { InnerJoin } from '../classes/joins/inner_join'; import { getSourceByType } from '../classes/sources/source_registry'; import { GeoJsonFileSource } from '../classes/sources/geojson_file_source'; import { + LAYER_TYPE, SOURCE_DATA_REQUEST_ID, SPATIAL_FILTERS_LAYER_ID, STYLE_TYPE, @@ -66,9 +70,9 @@ export function createLayerInstance( const source: ISource = createSourceInstance(layerDescriptor.sourceDescriptor, inspectorAdapters); switch (layerDescriptor.type) { - case TileLayer.type: + case LAYER_TYPE.TILE: return new TileLayer({ layerDescriptor, source: source as ITMSSource }); - case VectorLayer.type: + case LAYER_TYPE.VECTOR: const joins: InnerJoin[] = []; const vectorLayerDescriptor = layerDescriptor as VectorLayerDescriptor; if (vectorLayerDescriptor.joins) { @@ -77,27 +81,27 @@ export function createLayerInstance( joins.push(join); }); } - return new VectorLayer({ + return new GeoJsonVectorLayer({ layerDescriptor: vectorLayerDescriptor, source: source as IVectorSource, joins, chartsPaletteServiceGetColor, }); - case VectorTileLayer.type: + case LAYER_TYPE.VECTOR_TILE: return new VectorTileLayer({ layerDescriptor, source: source as ITMSSource }); - case HeatmapLayer.type: + case LAYER_TYPE.HEATMAP: return new HeatmapLayer({ layerDescriptor: layerDescriptor as HeatmapLayerDescriptor, source: source as ESGeoGridSource, }); - case BlendedVectorLayer.type: + case LAYER_TYPE.BLENDED_VECTOR: return new BlendedVectorLayer({ layerDescriptor: layerDescriptor as VectorLayerDescriptor, source: source as IVectorSource, chartsPaletteServiceGetColor, }); - case TiledVectorLayer.type: - return new TiledVectorLayer({ + case LAYER_TYPE.TILED_VECTOR: + return new MvtVectorLayer({ layerDescriptor: layerDescriptor as VectorLayerDescriptor, source: source as IVectorSource, }); @@ -266,8 +270,8 @@ export const getSpatialFiltersLayer = createSelector( name: 'spatialFilters', }); - return new VectorLayer({ - layerDescriptor: VectorLayer.createDescriptor({ + return new GeoJsonVectorLayer({ + layerDescriptor: GeoJsonVectorLayer.createDescriptor({ id: SPATIAL_FILTERS_LAYER_ID, visible: settings.showSpatialFilters, alpha: settings.spatialFiltersAlpa, diff --git a/x-pack/plugins/maps/server/embeddable_migrations.ts b/x-pack/plugins/maps/server/embeddable_migrations.ts index a49e776d4fe02..962f5c4fb0d7a 100644 --- a/x-pack/plugins/maps/server/embeddable_migrations.ts +++ b/x-pack/plugins/maps/server/embeddable_migrations.ts @@ -19,15 +19,27 @@ import { setEmsTmsDefaultModes } from '../common/migrations/set_ems_tms_default_ */ export const embeddableMigrations = { '7.14.0': (state: SerializableRecord) => { - return { - ...state, - attributes: moveAttribution(state as { attributes: MapSavedObjectAttributes }), - } as SerializableRecord; + try { + return { + ...state, + attributes: moveAttribution(state as { attributes: MapSavedObjectAttributes }), + } as SerializableRecord; + } catch (e) { + // Do not fail migration for invalid layerListJSON + // Maps application can display invalid layerListJSON error when saved object is viewed + return state; + } }, '8.0.0': (state: SerializableRecord) => { - return { - ...state, - attributes: setEmsTmsDefaultModes(state as { attributes: MapSavedObjectAttributes }), - } as SerializableRecord; + try { + return { + ...state, + attributes: setEmsTmsDefaultModes(state as { attributes: MapSavedObjectAttributes }), + } as SerializableRecord; + } catch (e) { + // Do not fail migration for invalid layerListJSON + // Maps application can display invalid layerListJSON error when saved object is viewed + return state; + } }, }; diff --git a/x-pack/plugins/maps/server/saved_objects/saved_object_migrations.js b/x-pack/plugins/maps/server/saved_objects/saved_object_migrations.js index 5fc15e8929714..6d23246860423 100644 --- a/x-pack/plugins/maps/server/saved_objects/saved_object_migrations.js +++ b/x-pack/plugins/maps/server/saved_objects/saved_object_migrations.js @@ -19,6 +19,14 @@ import { addTypeToTermJoin } from '../../common/migrations/add_type_to_termjoin' import { moveAttribution } from '../../common/migrations/move_attribution'; import { setEmsTmsDefaultModes } from '../../common/migrations/set_ems_tms_default_modes'; +function logMigrationWarning(context, errorMsg, doc) { + context.log.warning( + `map migration failed (${context.migrationVersion}). ${errorMsg}. attributes: ${JSON.stringify( + doc + )}` + ); +} + /* * Embeddables such as Maps, Lens, and Visualize can be embedded by value or by reference on a dashboard. * To ensure that any migrations (>7.12) are run correctly in both cases, @@ -27,95 +35,150 @@ import { setEmsTmsDefaultModes } from '../../common/migrations/set_ems_tms_defau * This is the saved object migration registry. */ export const savedObjectMigrations = { - '7.2.0': (doc) => { - const { attributes, references } = extractReferences(doc); + '7.2.0': (doc, context) => { + try { + const { attributes, references } = extractReferences(doc); - return { - ...doc, - attributes, - references, - }; + return { + ...doc, + attributes, + references, + }; + } catch (e) { + logMigrationWarning(context, e.message, doc); + return doc; + } }, - '7.4.0': (doc) => { - const attributes = emsRasterTileToEmsVectorTile(doc); + '7.4.0': (doc, context) => { + try { + const attributes = emsRasterTileToEmsVectorTile(doc); - return { - ...doc, - attributes, - }; + return { + ...doc, + attributes, + }; + } catch (e) { + logMigrationWarning(context, e.message, doc); + return doc; + } }, - '7.5.0': (doc) => { - const attributes = topHitsTimeToSort(doc); + '7.5.0': (doc, context) => { + try { + const attributes = topHitsTimeToSort(doc); - return { - ...doc, - attributes, - }; + return { + ...doc, + attributes, + }; + } catch (e) { + logMigrationWarning(context, e.message, doc); + return doc; + } }, - '7.6.0': (doc) => { - const attributesPhase1 = moveApplyGlobalQueryToSources(doc); - const attributesPhase2 = addFieldMetaOptions({ attributes: attributesPhase1 }); + '7.6.0': (doc, context) => { + try { + const attributesPhase1 = moveApplyGlobalQueryToSources(doc); + const attributesPhase2 = addFieldMetaOptions({ attributes: attributesPhase1 }); - return { - ...doc, - attributes: attributesPhase2, - }; + return { + ...doc, + attributes: attributesPhase2, + }; + } catch (e) { + logMigrationWarning(context, e.message, doc); + return doc; + } }, - '7.7.0': (doc) => { - const attributesPhase1 = migrateSymbolStyleDescriptor(doc); - const attributesPhase2 = migrateUseTopHitsToScalingType({ attributes: attributesPhase1 }); + '7.7.0': (doc, context) => { + try { + const attributesPhase1 = migrateSymbolStyleDescriptor(doc); + const attributesPhase2 = migrateUseTopHitsToScalingType({ attributes: attributesPhase1 }); - return { - ...doc, - attributes: attributesPhase2, - }; + return { + ...doc, + attributes: attributesPhase2, + }; + } catch (e) { + logMigrationWarning(context, e.message, doc); + return doc; + } }, - '7.8.0': (doc) => { - const attributes = migrateJoinAggKey(doc); + '7.8.0': (doc, context) => { + try { + const attributes = migrateJoinAggKey(doc); - return { - ...doc, - attributes, - }; + return { + ...doc, + attributes, + }; + } catch (e) { + logMigrationWarning(context, e.message, doc); + return doc; + } }, - '7.9.0': (doc) => { - const attributes = removeBoundsFromSavedObject(doc); + '7.9.0': (doc, context) => { + try { + const attributes = removeBoundsFromSavedObject(doc); - return { - ...doc, - attributes, - }; + return { + ...doc, + attributes, + }; + } catch (e) { + logMigrationWarning(context, e.message, doc); + return doc; + } }, - '7.10.0': (doc) => { - const attributes = setDefaultAutoFitToBounds(doc); + '7.10.0': (doc, context) => { + try { + const attributes = setDefaultAutoFitToBounds(doc); - return { - ...doc, - attributes, - }; + return { + ...doc, + attributes, + }; + } catch (e) { + logMigrationWarning(context, e.message, doc); + return doc; + } }, - '7.12.0': (doc) => { - const attributes = addTypeToTermJoin(doc); + '7.12.0': (doc, context) => { + try { + const attributes = addTypeToTermJoin(doc); - return { - ...doc, - attributes, - }; + return { + ...doc, + attributes, + }; + } catch (e) { + logMigrationWarning(context, e.message, doc); + return doc; + } }, - '7.14.0': (doc) => { - const attributes = moveAttribution(doc); + '7.14.0': (doc, context) => { + try { + const attributes = moveAttribution(doc); - return { - ...doc, - attributes, - }; + return { + ...doc, + attributes, + }; + } catch (e) { + logMigrationWarning(context, e.message, doc); + return doc; + } }, - '8.0.0': (doc) => { - const attributes = setEmsTmsDefaultModes(doc); + '8.0.0': (doc, context) => { + try { + const attributes = setEmsTmsDefaultModes(doc); - return { - ...doc, - attributes, - }; + return { + ...doc, + attributes, + }; + } catch (e) { + logMigrationWarning(context, e.message, doc); + return doc; + } }, }; diff --git a/x-pack/plugins/metrics_entities/jest.config.js b/x-pack/plugins/metrics_entities/jest.config.js deleted file mode 100644 index 98a391223cc0f..0000000000000 --- a/x-pack/plugins/metrics_entities/jest.config.js +++ /dev/null @@ -1,15 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -module.exports = { - collectCoverageFrom: ['/x-pack/plugins/metrics_entities/{common,server}/**/*.{ts,tsx}'], - coverageDirectory: '/target/kibana-coverage/jest/x-pack/plugins/metrics_entities', - coverageReporters: ['text', 'html'], - preset: '@kbn/test', - rootDir: '../../..', - roots: ['/x-pack/plugins/metrics_entities'], -}; diff --git a/x-pack/plugins/ml/common/constants/trained_models.ts b/x-pack/plugins/ml/common/constants/trained_models.ts new file mode 100644 index 0000000000000..019189ea13c05 --- /dev/null +++ b/x-pack/plugins/ml/common/constants/trained_models.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 const DEPLOYMENT_STATE = { + STARTED: 'started', + STARTING: 'starting', + STOPPING: 'stopping', +} as const; + +export type DeploymentState = typeof DEPLOYMENT_STATE[keyof typeof DEPLOYMENT_STATE]; diff --git a/x-pack/plugins/ml/common/types/locator.ts b/x-pack/plugins/ml/common/types/locator.ts index 79db780b791fd..e13dbf7c5b271 100644 --- a/x-pack/plugins/ml/common/types/locator.ts +++ b/x-pack/plugins/ml/common/types/locator.ts @@ -188,6 +188,10 @@ export interface TrainedModelsQueryState { modelId?: string; } +export interface TrainedModelsNodesQueryState { + nodeId?: string; +} + export type DataFrameAnalyticsUrlState = MLPageState< | typeof ML_PAGES.DATA_FRAME_ANALYTICS_JOBS_MANAGE | typeof ML_PAGES.DATA_FRAME_ANALYTICS_MAP @@ -255,7 +259,8 @@ export type MlLocatorState = | CalendarEditUrlState | FilterEditUrlState | MlGenericUrlState - | TrainedModelsUrlState; + | TrainedModelsUrlState + | TrainedModelsNodesUrlState; export type MlLocatorParams = MlLocatorState & SerializableRecord; @@ -265,3 +270,8 @@ export type TrainedModelsUrlState = MLPageState< typeof ML_PAGES.TRAINED_MODELS_MANAGE, TrainedModelsQueryState | undefined >; + +export type TrainedModelsNodesUrlState = MLPageState< + typeof ML_PAGES.TRAINED_MODELS_NODES, + TrainedModelsNodesQueryState | undefined +>; diff --git a/x-pack/plugins/ml/common/types/trained_models.ts b/x-pack/plugins/ml/common/types/trained_models.ts index 5ad1d85d9feb9..89b8a50846cb3 100644 --- a/x-pack/plugins/ml/common/types/trained_models.ts +++ b/x-pack/plugins/ml/common/types/trained_models.ts @@ -5,9 +5,10 @@ * 2.0. */ -import { DataFrameAnalyticsConfig } from './data_frame_analytics'; -import { FeatureImportanceBaseline, TotalFeatureImportance } from './feature_importance'; -import { XOR } from './common'; +import type { DataFrameAnalyticsConfig } from './data_frame_analytics'; +import type { FeatureImportanceBaseline, TotalFeatureImportance } from './feature_importance'; +import type { XOR } from './common'; +import type { DeploymentState } from '../constants/trained_models'; export interface IngestStats { count: number; @@ -17,8 +18,8 @@ export interface IngestStats { } export interface TrainedModelStat { - model_id: string; - pipeline_count: number; + model_id?: string; + pipeline_count?: number; inference_stats?: { failure_count: number; inference_count: number; @@ -100,6 +101,9 @@ export interface TrainedModelConfigResponse { tags: string[]; version: string; inference_config?: Record; + /** + * Associated pipelines. Extends response from the ES endpoint. + */ pipelines?: Record | null; } @@ -125,7 +129,7 @@ export interface TrainedModelDeploymentStatsResponse { model_size_bytes: number; inference_threads: number; model_threads: number; - state: string; + state: DeploymentState; allocation_status: { target_allocation_count: number; state: string; allocation_count: number }; nodes: Array<{ node: Record< @@ -150,24 +154,35 @@ export interface TrainedModelDeploymentStatsResponse { }>; } +export interface AllocatedModel { + inference_threads: number; + allocation_status: { + target_allocation_count: number; + state: string; + allocation_count: number; + }; + model_id: string; + state: string; + model_threads: number; + model_size_bytes: number; + node: { + average_inference_time_ms: number; + inference_count: number; + routing_state: { + routing_state: string; + reason?: string; + }; + last_access?: number; + }; +} + export interface NodeDeploymentStatsResponse { id: string; name: string; transport_address: string; attributes: Record; roles: string[]; - allocated_models: Array<{ - inference_threads: number; - allocation_status: { - target_allocation_count: number; - state: string; - allocation_count: number; - }; - model_id: string; - state: string; - model_threads: number; - model_size_bytes: number; - }>; + allocated_models: AllocatedModel[]; memory_overview: { machine_memory: { /** Total machine memory in bytes */ diff --git a/x-pack/plugins/ml/common/util/group_color_utils.ts b/x-pack/plugins/ml/common/util/group_color_utils.ts index bb3b347e25334..63f0e13676d58 100644 --- a/x-pack/plugins/ml/common/util/group_color_utils.ts +++ b/x-pack/plugins/ml/common/util/group_color_utils.ts @@ -5,7 +5,7 @@ * 2.0. */ -import euiVars from '@elastic/eui/dist/eui_theme_dark.json'; +import { euiDarkVars as euiVars } from '@kbn/ui-shared-deps-src/theme'; import { stringHash } from './string_utils'; diff --git a/x-pack/plugins/ml/public/application/components/anomalies_table/influencers_cell.js b/x-pack/plugins/ml/public/application/components/anomalies_table/influencers_cell.js index 1f2236ad3e6a7..0059bec2929d0 100644 --- a/x-pack/plugins/ml/public/application/components/anomalies_table/influencers_cell.js +++ b/x-pack/plugins/ml/public/application/components/anomalies_table/influencers_cell.js @@ -12,6 +12,7 @@ import React, { Component } from 'react'; import { EuiLink, EuiButtonIcon, EuiToolTip } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; import { i18n } from '@kbn/i18n'; +import { blurButtonOnClick } from '../../util/component_utils'; /* * Component for rendering a list of record influencers inside a cell in the anomalies table. @@ -59,13 +60,13 @@ export class InfluencersCell extends Component { + onClick={blurButtonOnClick(() => { influencerFilter( influencer.influencerFieldName, influencer.influencerFieldValue, '+' - ) - } + ); + })} iconType="plusInCircle" aria-label={i18n.translate( 'xpack.ml.anomaliesTable.influencersCell.addFilterAriaLabel', @@ -86,13 +87,13 @@ export class InfluencersCell extends Component { + onClick={blurButtonOnClick(() => { influencerFilter( influencer.influencerFieldName, influencer.influencerFieldValue, '-' - ) - } + ); + })} iconType="minusInCircle" aria-label={i18n.translate( 'xpack.ml.anomaliesTable.influencersCell.removeFilterAriaLabel', diff --git a/x-pack/plugins/ml/public/application/components/color_range_legend/use_color_range.ts b/x-pack/plugins/ml/public/application/components/color_range_legend/use_color_range.ts index 2809a4321e7bb..2ccc687d145d0 100644 --- a/x-pack/plugins/ml/public/application/components/color_range_legend/use_color_range.ts +++ b/x-pack/plugins/ml/public/application/components/color_range_legend/use_color_range.ts @@ -7,8 +7,10 @@ import d3 from 'd3'; import { useMemo } from 'react'; -import euiThemeLight from '@elastic/eui/dist/eui_theme_light.json'; -import euiThemeDark from '@elastic/eui/dist/eui_theme_dark.json'; +import { + euiLightVars as euiThemeLight, + euiDarkVars as euiThemeDark, +} from '@kbn/ui-shared-deps-src/theme'; import { i18n } from '@kbn/i18n'; diff --git a/x-pack/plugins/ml/public/application/components/entity_cell/entity_cell.tsx b/x-pack/plugins/ml/public/application/components/entity_cell/entity_cell.tsx index a79c8a63b3bc6..f4a3b6dbf69c4 100644 --- a/x-pack/plugins/ml/public/application/components/entity_cell/entity_cell.tsx +++ b/x-pack/plugins/ml/public/application/components/entity_cell/entity_cell.tsx @@ -13,6 +13,7 @@ import { i18n } from '@kbn/i18n'; import { EMPTY_FIELD_VALUE_LABEL } from '../../timeseriesexplorer/components/entity_control/entity_control'; import { MLCATEGORY } from '../../../../common/constants/field_types'; import { ENTITY_FIELD_OPERATIONS } from '../../../../common/util/anomaly_utils'; +import { blurButtonOnClick } from '../../util/component_utils'; export type EntityCellFilter = ( entityName: string, @@ -41,7 +42,9 @@ function getAddFilter({ entityName, entityValue, filter }: EntityCellProps) { filter(entityName, entityValue, ENTITY_FIELD_OPERATIONS.ADD)} + onClick={blurButtonOnClick(() => { + filter(entityName, entityValue, ENTITY_FIELD_OPERATIONS.ADD); + })} iconType="plusInCircle" aria-label={i18n.translate('xpack.ml.anomaliesTable.entityCell.addFilterAriaLabel', { defaultMessage: 'Add filter', @@ -66,7 +69,9 @@ function getRemoveFilter({ entityName, entityValue, filter }: EntityCellProps) { filter(entityName, entityValue, ENTITY_FIELD_OPERATIONS.REMOVE)} + onClick={blurButtonOnClick(() => { + filter(entityName, entityValue, ENTITY_FIELD_OPERATIONS.REMOVE); + })} iconType="minusInCircle" aria-label={i18n.translate('xpack.ml.anomaliesTable.entityCell.removeFilterAriaLabel', { defaultMessage: 'Remove filter', diff --git a/x-pack/plugins/ml/public/application/components/job_messages/job_messages.tsx b/x-pack/plugins/ml/public/application/components/job_messages/job_messages.tsx index 2311807b6bbe6..facef2c02d578 100644 --- a/x-pack/plugins/ml/public/application/components/job_messages/job_messages.tsx +++ b/x-pack/plugins/ml/public/application/components/job_messages/job_messages.tsx @@ -17,7 +17,7 @@ import { import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; -import theme from '@elastic/eui/dist/eui_theme_light.json'; +import { euiLightVars as theme } from '@kbn/ui-shared-deps-src/theme'; import { JobMessage } from '../../../../common/types/audit_message'; import { JobIcon } from '../job_message_icon'; diff --git a/x-pack/plugins/ml/public/application/components/navigation_menu/main_tabs.tsx b/x-pack/plugins/ml/public/application/components/navigation_menu/main_tabs.tsx index 78fc10e77b2da..614db1ba0df9d 100644 --- a/x-pack/plugins/ml/public/application/components/navigation_menu/main_tabs.tsx +++ b/x-pack/plugins/ml/public/application/components/navigation_menu/main_tabs.tsx @@ -7,7 +7,7 @@ import React, { FC, useState, useEffect } from 'react'; -import { EuiPageHeader, EuiBetaBadge } from '@elastic/eui'; +import { EuiPageHeader } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { TabId } from './navigation_menu'; import { useMlKibana, useMlLocator, useNavigateToPath } from '../../contexts/kibana'; @@ -57,20 +57,6 @@ function getTabs(disableLinks: boolean): Tab[] { defaultMessage: 'Model Management', }), disabled: disableLinks, - betaTag: ( - - ), }, { id: 'datavisualizer', @@ -201,7 +187,6 @@ export const MainTabs: FC = ({ tabId, disableLinks }) => { }, 'data-test-subj': testSubject + (id === selectedTabId ? ' selected' : ''), isSelected: id === selectedTabId, - append: tab.betaTag, }; })} /> diff --git a/x-pack/plugins/ml/public/application/components/scatterplot_matrix/scatterplot_matrix.test.tsx b/x-pack/plugins/ml/public/application/components/scatterplot_matrix/scatterplot_matrix.test.tsx index d0e70c38c23b4..846a8da83acb0 100644 --- a/x-pack/plugins/ml/public/application/components/scatterplot_matrix/scatterplot_matrix.test.tsx +++ b/x-pack/plugins/ml/public/application/components/scatterplot_matrix/scatterplot_matrix.test.tsx @@ -10,7 +10,7 @@ import { render, waitFor, screen } from '@testing-library/react'; import { __IntlProvider as IntlProvider } from '@kbn/i18n/react'; -import euiThemeLight from '@elastic/eui/dist/eui_theme_light.json'; +import { euiLightVars as euiThemeLight } from '@kbn/ui-shared-deps-src/theme'; import { ScatterplotMatrix } from './scatterplot_matrix'; 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 e401d70abe759..ed8a49cd36f02 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 @@ -10,7 +10,7 @@ import 'jest-canvas-mock'; // @ts-ignore import { compile } from 'vega-lite/build/vega-lite'; -import euiThemeLight from '@elastic/eui/dist/eui_theme_light.json'; +import { euiLightVars as euiThemeLight } from '@kbn/ui-shared-deps-src/theme'; import { LEGEND_TYPES } from '../vega_chart/common'; 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 861b3727cea1b..83525a4837dc9 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 @@ -9,7 +9,7 @@ // @ts-ignore import type { TopLevelSpec } from 'vega-lite/build/vega-lite'; -import euiThemeLight from '@elastic/eui/dist/eui_theme_light.json'; +import { euiLightVars as euiThemeLight } from '@kbn/ui-shared-deps-src/theme'; import { euiPaletteColorBlind, euiPaletteNegative, euiPalettePositive } from '@elastic/eui'; diff --git a/x-pack/plugins/ml/public/application/contexts/kibana/use_field_formatter.ts b/x-pack/plugins/ml/public/application/contexts/kibana/use_field_formatter.ts index 508ce66f40f47..d089a43b3fb39 100644 --- a/x-pack/plugins/ml/public/application/contexts/kibana/use_field_formatter.ts +++ b/x-pack/plugins/ml/public/application/contexts/kibana/use_field_formatter.ts @@ -6,12 +6,26 @@ */ import { useMlKibana } from './kibana_context'; +import { FIELD_FORMAT_IDS } from '../../../../../../../src/plugins/field_formats/common'; -export function useFieldFormatter(fieldType: 'bytes') { +/** + * Set of reasonable defaults for formatters for the ML app. + */ +const defaultParam = { + [FIELD_FORMAT_IDS.DURATION]: { + inputFormat: 'milliseconds', + outputFormat: 'humanizePrecise', + }, +} as Record; + +export function useFieldFormatter(fieldType: FIELD_FORMAT_IDS) { const { services: { fieldFormats }, } = useMlKibana(); - const fieldFormatter = fieldFormats.deserialize({ id: fieldType }); + const fieldFormatter = fieldFormats.deserialize({ + id: fieldType, + params: defaultParam[fieldType], + }); return fieldFormatter.convert.bind(fieldFormatter); } 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 21090ce671d02..720dcd232d2f3 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 @@ -667,17 +667,6 @@ export const ConfigurationStepForm: FC = ({ )} - - - - = ({ setUnsupportedFieldsError={setUnsupportedFieldsError} setFormState={setFormState} /> + + + + {showScatterplotMatrix && ( <> = (prop const forceInput = useRef(null); const { toasts } = useNotifications(); + const { + services: { + application: { capabilities }, + }, + } = useMlKibana(); const onChange = (str: string) => { setAdvancedEditorRawString(str); }; + const canCreateDataView = useMemo( + () => + capabilities.savedObjectsManagement.edit === true || capabilities.indexPatterns.save === true, + [capabilities] + ); + const debouncedJobIdCheck = useMemo( () => debounce(async () => { @@ -200,18 +220,34 @@ export const CreateAnalyticsAdvancedEditor: FC = (prop {!isJobCreated && ( + {i18n.translate( + 'xpack.ml.dataframe.analytics.create.dataViewPermissionWarning', + { + defaultMessage: 'You need permission to create data views.', + } + )} + , + ] + : []), + ...(createIndexPattern && destinationIndexPatternTitleExists + ? [ + i18n.translate('xpack.ml.dataframe.analytics.create.dataViewExistsError', { + defaultMessage: 'A data view with this title already exists.', + }), + ] + : []), + ]} > = ({ setCurrentStep, }) => { const { - services: { docLinks, notifications }, + services: { + docLinks, + notifications, + application: { capabilities }, + }, } = useMlKibana(); const createIndexLink = docLinks.links.apis.createIndex; const { setFormState } = actions; @@ -71,6 +75,11 @@ export const DetailsStepForm: FC = ({ (cloneJob !== undefined && resultsField === DEFAULT_RESULTS_FIELD) ); + const canCreateDataView = useMemo( + () => + capabilities.savedObjectsManagement.edit === true || capabilities.indexPatterns.save === true, + [capabilities] + ); const forceInput = useRef(null); const isStepInvalid = @@ -149,6 +158,12 @@ export const DetailsStepForm: FC = ({ } }, [destIndexSameAsId, jobId]); + useEffect(() => { + if (canCreateDataView === false) { + setFormState({ createIndexPattern: false }); + } + }, [capabilities]); + return ( = ({ + {i18n.translate('xpack.ml.dataframe.analytics.create.dataViewPermissionWarning', { + defaultMessage: 'You need permission to create data views.', + })} + , + ] + : []), ...(createIndexPattern && destinationIndexPatternTitleExists ? [ i18n.translate('xpack.ml.dataframe.analytics.create.dataViewExistsError', { @@ -373,7 +399,7 @@ export const DetailsStepForm: FC = ({ ]} > = ({ destIndex }) => { const { services: { http: { basePath }, + application: { capabilities }, }, } = useMlKibana(); + const canCreateDataView = useMemo( + () => + capabilities.savedObjectsManagement.edit === true || capabilities.indexPatterns.save === true, + [capabilities] + ); + return ( <> - - - ), }} /> + {canCreateDataView === true ? ( + + + + ), + }} + /> + ) : null} ); diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/total_feature_importance_summary/feature_importance_summary.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/total_feature_importance_summary/feature_importance_summary.tsx index 6fe32a59c7614..9157e1fe4b678 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/total_feature_importance_summary/feature_importance_summary.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/total_feature_importance_summary/feature_importance_summary.tsx @@ -21,7 +21,7 @@ import { BarSeriesSpec, } from '@elastic/charts'; import { i18n } from '@kbn/i18n'; -import euiVars from '@elastic/eui/dist/eui_theme_light.json'; +import { euiLightVars as euiVars } from '@kbn/ui-shared-deps-src/theme'; import { TotalFeatureImportance, isClassificationTotalFeatureImportance, diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_map/use_map_action.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_map/use_map_action.tsx index 94568b4f59809..6fdcc047bce9d 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_map/use_map_action.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_map/use_map_action.tsx @@ -6,10 +6,12 @@ */ import React, { useCallback, useMemo } from 'react'; +import { cloneDeep } from 'lodash'; import { useMlLocator, useNavigateToPath } from '../../../../../contexts/kibana'; import { DataFrameAnalyticsListAction, DataFrameAnalyticsListRow } from '../analytics_list/common'; import { ML_PAGES } from '../../../../../../../common/constants/locator'; import { getViewLinkStatus } from '../action_view/get_view_link_status'; +import { useUrlState } from '../../../../../util/url_state'; import { mapActionButtonText, MapButton } from './map_button'; @@ -18,14 +20,25 @@ export const useMapAction = () => { const mlLocator = useMlLocator()!; const navigateToPath = useNavigateToPath(); - const clickHandler = useCallback(async (item: DataFrameAnalyticsListRow) => { - const path = await mlLocator.getUrl({ - page: ML_PAGES.DATA_FRAME_ANALYTICS_MAP, - pageState: { jobId: item.id }, - }); + const [globalState] = useUrlState('_g'); - await navigateToPath(path, false); - }, []); + const clickHandler = useCallback( + async (item: DataFrameAnalyticsListRow) => { + const globalStateClone = cloneDeep(globalState || {}); + delete globalStateClone.ml; + + const path = await mlLocator.getUrl({ + page: ML_PAGES.DATA_FRAME_ANALYTICS_MAP, + pageState: { + jobId: item.id, + globalState: globalStateClone, + }, + }); + + await navigateToPath(path, false); + }, + [globalState] + ); const action: DataFrameAnalyticsListAction = useMemo( () => ({ diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/analytics_list.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/analytics_list.tsx index 8423e569a99f2..a773fffdac997 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/analytics_list.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/analytics_list.tsx @@ -39,6 +39,7 @@ import { useTableSettings } from './use_table_settings'; import { RefreshAnalyticsListButton } from '../refresh_analytics_list_button'; import { ListingPageUrlState } from '../../../../../../../common/types/common'; import { JobsAwaitingNodeWarning } from '../../../../../components/jobs_awaiting_node_warning'; +import { useRefresh } from '../../../../../routing/use_refresh'; const filters: EuiSearchBarProps['filters'] = [ { @@ -119,6 +120,8 @@ export const DataFrameAnalyticsList: FC = ({ const [errorMessage, setErrorMessage] = useState(undefined); const [jobsAwaitingNodeCount, setJobsAwaitingNodeCount] = useState(0); + const refreshObs = useRefresh(); + const disabled = !checkPermission('canCreateDataFrameAnalytics') || !checkPermission('canStartStopDataFrameAnalytics'); @@ -174,6 +177,13 @@ export const DataFrameAnalyticsList: FC = ({ isManagementTable ); + useEffect( + function updateOnTimerRefresh() { + getAnalyticsCallback(); + }, + [refreshObs] + ); + const { columns, modals } = useColumns( expandedRowItemIds, setExpandedRowItemIds, diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/use_refresh_interval.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/use_refresh_interval.ts index c2335e4d5d017..0f236984f587c 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/use_refresh_interval.ts +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/use_refresh_interval.ts @@ -8,6 +8,7 @@ import React, { useEffect } from 'react'; import { useMlKibana } from '../../../../../contexts/kibana'; +import { useUrlState } from '../../../../../util/url_state'; import { DEFAULT_REFRESH_INTERVAL_MS, @@ -20,6 +21,7 @@ export const useRefreshInterval = ( setBlockRefresh: React.Dispatch> ) => { const { services } = useMlKibana(); + const [globalState] = useUrlState('_g'); const { timefilter } = services.data.query.timefilter; const { refresh } = useRefreshAnalyticsList(); @@ -35,7 +37,9 @@ export const useRefreshInterval = ( initAutoRefresh(); function initAutoRefresh() { - const { value } = timefilter.getRefreshInterval(); + const interval = globalState?.refreshInterval ?? timefilter.getRefreshInterval(); + const { value } = interval; + if (value === 0) { // the auto refresher starts in an off state // so switch it on and set the interval to 30s diff --git a/x-pack/plugins/ml/public/application/explorer/explorer_charts/components/explorer_chart_label/entity_filter/entity_filter.tsx b/x-pack/plugins/ml/public/application/explorer/explorer_charts/components/explorer_chart_label/entity_filter/entity_filter.tsx index 2ede9d380f3bf..66f4052a6952f 100644 --- a/x-pack/plugins/ml/public/application/explorer/explorer_charts/components/explorer_chart_label/entity_filter/entity_filter.tsx +++ b/x-pack/plugins/ml/public/application/explorer/explorer_charts/components/explorer_chart_label/entity_filter/entity_filter.tsx @@ -12,6 +12,7 @@ import { ENTITY_FIELD_OPERATIONS, EntityFieldOperation, } from '../../../../../../../common/util/anomaly_utils'; +import { blurButtonOnClick } from '../../../../../util/component_utils'; import './_entity_filter.scss'; interface EntityFilterProps { @@ -41,13 +42,13 @@ export const EntityFilter: FC = ({ + onClick={blurButtonOnClick(() => { onFilter({ influencerFieldName, influencerFieldValue, action: ENTITY_FIELD_OPERATIONS.ADD, - }) - } + }); + })} iconType="plusInCircle" aria-label={i18n.translate('xpack.ml.entityFilter.addFilterAriaLabel', { defaultMessage: 'Add filter for {influencerFieldName} {influencerFieldValue}', @@ -66,13 +67,13 @@ export const EntityFilter: FC = ({ + onClick={blurButtonOnClick(() => { onFilter({ influencerFieldName, influencerFieldValue, action: ENTITY_FIELD_OPERATIONS.REMOVE, - }) - } + }); + })} iconType="minusInCircle" aria-label={i18n.translate('xpack.ml.entityFilter.removeFilterAriaLabel', { defaultMessage: 'Remove filter for {influencerFieldName} {influencerFieldValue}', diff --git a/x-pack/plugins/ml/public/application/explorer/swimlane_container.tsx b/x-pack/plugins/ml/public/application/explorer/swimlane_container.tsx index ef8e80381293e..2cdb18666d0ee 100644 --- a/x-pack/plugins/ml/public/application/explorer/swimlane_container.tsx +++ b/x-pack/plugins/ml/public/application/explorer/swimlane_container.tsx @@ -470,7 +470,16 @@ export const SwimlaneContainer: FC = ({ valueAccessor="value" highlightedData={highlightedData} valueFormatter={getFormattedSeverityScore} - xScaleType={ScaleType.Time} + xScale={{ + type: ScaleType.Time, + interval: { + type: 'fixed', + unit: 'ms', + // the xDomain.minInterval should always be available at rendering time + // adding a fallback to 1m bucket + value: xDomain?.minInterval ?? 1000 * 60, + }, + }} ySortPredicate="dataIndex" config={swimLaneConfig} /> diff --git a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/utils.js b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/utils.js index fe09ed45f1274..d6926950dce7d 100644 --- a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/utils.js +++ b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/utils.js @@ -220,9 +220,21 @@ export async function cloneJob(jobId) { ]); const dataViewNames = await getDataViewNames(); - const jobIndicesAvailable = dataViewNames.includes(datafeed.indices.join(',')); + const dataViewTitle = datafeed.indices.join(','); + const jobIndicesAvailable = dataViewNames.includes(dataViewTitle); if (jobIndicesAvailable === false) { + const warningText = i18n.translate( + 'xpack.ml.jobsList.managementActions.noSourceDataViewForClone', + { + defaultMessage: + 'Unable to clone the anomaly detection job {jobId}. No data view exists for index {dataViewTitle}.', + values: { jobId, dataViewTitle }, + } + ); + getToastNotificationService().displayDangerToast(warningText, { + 'data-test-subj': 'mlCloneJobNoDataViewExistsWarningToast', + }); return; } diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/charts/common/settings.ts b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/charts/common/settings.ts index 861b72a5a58b7..3d386073849f4 100644 --- a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/charts/common/settings.ts +++ b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/charts/common/settings.ts @@ -5,8 +5,10 @@ * 2.0. */ -import darkTheme from '@elastic/eui/dist/eui_theme_dark.json'; -import lightTheme from '@elastic/eui/dist/eui_theme_light.json'; +import { + euiLightVars as lightTheme, + euiDarkVars as darkTheme, +} from '@kbn/ui-shared-deps-src/theme'; import { JobCreatorType, isMultiMetricJobCreator, diff --git a/x-pack/plugins/ml/public/application/timeseriesexplorer/components/timeseries_chart/timeseries_chart.js b/x-pack/plugins/ml/public/application/timeseriesexplorer/components/timeseries_chart/timeseries_chart.js index cad5bb68fb62b..31cdfa5df0576 100644 --- a/x-pack/plugins/ml/public/application/timeseriesexplorer/components/timeseries_chart/timeseries_chart.js +++ b/x-pack/plugins/ml/public/application/timeseriesexplorer/components/timeseries_chart/timeseries_chart.js @@ -1203,15 +1203,16 @@ class TimeseriesChartIntl extends Component { .call(brush) .selectAll('rect') .attr('y', -1) - .attr('height', contextChartHeight + swimlaneHeight + 1); + .attr('height', contextChartHeight + swimlaneHeight + 1) + .attr('width', this.vizWidth); + + const handleBrushExtent = brush.extent(); // move the left and right resize areas over to // be under the handles contextGroup.selectAll('.w rect').attr('x', -10).attr('width', 10); - contextGroup.selectAll('.e rect').attr('x', 0).attr('width', 10); - - const handleBrushExtent = brush.extent(); + contextGroup.selectAll('.e rect').attr('transform', null).attr('width', 10); const topBorder = contextGroup .append('rect') @@ -1247,6 +1248,7 @@ class TimeseriesChartIntl extends Component { function brushing() { const brushExtent = brush.extent(); mask.reveal(brushExtent); + leftHandle.attr('x', contextXScale(brushExtent[0]) - 10); rightHandle.attr('x', contextXScale(brushExtent[1]) + 0); diff --git a/x-pack/plugins/ml/public/application/trained_models/models_management/expanded_row.tsx b/x-pack/plugins/ml/public/application/trained_models/models_management/expanded_row.tsx index 4b342fe02b4d5..6dd7db1dbb7b6 100644 --- a/x-pack/plugins/ml/public/application/trained_models/models_management/expanded_row.tsx +++ b/x-pack/plugins/ml/public/application/trained_models/models_management/expanded_row.tsx @@ -5,7 +5,8 @@ * 2.0. */ -import React, { FC, Fragment } from 'react'; +import React, { FC, Fragment, useEffect, useState } from 'react'; +import { omit } from 'lodash'; import { EuiBadge, EuiButtonEmpty, @@ -15,6 +16,7 @@ import { EuiFlexGroup, EuiFlexItem, EuiHorizontalRule, + EuiListGroup, EuiNotificationBadge, EuiPanel, EuiSpacer, @@ -25,11 +27,13 @@ import { } from '@elastic/eui'; import { EuiDescriptionListProps } from '@elastic/eui/src/components/description_list/description_list'; import { FormattedMessage } from '@kbn/i18n/react'; +import type { EuiListGroupItemProps } from '@elastic/eui/src/components/list_group/list_group_item'; import { ModelItemFull } from './models_list'; -import { useMlKibana } from '../../contexts/kibana'; +import { useMlKibana, useMlLocator } from '../../contexts/kibana'; import { timeFormatter } from '../../../../common/util/date_utils'; import { isDefined } from '../../../../common/types/guards'; import { isPopulatedObject } from '../../../../common'; +import { ML_PAGES } from '../../../../common/constants/locator'; interface ExpandedRowProps { item: ModelItemFull; @@ -85,6 +89,12 @@ export function formatToListItems( } export const ExpandedRow: FC = ({ item }) => { + const mlLocator = useMlLocator(); + + const [deploymentStatsItems, setDeploymentStats] = useState( + [] + ); + const { inference_config: inferenceConfig, stats, @@ -119,6 +129,42 @@ export const ExpandedRow: FC = ({ item }) => { services: { share }, } = useMlKibana(); + useEffect( + function updateDeploymentState() { + (async function () { + const { nodes, ...deploymentStats } = stats.deployment_stats ?? {}; + + if (!isPopulatedObject(deploymentStats)) return; + + const result = formatToListItems(deploymentStats)!; + + const items: EuiListGroupItemProps[] = await Promise.all( + nodes!.map(async (v) => { + const nodeObject = Object.values(v.node)[0]; + const href = await mlLocator!.getUrl({ + page: ML_PAGES.TRAINED_MODELS_NODES, + pageState: { + nodeId: nodeObject.name, + }, + }); + return { + label: nodeObject.name, + href, + }; + }) + ); + + result.push({ + title: 'nodes', + description: , + }); + + setDeploymentStats(result); + })(); + }, + [stats.deployment_stats] + ); + const tabs = [ { id: 'details', @@ -234,164 +280,168 @@ export const ExpandedRow: FC = ({ item }) => { }, ] : []), - { - id: 'stats', - name: ( - - ), - content: ( - <> - - {stats.deployment_stats && ( - <> - - -
    - -
    -
    + ...(isPopulatedObject(omit(stats, 'pipeline_count')) + ? [ + { + id: 'stats', + name: ( + + ), + content: ( + <> - -
    - - - )} - - {stats.inference_stats && ( - - - -
    - -
    -
    - - -
    -
    - )} - {stats.ingest?.total && ( - - - -
    - -
    -
    - - - - {stats.ingest?.pipelines && ( - <> - + {!!deploymentStatsItems?.length ? ( + <> +
    - - {Object.entries(stats.ingest.pipelines).map( - ([pipelineName, { processors, ...pipelineStats }], i) => { - return ( - - - - - -
    - {i + 1}. {pipelineName} -
    -
    -
    -
    - - - -
    - - - - -
    - -
    -
    - - <> - {processors.map((processor) => { - const name = Object.keys(processor)[0]; - const { stats: processorStats } = processor[name]; - return ( - - - - - -
    {name}
    -
    -
    -
    - - - -
    - - -
    - ); - })} - -
    - ); - } - )} - + + +
    + + + ) : null} + + {stats.inference_stats && ( + + + +
    + +
    +
    + + +
    +
    )} -
    -
    - )} -
    - - ), - }, + {stats.ingest?.total && ( + + + +
    + +
    +
    + + + + {stats.ingest?.pipelines && ( + <> + + +
    + +
    +
    + + {Object.entries(stats.ingest.pipelines).map( + ([pipelineName, { processors, ...pipelineStats }], i) => { + return ( + + + + + +
    + {i + 1}. {pipelineName} +
    +
    +
    +
    + + + +
    + + + + +
    + +
    +
    + + <> + {processors.map((processor) => { + const name = Object.keys(processor)[0]; + const { stats: processorStats } = processor[name]; + return ( + + + + + +
    {name}
    +
    +
    +
    + + + +
    + + +
    + ); + })} + +
    + ); + } + )} + + )} +
    +
    + )} + + + ), + }, + ] + : []), ...(pipelines && Object.keys(pipelines).length > 0 ? [ { diff --git a/x-pack/plugins/ml/public/application/trained_models/models_management/models_list.tsx b/x-pack/plugins/ml/public/application/trained_models/models_management/models_list.tsx index 16b9aa760f535..9c3cc1f93a9cd 100644 --- a/x-pack/plugins/ml/public/application/trained_models/models_management/models_list.tsx +++ b/x-pack/plugins/ml/public/application/trained_models/models_management/models_list.tsx @@ -5,19 +5,19 @@ * 2.0. */ -import React, { FC, useState, useCallback, useMemo } from 'react'; -import { groupBy } from 'lodash'; +import React, { FC, useCallback, useEffect, useMemo, useState } from 'react'; +import { omit } from 'lodash'; import { - EuiInMemoryTable, + EuiBadge, + EuiButton, + EuiButtonIcon, EuiFlexGroup, EuiFlexItem, - EuiTitle, - EuiButton, + EuiInMemoryTable, + EuiSearchBarProps, EuiSpacer, - EuiButtonIcon, - EuiBadge, + EuiTitle, SearchFilterConfig, - EuiSearchBarProps, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; @@ -48,9 +48,12 @@ import { ListingPageUrlState } from '../../../../common/types/common'; import { usePageUrlState } from '../../util/url_state'; import { ExpandedRow } from './expanded_row'; import { isPopulatedObject } from '../../../../common'; -import { timeFormatter } from '../../../../common/util/date_utils'; import { useTableSettings } from '../../data_frame_analytics/pages/analytics_management/components/analytics_list/use_table_settings'; import { useToastNotificationService } from '../../services/toast_notification_service'; +import { useFieldFormatter } from '../../contexts/kibana/use_field_formatter'; +import { FIELD_FORMAT_IDS } from '../../../../../../../src/plugins/field_formats/common'; +import { useRefresh } from '../../routing/use_refresh'; +import { DEPLOYMENT_STATE } from '../../../../common/constants/trained_models'; type Stats = Omit; @@ -82,11 +85,15 @@ export const ModelsList: FC = () => { } = useMlKibana(); const urlLocator = useMlLocator()!; + const dateFormatter = useFieldFormatter(FIELD_FORMAT_IDS.DATE); + const [pageState, updatePageState] = usePageUrlState( ML_PAGES.TRAINED_MODELS_MANAGE, getDefaultModelsListState() ); + const refresh = useRefresh(); + const searchQueryText = pageState.queryText ?? ''; const canDeleteDataFrameAnalytics = capabilities.ml.canDeleteDataFrameAnalytics as boolean; @@ -121,7 +128,7 @@ export const ModelsList: FC = () => { size: 1000, }); - const newItems = []; + const newItems: ModelItem[] = []; const expandedItemsToRefresh = []; for (const model of response) { @@ -145,6 +152,11 @@ export const ModelsList: FC = () => { } } + // Need to fetch state for 3rd party models to enable/disable actions + await fetchAndPopulateDeploymentStats( + newItems.filter((v) => v.model_type.includes('pytorch')) + ); + setItems(newItems); if (expandedItemsToRefresh.length > 0) { @@ -175,6 +187,13 @@ export const ModelsList: FC = () => { onRefresh: fetchModelsData, }); + useEffect( + function updateOnTimerRefresh() { + fetchModelsData(); + }, + [refresh] + ); + const modelsStats: ModelsBarStats = useMemo(() => { return { total: { @@ -191,8 +210,6 @@ export const ModelsList: FC = () => { * Fetches models stats and update the original object */ const fetchModelsStats = useCallback(async (models: ModelItem[]) => { - const { true: pytorchModels } = groupBy(models, (m) => m.model_type === 'pytorch'); - try { if (models) { const { trained_model_stats: modelsStatsResponse } = @@ -200,19 +217,12 @@ export const ModelsList: FC = () => { for (const { model_id: id, ...stats } of modelsStatsResponse) { const model = models.find((m) => m.model_id === id); - model!.stats = stats; - } - } - - if (pytorchModels) { - const { deployment_stats: deploymentStatsResponse } = - await trainedModelsApiService.getTrainedModelDeploymentStats( - pytorchModels.map((m) => m.model_id) - ); - - for (const { model_id: id, ...stats } of deploymentStatsResponse) { - const model = models.find((m) => m.model_id === id); - model!.stats!.deployment_stats = stats; + if (model) { + model.stats = { + ...(model.stats ?? {}), + ...stats, + }; + } } } @@ -227,6 +237,39 @@ export const ModelsList: FC = () => { } }, []); + /** + * Updates model items with deployment stats; + * + * We have to fetch all deployment stats on each update, + * because for stopped models the API returns 404 response. + */ + const fetchAndPopulateDeploymentStats = useCallback(async (modelItems: ModelItem[]) => { + try { + const { deployment_stats: deploymentStats } = + await trainedModelsApiService.getTrainedModelDeploymentStats('*'); + + for (const deploymentStat of deploymentStats) { + const deployedModel = modelItems.find( + (model) => model.model_id === deploymentStat.model_id + ); + + if (deployedModel) { + deployedModel.stats = { + ...(deployedModel.stats ?? {}), + deployment_stats: omit(deploymentStat, 'model_id'), + }; + } + } + } catch (error) { + displayErrorToast( + error, + i18n.translate('xpack.ml.trainedModels.modelsList.fetchDeploymentStatsErrorMessage', { + defaultMessage: 'Fetch deployment stats failed', + }) + ); + } + }, []); + /** * Unique inference types from models */ @@ -361,12 +404,19 @@ export const ModelsList: FC = () => { description: i18n.translate('xpack.ml.inference.modelsList.startModelAllocationActionLabel', { defaultMessage: 'Start allocation', }), - icon: 'download', + icon: 'play', type: 'icon', isPrimary: true, + enabled: (item) => { + const { state } = item.stats?.deployment_stats ?? {}; + return ( + !isLoading && state !== DEPLOYMENT_STATE.STARTED && state !== DEPLOYMENT_STATE.STARTING + ); + }, available: (item) => item.model_type === 'pytorch', onClick: async (item) => { try { + setIsLoading(true); await trainedModelsApiService.startModelAllocation(item.model_id); displaySuccessToast( i18n.translate('xpack.ml.trainedModels.modelsList.startSuccess', { @@ -376,6 +426,7 @@ export const ModelsList: FC = () => { }, }) ); + await fetchModelsData(); } catch (e) { displayErrorToast( e, @@ -386,6 +437,7 @@ export const ModelsList: FC = () => { }, }) ); + setIsLoading(false); } }, }, @@ -400,9 +452,14 @@ export const ModelsList: FC = () => { type: 'icon', isPrimary: true, available: (item) => item.model_type === 'pytorch', - enabled: (item) => !isPopulatedObject(item.pipelines), + enabled: (item) => + !isLoading && + !isPopulatedObject(item.pipelines) && + isPopulatedObject(item.stats?.deployment_stats) && + item.stats?.deployment_stats?.state !== DEPLOYMENT_STATE.STOPPING, onClick: async (item) => { try { + setIsLoading(true); await trainedModelsApiService.stopModelAllocation(item.model_id); displaySuccessToast( i18n.translate('xpack.ml.trainedModels.modelsList.stopSuccess', { @@ -412,6 +469,8 @@ export const ModelsList: FC = () => { }, }) ); + // Need to fetch model state updates + await fetchModelsData(); } catch (e) { displayErrorToast( e, @@ -422,6 +481,7 @@ export const ModelsList: FC = () => { }, }) ); + setIsLoading(false); } }, }, @@ -521,13 +581,25 @@ export const ModelsList: FC = () => { ), 'data-test-subj': 'mlModelsTableColumnType', }, + { + name: i18n.translate('xpack.ml.trainedModels.modelsList.stateHeader', { + defaultMessage: 'State', + }), + sortable: (item) => item.stats?.deployment_stats?.state, + align: 'left', + render: (model: ModelItem) => { + const state = model.stats?.deployment_stats?.state; + return state ? {state} : null; + }, + 'data-test-subj': 'mlModelsTableColumnDeploymentState', + }, { field: ModelsTableToConfigMapping.createdAt, name: i18n.translate('xpack.ml.trainedModels.modelsList.createdAtHeader', { defaultMessage: 'Created at', }), dataType: 'date', - render: timeFormatter, + render: (v: number) => dateFormatter(v), sortable: true, 'data-test-subj': 'mlModelsTableColumnCreatedAt', }, diff --git a/x-pack/plugins/ml/public/application/trained_models/nodes_overview/allocated_models.tsx b/x-pack/plugins/ml/public/application/trained_models/nodes_overview/allocated_models.tsx new file mode 100644 index 0000000000000..2aad8183b7998 --- /dev/null +++ b/x-pack/plugins/ml/public/application/trained_models/nodes_overview/allocated_models.tsx @@ -0,0 +1,131 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import React, { FC } from 'react'; +import { EuiBadge, EuiInMemoryTable, EuiToolTip } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { EuiBasicTableColumn } from '@elastic/eui/src/components/basic_table/basic_table'; +import type { + AllocatedModel, + NodeDeploymentStatsResponse, +} from '../../../../common/types/trained_models'; +import { useFieldFormatter } from '../../contexts/kibana/use_field_formatter'; +import { FIELD_FORMAT_IDS } from '../../../../../../../src/plugins/field_formats/common'; + +interface AllocatedModelsProps { + models: NodeDeploymentStatsResponse['allocated_models']; +} + +export const AllocatedModels: FC = ({ models }) => { + const bytesFormatter = useFieldFormatter(FIELD_FORMAT_IDS.BYTES); + const dateFormatter = useFieldFormatter(FIELD_FORMAT_IDS.DATE); + const durationFormatter = useFieldFormatter(FIELD_FORMAT_IDS.DURATION); + + const columns: Array> = [ + { + field: 'model_id', + name: i18n.translate('xpack.ml.trainedModels.nodesList.modelsList.modelNameHeader', { + defaultMessage: 'Name', + }), + width: '300px', + sortable: true, + truncateText: false, + 'data-test-subj': 'mlAllocatedModelsTableName', + }, + { + name: i18n.translate('xpack.ml.trainedModels.nodesList.modelsList.modelSizeHeader', { + defaultMessage: 'Size', + }), + width: '100px', + truncateText: true, + 'data-test-subj': 'mlAllocatedModelsTableSize', + render: (v: AllocatedModel) => { + return bytesFormatter(v.model_size_bytes); + }, + }, + { + field: 'state', + name: i18n.translate('xpack.ml.trainedModels.nodesList.modelsList.modelStateHeader', { + defaultMessage: 'State', + }), + width: '100px', + truncateText: false, + 'data-test-subj': 'mlAllocatedModelsTableState', + }, + { + name: i18n.translate( + 'xpack.ml.trainedModels.nodesList.modelsList.modelAvgInferenceTimeHeader', + { + defaultMessage: 'Avg inference time', + } + ), + width: '100px', + truncateText: false, + 'data-test-subj': 'mlAllocatedModelsTableAvgInferenceTime', + render: (v: AllocatedModel) => { + return v.node.average_inference_time_ms + ? durationFormatter(v.node.average_inference_time_ms) + : '-'; + }, + }, + { + name: i18n.translate( + 'xpack.ml.trainedModels.nodesList.modelsList.modelInferenceCountHeader', + { + defaultMessage: 'Inference count', + } + ), + width: '100px', + 'data-test-subj': 'mlAllocatedModelsTableInferenceCount', + render: (v: AllocatedModel) => { + return v.node.inference_count; + }, + }, + { + name: i18n.translate('xpack.ml.trainedModels.nodesList.modelsList.modelLastAccessHeader', { + defaultMessage: 'Last access', + }), + width: '200px', + 'data-test-subj': 'mlAllocatedModelsTableInferenceCount', + render: (v: AllocatedModel) => { + return dateFormatter(v.node.last_access); + }, + }, + { + name: i18n.translate('xpack.ml.trainedModels.nodesList.modelsList.modelRoutingStateHeader', { + defaultMessage: 'Routing state', + }), + width: '100px', + 'data-test-subj': 'mlAllocatedModelsTableRoutingState', + render: (v: AllocatedModel) => { + const { routing_state: routingState, reason } = v.node.routing_state; + + return ( + + {routingState} + + ); + }, + }, + ]; + + return ( + + allowNeutralSort={false} + columns={columns} + hasActions={false} + isExpandable={false} + isSelectable={false} + items={models} + itemId={'model_id'} + rowProps={(item) => ({ + 'data-test-subj': `mlAllocatedModelTableRow row-${item.model_id}`, + })} + onTableChange={() => {}} + data-test-subj={'mlNodesTable'} + /> + ); +}; diff --git a/x-pack/plugins/ml/public/application/trained_models/nodes_overview/expanded_row.tsx b/x-pack/plugins/ml/public/application/trained_models/nodes_overview/expanded_row.tsx index a32747185dcc8..508a5689e1c9b 100644 --- a/x-pack/plugins/ml/public/application/trained_models/nodes_overview/expanded_row.tsx +++ b/x-pack/plugins/ml/public/application/trained_models/nodes_overview/expanded_row.tsx @@ -9,17 +9,15 @@ import React, { FC } from 'react'; import { EuiDescriptionList, EuiFlexGrid, - EuiFlexGroup, EuiFlexItem, - EuiHorizontalRule, EuiPanel, EuiSpacer, - EuiTextColor, EuiTitle, } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; import { NodeItemWithStats } from './nodes_list'; import { formatToListItems } from '../models_management/expanded_row'; +import { AllocatedModels } from './allocated_models'; interface ExpandedRowProps { item: NodeItemWithStats; @@ -55,8 +53,6 @@ export const ExpandedRow: FC = ({ item }) => { listItems={formatToListItems(details)} /> - -
    @@ -76,10 +72,10 @@ export const ExpandedRow: FC = ({ item }) => { listItems={formatToListItems(attributes)} /> + - - - {allocatedModels.length > 0 ? ( + {allocatedModels.length > 0 ? ( +
    @@ -91,34 +87,10 @@ export const ExpandedRow: FC = ({ item }) => { - {allocatedModels.map(({ model_id: modelId, ...rest }) => { - return ( - <> - - - - -
    {modelId}
    -
    -
    -
    - - - -
    - - - - - ); - })} + - ) : null} - + + ) : null} ); diff --git a/x-pack/plugins/ml/public/application/trained_models/nodes_overview/memory_preview_chart.tsx b/x-pack/plugins/ml/public/application/trained_models/nodes_overview/memory_preview_chart.tsx index ba790ba1c2576..dd9b6f8253860 100644 --- a/x-pack/plugins/ml/public/application/trained_models/nodes_overview/memory_preview_chart.tsx +++ b/x-pack/plugins/ml/public/application/trained_models/nodes_overview/memory_preview_chart.tsx @@ -8,25 +8,26 @@ import { i18n } from '@kbn/i18n'; import React, { FC, useMemo } from 'react'; import { - Chart, - Settings, - BarSeries, - ScaleType, Axis, + BarSeries, + Chart, Position, + ScaleType, SeriesColorAccessor, + Settings, } from '@elastic/charts'; import { euiPaletteGray } from '@elastic/eui'; import { NodeDeploymentStatsResponse } from '../../../../common/types/trained_models'; import { useFieldFormatter } from '../../contexts/kibana/use_field_formatter'; import { useCurrentEuiTheme } from '../../components/color_range_legend'; +import { FIELD_FORMAT_IDS } from '../../../../../../../src/plugins/field_formats/common'; interface MemoryPreviewChartProps { memoryOverview: NodeDeploymentStatsResponse['memory_overview']; } export const MemoryPreviewChart: FC = ({ memoryOverview }) => { - const bytesFormatter = useFieldFormatter('bytes'); + const bytesFormatter = useFieldFormatter(FIELD_FORMAT_IDS.BYTES); const { euiTheme } = useCurrentEuiTheme(); @@ -112,7 +113,7 @@ export const MemoryPreviewChart: FC = ({ memoryOverview tooltip={{ headerFormatter: ({ value }) => i18n.translate('xpack.ml.trainedModels.nodesList.memoryBreakdown', { - defaultMessage: 'Approximate memory breakdown based on the node info', + defaultMessage: 'Approximate memory breakdown', }), }} /> diff --git a/x-pack/plugins/ml/public/application/trained_models/nodes_overview/nodes_list.tsx b/x-pack/plugins/ml/public/application/trained_models/nodes_overview/nodes_list.tsx index 42e51f1ab2971..b1cc18e698c9d 100644 --- a/x-pack/plugins/ml/public/application/trained_models/nodes_overview/nodes_list.tsx +++ b/x-pack/plugins/ml/public/application/trained_models/nodes_overview/nodes_list.tsx @@ -4,7 +4,7 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -import React, { FC, useCallback, useMemo, useState } from 'react'; +import React, { FC, useCallback, useEffect, useMemo, useState } from 'react'; import { EuiButtonIcon, EuiFlexGroup, @@ -31,6 +31,8 @@ import { MemoryPreviewChart } from './memory_preview_chart'; import { useFieldFormatter } from '../../contexts/kibana/use_field_formatter'; import { ListingPageUrlState } from '../../../../common/types/common'; import { useToastNotificationService } from '../../services/toast_notification_service'; +import { FIELD_FORMAT_IDS } from '../../../../../../../src/plugins/field_formats/common'; +import { useRefresh } from '../../routing/use_refresh'; export type NodeItem = NodeDeploymentStatsResponse; @@ -47,8 +49,11 @@ export const getDefaultNodesListState = (): ListingPageUrlState => ({ export const NodesList: FC = () => { const trainedModelsApiService = useTrainedModelsApiService(); + + const refresh = useRefresh(); + const { displayErrorToast } = useToastNotificationService(); - const bytesFormatter = useFieldFormatter('bytes'); + const bytesFormatter = useFieldFormatter(FIELD_FORMAT_IDS.BYTES); const [items, setItems] = useState([]); const [isLoading, setIsLoading] = useState(false); const [itemIdToExpandedRowMap, setItemIdToExpandedRowMap] = useState>( @@ -179,6 +184,13 @@ export const NodesList: FC = () => { onRefresh: fetchNodesData, }); + useEffect( + function updateOnTimerRefresh() { + fetchNodesData(); + }, + [refresh] + ); + return ( <> diff --git a/x-pack/plugins/ml/public/application/trained_models/page.tsx b/x-pack/plugins/ml/public/application/trained_models/page.tsx index a6d99ca0fedc0..54849f3e651df 100644 --- a/x-pack/plugins/ml/public/application/trained_models/page.tsx +++ b/x-pack/plugins/ml/public/application/trained_models/page.tsx @@ -10,6 +10,7 @@ import React, { FC, Fragment, useMemo } from 'react'; import { FormattedMessage } from '@kbn/i18n/react'; import { + EuiBetaBadge, EuiFlexGroup, EuiFlexItem, EuiPage, @@ -21,6 +22,7 @@ import { } from '@elastic/eui'; import { useLocation } from 'react-router-dom'; +import { i18n } from '@kbn/i18n'; import { NavigationMenu } from '../components/navigation_menu'; import { ModelsList } from './models_management'; import { TrainedModelsNavigationBar } from './navigation_bar'; @@ -44,14 +46,35 @@ export const Page: FC = () => { - -

    - + + +

    + +

    +
    +
    + + -

    -
    + +
    diff --git a/x-pack/plugins/ml/public/application/util/component_utils.ts b/x-pack/plugins/ml/public/application/util/component_utils.ts new file mode 100644 index 0000000000000..764e4f0edd83b --- /dev/null +++ b/x-pack/plugins/ml/public/application/util/component_utils.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 { MouseEvent } from 'react'; + +/** + * Removes focus from a button element when clicked, for example to + * ensure a wrapping tooltip is hidden on click. + */ +export const blurButtonOnClick = (callback: Function) => (event: MouseEvent) => { + (event.target as HTMLButtonElement).blur(); + callback(); +}; diff --git a/x-pack/plugins/ml/public/locator/formatters/trained_models.ts b/x-pack/plugins/ml/public/locator/formatters/trained_models.ts index d084c0675769f..9e1c5d92ce451 100644 --- a/x-pack/plugins/ml/public/locator/formatters/trained_models.ts +++ b/x-pack/plugins/ml/public/locator/formatters/trained_models.ts @@ -5,8 +5,13 @@ * 2.0. */ -import { TrainedModelsUrlState } from '../../../common/types/locator'; +import type { + TrainedModelsNodesUrlState, + TrainedModelsUrlState, +} from '../../../common/types/locator'; import { ML_PAGES } from '../../../common/constants/locator'; +import type { AppPageState, ListingPageUrlState } from '../../../common/types/common'; +import { setStateToKbnUrl } from '../../../../../../src/plugins/kibana_utils/public'; export function formatTrainedModelsManagementUrl( appBasePath: string, @@ -14,3 +19,31 @@ export function formatTrainedModelsManagementUrl( ): string { return `${appBasePath}/${ML_PAGES.TRAINED_MODELS_MANAGE}`; } + +export function formatTrainedModelsNodesManagementUrl( + appBasePath: string, + mlUrlGeneratorState: TrainedModelsNodesUrlState['pageState'] +): string { + let url = `${appBasePath}/${ML_PAGES.TRAINED_MODELS_NODES}`; + if (mlUrlGeneratorState) { + const { nodeId } = mlUrlGeneratorState; + if (nodeId) { + const nodesListState: Partial = { + queryText: `name:(${nodeId})`, + }; + + const queryState: AppPageState = { + [ML_PAGES.TRAINED_MODELS_NODES]: nodesListState, + }; + + url = setStateToKbnUrl>( + '_a', + queryState, + { useHash: false, storeInHashQuery: false }, + url + ); + } + } + + return url; +} diff --git a/x-pack/plugins/ml/public/locator/ml_locator.ts b/x-pack/plugins/ml/public/locator/ml_locator.ts index 7fa573c3e653d..c79c93078d04a 100644 --- a/x-pack/plugins/ml/public/locator/ml_locator.ts +++ b/x-pack/plugins/ml/public/locator/ml_locator.ts @@ -26,7 +26,10 @@ import { formatEditCalendarUrl, formatEditFilterUrl, } from './formatters'; -import { formatTrainedModelsManagementUrl } from './formatters/trained_models'; +import { + formatTrainedModelsManagementUrl, + formatTrainedModelsNodesManagementUrl, +} from './formatters/trained_models'; export type { MlLocatorParams, MlLocator }; @@ -70,6 +73,9 @@ export class MlLocatorDefinition implements LocatorDefinition { case ML_PAGES.TRAINED_MODELS_MANAGE: path = formatTrainedModelsManagementUrl('', params.pageState); break; + case ML_PAGES.TRAINED_MODELS_NODES: + path = formatTrainedModelsNodesManagementUrl('', params.pageState); + break; case ML_PAGES.ANOMALY_DETECTION_CREATE_JOB: case ML_PAGES.ANOMALY_DETECTION_CREATE_JOB_ADVANCED: case ML_PAGES.DATA_VISUALIZER: diff --git a/x-pack/plugins/ml/server/models/data_recognizer/data_recognizer.ts b/x-pack/plugins/ml/server/models/data_recognizer/data_recognizer.ts index 9e02a93a3c0f1..1cd9aae79777b 100644 --- a/x-pack/plugins/ml/server/models/data_recognizer/data_recognizer.ts +++ b/x-pack/plugins/ml/server/models/data_recognizer/data_recognizer.ts @@ -293,8 +293,6 @@ export class DataRecognizer { index, size, body: searchBody, - // Ignored indices that are frozen - ignore_throttled: true, }); // @ts-expect-error incorrect search response type diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/apm_transaction/manifest.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/apm_transaction/manifest.json index f8feaef3be5f8..3332fad66b3e2 100644 --- a/x-pack/plugins/ml/server/models/data_recognizer/modules/apm_transaction/manifest.json +++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/apm_transaction/manifest.json @@ -1,29 +1,29 @@ { "id": "apm_transaction", "title": "APM", - "description": "Detect anomalies in transactions from your APM services.", + "description": "Detect anomalies in transaction latency, throughput and failure rate from your APM services for metric data.", "type": "Transaction data", "logoFile": "logo.json", - "defaultIndexPattern": "apm-*-transaction", + "defaultIndexPattern": "apm-*-metric,metrics-apm*", "query": { "bool": { "filter": [ - { "term": { "processor.event": "transaction" } }, - { "exists": { "field": "transaction.duration" } } + { "term": { "processor.event": "metric" } }, + { "term": { "metricset.name": "transaction" } } ] } }, "jobs": [ { - "id": "high_mean_transaction_duration", - "file": "high_mean_transaction_duration.json" + "id": "apm_tx_metrics", + "file": "apm_tx_metrics.json" } ], "datafeeds": [ { - "id": "datafeed-high_mean_transaction_duration", - "file": "datafeed_high_mean_transaction_duration.json", - "job_id": "high_mean_transaction_duration" + "id": "datafeed-apm_tx_metrics", + "file": "datafeed_apm_tx_metrics.json", + "job_id": "apm_tx_metrics" } ] } diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/apm_transaction/ml/apm_tx_metrics.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/apm_transaction/ml/apm_tx_metrics.json new file mode 100644 index 0000000000000..f93b4fb009a14 --- /dev/null +++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/apm_transaction/ml/apm_tx_metrics.json @@ -0,0 +1,53 @@ +{ + "job_type": "anomaly_detector", + "groups": [ + "apm" + ], + "description": "Detects anomalies in transaction latency, throughput and error percentage for metric data.", + "analysis_config": { + "bucket_span": "15m", + "summary_count_field_name" : "doc_count", + "detectors" : [ + { + "detector_description" : "high latency by transaction type for an APM service", + "function" : "high_mean", + "field_name" : "transaction_latency", + "by_field_name" : "transaction.type", + "partition_field_name" : "service.name" + }, + { + "detector_description" : "transaction throughput for an APM service", + "function" : "mean", + "field_name" : "transaction_throughput", + "by_field_name" : "transaction.type", + "partition_field_name" : "service.name" + }, + { + "detector_description" : "failed transaction rate for an APM service", + "function" : "high_mean", + "field_name" : "failed_transaction_rate", + "by_field_name" : "transaction.type", + "partition_field_name" : "service.name" + } + ], + "influencers" : [ + "transaction.type", + "service.name" + ] + }, + "analysis_limits": { + "model_memory_limit": "32mb" + }, + "data_description": { + "time_field" : "@timestamp", + "time_format" : "epoch_ms" + }, + "model_plot_config": { + "enabled" : true, + "annotations_enabled" : true + }, + "results_index_name" : "custom-apm", + "custom_settings": { + "created_by": "ml-module-apm-transaction" + } +} diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/apm_transaction/ml/datafeed_apm_tx_metrics.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/apm_transaction/ml/datafeed_apm_tx_metrics.json new file mode 100644 index 0000000000000..4d19cdc9f533d --- /dev/null +++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/apm_transaction/ml/datafeed_apm_tx_metrics.json @@ -0,0 +1,98 @@ +{ + "job_id": "JOB_ID", + "indices": [ + "INDEX_PATTERN_NAME" + ], + "chunking_config" : { + "mode" : "off" + }, + "query": { + "bool": { + "filter": [ + { "term": { "processor.event": "metric" } }, + { "term": { "metricset.name": "transaction" } } + ] + } + }, + "aggregations" : { + "buckets" : { + "composite" : { + "size" : 5000, + "sources" : [ + { + "date" : { + "date_histogram" : { + "field" : "@timestamp", + "fixed_interval" : "90s" + } + } + }, + { + "transaction.type" : { + "terms" : { + "field" : "transaction.type" + } + } + }, + { + "service.name" : { + "terms" : { + "field" : "service.name" + } + } + } + ] + }, + "aggs" : { + "@timestamp" : { + "max" : { + "field" : "@timestamp" + } + }, + "transaction_throughput" : { + "rate" : { + "unit" : "minute" + } + }, + "transaction_latency" : { + "avg" : { + "field" : "transaction.duration.histogram" + } + }, + "error_count" : { + "filter" : { + "term" : { + "event.outcome" : "failure" + } + }, + "aggs" : { + "actual_error_count" : { + "value_count" : { + "field" : "event.outcome" + } + } + } + }, + "success_count" : { + "filter" : { + "term" : { + "event.outcome" : "success" + } + } + }, + "failed_transaction_rate" : { + "bucket_script" : { + "buckets_path" : { + "failure_count" : "error_count>_count", + "success_count" : "success_count>_count" + }, + "script" : "if ((params.failure_count + params.success_count)==0){return 0;}else{return 100 * (params.failure_count/(params.failure_count + params.success_count));}" + } + } + } + } + }, + "indices_options": { + "ignore_unavailable": true + } +} diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/apm_transaction/ml/datafeed_high_mean_transaction_duration.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/apm_transaction/ml/datafeed_high_mean_transaction_duration.json deleted file mode 100644 index d312577902f51..0000000000000 --- a/x-pack/plugins/ml/server/models/data_recognizer/modules/apm_transaction/ml/datafeed_high_mean_transaction_duration.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "job_id": "JOB_ID", - "indices": [ - "INDEX_PATTERN_NAME" - ], - "query": { - "bool": { - "filter": [ - { "term": { "processor.event": "transaction" } }, - { "exists": { "field": "transaction.duration.us" } } - ] - } - } -} diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/apm_transaction/ml/high_mean_transaction_duration.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/apm_transaction/ml/high_mean_transaction_duration.json deleted file mode 100644 index 77284cb275cd8..0000000000000 --- a/x-pack/plugins/ml/server/models/data_recognizer/modules/apm_transaction/ml/high_mean_transaction_duration.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "job_type": "anomaly_detector", - "groups": [ - "apm" - ], - "description": "Detect transaction duration anomalies across transaction types for your APM services.", - "analysis_config": { - "bucket_span": "15m", - "detectors": [ - { - "detector_description": "high duration by transaction type for an APM service", - "function": "high_mean", - "field_name": "transaction.duration.us", - "by_field_name": "transaction.type", - "partition_field_name": "service.name" - } - ], - "influencers": [ - "transaction.type", - "service.name" - ] - }, - "analysis_limits": { - "model_memory_limit": "32mb" - }, - "data_description": { - "time_field": "@timestamp" - }, - "model_plot_config": { - "enabled": true - }, - "custom_settings": { - "created_by": "ml-module-apm-transaction" - } -} diff --git a/x-pack/plugins/ml/server/routes/apidoc.json b/x-pack/plugins/ml/server/routes/apidoc.json index 77e5443d0a257..b7bd92c913891 100644 --- a/x-pack/plugins/ml/server/routes/apidoc.json +++ b/x-pack/plugins/ml/server/routes/apidoc.json @@ -1,6 +1,6 @@ { "name": "ml_kibana_api", - "version": "7.15.0", + "version": "8.0.0", "description": "This is the documentation of the REST API provided by the Machine Learning Kibana plugin. Each API is experimental and can include breaking changes in any version.", "title": "ML Kibana API", "order": [ diff --git a/x-pack/plugins/ml/server/routes/apidoc_scripts/template.md b/x-pack/plugins/ml/server/routes/apidoc_scripts/template.md index 72104e3e433da..11a469bfeec5d 100644 --- a/x-pack/plugins/ml/server/routes/apidoc_scripts/template.md +++ b/x-pack/plugins/ml/server/routes/apidoc_scripts/template.md @@ -3,7 +3,7 @@ <% } -%> -# <%= project.name %> v<%= project.version %> +v<%= project.version %> <%= project.description %> @@ -24,7 +24,7 @@ ``` <% if (sub.header && sub.header.fields && sub.header.fields.Header.length) { -%> -#### Headers +##### Headers | Name | Type | Description | |---------|-----------|--------------------------------------| <% sub.header.fields.Header.forEach(header => { -%> @@ -33,7 +33,7 @@ <% } // if parameters -%> <% if (sub.header && sub.header.examples && sub.header.examples.length) { -%> -#### Header examples +##### Header examples <% sub.header.examples.forEach(example => { -%> <%= example.title %> @@ -45,7 +45,7 @@ <% if (sub.parameter && sub.parameter.fields) { -%> <% Object.keys(sub.parameter.fields).forEach(g => { -%> -#### Parameters - `<%= g -%>` +##### Parameters - `<%= g -%>` | Name | Type | Description | |:---------|:-----------|:--------------------------------------| <% sub.parameter.fields[g].forEach(param => { -%> @@ -61,7 +61,7 @@ _Allowed values: <%- param.allowedValues %>_<% } -%> | <% } // if parameters -%> <% if (sub.examples && sub.examples.length) { -%> -#### Examples +##### Examples <% sub.examples.forEach(example => { -%> <%= example.title %> @@ -72,7 +72,7 @@ _Allowed values: <%- param.allowedValues %>_<% } -%> | <% } // if example -%> <% if (sub.parameter && sub.parameter.examples && sub.parameter.examples.length) { -%> -#### Parameters examples +##### Parameters examples <% sub.parameter.examples.forEach(exampleParam => { -%> `<%= exampleParam.type %>` - <%= exampleParam.title %> @@ -83,10 +83,10 @@ _Allowed values: <%- param.allowedValues %>_<% } -%> | <% } // if exampleParam -%> <% if (sub.success && sub.success.fields) { -%> -#### Success response +##### Success response <% Object.keys(sub.success.fields).forEach(g => { -%> -##### Success response - `<%= g %>` +###### Success response - `<%= g %>` | Name | Type | Description | |:---------|:-----------|:--------------------------------------| <% sub.success.fields[g].forEach(param => { -%> @@ -102,10 +102,10 @@ _Allowed values: <%- param.allowedValues %>_<% } -%> | <% } // if success.fields -%> <% if (sub.success && sub.success.examples && sub.success.examples.length) { -%> -#### Success response example +##### Success response example <% sub.success.examples.forEach(example => { -%> -##### Success response example - `<%= example.title %>` +###### Success response example - `<%= example.title %>` ``` <%- example.content %> @@ -114,10 +114,10 @@ _Allowed values: <%- param.allowedValues %>_<% } -%> | <% } // if success.examples -%> <% if (sub.error && sub.error.fields) { -%> -#### Error response +##### Error response <% Object.keys(sub.error.fields).forEach(g => { -%> -##### Error response - `<%= g %>` +###### Error response - `<%= g %>` | Name | Type | Description | |:---------|:-----------|:--------------------------------------| <% sub.error.fields[g].forEach(param => { -%> @@ -133,10 +133,10 @@ _Allowed values: <%- param.allowedValues %>_<% } -%> | <% } // if error.fields -%> <% if (sub.error && sub.error.examples && sub.error.examples.length) { -%> -#### Error response example +##### Error response example <% sub.error.examples.forEach(example => { -%> -##### Error response example - `<%= example.title %>` +###### Error response example - `<%= example.title %>` ``` <%- example.content %> diff --git a/x-pack/plugins/monitoring/public/alerts/ccr_read_exceptions_alert/index.tsx b/x-pack/plugins/monitoring/public/alerts/ccr_read_exceptions_alert/index.tsx index 64eab04cbd5ce..cd09185b8ad93 100644 --- a/x-pack/plugins/monitoring/public/alerts/ccr_read_exceptions_alert/index.tsx +++ b/x-pack/plugins/monitoring/public/alerts/ccr_read_exceptions_alert/index.tsx @@ -5,17 +5,20 @@ * 2.0. */ -import React from 'react'; import { i18n } from '@kbn/i18n'; -import { Expression, Props } from '../components/param_details_form/expression'; -import { AlertTypeModel, ValidationResult } from '../../../../triggers_actions_ui/public'; +import React from 'react'; +import type { AlertTypeParams } from '../../../../alerting/common'; +import type { AlertTypeModel, ValidationResult } from '../../../../triggers_actions_ui/public'; import { RULE_CCR_READ_EXCEPTIONS, RULE_DETAILS, RULE_REQUIRES_APP_CONTEXT, } from '../../../common/constants'; -import { AlertTypeParams } from '../../../../alerting/common'; -import { MonitoringConfig } from '../../types'; +import type { MonitoringConfig } from '../../types'; +import { + LazyExpression, + LazyExpressionProps, +} from '../components/param_details_form/lazy_expression'; interface ValidateOptions extends AlertTypeParams { duration: string; @@ -47,8 +50,8 @@ export function createCCRReadExceptionsAlertType( documentationUrl(docLinks) { return `${docLinks.links.monitoring.alertsKibanaCCRReadExceptions}`; }, - alertParamsExpression: (props: Props) => ( - ( + = (props) => { ); }; + +// for lazy loading +// eslint-disable-next-line import/no-default-export +export default Expression; diff --git a/x-pack/plugins/monitoring/public/alerts/components/param_details_form/lazy_expression.tsx b/x-pack/plugins/monitoring/public/alerts/components/param_details_form/lazy_expression.tsx new file mode 100644 index 0000000000000..b05eb57921b81 --- /dev/null +++ b/x-pack/plugins/monitoring/public/alerts/components/param_details_form/lazy_expression.tsx @@ -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 React from 'react'; + +export type { Props as LazyExpressionProps } from './expression'; +export const LazyExpression = React.lazy(() => import('./expression')); diff --git a/x-pack/plugins/monitoring/public/alerts/cpu_usage_alert/cpu_usage_alert.tsx b/x-pack/plugins/monitoring/public/alerts/cpu_usage_alert/cpu_usage_alert.tsx index f0e0a413435f9..337c3c8e1c496 100644 --- a/x-pack/plugins/monitoring/public/alerts/cpu_usage_alert/cpu_usage_alert.tsx +++ b/x-pack/plugins/monitoring/public/alerts/cpu_usage_alert/cpu_usage_alert.tsx @@ -6,12 +6,14 @@ */ import React from 'react'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { AlertTypeModel } from '../../../../triggers_actions_ui/public/types'; +import type { AlertTypeModel } from '../../../../triggers_actions_ui/public'; import { RULE_CPU_USAGE, RULE_DETAILS, RULE_REQUIRES_APP_CONTEXT } from '../../../common/constants'; -import { validate, MonitoringAlertTypeParams } from '../components/param_details_form/validation'; -import { Expression, Props } from '../components/param_details_form/expression'; -import { MonitoringConfig } from '../../types'; +import type { MonitoringConfig } from '../../types'; +import { + LazyExpression, + LazyExpressionProps, +} from '../components/param_details_form/lazy_expression'; +import { MonitoringAlertTypeParams, validate } from '../components/param_details_form/validation'; export function createCpuUsageAlertType( config: MonitoringConfig @@ -23,8 +25,8 @@ export function createCpuUsageAlertType( documentationUrl(docLinks) { return `${docLinks.links.monitoring.alertsKibanaCpuThreshold}`; }, - alertParamsExpression: (props: Props) => ( - ( + ( - ( + ( - ( + ); }; + +// for lazy loading +// eslint-disable-next-line import/no-default-export +export default Expression; diff --git a/x-pack/plugins/monitoring/public/alerts/legacy_alert/lazy_expression.tsx b/x-pack/plugins/monitoring/public/alerts/legacy_alert/lazy_expression.tsx new file mode 100644 index 0000000000000..7479c932e53ee --- /dev/null +++ b/x-pack/plugins/monitoring/public/alerts/legacy_alert/lazy_expression.tsx @@ -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 React from 'react'; + +export type { Props as LazyExpressionProps } from '../components/param_details_form/expression'; +export const LazyExpression = React.lazy(() => import('./expression')); diff --git a/x-pack/plugins/monitoring/public/alerts/legacy_alert/legacy_alert.tsx b/x-pack/plugins/monitoring/public/alerts/legacy_alert/legacy_alert.tsx index a6c22035c5a5a..f7bf8388886d6 100644 --- a/x-pack/plugins/monitoring/public/alerts/legacy_alert/legacy_alert.tsx +++ b/x-pack/plugins/monitoring/public/alerts/legacy_alert/legacy_alert.tsx @@ -6,16 +6,14 @@ */ import React from 'react'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { AlertTypeModel } from '../../../../triggers_actions_ui/public/types'; +import type { AlertTypeModel } from '../../../../triggers_actions_ui/public'; import { LEGACY_RULES, LEGACY_RULE_DETAILS, RULE_REQUIRES_APP_CONTEXT, } from '../../../common/constants'; -import { MonitoringConfig } from '../../types'; -import { Expression } from './expression'; -import { Props } from '../components/param_details_form/expression'; +import type { MonitoringConfig } from '../../types'; +import { LazyExpression, LazyExpressionProps } from './lazy_expression'; export function createLegacyAlertTypes(config: MonitoringConfig): AlertTypeModel[] { return LEGACY_RULES.map((legacyAlert) => { @@ -26,7 +24,9 @@ export function createLegacyAlertTypes(config: MonitoringConfig): AlertTypeModel documentationUrl(docLinks) { return `${docLinks.links.monitoring.alertsKibanaClusterAlerts}`; }, - alertParamsExpression: (props: Props) => , + alertParamsExpression: (props: LazyExpressionProps) => ( + + ), defaultActionMessage: '{{context.internalFullMessage}}', validate: () => ({ errors: {} }), requiresAppContext: RULE_REQUIRES_APP_CONTEXT, diff --git a/x-pack/plugins/monitoring/public/alerts/lib/alerts_toast.tsx b/x-pack/plugins/monitoring/public/alerts/lib/alerts_toast.tsx index d752ec154089b..10c8f7155134b 100644 --- a/x-pack/plugins/monitoring/public/alerts/lib/alerts_toast.tsx +++ b/x-pack/plugins/monitoring/public/alerts/lib/alerts_toast.tsx @@ -18,7 +18,7 @@ export interface EnableAlertResponse { disabledWatcherClusterAlerts?: boolean; } -const showTlsAndEncryptionError = () => { +const showApiKeyAndEncryptionError = () => { const settingsUrl = Legacy.shims.docLinks.links.alerting.generalSettings; Legacy.shims.toastNotifications.addWarning({ @@ -32,7 +32,7 @@ const showTlsAndEncryptionError = () => {

    {i18n.translate('xpack.monitoring.healthCheck.tlsAndEncryptionError', { - defaultMessage: `Stack monitoring alerts require Transport Layer Security between Kibana and Elasticsearch, and an encryption key in your kibana.yml file.`, + defaultMessage: `Stack Monitoring rules require API keys to be enabled and an encryption key to be configured.`, })}

    @@ -97,7 +97,7 @@ export const showAlertsToast = (response: EnableAlertResponse) => { response; if (isSufficientlySecure === false || hasPermanentEncryptionKey === false) { - showTlsAndEncryptionError(); + showApiKeyAndEncryptionError(); } else if (disabledWatcherClusterAlerts === false) { showUnableToDisableWatcherClusterAlertsError(); } else if (disabledWatcherClusterAlerts === true) { diff --git a/x-pack/plugins/monitoring/public/alerts/memory_usage_alert/index.tsx b/x-pack/plugins/monitoring/public/alerts/memory_usage_alert/index.tsx index 2fe0c9b77c0eb..18193fca860fa 100644 --- a/x-pack/plugins/monitoring/public/alerts/memory_usage_alert/index.tsx +++ b/x-pack/plugins/monitoring/public/alerts/memory_usage_alert/index.tsx @@ -6,17 +6,18 @@ */ import React from 'react'; -import { validate, MonitoringAlertTypeParams } from '../components/param_details_form/validation'; -import { Expression, Props } from '../components/param_details_form/expression'; - -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { AlertTypeModel } from '../../../../triggers_actions_ui/public/types'; +import type { AlertTypeModel } from '../../../../triggers_actions_ui/public'; import { - RULE_MEMORY_USAGE, RULE_DETAILS, + RULE_MEMORY_USAGE, RULE_REQUIRES_APP_CONTEXT, } from '../../../common/constants'; -import { MonitoringConfig } from '../../types'; +import type { MonitoringConfig } from '../../types'; +import { + LazyExpression, + LazyExpressionProps, +} from '../components/param_details_form/lazy_expression'; +import { MonitoringAlertTypeParams, validate } from '../components/param_details_form/validation'; export function createMemoryUsageAlertType( config: MonitoringConfig @@ -28,8 +29,8 @@ export function createMemoryUsageAlertType( documentationUrl(docLinks) { return `${docLinks.links.monitoring.alertsKibanaJvmThreshold}`; }, - alertParamsExpression: (props: Props) => ( - ( + = (props) => { ); }; + +// for lazy loading +// eslint-disable-next-line import/no-default-export +export default Expression; diff --git a/x-pack/plugins/monitoring/public/alerts/missing_monitoring_data_alert/lazy_expression.tsx b/x-pack/plugins/monitoring/public/alerts/missing_monitoring_data_alert/lazy_expression.tsx new file mode 100644 index 0000000000000..b05eb57921b81 --- /dev/null +++ b/x-pack/plugins/monitoring/public/alerts/missing_monitoring_data_alert/lazy_expression.tsx @@ -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 React from 'react'; + +export type { Props as LazyExpressionProps } from './expression'; +export const LazyExpression = React.lazy(() => import('./expression')); diff --git a/x-pack/plugins/monitoring/public/alerts/missing_monitoring_data_alert/missing_monitoring_data_alert.tsx b/x-pack/plugins/monitoring/public/alerts/missing_monitoring_data_alert/missing_monitoring_data_alert.tsx index 4c90f067d47c0..4d19a2c3865c3 100644 --- a/x-pack/plugins/monitoring/public/alerts/missing_monitoring_data_alert/missing_monitoring_data_alert.tsx +++ b/x-pack/plugins/monitoring/public/alerts/missing_monitoring_data_alert/missing_monitoring_data_alert.tsx @@ -6,15 +6,14 @@ */ import React from 'react'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { AlertTypeModel } from '../../../../triggers_actions_ui/public/types'; -import { validate } from './validation'; +import type { AlertTypeModel } from '../../../../triggers_actions_ui/public'; import { - RULE_MISSING_MONITORING_DATA, RULE_DETAILS, + RULE_MISSING_MONITORING_DATA, RULE_REQUIRES_APP_CONTEXT, } from '../../../common/constants'; -import { Expression } from './expression'; +import { LazyExpression, LazyExpressionProps } from './lazy_expression'; +import { validate } from './validation'; export function createMissingMonitoringDataAlertType(): AlertTypeModel { return { @@ -24,8 +23,8 @@ export function createMissingMonitoringDataAlertType(): AlertTypeModel { documentationUrl(docLinks) { return `${docLinks.links.monitoring.alertsKibanaMissingData}`; }, - alertParamsExpression: (props: any) => ( - ( + diff --git a/x-pack/plugins/monitoring/public/alerts/thread_pool_rejections_alert/index.tsx b/x-pack/plugins/monitoring/public/alerts/thread_pool_rejections_alert/index.tsx index e8a15ad835581..11c5d783ca439 100644 --- a/x-pack/plugins/monitoring/public/alerts/thread_pool_rejections_alert/index.tsx +++ b/x-pack/plugins/monitoring/public/alerts/thread_pool_rejections_alert/index.tsx @@ -5,15 +5,17 @@ * 2.0. */ -import React from 'react'; -import { i18n } from '@kbn/i18n'; import { EuiSpacer } from '@elastic/eui'; -import { Expression, Props } from '../components/param_details_form/expression'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { AlertTypeModel } from '../../../../triggers_actions_ui/public/types'; -import { CommonAlertParamDetails } from '../../../common/types/alerts'; +import { i18n } from '@kbn/i18n'; +import React from 'react'; +import type { AlertTypeModel } from '../../../../triggers_actions_ui/public'; import { RULE_REQUIRES_APP_CONTEXT } from '../../../common/constants'; -import { MonitoringConfig } from '../../types'; +import type { CommonAlertParamDetails } from '../../../common/types/alerts'; +import type { MonitoringConfig } from '../../types'; +import { + LazyExpression, + LazyExpressionProps, +} from '../components/param_details_form/lazy_expression'; interface ThreadPoolTypes { [key: string]: unknown; @@ -37,10 +39,14 @@ export function createThreadPoolRejectionsAlertType( documentationUrl(docLinks) { return `${docLinks.links.monitoring.alertsKibanaThreadpoolRejections}`; }, - alertParamsExpression: (props: Props) => ( + alertParamsExpression: (props: LazyExpressionProps) => ( <> - + ), validate: (inputValues: ThreadPoolTypes) => { diff --git a/x-pack/plugins/monitoring/public/plugin.ts b/x-pack/plugins/monitoring/public/plugin.ts index 78da3c57db534..d303a83bfee0c 100644 --- a/x-pack/plugins/monitoring/public/plugin.ts +++ b/x-pack/plugins/monitoring/public/plugin.ts @@ -14,21 +14,29 @@ import { Plugin, PluginInitializerContext, } from 'kibana/public'; -import { Legacy } from './legacy_shims'; -import { UsageCollectionSetup } from '../../../../src/plugins/usage_collection/public'; +import { DEFAULT_APP_CATEGORIES } from '../../../../src/core/public'; import { FeatureCatalogueCategory, HomePublicPluginSetup, } from '../../../../src/plugins/home/public'; -import { DEFAULT_APP_CATEGORIES } from '../../../../src/core/public'; -import { MonitoringStartPluginDependencies, MonitoringConfig } from './types'; +import { UsageCollectionSetup } from '../../../../src/plugins/usage_collection/public'; import { TriggersAndActionsUIPublicPluginSetup } from '../../triggers_actions_ui/public'; import { + RULE_DETAILS, RULE_THREAD_POOL_SEARCH_REJECTIONS, RULE_THREAD_POOL_WRITE_REJECTIONS, - RULE_DETAILS, } from '../common/constants'; +import { createCCRReadExceptionsAlertType } from './alerts/ccr_read_exceptions_alert'; +import { createCpuUsageAlertType } from './alerts/cpu_usage_alert'; +import { createDiskUsageAlertType } from './alerts/disk_usage_alert'; +import { createLargeShardSizeAlertType } from './alerts/large_shard_size_alert'; +import { createLegacyAlertTypes } from './alerts/legacy_alert'; +import { createMemoryUsageAlertType } from './alerts/memory_usage_alert'; +import { createMissingMonitoringDataAlertType } from './alerts/missing_monitoring_data_alert'; +import { createThreadPoolRejectionsAlertType } from './alerts/thread_pool_rejections_alert'; import { setConfig } from './external_config'; +import { Legacy } from './legacy_shims'; +import { MonitoringConfig, MonitoringStartPluginDependencies } from './types'; interface MonitoringSetupPluginDependencies { home?: HomePublicPluginSetup; @@ -42,7 +50,7 @@ export class MonitoringPlugin { constructor(private initializerContext: PluginInitializerContext) {} - public async setup( + public setup( core: CoreSetup, plugins: MonitoringSetupPluginDependencies ) { @@ -75,7 +83,7 @@ export class MonitoringPlugin }); } - await this.registerAlerts(plugins, monitoring); + this.registerAlerts(plugins, monitoring); const app: App = { id, @@ -136,27 +144,11 @@ export class MonitoringPlugin ]; } - private async registerAlerts( - plugins: MonitoringSetupPluginDependencies, - config: MonitoringConfig - ) { + private registerAlerts(plugins: MonitoringSetupPluginDependencies, config: MonitoringConfig) { const { triggersActionsUi: { ruleTypeRegistry }, } = plugins; - const { createCpuUsageAlertType } = await import('./alerts/cpu_usage_alert'); - const { createMissingMonitoringDataAlertType } = await import( - './alerts/missing_monitoring_data_alert' - ); - const { createLegacyAlertTypes } = await import('./alerts/legacy_alert'); - const { createDiskUsageAlertType } = await import('./alerts/disk_usage_alert'); - const { createThreadPoolRejectionsAlertType } = await import( - './alerts/thread_pool_rejections_alert' - ); - const { createMemoryUsageAlertType } = await import('./alerts/memory_usage_alert'); - const { createCCRReadExceptionsAlertType } = await import('./alerts/ccr_read_exceptions_alert'); - const { createLargeShardSizeAlertType } = await import('./alerts/large_shard_size_alert'); - ruleTypeRegistry.register(createCpuUsageAlertType(config)); ruleTypeRegistry.register(createDiskUsageAlertType(config)); ruleTypeRegistry.register(createMemoryUsageAlertType(config)); diff --git a/x-pack/plugins/monitoring/server/deprecations.ts b/x-pack/plugins/monitoring/server/deprecations.ts index 42868e3fa2584..14c3b9cdc6474 100644 --- a/x-pack/plugins/monitoring/server/deprecations.ts +++ b/x-pack/plugins/monitoring/server/deprecations.ts @@ -23,27 +23,39 @@ export const deprecations = ({ }: ConfigDeprecationFactory): ConfigDeprecation[] => { return [ // This order matters. The "blanket rename" needs to happen at the end - renameFromRoot('xpack.monitoring.max_bucket_size', 'monitoring.ui.max_bucket_size'), - renameFromRoot('xpack.monitoring.min_interval_seconds', 'monitoring.ui.min_interval_seconds'), + renameFromRoot('xpack.monitoring.max_bucket_size', 'monitoring.ui.max_bucket_size', { + level: 'warning', + }), + renameFromRoot('xpack.monitoring.min_interval_seconds', 'monitoring.ui.min_interval_seconds', { + level: 'warning', + }), renameFromRoot( 'xpack.monitoring.show_license_expiration', - 'monitoring.ui.show_license_expiration' + 'monitoring.ui.show_license_expiration', + { level: 'warning' } ), renameFromRoot( 'xpack.monitoring.ui.container.elasticsearch.enabled', - 'monitoring.ui.container.elasticsearch.enabled' + 'monitoring.ui.container.elasticsearch.enabled', + { level: 'warning' } ), renameFromRoot( 'xpack.monitoring.ui.container.logstash.enabled', - 'monitoring.ui.container.logstash.enabled' + 'monitoring.ui.container.logstash.enabled', + { level: 'warning' } ), - renameFromRoot('xpack.monitoring.elasticsearch', 'monitoring.ui.elasticsearch'), - renameFromRoot('xpack.monitoring.ccs.enabled', 'monitoring.ui.ccs.enabled'), + renameFromRoot('xpack.monitoring.elasticsearch', 'monitoring.ui.elasticsearch', { + level: 'warning', + }), + renameFromRoot('xpack.monitoring.ccs.enabled', 'monitoring.ui.ccs.enabled', { + level: 'warning', + }), renameFromRoot( 'xpack.monitoring.elasticsearch.logFetchCount', - 'monitoring.ui.elasticsearch.logFetchCount' + 'monitoring.ui.elasticsearch.logFetchCount', + { level: 'warning' } ), - renameFromRoot('xpack.monitoring', 'monitoring'), + renameFromRoot('xpack.monitoring', 'monitoring', { level: 'warning' }), (config, fromPath, addDeprecation) => { const emailNotificationsEnabled = get(config, 'cluster_alerts.email_notifications.enabled'); if (emailNotificationsEnabled && !get(config, CLUSTER_ALERTS_ADDRESS_CONFIG_KEY)) { @@ -55,11 +67,14 @@ export const deprecations = ({ `Add [${fromPath}.${CLUSTER_ALERTS_ADDRESS_CONFIG_KEY}] to your kibana configs."`, ], }, + level: 'critical', }); } return config; }, - rename('xpack_api_polling_frequency_millis', 'licensing.api_polling_frequency'), + rename('xpack_api_polling_frequency_millis', 'licensing.api_polling_frequency', { + level: 'warning', + }), // TODO: Add deprecations for "monitoring.ui.elasticsearch.username: elastic" and "monitoring.ui.elasticsearch.username: kibana". // TODO: Add deprecations for using "monitoring.ui.elasticsearch.ssl.certificate" without "monitoring.ui.elasticsearch.ssl.key", and diff --git a/x-pack/plugins/monitoring/server/lib/details/get_metrics.ts b/x-pack/plugins/monitoring/server/lib/details/get_metrics.ts index 83bb18169ae1e..a8de5529d8ca6 100644 --- a/x-pack/plugins/monitoring/server/lib/details/get_metrics.ts +++ b/x-pack/plugins/monitoring/server/lib/details/get_metrics.ts @@ -6,7 +6,6 @@ */ import moment from 'moment'; -import Bluebird from 'bluebird'; import { checkParam } from '../error_missing_required'; import { getSeries } from './get_series'; import { calculateTimeseriesInterval } from '../calculate_timeseries_interval'; @@ -40,25 +39,29 @@ export async function getMetrics( min = max - numOfBuckets * bucketSize * 1000; } - return Bluebird.map(metricSet, (metric: Metric) => { - // metric names match the literal metric name, but they can be supplied in groups or individually - let metricNames; + return Promise.all( + metricSet.map((metric: Metric) => { + // metric names match the literal metric name, but they can be supplied in groups or individually + let metricNames; - if (typeof metric !== 'string') { - metricNames = metric.keys; - } else { - metricNames = [metric]; - } + if (typeof metric !== 'string') { + metricNames = typeof metric.keys === 'string' ? [metric.keys] : metric.keys; + } else { + metricNames = [metric]; + } - return Bluebird.map(metricNames, (metricName) => { - return getSeries(req, indexPattern, metricName, metricOptions, filters, groupBy, { - min, - max, - bucketSize, - timezone, - }); - }); - }).then((rows) => { + return Promise.all( + metricNames.map((metricName) => { + return getSeries(req, indexPattern, metricName, metricOptions, filters, groupBy, { + min, + max, + bucketSize, + timezone, + }); + }) + ); + }) + ).then((rows) => { const data: Record = {}; metricSet.forEach((key, index) => { // keyName must match the value stored in the html template diff --git a/x-pack/plugins/monitoring/server/lib/elasticsearch/verify_alerting_security.ts b/x-pack/plugins/monitoring/server/lib/elasticsearch/verify_alerting_security.ts deleted file mode 100644 index f5f9c80e0e4d3..0000000000000 --- a/x-pack/plugins/monitoring/server/lib/elasticsearch/verify_alerting_security.ts +++ /dev/null @@ -1,49 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { RequestHandlerContext } from 'kibana/server'; -import { EncryptedSavedObjectsPluginSetup } from '../../../../encrypted_saved_objects/server'; - -export interface AlertingFrameworkHealth { - isSufficientlySecure: boolean; - hasPermanentEncryptionKey: boolean; -} - -export interface XPackUsageSecurity { - security?: { - enabled?: boolean; - ssl?: { - http?: { - enabled?: boolean; - }; - }; - }; -} - -export class AlertingSecurity { - public static readonly getSecurityHealth = async ( - context: RequestHandlerContext, - encryptedSavedObjects?: EncryptedSavedObjectsPluginSetup - ): Promise => { - const { - security: { - enabled: isSecurityEnabled = false, - ssl: { http: { enabled: isTLSEnabled = false } = {} } = {}, - } = {}, - } = ( - await context.core.elasticsearch.client.asInternalUser.transport.request({ - method: 'GET', - path: '/_xpack/usage', - }) - ).body as XPackUsageSecurity; - - return { - isSufficientlySecure: !isSecurityEnabled || (isSecurityEnabled && isTLSEnabled), - hasPermanentEncryptionKey: encryptedSavedObjects?.canEncrypt === true, - }; - }; -} diff --git a/x-pack/plugins/monitoring/server/lib/kibana/get_kibanas_for_clusters.ts b/x-pack/plugins/monitoring/server/lib/kibana/get_kibanas_for_clusters.ts index 4e806c07ee660..5326976ec99ac 100644 --- a/x-pack/plugins/monitoring/server/lib/kibana/get_kibanas_for_clusters.ts +++ b/x-pack/plugins/monitoring/server/lib/kibana/get_kibanas_for_clusters.ts @@ -5,7 +5,6 @@ * 2.0. */ -import Bluebird from 'bluebird'; import { chain, find } from 'lodash'; import { LegacyRequest, Cluster, Bucket } from '../../types'; import { checkParam } from '../error_missing_required'; @@ -36,182 +35,184 @@ export function getKibanasForClusters( const start = req.payload.timeRange.min; const end = req.payload.timeRange.max; - return Bluebird.map(clusters, (cluster) => { - const clusterUuid = cluster.elasticsearch?.cluster?.id ?? cluster.cluster_uuid; - const metric = KibanaClusterMetric.getMetricFields(); - const params = { - index: kbnIndexPattern, - size: 0, - ignore_unavailable: true, - body: { - query: createQuery({ - types: ['stats', 'kibana_stats'], - start, - end, - clusterUuid, - metric, - }), - aggs: { - kibana_uuids: { - terms: { - field: 'kibana_stats.kibana.uuid', - size: config.get('monitoring.ui.max_bucket_size'), - }, - aggs: { - latest_report: { - terms: { - field: 'kibana_stats.timestamp', - size: 1, - order: { - _key: 'desc', - }, - }, - aggs: { - response_time_max: { - max: { - field: 'kibana_stats.response_times.max', + return Promise.all( + clusters.map((cluster) => { + const clusterUuid = cluster.elasticsearch?.cluster?.id ?? cluster.cluster_uuid; + const metric = KibanaClusterMetric.getMetricFields(); + const params = { + index: kbnIndexPattern, + size: 0, + ignore_unavailable: true, + body: { + query: createQuery({ + types: ['stats', 'kibana_stats'], + start, + end, + clusterUuid, + metric, + }), + aggs: { + kibana_uuids: { + terms: { + field: 'kibana_stats.kibana.uuid', + size: config.get('monitoring.ui.max_bucket_size'), + }, + aggs: { + latest_report: { + terms: { + field: 'kibana_stats.timestamp', + size: 1, + order: { + _key: 'desc', }, }, - memory_rss: { - max: { - field: 'kibana_stats.process.memory.resident_set_size_in_bytes', + aggs: { + response_time_max: { + max: { + field: 'kibana_stats.response_times.max', + }, }, - }, - memory_heap_size_limit: { - max: { - field: 'kibana_stats.process.memory.heap.size_limit', + memory_rss: { + max: { + field: 'kibana_stats.process.memory.resident_set_size_in_bytes', + }, }, - }, - concurrent_connections: { - max: { - field: 'kibana_stats.concurrent_connections', + memory_heap_size_limit: { + max: { + field: 'kibana_stats.process.memory.heap.size_limit', + }, }, - }, - requests_total: { - max: { - field: 'kibana_stats.requests.total', + concurrent_connections: { + max: { + field: 'kibana_stats.concurrent_connections', + }, + }, + requests_total: { + max: { + field: 'kibana_stats.requests.total', + }, }, }, }, - }, - response_time_max_per: { - max_bucket: { - buckets_path: 'latest_report>response_time_max', + response_time_max_per: { + max_bucket: { + buckets_path: 'latest_report>response_time_max', + }, }, - }, - memory_rss_per: { - max_bucket: { - buckets_path: 'latest_report>memory_rss', + memory_rss_per: { + max_bucket: { + buckets_path: 'latest_report>memory_rss', + }, }, - }, - memory_heap_size_limit_per: { - max_bucket: { - buckets_path: 'latest_report>memory_heap_size_limit', + memory_heap_size_limit_per: { + max_bucket: { + buckets_path: 'latest_report>memory_heap_size_limit', + }, }, - }, - concurrent_connections_per: { - max_bucket: { - buckets_path: 'latest_report>concurrent_connections', + concurrent_connections_per: { + max_bucket: { + buckets_path: 'latest_report>concurrent_connections', + }, }, - }, - requests_total_per: { - max_bucket: { - buckets_path: 'latest_report>requests_total', + requests_total_per: { + max_bucket: { + buckets_path: 'latest_report>requests_total', + }, }, }, }, - }, - response_time_max: { - max_bucket: { - buckets_path: 'kibana_uuids>response_time_max_per', - }, - }, - memory_rss: { - sum_bucket: { - buckets_path: 'kibana_uuids>memory_rss_per', + response_time_max: { + max_bucket: { + buckets_path: 'kibana_uuids>response_time_max_per', + }, }, - }, - memory_heap_size_limit: { - sum_bucket: { - buckets_path: 'kibana_uuids>memory_heap_size_limit_per', + memory_rss: { + sum_bucket: { + buckets_path: 'kibana_uuids>memory_rss_per', + }, }, - }, - concurrent_connections: { - sum_bucket: { - buckets_path: 'kibana_uuids>concurrent_connections_per', + memory_heap_size_limit: { + sum_bucket: { + buckets_path: 'kibana_uuids>memory_heap_size_limit_per', + }, }, - }, - requests_total: { - sum_bucket: { - buckets_path: 'kibana_uuids>requests_total_per', + concurrent_connections: { + sum_bucket: { + buckets_path: 'kibana_uuids>concurrent_connections_per', + }, }, - }, - status: { - terms: { - field: 'kibana_stats.kibana.status', - order: { - max_timestamp: 'desc', + requests_total: { + sum_bucket: { + buckets_path: 'kibana_uuids>requests_total_per', }, }, - aggs: { - max_timestamp: { - max: { - field: 'timestamp', + status: { + terms: { + field: 'kibana_stats.kibana.status', + order: { + max_timestamp: 'desc', + }, + }, + aggs: { + max_timestamp: { + max: { + field: 'timestamp', + }, }, }, }, }, }, - }, - }; + }; - const { callWithRequest } = req.server.plugins.elasticsearch.getCluster('monitoring'); - return callWithRequest(req, 'search', params).then((result) => { - const aggregations = result.aggregations ?? {}; - const kibanaUuids = aggregations.kibana_uuids?.buckets ?? []; - const statusBuckets = aggregations.status?.buckets ?? []; + const { callWithRequest } = req.server.plugins.elasticsearch.getCluster('monitoring'); + return callWithRequest(req, 'search', params).then((result) => { + const aggregations = result.aggregations ?? {}; + const kibanaUuids = aggregations.kibana_uuids?.buckets ?? []; + const statusBuckets = aggregations.status?.buckets ?? []; - // everything is initialized such that it won't impact any rollup - let status = null; - let requestsTotal = 0; - let connections = 0; - let responseTime = 0; - let memorySize = 0; - let memoryLimit = 0; + // everything is initialized such that it won't impact any rollup + let status = null; + let requestsTotal = 0; + let connections = 0; + let responseTime = 0; + let memorySize = 0; + let memoryLimit = 0; - // if the cluster has kibana instances at all - if (kibanaUuids.length) { - // get instance status by finding the latest status bucket - const latestTimestamp = chain(statusBuckets) - .map((bucket) => bucket.max_timestamp.value) - .max() - .value(); - const latestBucket = find( - statusBuckets, - (bucket) => bucket.max_timestamp.value === latestTimestamp - ); - status = latestBucket.key; + // if the cluster has kibana instances at all + if (kibanaUuids.length) { + // get instance status by finding the latest status bucket + const latestTimestamp = chain(statusBuckets) + .map((bucket) => bucket.max_timestamp.value) + .max() + .value(); + const latestBucket = find( + statusBuckets, + (bucket) => bucket.max_timestamp.value === latestTimestamp + ); + status = latestBucket.key; - requestsTotal = aggregations.requests_total?.value; - connections = aggregations.concurrent_connections?.value; - responseTime = aggregations.response_time_max?.value; - memorySize = aggregations.memory_rss?.value; - memoryLimit = aggregations.memory_heap_size_limit?.value; - } + requestsTotal = aggregations.requests_total?.value; + connections = aggregations.concurrent_connections?.value; + responseTime = aggregations.response_time_max?.value; + memorySize = aggregations.memory_rss?.value; + memoryLimit = aggregations.memory_heap_size_limit?.value; + } - return { - clusterUuid, - stats: { - uuids: kibanaUuids.map(({ key }: Bucket) => key), - status, - requests_total: requestsTotal, - concurrent_connections: connections, - response_time_max: responseTime, - memory_size: memorySize, - memory_limit: memoryLimit, - count: kibanaUuids.length, - }, - }; - }); - }); + return { + clusterUuid, + stats: { + uuids: kibanaUuids.map(({ key }: Bucket) => key), + status, + requests_total: requestsTotal, + concurrent_connections: connections, + response_time_max: responseTime, + memory_size: memorySize, + memory_limit: memoryLimit, + count: kibanaUuids.length, + }, + }; + }); + }) + ); } diff --git a/x-pack/plugins/monitoring/server/lib/logstash/get_logstash_for_clusters.ts b/x-pack/plugins/monitoring/server/lib/logstash/get_logstash_for_clusters.ts index 480b7176b7aba..03c87bfdde1ac 100644 --- a/x-pack/plugins/monitoring/server/lib/logstash/get_logstash_for_clusters.ts +++ b/x-pack/plugins/monitoring/server/lib/logstash/get_logstash_for_clusters.ts @@ -5,7 +5,6 @@ * 2.0. */ -import Bluebird from 'bluebird'; import { get } from 'lodash'; import { LegacyRequest, Cluster, Bucket } from '../../types'; import { LOGSTASH } from '../../../common/constants'; @@ -48,208 +47,210 @@ export function getLogstashForClusters( const end = req.payload.timeRange.max; const config = req.server.config(); - return Bluebird.map(clusters, (cluster) => { - const clusterUuid = get(cluster, 'elasticsearch.cluster.id', cluster.cluster_uuid); - const params = { - index: lsIndexPattern, - size: 0, - ignore_unavailable: true, - body: { - query: createQuery({ - types: ['stats', 'logstash_stats'], - start, - end, - clusterUuid, - metric: LogstashClusterMetric.getMetricFields(), - }), - aggs: { - logstash_uuids: { - terms: { - field: 'logstash_stats.logstash.uuid', - size: config.get('monitoring.ui.max_bucket_size'), - }, - aggs: { - latest_report: { - terms: { - field: 'logstash_stats.timestamp', - size: 1, - order: { - _key: 'desc', - }, - }, - aggs: { - memory_used: { - max: { - field: 'logstash_stats.jvm.mem.heap_used_in_bytes', + return Promise.all( + clusters.map((cluster) => { + const clusterUuid = get(cluster, 'elasticsearch.cluster.id', cluster.cluster_uuid); + const params = { + index: lsIndexPattern, + size: 0, + ignore_unavailable: true, + body: { + query: createQuery({ + types: ['stats', 'logstash_stats'], + start, + end, + clusterUuid, + metric: LogstashClusterMetric.getMetricFields(), + }), + aggs: { + logstash_uuids: { + terms: { + field: 'logstash_stats.logstash.uuid', + size: config.get('monitoring.ui.max_bucket_size'), + }, + aggs: { + latest_report: { + terms: { + field: 'logstash_stats.timestamp', + size: 1, + order: { + _key: 'desc', }, }, - memory: { - max: { - field: 'logstash_stats.jvm.mem.heap_max_in_bytes', + aggs: { + memory_used: { + max: { + field: 'logstash_stats.jvm.mem.heap_used_in_bytes', + }, }, - }, - events_in_total: { - max: { - field: 'logstash_stats.events.in', + memory: { + max: { + field: 'logstash_stats.jvm.mem.heap_max_in_bytes', + }, }, - }, - events_out_total: { - max: { - field: 'logstash_stats.events.out', + events_in_total: { + max: { + field: 'logstash_stats.events.in', + }, + }, + events_out_total: { + max: { + field: 'logstash_stats.events.out', + }, }, }, }, - }, - memory_used_per_node: { - max_bucket: { - buckets_path: 'latest_report>memory_used', + memory_used_per_node: { + max_bucket: { + buckets_path: 'latest_report>memory_used', + }, }, - }, - memory_per_node: { - max_bucket: { - buckets_path: 'latest_report>memory', + memory_per_node: { + max_bucket: { + buckets_path: 'latest_report>memory', + }, }, - }, - events_in_total_per_node: { - max_bucket: { - buckets_path: 'latest_report>events_in_total', + events_in_total_per_node: { + max_bucket: { + buckets_path: 'latest_report>events_in_total', + }, }, - }, - events_out_total_per_node: { - max_bucket: { - buckets_path: 'latest_report>events_out_total', + events_out_total_per_node: { + max_bucket: { + buckets_path: 'latest_report>events_out_total', + }, }, }, }, - }, - logstash_versions: { - terms: { - field: 'logstash_stats.logstash.version', - size: config.get('monitoring.ui.max_bucket_size'), - }, - }, - pipelines_nested: { - nested: { - path: 'logstash_stats.pipelines', + logstash_versions: { + terms: { + field: 'logstash_stats.logstash.version', + size: config.get('monitoring.ui.max_bucket_size'), + }, }, - aggs: { - pipelines: { - sum_bucket: { - buckets_path: 'queue_types>num_pipelines', - }, + pipelines_nested: { + nested: { + path: 'logstash_stats.pipelines', }, - queue_types: { - terms: { - field: 'logstash_stats.pipelines.queue.type', - size: config.get('monitoring.ui.max_bucket_size'), + aggs: { + pipelines: { + sum_bucket: { + buckets_path: 'queue_types>num_pipelines', + }, }, - aggs: { - num_pipelines: { - cardinality: { - field: 'logstash_stats.pipelines.id', + queue_types: { + terms: { + field: 'logstash_stats.pipelines.queue.type', + size: config.get('monitoring.ui.max_bucket_size'), + }, + aggs: { + num_pipelines: { + cardinality: { + field: 'logstash_stats.pipelines.id', + }, }, }, }, }, }, - }, - pipelines_nested_mb: { - nested: { - path: 'logstash.node.stats.pipelines', - }, - aggs: { - pipelines: { - sum_bucket: { - buckets_path: 'queue_types>num_pipelines', - }, + pipelines_nested_mb: { + nested: { + path: 'logstash.node.stats.pipelines', }, - queue_types: { - terms: { - field: 'logstash.node.stats.pipelines.queue.type', - size: config.get('monitoring.ui.max_bucket_size'), + aggs: { + pipelines: { + sum_bucket: { + buckets_path: 'queue_types>num_pipelines', + }, }, - aggs: { - num_pipelines: { - cardinality: { - field: 'logstash.node.stats.pipelines.id', + queue_types: { + terms: { + field: 'logstash.node.stats.pipelines.queue.type', + size: config.get('monitoring.ui.max_bucket_size'), + }, + aggs: { + num_pipelines: { + cardinality: { + field: 'logstash.node.stats.pipelines.id', + }, }, }, }, }, }, - }, - events_in_total: { - sum_bucket: { - buckets_path: 'logstash_uuids>events_in_total_per_node', + events_in_total: { + sum_bucket: { + buckets_path: 'logstash_uuids>events_in_total_per_node', + }, }, - }, - events_out_total: { - sum_bucket: { - buckets_path: 'logstash_uuids>events_out_total_per_node', + events_out_total: { + sum_bucket: { + buckets_path: 'logstash_uuids>events_out_total_per_node', + }, }, - }, - memory_used: { - sum_bucket: { - buckets_path: 'logstash_uuids>memory_used_per_node', + memory_used: { + sum_bucket: { + buckets_path: 'logstash_uuids>memory_used_per_node', + }, }, - }, - memory: { - sum_bucket: { - buckets_path: 'logstash_uuids>memory_per_node', + memory: { + sum_bucket: { + buckets_path: 'logstash_uuids>memory_per_node', + }, }, - }, - max_uptime: { - max: { - field: 'logstash_stats.jvm.uptime_in_millis', + max_uptime: { + max: { + field: 'logstash_stats.jvm.uptime_in_millis', + }, }, }, }, - }, - }; + }; - const { callWithRequest } = req.server.plugins.elasticsearch.getCluster('monitoring'); - return callWithRequest(req, 'search', params).then((result) => { - const aggregations = get(result, 'aggregations', {}); - const logstashUuids = get(aggregations, 'logstash_uuids.buckets', []); - const logstashVersions = get(aggregations, 'logstash_versions.buckets', []); + const { callWithRequest } = req.server.plugins.elasticsearch.getCluster('monitoring'); + return callWithRequest(req, 'search', params).then((result) => { + const aggregations = get(result, 'aggregations', {}); + const logstashUuids = get(aggregations, 'logstash_uuids.buckets', []); + const logstashVersions = get(aggregations, 'logstash_versions.buckets', []); - // everything is initialized such that it won't impact any rollup - let eventsInTotal = 0; - let eventsOutTotal = 0; - let memory = 0; - let memoryUsed = 0; - let maxUptime = 0; + // everything is initialized such that it won't impact any rollup + let eventsInTotal = 0; + let eventsOutTotal = 0; + let memory = 0; + let memoryUsed = 0; + let maxUptime = 0; - // if the cluster has logstash instances at all - if (logstashUuids.length) { - eventsInTotal = get(aggregations, 'events_in_total.value'); - eventsOutTotal = get(aggregations, 'events_out_total.value'); - memory = get(aggregations, 'memory.value'); - memoryUsed = get(aggregations, 'memory_used.value'); - maxUptime = get(aggregations, 'max_uptime.value'); - } + // if the cluster has logstash instances at all + if (logstashUuids.length) { + eventsInTotal = get(aggregations, 'events_in_total.value'); + eventsOutTotal = get(aggregations, 'events_out_total.value'); + memory = get(aggregations, 'memory.value'); + memoryUsed = get(aggregations, 'memory_used.value'); + maxUptime = get(aggregations, 'max_uptime.value'); + } - let types = get(aggregations, 'pipelines_nested_mb.queue_types.buckets', []); - if (!types || types.length === 0) { - types = aggregations.pipelines_nested?.queue_types.buckets ?? []; - } + let types = get(aggregations, 'pipelines_nested_mb.queue_types.buckets', []); + if (!types || types.length === 0) { + types = aggregations.pipelines_nested?.queue_types.buckets ?? []; + } - return { - clusterUuid, - stats: { - node_count: logstashUuids.length, - events_in_total: eventsInTotal, - events_out_total: eventsOutTotal, - avg_memory: memory, - avg_memory_used: memoryUsed, - max_uptime: maxUptime, - pipeline_count: - get(aggregations, 'pipelines_nested_mb.pipelines.value') || - get(aggregations, 'pipelines_nested.pipelines.value', 0), - queue_types: getQueueTypes(types), - versions: logstashVersions.map((versionBucket: Bucket) => versionBucket.key), - }, - }; - }); - }); + return { + clusterUuid, + stats: { + node_count: logstashUuids.length, + events_in_total: eventsInTotal, + events_out_total: eventsOutTotal, + avg_memory: memory, + avg_memory_used: memoryUsed, + max_uptime: maxUptime, + pipeline_count: + get(aggregations, 'pipelines_nested_mb.pipelines.value') || + get(aggregations, 'pipelines_nested.pipelines.value', 0), + queue_types: getQueueTypes(types), + versions: logstashVersions.map((versionBucket: Bucket) => versionBucket.key), + }, + }; + }); + }) + ); } diff --git a/x-pack/plugins/monitoring/server/plugin.ts b/x-pack/plugins/monitoring/server/plugin.ts index 557a9b5e2a3d2..ff07ea0f4a26d 100644 --- a/x-pack/plugins/monitoring/server/plugin.ts +++ b/x-pack/plugins/monitoring/server/plugin.ts @@ -202,6 +202,7 @@ export class MonitoringPlugin router, licenseService: this.licenseService, encryptedSavedObjects: plugins.encryptedSavedObjects, + alerting: plugins.alerting, logger: this.log, }); initInfraSource(config, plugins.infra); diff --git a/x-pack/plugins/monitoring/server/routes/api/v1/alerts/enable.ts b/x-pack/plugins/monitoring/server/routes/api/v1/alerts/enable.ts index 6724819c30d56..7185d399b3534 100644 --- a/x-pack/plugins/monitoring/server/routes/api/v1/alerts/enable.ts +++ b/x-pack/plugins/monitoring/server/routes/api/v1/alerts/enable.ts @@ -11,7 +11,6 @@ import { AlertsFactory } from '../../../../alerts'; import { LegacyServer, RouteDependencies } from '../../../../types'; import { ALERT_ACTION_TYPE_LOG } from '../../../../../common/constants'; import { ActionResult } from '../../../../../../actions/common'; -import { AlertingSecurity } from '../../../../lib/elasticsearch/verify_alerting_security'; import { disableWatcherClusterAlerts } from '../../../../lib/alerts/disable_watcher_cluster_alerts'; import { AlertTypeParams, SanitizedAlert } from '../../../../../../alerting/common'; @@ -38,12 +37,14 @@ export function enableAlertsRoute(server: LegacyServer, npRoute: RouteDependenci const alerts = AlertsFactory.getAll(); if (alerts.length) { - const { isSufficientlySecure, hasPermanentEncryptionKey } = - await AlertingSecurity.getSecurityHealth(context, npRoute.encryptedSavedObjects); + const { isSufficientlySecure, hasPermanentEncryptionKey } = npRoute.alerting + ?.getSecurityHealth + ? await npRoute.alerting?.getSecurityHealth() + : { isSufficientlySecure: false, hasPermanentEncryptionKey: false }; if (!isSufficientlySecure || !hasPermanentEncryptionKey) { server.log.info( - `Skipping alert creation for "${context.infra.spaceId}" space; Stack monitoring alerts require Transport Layer Security between Kibana and Elasticsearch, and an encryption key in your kibana.yml file.` + `Skipping rule creation for "${context.infra.spaceId}" space; Stack Monitoring rules require API keys to be enabled and an encryption key to be configured.` ); return response.ok({ body: { diff --git a/x-pack/plugins/monitoring/server/types.ts b/x-pack/plugins/monitoring/server/types.ts index 14071aafaea12..14023ccce41ae 100644 --- a/x-pack/plugins/monitoring/server/types.ts +++ b/x-pack/plugins/monitoring/server/types.ts @@ -28,6 +28,7 @@ import { PluginSetupContract as AlertingPluginSetupContract, } from '../../alerting/server'; import { InfraPluginSetup, InfraRequestHandlerContext } from '../../infra/server'; +import { PluginSetupContract as AlertingPluginSetup } from '../../alerting/server'; import { LicensingPluginStart } from '../../licensing/server'; import { PluginSetupContract as FeaturesPluginSetupContract } from '../../features/server'; import { EncryptedSavedObjectsPluginSetup } from '../../encrypted_saved_objects/server'; @@ -80,6 +81,7 @@ export interface RouteDependencies { router: IRouter; licenseService: MonitoringLicenseService; encryptedSavedObjects?: EncryptedSavedObjectsPluginSetup; + alerting?: AlertingPluginSetup; logger: Logger; } diff --git a/x-pack/plugins/observability/common/index.ts b/x-pack/plugins/observability/common/index.ts index bfb2eedf6deb2..6b4ea62c16762 100644 --- a/x-pack/plugins/observability/common/index.ts +++ b/x-pack/plugins/observability/common/index.ts @@ -6,7 +6,11 @@ */ export type { AsDuration, AsPercent } from './utils/formatters'; -export { enableInspectEsQueries, maxSuggestions } from './ui_settings_keys'; +export { + enableInspectEsQueries, + maxSuggestions, + enableComparisonByDefault, +} from './ui_settings_keys'; export const casesFeatureId = 'observabilityCases'; diff --git a/x-pack/plugins/observability/common/ui_settings_keys.ts b/x-pack/plugins/observability/common/ui_settings_keys.ts index 69eb507328719..4d34e216a017c 100644 --- a/x-pack/plugins/observability/common/ui_settings_keys.ts +++ b/x-pack/plugins/observability/common/ui_settings_keys.ts @@ -7,3 +7,4 @@ export const enableInspectEsQueries = 'observability:enableInspectEsQueries'; export const maxSuggestions = 'observability:maxSuggestions'; +export const enableComparisonByDefault = 'observability:enableComparisonByDefault'; diff --git a/x-pack/plugins/observability/public/components/app/cases/all_cases/index.tsx b/x-pack/plugins/observability/public/components/app/cases/all_cases/index.tsx index bc0151052434a..4b57475343605 100644 --- a/x-pack/plugins/observability/public/components/app/cases/all_cases/index.tsx +++ b/x-pack/plugins/observability/public/components/app/cases/all_cases/index.tsx @@ -61,7 +61,7 @@ export const AllCases = React.memo(({ userCanCrud }) => { }, }, disableAlerts: true, - showTitle: false, + showTitle: true, userCanCrud, owner: [CASES_OWNER], }); diff --git a/x-pack/plugins/observability/public/components/app/cases/callout/callout.test.tsx b/x-pack/plugins/observability/public/components/app/cases/callout/callout.test.tsx index b0b6fc0e3b793..25d32d0cae884 100644 --- a/x-pack/plugins/observability/public/components/app/cases/callout/callout.test.tsx +++ b/x-pack/plugins/observability/public/components/app/cases/callout/callout.test.tsx @@ -32,25 +32,25 @@ describe('Callout', () => { it('renders the callout', () => { const wrapper = mount(); - expect(wrapper.find(`[data-test-subj="case-callout-md5-hex"]`).exists()).toBeTruthy(); - expect(wrapper.find(`[data-test-subj="callout-messages-md5-hex"]`).exists()).toBeTruthy(); - expect(wrapper.find(`[data-test-subj="callout-dismiss-md5-hex"]`).exists()).toBeTruthy(); + expect(wrapper.find(`[data-test-subj="caseCallout-md5-hex"]`).exists()).toBeTruthy(); + expect(wrapper.find(`[data-test-subj="calloutMessages-md5-hex"]`).exists()).toBeTruthy(); + expect(wrapper.find(`[data-test-subj="calloutDismiss-md5-hex"]`).exists()).toBeTruthy(); }); it('hides the callout', () => { const wrapper = mount(); - expect(wrapper.find(`[data-test-subj="case-callout-md5-hex"]`).exists()).toBeFalsy(); + expect(wrapper.find(`[data-test-subj="caseCallout-md5-hex"]`).exists()).toBeFalsy(); }); it('does not show any messages when the list is empty', () => { const wrapper = mount(); - expect(wrapper.find(`[data-test-subj="callout-messages-md5-hex"]`).exists()).toBeFalsy(); + expect(wrapper.find(`[data-test-subj="calloutMessages-md5-hex"]`).exists()).toBeFalsy(); }); it('transform the button color correctly - primary', () => { const wrapper = mount(); const className = - wrapper.find(`button[data-test-subj="callout-dismiss-md5-hex"]`).first().prop('className') ?? + wrapper.find(`button[data-test-subj="calloutDismiss-md5-hex"]`).first().prop('className') ?? ''; expect(className.includes('euiButton--primary')).toBeTruthy(); }); @@ -58,7 +58,7 @@ describe('Callout', () => { it('transform the button color correctly - success', () => { const wrapper = mount(); const className = - wrapper.find(`button[data-test-subj="callout-dismiss-md5-hex"]`).first().prop('className') ?? + wrapper.find(`button[data-test-subj="calloutDismiss-md5-hex"]`).first().prop('className') ?? ''; expect(className.includes('euiButton--secondary')).toBeTruthy(); }); @@ -66,7 +66,7 @@ describe('Callout', () => { it('transform the button color correctly - warning', () => { const wrapper = mount(); const className = - wrapper.find(`button[data-test-subj="callout-dismiss-md5-hex"]`).first().prop('className') ?? + wrapper.find(`button[data-test-subj="calloutDismiss-md5-hex"]`).first().prop('className') ?? ''; expect(className.includes('euiButton--warning')).toBeTruthy(); }); @@ -74,15 +74,15 @@ describe('Callout', () => { it('transform the button color correctly - danger', () => { const wrapper = mount(); const className = - wrapper.find(`button[data-test-subj="callout-dismiss-md5-hex"]`).first().prop('className') ?? + wrapper.find(`button[data-test-subj="calloutDismiss-md5-hex"]`).first().prop('className') ?? ''; expect(className.includes('euiButton--danger')).toBeTruthy(); }); it('dismiss the callout correctly', () => { const wrapper = mount(); - expect(wrapper.find(`[data-test-subj="callout-dismiss-md5-hex"]`).exists()).toBeTruthy(); - wrapper.find(`button[data-test-subj="callout-dismiss-md5-hex"]`).simulate('click'); + expect(wrapper.find(`[data-test-subj="calloutDismiss-md5-hex"]`).exists()).toBeTruthy(); + wrapper.find(`button[data-test-subj="calloutDismiss-md5-hex"]`).simulate('click'); wrapper.update(); expect(defaultProps.handleDismissCallout).toHaveBeenCalledWith('md5-hex', 'primary'); diff --git a/x-pack/plugins/observability/public/components/app/cases/callout/callout.tsx b/x-pack/plugins/observability/public/components/app/cases/callout/callout.tsx index 5aa637c8f806d..15bd250c6ceb6 100644 --- a/x-pack/plugins/observability/public/components/app/cases/callout/callout.tsx +++ b/x-pack/plugins/observability/public/components/app/cases/callout/callout.tsx @@ -35,10 +35,10 @@ function CallOutComponent({ ); return showCallOut && !isEmpty(messages) ? ( - - + + diff --git a/x-pack/plugins/observability/public/components/app/cases/callout/index.test.tsx b/x-pack/plugins/observability/public/components/app/cases/callout/index.test.tsx index 18d4dee45b9d5..bb0284398f4b3 100644 --- a/x-pack/plugins/observability/public/components/app/cases/callout/index.test.tsx +++ b/x-pack/plugins/observability/public/components/app/cases/callout/index.test.tsx @@ -42,7 +42,7 @@ describe('CaseCallOut ', () => { ); const id = createCalloutId(['message-one', 'message-two']); - expect(wrapper.find(`[data-test-subj="callout-messages-${id}"]`).last().exists()).toBeTruthy(); + expect(wrapper.find(`[data-test-subj="calloutMessages-${id}"]`).last().exists()).toBeTruthy(); }); it('groups the messages correctly', () => { @@ -69,11 +69,9 @@ describe('CaseCallOut ', () => { const idPrimary = createCalloutId(['message-two']); expect( - wrapper.find(`[data-test-subj="case-callout-${idPrimary}"]`).last().exists() - ).toBeTruthy(); - expect( - wrapper.find(`[data-test-subj="case-callout-${idDanger}"]`).last().exists() + wrapper.find(`[data-test-subj="caseCallout-${idPrimary}"]`).last().exists() ).toBeTruthy(); + expect(wrapper.find(`[data-test-subj="caseCallout-${idDanger}"]`).last().exists()).toBeTruthy(); }); it('dismisses the callout correctly', () => { @@ -91,9 +89,9 @@ describe('CaseCallOut ', () => { const id = createCalloutId(['message-one']); - expect(wrapper.find(`[data-test-subj="case-callout-${id}"]`).last().exists()).toBeTruthy(); - wrapper.find(`[data-test-subj="callout-dismiss-${id}"]`).last().simulate('click'); - expect(wrapper.find(`[data-test-subj="case-callout-${id}"]`).exists()).toBeFalsy(); + expect(wrapper.find(`[data-test-subj="caseCallout-${id}"]`).last().exists()).toBeTruthy(); + wrapper.find(`[data-test-subj="calloutDismiss-${id}"]`).last().simulate('click'); + expect(wrapper.find(`[data-test-subj="caseCallout-${id}"]`).exists()).toBeFalsy(); }); it('persist the callout of type primary when dismissed', () => { @@ -112,7 +110,7 @@ describe('CaseCallOut ', () => { const id = createCalloutId(['message-one']); expect(securityLocalStorageMock.getMessages).toHaveBeenCalledWith(observabilityAppId); - wrapper.find(`[data-test-subj="callout-dismiss-${id}"]`).last().simulate('click'); + wrapper.find(`[data-test-subj="calloutDismiss-${id}"]`).last().simulate('click'); expect(securityLocalStorageMock.addMessage).toHaveBeenCalledWith(observabilityAppId, id); }); @@ -137,7 +135,7 @@ describe('CaseCallOut ', () => { ); - expect(wrapper.find(`[data-test-subj="case-callout-${id}"]`).last().exists()).toBeFalsy(); + expect(wrapper.find(`[data-test-subj="caseCallout-${id}"]`).last().exists()).toBeFalsy(); }); it('do not persist a callout of type danger', () => { @@ -160,7 +158,7 @@ describe('CaseCallOut ', () => { ); const id = createCalloutId(['message-one']); - wrapper.find(`button[data-test-subj="callout-dismiss-${id}"]`).simulate('click'); + wrapper.find(`button[data-test-subj="calloutDismiss-${id}"]`).simulate('click'); wrapper.update(); expect(securityLocalStorageMock.addMessage).not.toHaveBeenCalled(); }); @@ -185,7 +183,7 @@ describe('CaseCallOut ', () => { ); const id = createCalloutId(['message-one']); - wrapper.find(`button[data-test-subj="callout-dismiss-${id}"]`).simulate('click'); + wrapper.find(`button[data-test-subj="calloutDismiss-${id}"]`).simulate('click'); wrapper.update(); expect(securityLocalStorageMock.addMessage).not.toHaveBeenCalled(); }); @@ -210,7 +208,7 @@ describe('CaseCallOut ', () => { ); const id = createCalloutId(['message-one']); - wrapper.find(`button[data-test-subj="callout-dismiss-${id}"]`).simulate('click'); + wrapper.find(`button[data-test-subj="calloutDismiss-${id}"]`).simulate('click'); wrapper.update(); expect(securityLocalStorageMock.addMessage).not.toHaveBeenCalled(); }); diff --git a/x-pack/plugins/observability/public/components/app/cases/create/flyout.test.tsx b/x-pack/plugins/observability/public/components/app/cases/create/flyout.test.tsx index dc3db695a3fbf..977263a9721ea 100644 --- a/x-pack/plugins/observability/public/components/app/cases/create/flyout.test.tsx +++ b/x-pack/plugins/observability/public/components/app/cases/create/flyout.test.tsx @@ -45,7 +45,7 @@ describe('CreateCaseFlyout', () => { ); - expect(wrapper.find(`[data-test-subj='create-case-flyout']`).exists()).toBeTruthy(); + expect(wrapper.find(`[data-test-subj='createCaseFlyout']`).exists()).toBeTruthy(); }); it('Closing modal calls onCloseCaseModal', () => { diff --git a/x-pack/plugins/observability/public/components/app/cases/create/flyout.tsx b/x-pack/plugins/observability/public/components/app/cases/create/flyout.tsx index 896bc27a97674..e8147ef7098ad 100644 --- a/x-pack/plugins/observability/public/components/app/cases/create/flyout.tsx +++ b/x-pack/plugins/observability/public/components/app/cases/create/flyout.tsx @@ -54,7 +54,7 @@ function CreateCaseFlyoutComponent({ }: CreateCaseModalProps) { const { cases } = useKibana().services; return ( - +

    {i18n.CREATE_TITLE}

    diff --git a/x-pack/plugins/observability/public/components/app/cases/translations.ts b/x-pack/plugins/observability/public/components/app/cases/translations.ts index a85b0bc744e66..af016be0182a3 100644 --- a/x-pack/plugins/observability/public/components/app/cases/translations.ts +++ b/x-pack/plugins/observability/public/components/app/cases/translations.ts @@ -92,10 +92,6 @@ export const OPTIONAL = i18n.translate('xpack.observability.cases.caseView.optio defaultMessage: 'Optional', }); -export const PAGE_TITLE = i18n.translate('xpack.observability.cases.pageTitle', { - defaultMessage: 'Cases', -}); - export const CREATE_CASE = i18n.translate('xpack.observability.cases.caseView.createCase', { defaultMessage: 'Create case', }); diff --git a/x-pack/plugins/observability/public/components/app/news_feed/index.test.tsx b/x-pack/plugins/observability/public/components/app/news_feed/index.test.tsx index 97b5dbc679839..f6e641082e557 100644 --- a/x-pack/plugins/observability/public/components/app/news_feed/index.test.tsx +++ b/x-pack/plugins/observability/public/components/app/news_feed/index.test.tsx @@ -59,6 +59,6 @@ describe('News', () => { ); expect(getByText("What's new")).toBeInTheDocument(); expect(getAllByText('Read full story').length).toEqual(3); - expect(queryAllByTestId('news_image').length).toEqual(1); + expect(queryAllByTestId('newsImage').length).toEqual(1); }); }); diff --git a/x-pack/plugins/observability/public/components/app/news_feed/index.tsx b/x-pack/plugins/observability/public/components/app/news_feed/index.tsx index 68039f80b0b94..6f1a5f33b9ba7 100644 --- a/x-pack/plugins/observability/public/components/app/news_feed/index.tsx +++ b/x-pack/plugins/observability/public/components/app/news_feed/index.tsx @@ -85,7 +85,7 @@ function NewsItem({ item }: { item: INewsItem }) { {item.image_url?.en && (
    - + ); } diff --git a/x-pack/plugins/observability/public/components/app/section/alerts/index.tsx b/x-pack/plugins/observability/public/components/app/section/alerts/index.tsx index 44f699c6c390b..cf3ac2b6c7be5 100644 --- a/x-pack/plugins/observability/public/components/app/section/alerts/index.tsx +++ b/x-pack/plugins/observability/public/components/app/section/alerts/index.tsx @@ -24,6 +24,7 @@ import { EuiSelect } from '@elastic/eui'; import { uniqBy } from 'lodash'; import { Alert } from '../../../../../../alerting/common'; import { usePluginContext } from '../../../../hooks/use_plugin_context'; +import { paths } from '../../../../config'; const ALL_TYPES = 'ALL_TYPES'; const allTypes = { @@ -41,8 +42,8 @@ export function AlertsSection({ alerts }: Props) { const { config, core } = usePluginContext(); const [filter, setFilter] = useState(ALL_TYPES); const manageLink = config.unsafe.alertingExperience.enabled - ? core.http.basePath.prepend(`/app/observability/alerts`) - : core.http.basePath.prepend(`/app/management/insightsAndAlerting/triggersActions/rules`); + ? core.http.basePath.prepend(paths.observability.alerts) + : core.http.basePath.prepend(paths.management.rules); const filterOptions = uniqBy(alerts, (alert) => alert.consumer).map(({ consumer }) => ({ value: consumer, text: consumer, @@ -89,9 +90,7 @@ export function AlertsSection({ alerts }: Props) { {alert.name} diff --git a/x-pack/plugins/observability/public/components/shared/core_web_vitals/palette_legends.tsx b/x-pack/plugins/observability/public/components/shared/core_web_vitals/palette_legends.tsx index 840702c744379..70ae61b5e0d74 100644 --- a/x-pack/plugins/observability/public/components/shared/core_web_vitals/palette_legends.tsx +++ b/x-pack/plugins/observability/public/components/shared/core_web_vitals/palette_legends.tsx @@ -16,8 +16,7 @@ import { } from '@elastic/eui'; import styled from 'styled-components'; import { FormattedMessage } from '@kbn/i18n/react'; -import euiLightVars from '@elastic/eui/dist/eui_theme_light.json'; -import euiDarkVars from '@elastic/eui/dist/eui_theme_dark.json'; +import { euiLightVars, euiDarkVars } from '@kbn/ui-shared-deps-src/theme'; import { getCoreVitalTooltipMessage, Thresholds } from './core_vital_item'; import { useUiSetting$ } from '../../../../../../../src/plugins/kibana_react/public'; import { diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/components/action_menu/action_menu.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/components/action_menu/action_menu.tsx index 08b4a3b948c57..512a6389bbf72 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/components/action_menu/action_menu.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/components/action_menu/action_menu.tsx @@ -36,9 +36,11 @@ export function ExpViewActionMenuContent({ responsive={false} style={{ paddingRight: 20 }} > - - - + {timeRange && ( + + + + )} { palette: undefined, seriesType: 'line', xAccessor: 'x-axis-column-layer0', - yConfig: [{ color: 'green', forAccessor: 'y-axis-column-layer0' }], + yConfig: [{ color: 'green', forAccessor: 'y-axis-column-layer0', axisMode: 'left' }], }, ], legend: { isVisible: true, showSingleSeries: true, position: 'right' }, @@ -510,7 +511,7 @@ describe('Lens Attribute', () => { seriesType: 'line', splitAccessor: 'breakdown-column-layer0', xAccessor: 'x-axis-column-layer0', - yConfig: [{ color: 'green', forAccessor: 'y-axis-column-layer0' }], + yConfig: [{ color: 'green', forAccessor: 'y-axis-column-layer0', axisMode: 'left' }], }, ]); 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 index 5e769882a2793..3e6e6d9cb83b0 100644 --- 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 @@ -732,7 +732,19 @@ export class LensAttributes { seriesType: layerConfig.seriesType || layerConfig.seriesConfig.defaultSeriesType, palette: layerConfig.seriesConfig.palette, yConfig: layerConfig.seriesConfig.yConfig || [ - { forAccessor: `y-axis-column-layer${index}`, color: layerConfig.color }, + { + forAccessor: `y-axis-column-layer${index}`, + color: layerConfig.color, + /* if the fields format matches the field format of the first layer, use the default y axis (right) + * if not, use the secondary y axis (left) */ + axisMode: + layerConfig.indexPattern.fieldFormatMap[layerConfig.selectedMetricField]?.id === + this.layerConfigs[0].indexPattern.fieldFormatMap[ + this.layerConfigs[0].selectedMetricField + ]?.id + ? 'left' + : 'right', + }, ], xAccessor: `x-axis-column-layer${index}`, ...(layerConfig.breakdown && diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/test_data/sample_attribute.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/test_data/sample_attribute.ts index 8254a5a816921..cfbd2a5df0358 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/test_data/sample_attribute.ts +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/test_data/sample_attribute.ts @@ -181,6 +181,7 @@ export const sampleAttribute = { { color: 'green', forAccessor: 'y-axis-column-layer0', + axisMode: 'left', }, ], }, diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/test_data/sample_attribute_kpi.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/test_data/sample_attribute_kpi.ts index 8fbda9f6adc52..668049dcc122b 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/test_data/sample_attribute_kpi.ts +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/test_data/sample_attribute_kpi.ts @@ -83,6 +83,7 @@ export const sampleAttributeKpi = { { color: 'green', forAccessor: 'y-axis-column-layer0', + axisMode: 'left', }, ], }, diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/header/add_to_case_action.test.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/header/add_to_case_action.test.tsx index b8f16f3e5effb..e4c9e25f6b29f 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/header/add_to_case_action.test.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/header/add_to_case_action.test.tsx @@ -9,6 +9,9 @@ import React from 'react'; import { render } from '../rtl_helpers'; import { fireEvent } from '@testing-library/dom'; import { AddToCaseAction } from './add_to_case_action'; +import * as useCaseHook from '../hooks/use_add_to_case'; +import * as datePicker from '../components/date_range_picker'; +import moment from 'moment'; describe('AddToCaseAction', function () { it('should render properly', async function () { @@ -21,6 +24,31 @@ describe('AddToCaseAction', function () { expect(await findByText('Add to case')).toBeInTheDocument(); }); + it('should parse relative data to the useAddToCase hook', async function () { + const useAddToCaseHook = jest.spyOn(useCaseHook, 'useAddToCase'); + jest.spyOn(datePicker, 'parseRelativeDate').mockReturnValue(moment('2021-11-10T10:52:06.091Z')); + + const { findByText } = render( + + ); + expect(await findByText('Add to case')).toBeInTheDocument(); + + expect(useAddToCaseHook).toHaveBeenCalledWith( + expect.objectContaining({ + lensAttributes: { + title: 'Performance distribution', + }, + timeRange: { + from: '2021-11-10T10:52:06.091Z', + to: '2021-11-10T10:52:06.091Z', + }, + }) + ); + }); + it('should be able to click add to case button', async function () { const initSeries = { data: [ diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/header/add_to_case_action.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/header/add_to_case_action.tsx index bc813a4980e78..1d230c765edae 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/header/add_to_case_action.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/header/add_to_case_action.tsx @@ -15,9 +15,10 @@ import { TypedLensByValueInput } from '../../../../../../lens/public'; import { useAddToCase } from '../hooks/use_add_to_case'; import { Case, SubCase } from '../../../../../../cases/common'; import { observabilityFeatureId } from '../../../../../common'; +import { parseRelativeDate } from '../components/date_range_picker'; export interface AddToCaseProps { - timeRange?: { from: string; to: string }; + timeRange: { from: string; to: string }; lensAttributes: TypedLensByValueInput['attributes'] | null; } @@ -31,11 +32,14 @@ export function AddToCaseAction({ lensAttributes, timeRange }: AddToCaseProps) { [http.basePath] ); + const absoluteFromDate = parseRelativeDate(timeRange.from); + const absoluteToDate = parseRelativeDate(timeRange.to, { roundUp: true }); + const { createCaseUrl, goToCreateCase, onCaseClicked, isCasesOpen, setIsCasesOpen, isSaving } = useAddToCase({ lensAttributes, getToastText, - timeRange, + timeRange: { from: absoluteFromDate.toISOString(), to: absoluteToDate.toISOString() }, }); const getAllCasesSelectorModalProps: AllCasesSelectorModalProps = { diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_app_index_pattern.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_app_index_pattern.tsx index 8a766075ef8d2..9bd611c05e956 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_app_index_pattern.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_app_index_pattern.tsx @@ -6,6 +6,7 @@ */ import React, { createContext, useContext, Context, useState, useCallback, useMemo } from 'react'; +import { HttpFetchError } from 'kibana/public'; import { IndexPattern } from '../../../../../../../../src/plugins/data/common'; import { AppDataType } from '../types'; import { useKibana } from '../../../../../../../../src/plugins/kibana_react/public'; @@ -16,6 +17,7 @@ import { getDataHandler } from '../../../../data_handler'; export interface IndexPatternContext { loading: boolean; indexPatterns: IndexPatternState; + indexPatternErrors: IndexPatternErrors; hasAppData: HasAppDataState; loadIndexPattern: (params: { dataType: AppDataType }) => void; } @@ -28,11 +30,15 @@ interface ProviderProps { type HasAppDataState = Record; export type IndexPatternState = Record; +export type IndexPatternErrors = Record; type LoadingState = Record; export function IndexPatternContextProvider({ children }: ProviderProps) { const [loading, setLoading] = useState({} as LoadingState); const [indexPatterns, setIndexPatterns] = useState({} as IndexPatternState); + const [indexPatternErrors, setIndexPatternErrors] = useState( + {} as IndexPatternErrors + ); const [hasAppData, setHasAppData] = useState({ infra_metrics: null, infra_logs: null, @@ -78,6 +84,9 @@ export function IndexPatternContextProvider({ children }: ProviderProps) { } setLoading((prevState) => ({ ...prevState, [dataType]: false })); } catch (e) { + if ((e as HttpFetchError).body.error === 'Forbidden') { + setIndexPatternErrors((prevState) => ({ ...prevState, [dataType]: e })); + } setLoading((prevState) => ({ ...prevState, [dataType]: false })); } } @@ -91,6 +100,7 @@ export function IndexPatternContextProvider({ children }: ProviderProps) { hasAppData, indexPatterns, loadIndexPattern, + indexPatternErrors, loading: !!Object.values(loading).find((loadingT) => loadingT), }} > @@ -100,7 +110,7 @@ export function IndexPatternContextProvider({ children }: ProviderProps) { } export const useAppIndexPatternContext = (dataType?: AppDataType) => { - const { loading, hasAppData, loadIndexPattern, indexPatterns } = useContext( + const { loading, hasAppData, loadIndexPattern, indexPatterns, indexPatternErrors } = useContext( IndexPatternContext as unknown as Context ); @@ -113,9 +123,10 @@ export const useAppIndexPatternContext = (dataType?: AppDataType) => { hasAppData, loading, indexPatterns, + indexPatternErrors, indexPattern: dataType ? indexPatterns?.[dataType] : undefined, hasData: dataType ? hasAppData?.[dataType] : undefined, loadIndexPattern, }; - }, [dataType, hasAppData, indexPatterns, loadIndexPattern, loading]); + }, [dataType, hasAppData, indexPatternErrors, indexPatterns, loadIndexPattern, loading]); }; diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_lens_attributes.test.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_lens_attributes.test.tsx new file mode 100644 index 0000000000000..3334b69e5becc --- /dev/null +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_lens_attributes.test.tsx @@ -0,0 +1,73 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; + +import { allSeriesKey, reportTypeKey, UrlStorageContextProvider } from './use_series_storage'; +import { renderHook } from '@testing-library/react-hooks'; +import { useLensAttributes } from './use_lens_attributes'; +import { ReportTypes } from '../configurations/constants'; +import { mockIndexPattern } from '../rtl_helpers'; +import { createKbnUrlStateStorage } from '../../../../../../../../src/plugins/kibana_utils/public'; +import { TRANSACTION_DURATION } from '../configurations/constants/elasticsearch_fieldnames'; +import * as lensAttributes from '../configurations/lens_attributes'; +import * as indexPattern from './use_app_index_pattern'; +import * as theme from '../../../../hooks/use_theme'; + +const mockSingleSeries = [ + { + name: 'performance-distribution', + dataType: 'ux', + breakdown: 'user_agent.name', + time: { from: 'now-15m', to: 'now' }, + selectedMetricField: TRANSACTION_DURATION, + reportDefinitions: { 'service.name': ['elastic-co'] }, + }, +]; + +describe('useExpViewTimeRange', function () { + const storage = createKbnUrlStateStorage({ useHash: false }); + // @ts-ignore + jest.spyOn(indexPattern, 'useAppIndexPatternContext').mockReturnValue({ + indexPatterns: { + ux: mockIndexPattern, + apm: mockIndexPattern, + mobile: mockIndexPattern, + infra_logs: mockIndexPattern, + infra_metrics: mockIndexPattern, + synthetics: mockIndexPattern, + }, + }); + jest.spyOn(theme, 'useTheme').mockReturnValue({ + // @ts-ignore + eui: { + euiColorVis1: '#111111', + }, + }); + const lensAttributesSpy = jest.spyOn(lensAttributes, 'LensAttributes'); + + function Wrapper({ children }: { children: JSX.Element }) { + return {children}; + } + + it('updates lens attributes with report type from storage', async function () { + await storage.set(allSeriesKey, mockSingleSeries); + await storage.set(reportTypeKey, ReportTypes.KPI); + + renderHook(() => useLensAttributes(), { + wrapper: Wrapper, + }); + + expect(lensAttributesSpy).toBeCalledWith( + expect.arrayContaining([ + expect.objectContaining({ + seriesConfig: expect.objectContaining({ reportType: ReportTypes.KPI }), + }), + ]) + ); + }); +}); 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 index ae3d57b3c9652..f81494e8f9ac7 100644 --- 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 @@ -13,6 +13,7 @@ import { AllSeries, allSeriesKey, convertAllShortSeries, + reportTypeKey, useSeriesStorage, } from './use_series_storage'; import { getDefaultConfigs } from '../configurations/default_configs'; @@ -93,11 +94,12 @@ export const useLensAttributes = (): TypedLensByValueInput['attributes'] | null return useMemo(() => { // we only use the data from url to apply, since that gets updated to apply changes const allSeriesT: AllSeries = convertAllShortSeries(storage.get(allSeriesKey) ?? []); + const reportTypeT: ReportViewType = storage.get(reportTypeKey) as ReportViewType; - if (isEmpty(indexPatterns) || isEmpty(allSeriesT) || !reportType) { + if (isEmpty(indexPatterns) || isEmpty(allSeriesT) || !reportTypeT) { return null; } - const layerConfigs = getLayerConfigs(allSeriesT, reportType, theme, indexPatterns); + const layerConfigs = getLayerConfigs(allSeriesT, reportTypeT, theme, indexPatterns); if (layerConfigs.length < 1) { return null; diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_series_storage.test.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_series_storage.test.tsx index ffe7db0568344..6abb0416d0908 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_series_storage.test.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_series_storage.test.tsx @@ -9,9 +9,11 @@ import React, { useEffect } from 'react'; import { act, renderHook } from '@testing-library/react-hooks'; import { Route, Router } from 'react-router-dom'; import { render } from '@testing-library/react'; -import { UrlStorageContextProvider, useSeriesStorage } from './use_series_storage'; +import { UrlStorageContextProvider, useSeriesStorage, reportTypeKey } from './use_series_storage'; import { getHistoryFromUrl } from '../rtl_helpers'; import type { AppDataType } from '../types'; +import { ReportTypes } from '../configurations/constants'; +import * as useTrackMetric from '../../../../hooks/use_track_metric'; const mockSingleSeries = [ { @@ -28,12 +30,26 @@ const mockMultipleSeries = [ dataType: 'ux' as AppDataType, breakdown: 'user_agent.name', time: { from: 'now-15m', to: 'now' }, + filters: [ + { + field: 'url.full', + value: 'https://elastic.co', + }, + ], + selectedMetricField: 'transaction.duration.us', }, { name: 'kpi-over-time', dataType: 'synthetics' as AppDataType, breakdown: 'user_agent.name', time: { from: 'now-15m', to: 'now' }, + filters: [ + { + field: 'monitor.type', + value: 'browser', + }, + ], + selectedMetricField: 'monitor.duration.us', }, ]; @@ -100,26 +116,8 @@ describe('userSeriesStorage', function () { expect(setData).toHaveBeenCalledTimes(2); expect(setData).toHaveBeenLastCalledWith( expect.objectContaining({ - allSeries: [ - { - name: 'performance-distribution', - dataType: 'ux', - breakdown: 'user_agent.name', - time: { from: 'now-15m', to: 'now' }, - }, - { - name: 'kpi-over-time', - dataType: 'synthetics', - breakdown: 'user_agent.name', - time: { from: 'now-15m', to: 'now' }, - }, - ], - firstSeries: { - name: 'performance-distribution', - dataType: 'ux', - breakdown: 'user_agent.name', - time: { from: 'now-15m', to: 'now' }, - }, + allSeries: mockMultipleSeries, + firstSeries: mockMultipleSeries[0], }) ); }); @@ -158,18 +156,135 @@ describe('userSeriesStorage', function () { }); expect(result.current.allSeries).toEqual([ + mockMultipleSeries[0], { - name: 'performance-distribution', - dataType: 'ux', - breakdown: 'user_agent.name', - time: { from: 'now-15m', to: 'now' }, - }, - { - name: 'kpi-over-time', - dataType: 'synthetics', + ...mockMultipleSeries[1], breakdown: undefined, - time: { from: 'now-15m', to: 'now' }, }, ]); }); + + it('sets reportType when calling applyChanges', () => { + const setStorage = jest.fn(); + function wrapper({ children }: { children: React.ReactElement }) { + return ( + + key === 'sr' ? mockMultipleSeries : 'kpi-over-time' + ), + set: setStorage, + }} + > + {children} + + ); + } + const { result } = renderHook(() => useSeriesStorage(), { wrapper }); + + act(() => { + result.current.setReportType(ReportTypes.DISTRIBUTION); + }); + + act(() => { + result.current.applyChanges(); + }); + + expect(setStorage).toBeCalledWith(reportTypeKey, ReportTypes.DISTRIBUTION); + }); + + it('returns reportType in state, not url storage, from hook', () => { + const setStorage = jest.fn(); + function wrapper({ children }: { children: React.ReactElement }) { + return ( + + key === 'sr' ? mockMultipleSeries : 'kpi-over-time' + ), + set: setStorage, + }} + > + {children} + + ); + } + const { result } = renderHook(() => useSeriesStorage(), { wrapper }); + + act(() => { + result.current.setReportType(ReportTypes.DISTRIBUTION); + }); + + expect(result.current.reportType).toEqual(ReportTypes.DISTRIBUTION); + }); + + it('ensures that telemetry is called', () => { + const trackEvent = jest.fn(); + jest.spyOn(useTrackMetric, 'useUiTracker').mockReturnValue(trackEvent); + function wrapper({ children }: { children: React.ReactElement }) { + return ( + + key === 'sr' ? mockMultipleSeries : 'kpi-over-time' + ), + set: jest.fn(), + }} + > + {children} + + ); + } + const { result } = renderHook(() => useSeriesStorage(), { wrapper }); + + act(() => { + result.current.applyChanges(); + }); + + expect(trackEvent).toBeCalledTimes(7); + expect(trackEvent).toBeCalledWith({ + app: 'observability-overview', + metric: 'exploratory_view__filters__filter_url.full', + metricType: 'count', + }); + expect(trackEvent).toBeCalledWith({ + app: 'observability-overview', + metric: 'exploratory_view__filters__filter_monitor.type', + metricType: 'count', + }); + expect(trackEvent).toBeCalledWith({ + app: 'observability-overview', + metric: 'exploratory_view__filters__report_type_kpi-over-time__data_type_ux__filter_url.full', + metricType: 'count', + }); + expect(trackEvent).toBeCalledWith({ + app: 'observability-overview', + metric: + 'exploratory_view__filters__report_type_kpi-over-time__data_type_synthetics__filter_monitor.type', + metricType: 'count', + }); + expect(trackEvent).toBeCalledWith({ + app: 'observability-overview', + metric: + 'exploratory_view__report_type_kpi-over-time__data_type_synthetics__metric_type_monitor.duration.us', + metricType: 'count', + }); + expect(trackEvent).toBeCalledWith({ + app: 'observability-overview', + metric: + 'exploratory_view__report_type_kpi-over-time__data_type_ux__metric_type_transaction.duration.us', + metricType: 'count', + }); + expect(trackEvent).toBeCalledWith({ + app: 'observability-overview', + metric: 'exploratory_view_apply_changes', + metricType: 'count', + }); + }); }); diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_series_storage.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_series_storage.tsx index e71c66ba1f11b..3fca13f7978d6 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_series_storage.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_series_storage.tsx @@ -10,6 +10,7 @@ import { IKbnUrlStateStorage, ISessionStorageStateStorage, } from '../../../../../../../../src/plugins/kibana_utils/public'; +import { useUiTracker } from '../../../../hooks/use_track_metric'; import type { AppDataType, ReportViewType, @@ -20,6 +21,7 @@ import type { import { convertToShortUrl } from '../configurations/utils'; import { OperationType, SeriesType } from '../../../../../../lens/public'; import { URL_KEYS } from '../configurations/constants/url_constants'; +import { trackTelemetryOnApply } from '../utils/telemetry'; export interface SeriesContextValue { firstSeries?: SeriesUrl; @@ -30,7 +32,7 @@ export interface SeriesContextValue { setSeries: (seriesIndex: number, newValue: SeriesUrl) => void; getSeries: (seriesIndex: number) => SeriesUrl | undefined; removeSeries: (seriesIndex: number) => void; - setReportType: (reportType: string) => void; + setReportType: (reportType: ReportViewType) => void; storage: IKbnUrlStateStorage | ISessionStorageStateStorage; reportType: ReportViewType; } @@ -57,12 +59,14 @@ export function UrlStorageContextProvider({ const [lastRefresh, setLastRefresh] = useState(() => Date.now()); - const [reportType, setReportType] = useState( - () => (storage as IKbnUrlStateStorage).get(reportTypeKey) ?? '' + const [reportType, setReportType] = useState( + () => ((storage as IKbnUrlStateStorage).get(reportTypeKey) ?? '') as ReportViewType ); const [firstSeries, setFirstSeries] = useState(); + const trackEvent = useUiTracker(); + useEffect(() => { const firstSeriesT = allSeries?.[0]; @@ -93,10 +97,6 @@ export function UrlStorageContextProvider({ }); }, []); - useEffect(() => { - (storage as IKbnUrlStateStorage).set(reportTypeKey, reportType); - }, [reportType, storage]); - const removeSeries = useCallback((seriesIndex: number) => { setAllSeries((prevAllSeries) => prevAllSeries.filter((seriesT, index) => index !== seriesIndex) @@ -113,14 +113,18 @@ export function UrlStorageContextProvider({ const applyChanges = useCallback( (onApply?: () => void) => { const allShortSeries = allSeries.map((series) => convertToShortUrl(series)); + (storage as IKbnUrlStateStorage).set(reportTypeKey, reportType); (storage as IKbnUrlStateStorage).set(allSeriesKey, allShortSeries); setLastRefresh(Date.now()); + + trackTelemetryOnApply(trackEvent, allSeries, reportType); + if (onApply) { onApply(); } }, - [allSeries, storage] + [allSeries, storage, trackEvent, reportType] ); const value = { @@ -133,7 +137,7 @@ export function UrlStorageContextProvider({ lastRefresh, setLastRefresh, setReportType, - reportType: storage.get(reportTypeKey) as ReportViewType, + reportType, firstSeries: firstSeries!, }; return {children}; 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 index 3de29b02853e8..1fc38ab79de7f 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/index.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/index.tsx @@ -36,7 +36,7 @@ export function ExploratoryViewPage({ useBreadcrumbs([ { text: i18n.translate('xpack.observability.overview.exploratoryView', { - defaultMessage: 'Analyze data', + defaultMessage: 'Explore data', }), }, ]); diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/lens_embeddable.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/lens_embeddable.tsx index b9dced8036eae..437981baf81d5 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/lens_embeddable.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/lens_embeddable.tsx @@ -9,11 +9,13 @@ import { i18n } from '@kbn/i18n'; import React, { Dispatch, SetStateAction, useCallback } from 'react'; import styled from 'styled-components'; import { TypedLensByValueInput } from '../../../../../lens/public'; +import { useUiTracker } from '../../../hooks/use_track_metric'; import { useSeriesStorage } from './hooks/use_series_storage'; import { ObservabilityPublicPluginsStart } from '../../../plugin'; import { useKibana } from '../../../../../../../src/plugins/kibana_react/public'; import { useExpViewTimeRange } from './hooks/use_time_range'; import { parseRelativeDate } from './components/date_range_picker'; +import { trackTelemetryOnLoad } from './utils/telemetry'; import type { ChartTimeRange } from './header/last_updated'; interface Props { @@ -23,26 +25,36 @@ interface Props { export function LensEmbeddable(props: Props) { const { lensAttributes, setChartTimeRangeContext } = props; - const { services: { lens, notifications }, } = useKibana(); const LensComponent = lens?.EmbeddableComponent; - const { firstSeries, setSeries, reportType } = useSeriesStorage(); + const { firstSeries, setSeries, reportType, lastRefresh } = useSeriesStorage(); const firstSeriesId = 0; const timeRange = useExpViewTimeRange(); - const onLensLoad = useCallback(() => { - setChartTimeRangeContext({ - lastUpdated: Date.now(), - to: parseRelativeDate(timeRange?.to || '').valueOf(), - from: parseRelativeDate(timeRange?.from || '').valueOf(), - }); - }, [setChartTimeRangeContext, timeRange]); + const trackEvent = useUiTracker(); + + const onLensLoad = useCallback( + (isLoading) => { + const timeLoaded = Date.now(); + + setChartTimeRangeContext({ + lastUpdated: timeLoaded, + to: parseRelativeDate(timeRange?.to || '').valueOf(), + from: parseRelativeDate(timeRange?.from || '').valueOf(), + }); + + if (!isLoading) { + trackTelemetryOnLoad(trackEvent, lastRefresh, timeLoaded); + } + }, + [setChartTimeRangeContext, timeRange, lastRefresh, trackEvent] + ); const onBrushEnd = useCallback( ({ range }: { range: number[] }) => { 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 index efca1152e175d..04d74844beb83 100644 --- 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 @@ -23,7 +23,7 @@ import { ObservabilityPublicPluginsStart } from '../../../plugin'; import { EuiThemeProvider } from '../../../../../../../src/plugins/kibana_react/common'; import { lensPluginMock } from '../../../../../lens/public/mocks'; import * as useAppIndexPatternHook from './hooks/use_app_index_pattern'; -import { IndexPatternContextProvider } from './hooks/use_app_index_pattern'; +import { IndexPatternContext, IndexPatternContextProvider } from './hooks/use_app_index_pattern'; import { AllSeries, SeriesContextValue, UrlStorageContext } from './hooks/use_series_storage'; import * as fetcherHook from '../../../hooks/use_fetcher'; @@ -234,7 +234,7 @@ export const mockUseHasData = () => { return { spy, onRefreshTimeRange }; }; -export const mockAppIndexPattern = () => { +export const mockAppIndexPattern = (props?: Partial) => { const loadIndexPattern = jest.fn(); const spy = jest.spyOn(useAppIndexPatternHook, 'useAppIndexPatternContext').mockReturnValue({ indexPattern: mockIndexPattern, @@ -243,6 +243,8 @@ export const mockAppIndexPattern = () => { hasAppData: { ux: true } as any, loadIndexPattern, indexPatterns: { ux: mockIndexPattern } as unknown as Record, + indexPatternErrors: {} as any, + ...(props || {}), }); return { spy, loadIndexPattern }; }; diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/series_actions.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/series_actions.tsx index 37b5b1571f84d..c1462ce74b426 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/series_actions.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/series_actions.tsx @@ -30,7 +30,7 @@ export function SeriesActions({ seriesId, series, seriesConfig, onEditClick }: P if (allSeries.find(({ name }) => name === copySeriesId)) { copySeriesId = copySeriesId + allSeries.length; } - setSeries(allSeries.length, { ...series, name: copySeriesId }); + setSeries(allSeries.length, { ...series, name: copySeriesId, breakdown: undefined }); }; const toggleSeries = () => { diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/report_metric_options.test.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/report_metric_options.test.tsx new file mode 100644 index 0000000000000..767b765ba1f19 --- /dev/null +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/report_metric_options.test.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 React from 'react'; +import { screen } from '@testing-library/react'; +import { mockAppIndexPattern, mockIndexPattern, mockUxSeries, render } from '../rtl_helpers'; +import { getDefaultConfigs } from '../configurations/default_configs'; +import { PERCENTILE } from '../configurations/constants'; +import { ReportMetricOptions } from './report_metric_options'; + +describe('ReportMetricOptions', function () { + const dataViewSeries = getDefaultConfigs({ + reportType: 'kpi-over-time', + indexPattern: mockIndexPattern, + dataType: 'ux', + }); + + it('should render properly', async function () { + render( + + ); + + expect(await screen.findByText('No data available')).toBeInTheDocument(); + }); + + it('should display loading if index pattern is not available and is loading', async function () { + mockAppIndexPattern({ loading: true, indexPatterns: undefined }); + const { container } = render( + + ); + + expect(container.getElementsByClassName('euiLoadingSpinner').length).toBe(1); + }); + + it('should not display loading if index pattern is already loaded', async function () { + mockAppIndexPattern({ loading: true }); + render( + + ); + + expect(await screen.findByText('Page load time')).toBeInTheDocument(); + }); +}); diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/report_metric_options.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/report_metric_options.tsx index eca18f0eb0dd4..bc7c2328dcbba 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/report_metric_options.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/report_metric_options.tsx @@ -35,7 +35,7 @@ export function ReportMetricOptions({ seriesId, series, seriesConfig }: Props) { const [showOptions, setShowOptions] = useState(false); const metricOptions = seriesConfig?.metricOptions; - const { indexPatterns, loading } = useAppIndexPatternContext(); + const { indexPatterns, indexPatternErrors, loading } = useAppIndexPatternContext(); const onChange = (value?: string) => { setSeries(seriesId, { @@ -49,6 +49,7 @@ export function ReportMetricOptions({ seriesId, series, seriesConfig }: Props) { } const indexPattern = indexPatterns?.[series.dataType]; + const indexPatternError = indexPatternErrors?.[series.dataType]; const options = (metricOptions ?? []).map(({ label, field, id }) => { let disabled = false; @@ -80,6 +81,17 @@ export function ReportMetricOptions({ seriesId, series, seriesConfig }: Props) { }; }); + if (indexPatternError && !indexPattern && !loading) { + // TODO: Add a link to docs to explain how to add index patterns + return ( + + {indexPatternError.body.error === 'Forbidden' + ? NO_PERMISSIONS + : indexPatternError.body.message} + + ); + } + if (!indexPattern && !loading) { return {NO_DATA_AVAILABLE}; } @@ -115,7 +127,7 @@ export function ReportMetricOptions({ seriesId, series, seriesConfig }: Props) { )} {series.selectedMetricField && - (indexPattern && !loading ? ( + (indexPattern ? ( ([]); - const { getSeries, allSeries, reportType, removeSeries } = useSeriesStorage(); + const { getSeries, allSeries, reportType } = useSeriesStorage(); const { loading, indexPatterns } = useAppIndexPatternContext(); @@ -120,15 +113,6 @@ export const SeriesEditor = React.memo(function () { setItemIdToExpandedRowMap(itemIdToExpandedRowMapValues); }; - const resetView = () => { - const totalSeries = allSeries.length; - for (let i = totalSeries; i >= 0; i--) { - removeSeries(i); - } - setEditorItems([]); - setItemIdToExpandedRowMap({}); - }; - return (
    @@ -138,13 +122,6 @@ export const SeriesEditor = React.memo(function () { - {reportType && ( - - resetView()} color="text"> - {RESET_LABEL} - - - )} setItemIdToExpandedRowMap({})} /> diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/utils/observability_index_patterns.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/utils/observability_index_patterns.ts index b61af3a61c3dc..f591ef63a61fb 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/utils/observability_index_patterns.ts +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/utils/observability_index_patterns.ts @@ -86,7 +86,6 @@ export class ObservabilityIndexPatterns { } const appIndicesPattern = getAppIndicesWithPattern(app, indices); - return await this.data.indexPatterns.createAndSave({ title: appIndicesPattern, id: getAppIndexPatternId(app, indices), diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/utils/telemetry.test.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/utils/telemetry.test.tsx new file mode 100644 index 0000000000000..cf24cd47d9d10 --- /dev/null +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/utils/telemetry.test.tsx @@ -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 type { AppDataType } from '../types'; +import { trackTelemetryOnApply, trackTelemetryOnLoad } from './telemetry'; + +const mockMultipleSeries = [ + { + name: 'performance-distribution', + dataType: 'ux' as AppDataType, + breakdown: 'user_agent.name', + time: { from: 'now-15m', to: 'now' }, + filters: [ + { + field: 'url.full', + value: 'https://elastic.co', + }, + ], + selectedMetricField: 'transaction.duration.us', + }, + { + name: 'kpi-over-time', + dataType: 'synthetics' as AppDataType, + breakdown: 'user_agent.name', + time: { from: 'now-15m', to: 'now' }, + filters: [ + { + field: 'monitor.type', + value: 'browser', + }, + ], + selectedMetricField: 'monitor.duration.us', + }, +]; + +describe('telemetry', function () { + it('ensures that appropriate telemetry is called when settings are applied', () => { + const trackEvent = jest.fn(); + trackTelemetryOnApply(trackEvent, mockMultipleSeries, 'kpi-over-time'); + + expect(trackEvent).toBeCalledTimes(7); + expect(trackEvent).toBeCalledWith({ + app: 'observability-overview', + metric: 'exploratory_view__filters__filter_url.full', + metricType: 'count', + }); + expect(trackEvent).toBeCalledWith({ + app: 'observability-overview', + metric: 'exploratory_view__filters__filter_monitor.type', + metricType: 'count', + }); + expect(trackEvent).toBeCalledWith({ + app: 'observability-overview', + metric: 'exploratory_view__filters__report_type_kpi-over-time__data_type_ux__filter_url.full', + metricType: 'count', + }); + expect(trackEvent).toBeCalledWith({ + app: 'observability-overview', + metric: + 'exploratory_view__filters__report_type_kpi-over-time__data_type_synthetics__filter_monitor.type', + metricType: 'count', + }); + expect(trackEvent).toBeCalledWith({ + app: 'observability-overview', + metric: + 'exploratory_view__report_type_kpi-over-time__data_type_synthetics__metric_type_monitor.duration.us', + metricType: 'count', + }); + expect(trackEvent).toBeCalledWith({ + app: 'observability-overview', + metric: + 'exploratory_view__report_type_kpi-over-time__data_type_ux__metric_type_transaction.duration.us', + metricType: 'count', + }); + expect(trackEvent).toBeCalledWith({ + app: 'observability-overview', + metric: 'exploratory_view_apply_changes', + metricType: 'count', + }); + }); + + it('does not call track event for report type/data type/metric type config unless all values are truthy', () => { + const trackEvent = jest.fn(); + const series = { + ...mockMultipleSeries[1], + filters: undefined, + selectedMetricField: undefined, + }; + + trackTelemetryOnApply(trackEvent, [series], 'kpi-over-time'); + + expect(trackEvent).toBeCalledTimes(1); + expect(trackEvent).toBeCalledWith({ + app: 'observability-overview', + metric: 'exploratory_view_apply_changes', + metricType: 'count', + }); + }); + + it.each([ + [1635784025000, '5-10'], + [1635784030000, '10-20'], + [1635784040000, '20-30'], + [1635784050000, '30-60'], + [1635784080000, '60+'], + ])('ensures that appropriate telemetry is called when chart is loaded', (endTime, range) => { + const trackEvent = jest.fn(); + trackTelemetryOnLoad(trackEvent, 1635784020000, endTime); + + expect(trackEvent).toBeCalledTimes(1); + expect(trackEvent).toBeCalledWith({ + app: 'observability-overview', + metric: `exploratory_view__chart_loading_in_seconds_${range}`, + metricType: 'count', + }); + }); +}); diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/utils/telemetry.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/utils/telemetry.ts new file mode 100644 index 0000000000000..76d99824c26f0 --- /dev/null +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/utils/telemetry.ts @@ -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 { TrackEvent, METRIC_TYPE } from '../../../../hooks/use_track_metric'; +import type { SeriesUrl } from '../types'; + +export const trackTelemetryOnApply = ( + trackEvent: TrackEvent, + allSeries: SeriesUrl[], + reportType: string +) => { + trackFilters(trackEvent, allSeries, reportType); + trackDataType(trackEvent, allSeries, reportType); + trackApplyChanges(trackEvent); +}; + +export const trackTelemetryOnLoad = (trackEvent: TrackEvent, start: number, end: number) => { + trackChartLoadingTime(trackEvent, start, end); +}; + +const getAppliedFilters = (allSeries: SeriesUrl[]) => { + const filtersByDataType = new Map(); + allSeries.forEach((series) => { + const seriesFilters = filtersByDataType.get(series.dataType); + const filterFields = (series.filters || []).map((filter) => filter.field); + + if (seriesFilters) { + seriesFilters.push(...filterFields); + } else { + filtersByDataType.set(series.dataType, [...filterFields]); + } + }); + return filtersByDataType; +}; + +const trackFilters = (trackEvent: TrackEvent, allSeries: SeriesUrl[], reportType: string) => { + const filtersByDataType = getAppliedFilters(allSeries); + [...filtersByDataType.keys()].forEach((dataType) => { + const filtersForDataType = filtersByDataType.get(dataType); + + (filtersForDataType || []).forEach((filter) => { + trackEvent({ + app: 'observability-overview', + metricType: METRIC_TYPE.COUNT, + metric: `exploratory_view__filters__filter_${filter}`, + }); + trackEvent({ + app: 'observability-overview', + metricType: METRIC_TYPE.COUNT, + metric: `exploratory_view__filters__report_type_${reportType}__data_type_${dataType}__filter_${filter}`, + }); + }); + }); +}; + +const trackApplyChanges = (trackEvent: TrackEvent) => { + trackEvent({ + app: 'observability-overview', + metricType: METRIC_TYPE.COUNT, + metric: 'exploratory_view_apply_changes', + }); +}; + +const trackDataType = (trackEvent: TrackEvent, allSeries: SeriesUrl[], reportType: string) => { + const metrics = allSeries.map((series) => ({ + dataType: series.dataType, + metricType: series.selectedMetricField, + })); + + metrics.forEach(({ dataType, metricType }) => { + if (reportType && dataType && metricType) { + trackEvent({ + app: 'observability-overview', + metricType: METRIC_TYPE.COUNT, + metric: `exploratory_view__report_type_${reportType}__data_type_${dataType}__metric_type_${metricType}`, + }); + } + }); +}; + +export const trackChartLoadingTime = (trackEvent: TrackEvent, start: number, end: number) => { + const secondsLoading = (end - start) / 1000; + const rangeStr = toRangeStr(secondsLoading); + + if (rangeStr) { + trackChartLoadingMetric(trackEvent, rangeStr); + } +}; + +function toRangeStr(n: number) { + if (n < 0 || isNaN(n)) return null; + if (n >= 60) return '60+'; + else if (n >= 30) return '30-60'; + else if (n >= 20) return '20-30'; + else if (n >= 10) return '10-20'; + else if (n >= 5) return '5-10'; + return '0-5'; +} + +const trackChartLoadingMetric = (trackEvent: TrackEvent, range: string) => { + trackEvent({ + app: 'observability-overview', + metricType: METRIC_TYPE.COUNT, + metric: `exploratory_view__chart_loading_in_seconds_${range}`, + }); +}; diff --git a/x-pack/plugins/observability/public/config/index.ts b/x-pack/plugins/observability/public/config/index.ts new file mode 100644 index 0000000000000..fc6300acc4716 --- /dev/null +++ b/x-pack/plugins/observability/public/config/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 { paths } from './paths'; +export { translations } from './translations'; diff --git a/x-pack/plugins/observability/public/config/paths.ts b/x-pack/plugins/observability/public/config/paths.ts new file mode 100644 index 0000000000000..57bbc95fef40b --- /dev/null +++ b/x-pack/plugins/observability/public/config/paths.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. + */ + +export const paths = { + observability: { + alerts: '/app/observability/alerts', + }, + management: { + rules: '/app/management/insightsAndAlerting/triggersActions/rules', + ruleDetails: (ruleId: string) => + `/app/management/insightsAndAlerting/triggersActions/rule/${encodeURI(ruleId)}`, + alertDetails: (alertId: string) => + `/app/management/insightsAndAlerting/triggersActions/alert/${encodeURI(alertId)}`, + }, +}; diff --git a/x-pack/plugins/observability/public/config/translations.ts b/x-pack/plugins/observability/public/config/translations.ts new file mode 100644 index 0000000000000..265787ede4473 --- /dev/null +++ b/x-pack/plugins/observability/public/config/translations.ts @@ -0,0 +1,104 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { i18n } from '@kbn/i18n'; + +export const translations = { + alertsTable: { + viewDetailsTextLabel: i18n.translate('xpack.observability.alertsTable.viewDetailsTextLabel', { + defaultMessage: 'View details', + }), + viewInAppTextLabel: i18n.translate('xpack.observability.alertsTable.viewInAppTextLabel', { + defaultMessage: 'View in app', + }), + moreActionsTextLabel: i18n.translate('xpack.observability.alertsTable.moreActionsTextLabel', { + defaultMessage: 'More actions', + }), + notEnoughPermissions: i18n.translate('xpack.observability.alertsTable.notEnoughPermissions', { + defaultMessage: 'Additional privileges required', + }), + statusColumnDescription: i18n.translate( + 'xpack.observability.alertsTGrid.statusColumnDescription', + { + defaultMessage: 'Alert Status', + } + ), + lastUpdatedColumnDescription: i18n.translate( + 'xpack.observability.alertsTGrid.lastUpdatedColumnDescription', + { + defaultMessage: 'Last updated', + } + ), + durationColumnDescription: i18n.translate( + 'xpack.observability.alertsTGrid.durationColumnDescription', + { + defaultMessage: 'Duration', + } + ), + reasonColumnDescription: i18n.translate( + 'xpack.observability.alertsTGrid.reasonColumnDescription', + { + defaultMessage: 'Reason', + } + ), + actionsTextLabel: i18n.translate('xpack.observability.alertsTable.actionsTextLabel', { + defaultMessage: 'Actions', + }), + loadingTextLabel: i18n.translate('xpack.observability.alertsTable.loadingTextLabel', { + defaultMessage: 'loading alerts', + }), + footerTextLabel: i18n.translate('xpack.observability.alertsTable.footerTextLabel', { + defaultMessage: 'alerts', + }), + showingAlertsTitle: (totalAlerts: number) => + i18n.translate('xpack.observability.alertsTable.showingAlertsTitle', { + values: { totalAlerts }, + defaultMessage: '{totalAlerts, plural, =1 {alert} other {alerts}}', + }), + viewRuleDetailsButtonText: i18n.translate( + 'xpack.observability.alertsTable.viewRuleDetailsButtonText', + { + defaultMessage: 'View rule details', + } + ), + }, + alertsFlyout: { + statusLabel: i18n.translate('xpack.observability.alertsFlyout.statusLabel', { + defaultMessage: 'Status', + }), + lastUpdatedLabel: i18n.translate('xpack.observability.alertsFlyout.lastUpdatedLabel', { + defaultMessage: 'Last updated', + }), + durationLabel: i18n.translate('xpack.observability.alertsFlyout.durationLabel', { + defaultMessage: 'Duration', + }), + expectedValueLabel: i18n.translate('xpack.observability.alertsFlyout.expectedValueLabel', { + defaultMessage: 'Expected value', + }), + actualValueLabel: i18n.translate('xpack.observability.alertsFlyout.actualValueLabel', { + defaultMessage: 'Actual value', + }), + ruleTypeLabel: i18n.translate('xpack.observability.alertsFlyout.ruleTypeLabel', { + defaultMessage: 'Rule type', + }), + reasonTitle: i18n.translate('xpack.observability.alertsFlyout.reasonTitle', { + defaultMessage: 'Reason', + }), + viewRulesDetailsLinkText: i18n.translate( + 'xpack.observability.alertsFlyout.viewRulesDetailsLinkText', + { + defaultMessage: 'View rule details', + } + ), + documentSummaryTitle: i18n.translate('xpack.observability.alertsFlyout.documentSummaryTitle', { + defaultMessage: 'Document Summary', + }), + viewInAppButtonText: i18n.translate('xpack.observability.alertsFlyout.viewInAppButtonText', { + defaultMessage: 'View in app', + }), + }, +}; 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 a586a8bf0bcce..1eb4108b12181 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 @@ -556,7 +556,7 @@ describe('HasDataContextProvider', () => { status: 'success', }, }, - hasAnyData: false, + hasAnyData: true, isAllRequestsComplete: true, forceUpdate: expect.any(String), onRefreshTimeRange: expect.any(Function), 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 caed130543acc..b6a45784a53b4 100644 --- a/x-pack/plugins/observability/public/context/has_data_context.tsx +++ b/x-pack/plugins/observability/public/context/has_data_context.tsx @@ -146,9 +146,12 @@ export function HasDataContextProvider({ children }: { children: React.ReactNode return appStatus !== undefined && appStatus !== FETCH_STATUS.LOADING; }); - const hasAnyData = (Object.keys(hasDataMap) as ObservabilityFetchDataPlugins[]).some( - (app) => hasDataMap[app]?.hasData === true - ); + const hasAnyData = (Object.keys(hasDataMap) as ObservabilityFetchDataPlugins[]).some((app) => { + const appHasData = hasDataMap[app]?.hasData; + return ( + appHasData === true || (Array.isArray(appHasData) && (appHasData as Alert[])?.length > 0) + ); + }); return ( ; +export type TrackEvent = (options: TrackMetricOptions) => void; export { METRIC_TYPE }; export function useUiTracker({ app: defaultApp, -}: { app?: ObservabilityApp } = {}) { +}: { app?: ObservabilityApp } = {}): TrackEvent { const reportUiCounter = useKibana().services?.usageCollection?.reportUiCounter; const trackEvent = useMemo(() => { return ({ app = defaultApp, metric, metricType = METRIC_TYPE.COUNT }: TrackMetricOptions) => { diff --git a/x-pack/plugins/observability/public/index.ts b/x-pack/plugins/observability/public/index.ts index 3eebf8a84db19..a4ce62ddde0c7 100644 --- a/x-pack/plugins/observability/public/index.ts +++ b/x-pack/plugins/observability/public/index.ts @@ -85,3 +85,5 @@ export type { ExploratoryEmbeddableProps } from './components/shared/exploratory export type { AddInspectorRequest } from './context/inspector/inspector_context'; export { InspectorContextProvider } from './context/inspector/inspector_context'; export { useInspectorContext } from './context/inspector/use_inspector_context'; + +export { enableComparisonByDefault } from '../common/ui_settings_keys'; diff --git a/x-pack/plugins/observability/public/pages/alerts/alerts_disclaimer.tsx b/x-pack/plugins/observability/public/pages/alerts/alerts_disclaimer.tsx index 6a57b08bf8d38..1d1aaf12cf785 100644 --- a/x-pack/plugins/observability/public/pages/alerts/alerts_disclaimer.tsx +++ b/x-pack/plugins/observability/public/pages/alerts/alerts_disclaimer.tsx @@ -5,33 +5,68 @@ * 2.0. */ -import React from 'react'; -import { EuiLink, EuiCallOut } from '@elastic/eui'; +import React, { useState } from 'react'; +import { EuiLink, EuiCallOut, EuiButton, EuiSpacer } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; +const LOCAL_STORAGE_KEY_MSG_ACK = 'xpack.observability.alert.ack.experimental.message'; + export function AlertsDisclaimer() { + const getCurrentExperimentalMsgAckState = () => { + try { + const isExperimentalMsgAck = localStorage.getItem(LOCAL_STORAGE_KEY_MSG_ACK); + return isExperimentalMsgAck && JSON.parse(isExperimentalMsgAck) === true; + } catch { + return false; + } + }; + + const [experimentalMsgAck, setExperimentalMsgAck] = useState(getCurrentExperimentalMsgAckState); + + const dismissMessage = () => { + setExperimentalMsgAck(true); + localStorage.setItem(LOCAL_STORAGE_KEY_MSG_ACK, 'true'); + }; + return ( - - - {i18n.translate('xpack.observability.alertsDisclaimerLinkText', { - defaultMessage: 'feedback', - })} - - ), - }} - /> - + <> + {!experimentalMsgAck && ( + + + {i18n.translate('xpack.observability.alertsDisclaimerLinkText', { + defaultMessage: 'feedback', + })} + + ), + }} + /> + + + + {i18n.translate('xpack.observability.alertsDisclaimerDismissMessage', { + defaultMessage: 'Dismiss message', + })} + + + )} + ); } diff --git a/x-pack/plugins/observability/public/pages/alerts/alerts_flyout/index.tsx b/x-pack/plugins/observability/public/pages/alerts/alerts_flyout/index.tsx index 034b7522b9136..bb746d0acc1cc 100644 --- a/x-pack/plugins/observability/public/pages/alerts/alerts_flyout/index.tsx +++ b/x-pack/plugins/observability/public/pages/alerts/alerts_flyout/index.tsx @@ -15,11 +15,12 @@ import { EuiFlyoutFooter, EuiFlyoutHeader, EuiFlyoutProps, + EuiLink, EuiSpacer, EuiText, EuiTitle, + EuiHorizontalRule, } from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; import type { ALERT_DURATION as ALERT_DURATION_TYPED, ALERT_EVALUATION_THRESHOLD as ALERT_EVALUATION_THRESHOLD_TYPED, @@ -47,6 +48,7 @@ import type { ObservabilityRuleTypeRegistry } from '../../../rules/create_observ import { parseAlert } from '../parse_alert'; import { AlertStatusIndicator } from '../../../components/shared/alert_status_indicator'; import { ExperimentalBadge } from '../../../components/shared/experimental_badge'; +import { translations, paths } from '../../../config'; type AlertsFlyoutProps = { alert?: TopAlert; @@ -77,6 +79,7 @@ export function AlertsFlyout({ const { services } = useKibana(); const { http } = services; const prepend = http?.basePath.prepend; + const decoratedAlerts = useMemo(() => { const parseObservabilityAlert = parseAlert(observabilityRuleTypeRegistry); return (alerts ?? []).map(parseObservabilityAlert); @@ -90,11 +93,12 @@ export function AlertsFlyout({ return null; } + const ruleId = alertData.fields['kibana.alert.rule.uuid'] ?? null; + const linkToRule = ruleId && prepend ? prepend(paths.management.ruleDetails(ruleId)) : null; + const overviewListItems = [ { - title: i18n.translate('xpack.observability.alertsFlyout.statusLabel', { - defaultMessage: 'Status', - }), + title: translations.alertsFlyout.statusLabel, description: ( {moment(alertData.start).format(dateFormat)} ), }, { - title: i18n.translate('xpack.observability.alertsFlyout.durationLabel', { - defaultMessage: 'Duration', - }), + title: translations.alertsFlyout.durationLabel, description: asDuration(alertData.fields[ALERT_DURATION], { extended: true }), }, { - title: i18n.translate('xpack.observability.alertsFlyout.expectedValueLabel', { - defaultMessage: 'Expected value', - }), + title: translations.alertsFlyout.expectedValueLabel, description: alertData.fields[ALERT_EVALUATION_THRESHOLD] ?? '-', }, { - title: i18n.translate('xpack.observability.alertsFlyout.actualValueLabel', { - defaultMessage: 'Actual value', - }), + title: translations.alertsFlyout.actualValueLabel, description: alertData.fields[ALERT_EVALUATION_VALUE] ?? '-', }, { - title: i18n.translate('xpack.observability.alertsFlyout.ruleTypeLabel', { - defaultMessage: 'Rule type', - }), + title: translations.alertsFlyout.ruleTypeLabel, description: alertData.fields[ALERT_RULE_CATEGORY] ?? '-', }, ]; return ( - +

    {alertData.fields[ALERT_RULE_NAME]}

    - - {alertData.reason}
    + +

    {translations.alertsFlyout.reasonTitle}

    +
    + + {alertData.reason} + {!!linkToRule && ( + + {translations.alertsFlyout.viewRulesDetailsLinkText} + + )} + + +

    {translations.alertsFlyout.documentSummaryTitle}

    +
    +
    {alertData.link && !isInApp && ( @@ -173,7 +176,7 @@ export function AlertsFlyout({ data-test-subj="alertsFlyoutViewInAppButton" fill > - View in app + {translations.alertsFlyout.viewInAppButtonText} diff --git a/x-pack/plugins/observability/public/pages/alerts/alerts_table_t_grid.tsx b/x-pack/plugins/observability/public/pages/alerts/alerts_table_t_grid.tsx index ace01aa851ce8..523d0f19be2be 100644 --- a/x-pack/plugins/observability/public/pages/alerts/alerts_table_t_grid.tsx +++ b/x-pack/plugins/observability/public/pages/alerts/alerts_table_t_grid.tsx @@ -34,11 +34,12 @@ import { EuiDataGridColumn, EuiFlexGroup, EuiFlexItem, + EuiContextMenuItem, EuiContextMenuPanel, EuiPopover, EuiToolTip, } from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; + import styled from 'styled-components'; import React, { Suspense, useMemo, useState, useCallback, useEffect } from 'react'; import usePrevious from 'react-use/lib/usePrevious'; @@ -66,6 +67,7 @@ import { getDefaultCellActions } from './default_cell_actions'; import { LazyAlertsFlyout } from '../..'; import { parseAlert } from './parse_alert'; import { CoreStart } from '../../../../../../src/core/public'; +import { translations, paths } from '../../config'; const ALERT_DURATION: typeof ALERT_DURATION_TYPED = ALERT_DURATION_NON_TYPED; const ALERT_REASON: typeof ALERT_REASON_TYPED = ALERT_REASON_NON_TYPED; @@ -115,33 +117,25 @@ export const columns: Array< > = [ { columnHeaderType: 'not-filtered', - displayAsText: i18n.translate('xpack.observability.alertsTGrid.statusColumnDescription', { - defaultMessage: 'Alert Status', - }), + displayAsText: translations.alertsTable.statusColumnDescription, id: ALERT_STATUS, initialWidth: 110, }, { columnHeaderType: 'not-filtered', - displayAsText: i18n.translate('xpack.observability.alertsTGrid.lastUpdatedColumnDescription', { - defaultMessage: 'Last updated', - }), + displayAsText: translations.alertsTable.lastUpdatedColumnDescription, id: TIMESTAMP, initialWidth: 230, }, { columnHeaderType: 'not-filtered', - displayAsText: i18n.translate('xpack.observability.alertsTGrid.durationColumnDescription', { - defaultMessage: 'Duration', - }), + displayAsText: translations.alertsTable.durationColumnDescription, id: ALERT_DURATION, initialWidth: 116, }, { columnHeaderType: 'not-filtered', - displayAsText: i18n.translate('xpack.observability.alertsTGrid.reasonColumnDescription', { - defaultMessage: 'Reason', - }), + displayAsText: translations.alertsTable.reasonColumnDescription, id: ALERT_REASON, linkField: '*', }, @@ -196,6 +190,7 @@ function ObservabilityActions({ const toggleActionsPopover = useCallback((id) => { setActionsPopover((current) => (current ? null : id)); }, []); + const casePermissions = useGetUserCasesPermissions(); const event = useMemo(() => { return { @@ -227,6 +222,9 @@ function ObservabilityActions({ onUpdateFailure: onAlertStatusUpdated, }); + const ruleId = alert.fields['kibana.alert.rule.uuid'] ?? null; + const linkToRule = ruleId ? prepend(paths.management.ruleDetails(ruleId)) : null; + const actionsMenuItems = useMemo(() => { return [ ...(casePermissions?.crud @@ -235,87 +233,95 @@ function ObservabilityActions({ event, casePermissions, appId: observabilityFeatureId, + owner: observabilityFeatureId, onClose: afterCaseSelection, }), timelines.getAddToNewCaseButton({ event, casePermissions, appId: observabilityFeatureId, + owner: observabilityFeatureId, onClose: afterCaseSelection, }), ] : []), ...(alertPermissions.crud ? statusActionItems : []), + ...(!!linkToRule + ? [ + + {translations.alertsTable.viewRuleDetailsButtonText} + , + ] + : []), ]; - }, [afterCaseSelection, casePermissions, timelines, event, statusActionItems, alertPermissions]); + }, [ + afterCaseSelection, + casePermissions, + timelines, + event, + statusActionItems, + alertPermissions, + linkToRule, + ]); - const viewDetailsTextLabel = i18n.translate( - 'xpack.observability.alertsTable.viewDetailsTextLabel', - { - defaultMessage: 'View details', - } - ); - const viewInAppTextLabel = i18n.translate('xpack.observability.alertsTable.viewInAppTextLabel', { - defaultMessage: 'View in app', - }); - const moreActionsTextLabel = i18n.translate( - 'xpack.observability.alertsTable.moreActionsTextLabel', - { - defaultMessage: 'More actions', - } - ); + const actionsToolTip = + actionsMenuItems.length <= 0 + ? translations.alertsTable.notEnoughPermissions + : translations.alertsTable.moreActionsTextLabel; return ( <> - + setFlyoutAlert(alert)} data-test-subj="openFlyoutButton" - aria-label={viewDetailsTextLabel} + aria-label={translations.alertsTable.viewDetailsTextLabel} /> - + - {actionsMenuItems.length > 0 && ( - - - toggleActionsPopover(eventId)} - data-test-subj="alerts-table-row-action-more" - /> - - } - isOpen={openActionsPopoverId === eventId} - closePopover={closeActionsPopover} - panelPaddingSize="none" - anchorPosition="downLeft" - > - - - - )} + + + toggleActionsPopover(eventId)} + data-test-subj="alertsTableRowActionMore" + /> + + } + isOpen={openActionsPopoverId === eventId} + closePopover={closeActionsPopover} + panelPaddingSize="none" + anchorPosition="downLeft" + > + + + ); @@ -363,13 +369,7 @@ export function AlertsTableTGrid(props: AlertsTableTGridProps) { id: 'expand', width: 120, headerCellRender: () => { - return ( - - {i18n.translate('xpack.observability.alertsTable.actionsTextLabel', { - defaultMessage: 'Actions', - })} - - ); + return {translations.alertsTable.actionsTextLabel}; }, rowCellRender: (actionProps: ActionProps) => { return ( @@ -390,6 +390,7 @@ export function AlertsTableTGrid(props: AlertsTableTGridProps) { const sortDirection: SortDirection = 'desc'; return { appId: observabilityFeatureId, + casesOwner: observabilityFeatureId, casePermissions, type, columns, @@ -400,18 +401,16 @@ export function AlertsTableTGrid(props: AlertsTableTGridProps) { hasAlertsCrudPermissions, indexNames, itemsPerPageOptions: [10, 25, 50], - loadingText: i18n.translate('xpack.observability.alertsTable.loadingTextLabel', { - defaultMessage: 'loading alerts', - }), - footerText: i18n.translate('xpack.observability.alertsTable.footerTextLabel', { - defaultMessage: 'alerts', - }), + loadingText: translations.alertsTable.loadingTextLabel, + footerText: translations.alertsTable.footerTextLabel, query: { query: `${ALERT_WORKFLOW_STATUS}: ${workflowStatus}${kuery !== '' ? ` and ${kuery}` : ''}`, language: 'kuery', }, renderCellValue: getRenderCellValue({ setFlyoutAlert }), rowRenderers: NO_ROW_RENDER, + // TODO: implement Kibana data view runtime fields in observability + runtimeMappings: {}, start: rangeFrom, setRefetch, sort: [ @@ -424,11 +423,7 @@ export function AlertsTableTGrid(props: AlertsTableTGridProps) { filterStatus: workflowStatus as AlertWorkflowStatus, leadingControlColumns, trailingControlColumns, - unit: (totalAlerts: number) => - i18n.translate('xpack.observability.alertsTable.showingAlertsTitle', { - values: { totalAlerts }, - defaultMessage: '{totalAlerts, plural, =1 {alert} other {alerts}}', - }), + unit: (totalAlerts: number) => translations.alertsTable.showingAlertsTitle(totalAlerts), }; }, [ casePermissions, @@ -443,6 +438,7 @@ export function AlertsTableTGrid(props: AlertsTableTGridProps) { leadingControlColumns, deletedEventIds, ]); + const handleFlyoutClose = () => setFlyoutAlert(undefined); const { observabilityRuleTypeRegistry } = usePluginContext(); diff --git a/x-pack/plugins/observability/public/pages/alerts/filter_for_value.tsx b/x-pack/plugins/observability/public/pages/alerts/filter_for_value.tsx index f75ae488c9b28..7017f573415da 100644 --- a/x-pack/plugins/observability/public/pages/alerts/filter_for_value.tsx +++ b/x-pack/plugins/observability/public/pages/alerts/filter_for_value.tsx @@ -35,7 +35,7 @@ const FilterForValueButton: React.FC = React.memo( Component ? ( = React.memo( { const props = { onChange, status }; const { getByTestId } = render(); - const button = getByTestId(`workflow-status-filter-${status}-button`); + const button = getByTestId(`workflowStatusFilterButton-${status}`); const input = button.querySelector('input') as Element; Simulate.change(input); diff --git a/x-pack/plugins/observability/public/pages/alerts/workflow_status_filter.tsx b/x-pack/plugins/observability/public/pages/alerts/workflow_status_filter.tsx index 20073e9937b4f..d857b9d6bd650 100644 --- a/x-pack/plugins/observability/public/pages/alerts/workflow_status_filter.tsx +++ b/x-pack/plugins/observability/public/pages/alerts/workflow_status_filter.tsx @@ -21,7 +21,7 @@ const options: Array = label: i18n.translate('xpack.observability.alerts.workflowStatusFilter.openButtonLabel', { defaultMessage: 'Open', }), - 'data-test-subj': 'workflow-status-filter-open-button', + 'data-test-subj': 'workflowStatusFilterButton-open', }, { id: 'acknowledged', @@ -31,14 +31,14 @@ const options: Array = defaultMessage: 'Acknowledged', } ), - 'data-test-subj': 'workflow-status-filter-acknowledged-button', + 'data-test-subj': 'workflowStatusFilterButton-acknowledged', }, { id: 'closed', label: i18n.translate('xpack.observability.alerts.workflowStatusFilter.closedButtonLabel', { defaultMessage: 'Closed', }), - 'data-test-subj': 'workflow-status-filter-closed-button', + 'data-test-subj': 'workflowStatusFilterButton-closed', }, ]; diff --git a/x-pack/plugins/observability/public/pages/cases/all_cases.tsx b/x-pack/plugins/observability/public/pages/cases/all_cases.tsx index 4ac7c4cfd92a5..76c54a470ff83 100644 --- a/x-pack/plugins/observability/public/pages/cases/all_cases.tsx +++ b/x-pack/plugins/observability/public/pages/cases/all_cases.tsx @@ -8,7 +8,6 @@ import React from 'react'; import { AllCases } from '../../components/app/cases/all_cases'; -import * as i18n from '../../components/app/cases/translations'; import { CaseFeatureNoPermissions } from './feature_no_permissions'; import { useGetUserCasesPermissions } from '../../hooks/use_get_user_cases_permissions'; @@ -45,9 +44,6 @@ export const AllCasesPage = React.memo(() => { {i18n.PAGE_TITLE}, - }} > diff --git a/x-pack/plugins/observability/public/pages/cases/empty_page.tsx b/x-pack/plugins/observability/public/pages/cases/empty_page.tsx index c6fc4b59ef77c..faeafa6b4730f 100644 --- a/x-pack/plugins/observability/public/pages/cases/empty_page.tsx +++ b/x-pack/plugins/observability/public/pages/cases/empty_page.tsx @@ -59,7 +59,7 @@ const EmptyPageComponent = React.memo(({ actions, message, title (({ actions, message, title iconType={icon} target={target} fill={fill} - data-test-subj={`empty-page-${titles[idx]}-action`} + data-test-subj={`emptyPageAction-${titles[idx]}`} > {label} @@ -83,7 +83,7 @@ const EmptyPageComponent = React.memo(({ actions, message, title {/* eslint-disable-next-line @elastic/eui/href-or-on-click */} (({ actions, message, title onClick={onClick} iconType={icon} target={target} - data-test-subj={`empty-page-${titles[idx]}-action`} + data-test-subj={`emptyPageAction-${titles[idx]}`} > {label} diff --git a/x-pack/plugins/observability/public/pages/cases/feature_no_permissions.tsx b/x-pack/plugins/observability/public/pages/cases/feature_no_permissions.tsx index 5075570c15b3e..2d8631a94e04c 100644 --- a/x-pack/plugins/observability/public/pages/cases/feature_no_permissions.tsx +++ b/x-pack/plugins/observability/public/pages/cases/feature_no_permissions.tsx @@ -29,7 +29,7 @@ export const CaseFeatureNoPermissions = React.memo(() => { ); diff --git a/x-pack/plugins/observability/public/utils/no_data_config.ts b/x-pack/plugins/observability/public/utils/no_data_config.ts index c8e7daaf688bc..fdd70401e7097 100644 --- a/x-pack/plugins/observability/public/utils/no_data_config.ts +++ b/x-pack/plugins/observability/public/utils/no_data_config.ts @@ -32,7 +32,7 @@ export function getNoDataConfig({ defaultMessage: 'Use Beats and APM agents to send observability data to Elasticsearch. We make it easy with support for many popular systems, apps, and languages.', }), - href: basePath.prepend(`/app/home#/tutorial/apm`), + href: basePath.prepend(`/app/integrations/browse`), }, }, docsLink, diff --git a/x-pack/plugins/observability/server/index.ts b/x-pack/plugins/observability/server/index.ts index d4d7127d8baee..d99cf0865c0dd 100644 --- a/x-pack/plugins/observability/server/index.ts +++ b/x-pack/plugins/observability/server/index.ts @@ -17,7 +17,7 @@ import { unwrapEsResponse, WrappedElasticsearchClientError, } from '../common/utils/unwrap_es_response'; -export { rangeQuery, kqlQuery } from './utils/queries'; +export { rangeQuery, kqlQuery, termQuery } from './utils/queries'; export { getInspectResponse } from '../common/utils/get_inspect_response'; export * from './types'; diff --git a/x-pack/plugins/observability/server/ui_settings.ts b/x-pack/plugins/observability/server/ui_settings.ts index 0bd9f99b5b145..ad0aa31542e8c 100644 --- a/x-pack/plugins/observability/server/ui_settings.ts +++ b/x-pack/plugins/observability/server/ui_settings.ts @@ -9,7 +9,11 @@ import { schema } from '@kbn/config-schema'; import { i18n } from '@kbn/i18n'; import { UiSettingsParams } from '../../../../src/core/types'; import { observabilityFeatureId } from '../common'; -import { enableInspectEsQueries, maxSuggestions } from '../common/ui_settings_keys'; +import { + enableComparisonByDefault, + enableInspectEsQueries, + maxSuggestions, +} from '../common/ui_settings_keys'; /** * uiSettings definitions for Observability. @@ -37,4 +41,15 @@ export const uiSettings: Record> = { }), schema: schema.number(), }, + [enableComparisonByDefault]: { + category: [observabilityFeatureId], + name: i18n.translate('xpack.observability.enableComparisonByDefault', { + defaultMessage: 'Comparison feature', + }), + value: true, + description: i18n.translate('xpack.observability.enableComparisonByDefaultDescription', { + defaultMessage: 'Enable the comparison feature on APM UI', + }), + schema: schema.boolean(), + }, }; diff --git a/x-pack/plugins/observability/server/utils/queries.ts b/x-pack/plugins/observability/server/utils/queries.ts index 953c0021636d4..54900cd46ea47 100644 --- a/x-pack/plugins/observability/server/utils/queries.ts +++ b/x-pack/plugins/observability/server/utils/queries.ts @@ -8,6 +8,14 @@ import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; import { fromKueryExpression, toElasticsearchQuery } from '@kbn/es-query'; +export function termQuery(field: T, value: string | undefined) { + if (!value) { + return []; + } + + return [{ term: { [field]: value } as Record }]; +} + export function rangeQuery( start?: number, end?: number, diff --git a/x-pack/plugins/osquery/common/schemas/common/schemas.ts b/x-pack/plugins/osquery/common/schemas/common/schemas.ts index 4547db731ce1b..24eaa11a7bf84 100644 --- a/x-pack/plugins/osquery/common/schemas/common/schemas.ts +++ b/x-pack/plugins/osquery/common/schemas/common/schemas.ts @@ -57,7 +57,7 @@ export const ecsMapping = t.record( t.string, t.partial({ field: t.string, - value: t.string, + value: t.union([t.string, t.array(t.string)]), }) ); export type ECSMapping = t.TypeOf; diff --git a/x-pack/plugins/osquery/public/actions/actions_table.tsx b/x-pack/plugins/osquery/public/actions/actions_table.tsx index 6f19979345ddf..b3464bad56340 100644 --- a/x-pack/plugins/osquery/public/actions/actions_table.tsx +++ b/x-pack/plugins/osquery/public/actions/actions_table.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import { isArray, pickBy } from 'lodash'; +import { isArray, isEmpty, pickBy } from 'lodash'; import { i18n } from '@kbn/i18n'; import { EuiBasicTable, EuiButtonIcon, EuiCodeBlock, formatDate } from '@elastic/eui'; import React, { useState, useCallback, useMemo } from 'react'; @@ -72,12 +72,15 @@ const ActionsTableComponent = () => { const handlePlayClick = useCallback( (item) => push('/live_queries/new', { - form: pickBy({ - agentIds: item.fields.agents, - query: item._source.data.query, - ecs_mapping: item._source.data.ecs_mapping, - savedQueryId: item._source.data.saved_query_id, - }), + form: pickBy( + { + agentIds: item.fields.agents, + query: item._source.data.query, + ecs_mapping: item._source.data.ecs_mapping, + savedQueryId: item._source.data.saved_query_id, + }, + (value) => !isEmpty(value) + ), }), [push] ); diff --git a/x-pack/plugins/osquery/public/agents/use_agent_policy_agent_ids.ts b/x-pack/plugins/osquery/public/agents/use_agent_policy_agent_ids.ts index 65a2520e07d0b..77ca08e284182 100644 --- a/x-pack/plugins/osquery/public/agents/use_agent_policy_agent_ids.ts +++ b/x-pack/plugins/osquery/public/agents/use_agent_policy_agent_ids.ts @@ -9,7 +9,7 @@ import { map } from 'lodash'; import { i18n } from '@kbn/i18n'; import { useQuery } from 'react-query'; -import { AGENT_SAVED_OBJECT_TYPE, Agent } from '../../../fleet/common'; +import { AGENTS_PREFIX, Agent } from '../../../fleet/common'; import { useErrorToast } from '../common/hooks/use_error_toast'; import { useKibana } from '../common/lib/kibana'; @@ -30,7 +30,7 @@ export const useAgentPolicyAgentIds = ({ return useQuery<{ agents: Agent[] }, unknown, string[]>( ['agentPolicyAgentIds', agentPolicyId], () => { - const kuery = `${AGENT_SAVED_OBJECT_TYPE}.policy_id:${agentPolicyId}`; + const kuery = `${AGENTS_PREFIX}.policy_id:${agentPolicyId}`; return http.get(`/internal/osquery/fleet_wrapper/agents`, { query: { diff --git a/x-pack/plugins/osquery/public/application.tsx b/x-pack/plugins/osquery/public/application.tsx index 3e959132e21a8..3e046a138cd4b 100644 --- a/x-pack/plugins/osquery/public/application.tsx +++ b/x-pack/plugins/osquery/public/application.tsx @@ -6,8 +6,7 @@ */ import { EuiErrorBoundary } from '@elastic/eui'; -import euiDarkVars from '@elastic/eui/dist/eui_theme_dark.json'; -import euiLightVars from '@elastic/eui/dist/eui_theme_light.json'; +import { euiLightVars, euiDarkVars } from '@kbn/ui-shared-deps-src/theme'; import React, { useMemo } from 'react'; import ReactDOM from 'react-dom'; import { Router } from 'react-router-dom'; diff --git a/x-pack/plugins/osquery/public/common/lib/kibana/kibana_react.ts b/x-pack/plugins/osquery/public/common/lib/kibana/kibana_react.ts index a77ddaee9f250..affefda1e61e0 100644 --- a/x-pack/plugins/osquery/public/common/lib/kibana/kibana_react.ts +++ b/x-pack/plugins/osquery/public/common/lib/kibana/kibana_react.ts @@ -7,6 +7,7 @@ import React from 'react'; import { useHistory } from 'react-router-dom'; +import { FieldIcon } from '@kbn/react-field/field_icon'; import { KibanaContextProvider, KibanaReactContextValue, @@ -15,7 +16,6 @@ import { useUiSetting$, withKibana, reactRouterNavigate, - FieldIcon, } from '../../../../../../../src/plugins/kibana_react/public'; import { StartServices } from '../../../types'; diff --git a/x-pack/plugins/osquery/public/components/app.tsx b/x-pack/plugins/osquery/public/components/app.tsx index f4c805d375351..ef249d5b8c7aa 100644 --- a/x-pack/plugins/osquery/public/components/app.tsx +++ b/x-pack/plugins/osquery/public/components/app.tsx @@ -9,7 +9,17 @@ import React, { useMemo } from 'react'; import { FormattedMessage } from '@kbn/i18n/react'; -import { EuiButtonEmpty, EuiFlexGroup, EuiFlexItem, EuiTabs, EuiTab } from '@elastic/eui'; +import { + EuiButtonEmpty, + EuiFlexGroup, + EuiFlexItem, + EuiTabs, + EuiTab, + EuiLoadingElastic, + EuiPage, + EuiPageBody, + EuiPageContent, +} from '@elastic/eui'; import { useLocation } from 'react-router-dom'; import { Container, Nav, Wrapper } from './layouts'; @@ -24,6 +34,24 @@ const OsqueryAppComponent = () => { const section = useMemo(() => location.pathname.split('/')[1] ?? 'overview', [location.pathname]); const { data: osqueryIntegration, isFetched } = useOsqueryIntegrationStatus(); + if (!isFetched) { + return ( + + + + + + + + ); + } + if (isFetched && osqueryIntegration?.install_status !== 'installed') { return ; } diff --git a/x-pack/plugins/osquery/public/components/osquery_icon/index.tsx b/x-pack/plugins/osquery/public/components/osquery_icon/index.tsx index 0c2a24ef7b694..83e328dc7c615 100644 --- a/x-pack/plugins/osquery/public/components/osquery_icon/index.tsx +++ b/x-pack/plugins/osquery/public/components/osquery_icon/index.tsx @@ -5,14 +5,25 @@ * 2.0. */ -import React from 'react'; +import React, { useEffect, useState } from 'react'; import { EuiIcon, EuiIconProps } from '@elastic/eui'; import OsqueryLogo from './osquery.svg'; export type OsqueryIconProps = Omit; -const OsqueryIconComponent: React.FC = (props) => ( - -); +const OsqueryIconComponent: React.FC = (props) => { + const [Icon, setIcon] = useState(null); + + // FIXME: This is a hack to force the icon to be loaded asynchronously. + useEffect(() => { + const interval = setInterval(() => { + setIcon(); + }, 0); + + return () => clearInterval(interval); + }, [props, setIcon]); + + return Icon; +}; export const OsqueryIcon = React.memo(OsqueryIconComponent); diff --git a/x-pack/plugins/osquery/public/fleet_integration/osquery_managed_policy_create_import_extension.tsx b/x-pack/plugins/osquery/public/fleet_integration/osquery_managed_policy_create_import_extension.tsx index c2ac84ce191da..39975cb65ce2b 100644 --- a/x-pack/plugins/osquery/public/fleet_integration/osquery_managed_policy_create_import_extension.tsx +++ b/x-pack/plugins/osquery/public/fleet_integration/osquery_managed_policy_create_import_extension.tsx @@ -318,6 +318,16 @@ export const OsqueryManagedPolicyCreateImportExtension = React.memo< streams: [], policy_template: 'osquery_manager', }); + } else { + if (!draft.inputs[0].type) { + set(draft, 'inputs[0].type', 'osquery'); + } + if (!draft.inputs[0].policy_template) { + set(draft, 'inputs[0].policy_template', 'osquery_manager'); + } + if (!draft.inputs[0].enabled) { + set(draft, 'inputs[0].enabled', true); + } } }); onChange({ diff --git a/x-pack/plugins/osquery/public/live_queries/form/index.tsx b/x-pack/plugins/osquery/public/live_queries/form/index.tsx index 0c0151b36203c..b8a9de25ac7f8 100644 --- a/x-pack/plugins/osquery/public/live_queries/form/index.tsx +++ b/x-pack/plugins/osquery/public/live_queries/form/index.tsx @@ -86,6 +86,7 @@ const LiveQueryFormComponent: React.FC = ({ const { data, isLoading, mutateAsync, isError, isSuccess } = useMutation( (payload: Record) => + // eslint-disable-next-line @typescript-eslint/no-explicit-any http.post('/internal/osquery/action', { body: JSON.stringify(payload), }), @@ -137,11 +138,6 @@ const LiveQueryFormComponent: React.FC = ({ type: FIELD_TYPES.JSON, validations: [], }, - hidden: { - defaultValue: false, - type: FIELD_TYPES.TOGGLE, - validations: [], - }, }; const { form } = useForm({ @@ -152,10 +148,15 @@ const LiveQueryFormComponent: React.FC = ({ if (isValid) { try { - await mutateAsync({ - ...formData, - ...(isEmpty(ecsFieldValue) ? {} : { ecs_mapping: ecsFieldValue }), - }); + await mutateAsync( + pickBy( + { + ...formData, + ...(isEmpty(ecsFieldValue) ? {} : { ecs_mapping: ecsFieldValue }), + }, + (value) => !isEmpty(value) + ) + ); // eslint-disable-next-line no-empty } catch (e) {} } @@ -163,10 +164,8 @@ const LiveQueryFormComponent: React.FC = ({ options: { stripEmptyFields: false, }, - serializer: ({ savedQueryId, hidden, ...formData }) => ({ - ...pickBy({ ...formData, saved_query_id: savedQueryId }), - ...(hidden != null && hidden ? { hidden } : {}), - }), + serializer: ({ savedQueryId, ...formData }) => + pickBy({ ...formData, saved_query_id: savedQueryId }, (value) => !isEmpty(value)), defaultValue: deepMerge( { agentSelection: { @@ -177,7 +176,6 @@ const LiveQueryFormComponent: React.FC = ({ }, query: '', savedQueryId: null, - hidden: false, }, defaultValue ?? {} ), @@ -419,9 +417,6 @@ const LiveQueryFormComponent: React.FC = ({ if (defaultValue?.query) { setFieldValue('query', defaultValue?.query); } - if (defaultValue?.hidden) { - setFieldValue('hidden', defaultValue?.hidden); - } // TODO: Set query and ECS mapping from savedQueryId object if (defaultValue?.savedQueryId) { setFieldValue('savedQueryId', defaultValue?.savedQueryId); @@ -436,7 +431,6 @@ const LiveQueryFormComponent: React.FC = ({
    {formType === 'steps' ? : simpleForm} - {showSavedQueryFlyout ? ( = ({ defaultValue, editMode = f defaultValue: [], type: FIELD_TYPES.COMBO_BOX, label: i18n.translate('xpack.osquery.pack.form.agentPoliciesFieldLabel', { - defaultMessage: 'Agent policies (optional)', + defaultMessage: 'Scheduled agent policies (optional)', }), helpText: i18n.translate('xpack.osquery.pack.form.agentPoliciesFieldHelpText', { defaultMessage: 'Queries in this pack are scheduled for agents in the selected policies.', diff --git a/x-pack/plugins/osquery/public/packs/form/queries_field.tsx b/x-pack/plugins/osquery/public/packs/form/queries_field.tsx index 03993bf35371c..b4b67fc8929db 100644 --- a/x-pack/plugins/osquery/public/packs/form/queries_field.tsx +++ b/x-pack/plugins/osquery/public/packs/form/queries_field.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import { findIndex, forEach, pullAt, pullAllBy, pickBy } from 'lodash'; +import { isEmpty, findIndex, forEach, pullAt, pullAllBy, pickBy } from 'lodash'; import { EuiFlexGroup, EuiFlexItem, EuiButton, EuiSpacer } from '@elastic/eui'; import { produce } from 'immer'; import React, { useCallback, useMemo, useState } from 'react'; @@ -133,13 +133,16 @@ const QueriesFieldComponent: React.FC = ({ field, handleNameC produce((draft) => { forEach(parsedContent.queries, (newQuery, newQueryId) => { draft.push( - pickBy({ - id: newQueryId, - interval: newQuery.interval ?? parsedContent.interval, - query: newQuery.query, - version: newQuery.version ?? parsedContent.version, - platform: getSupportedPlatforms(newQuery.platform ?? parsedContent.platform), - }) + pickBy( + { + id: newQueryId, + interval: newQuery.interval ?? parsedContent.interval, + query: newQuery.query, + version: newQuery.version ?? parsedContent.version, + platform: getSupportedPlatforms(newQuery.platform ?? parsedContent.platform), + }, + (value) => !isEmpty(value) + ) ); }); diff --git a/x-pack/plugins/osquery/public/packs/pack_queries_status_table.tsx b/x-pack/plugins/osquery/public/packs/pack_queries_status_table.tsx index 0b661c61a9057..5aacffb52cb49 100644 --- a/x-pack/plugins/osquery/public/packs/pack_queries_status_table.tsx +++ b/x-pack/plugins/osquery/public/packs/pack_queries_status_table.tsx @@ -29,7 +29,7 @@ import { PersistedIndexPatternLayer, PieVisualizationState, } from '../../../lens/public'; -import { FilterStateStore, IndexPattern } from '../../../../../src/plugins/data/common'; +import { FilterStateStore, DataView } from '../../../../../src/plugins/data/common'; import { useKibana } from '../common/lib/kibana'; import { OsqueryManagerPackagePolicyInputStream } from '../../common/types'; import { ScheduledQueryErrorsTable } from './scheduled_query_errors_table'; @@ -130,12 +130,12 @@ function getLensAttributes( references: [ { id: 'logs-*', - name: 'indexpattern-datasource-current-indexpattern', + name: 'dataView-datasource-current-dataView', type: 'index-pattern', }, { id: 'logs-*', - name: 'indexpattern-datasource-layer-layer1', + name: 'dataView-datasource-layer-layer1', type: 'index-pattern', }, { @@ -377,7 +377,7 @@ interface ScheduledQueryLastResultsProps { actionId: string; queryId: string; interval: number; - logsIndexPattern: IndexPattern | undefined; + logsDataView: DataView | undefined; toggleErrors: (payload: { queryId: string; interval: number }) => void; expanded: boolean; } @@ -386,20 +386,20 @@ const ScheduledQueryLastResults: React.FC = ({ actionId, queryId, interval, - logsIndexPattern, + logsDataView, toggleErrors, expanded, }) => { const { data: lastResultsData, isFetched } = usePackQueryLastResults({ actionId, interval, - logsIndexPattern, + logsDataView, }); const { data: errorsData, isFetched: errorsFetched } = usePackQueryErrors({ actionId, interval, - logsIndexPattern, + logsDataView, }); const handleErrorsToggle = useCallback( @@ -512,14 +512,14 @@ interface PackViewInActionProps { id: string; interval: number; }; - logsIndexPattern: IndexPattern | undefined; + logsDataView: DataView | undefined; packName: string; agentIds?: string[]; } const PackViewInDiscoverActionComponent: React.FC = ({ item, - logsIndexPattern, + logsDataView, packName, agentIds, }) => { @@ -528,7 +528,7 @@ const PackViewInDiscoverActionComponent: React.FC = ({ const { data: lastResultsData } = usePackQueryLastResults({ actionId, interval, - logsIndexPattern, + logsDataView, }); const startDate = lastResultsData?.['@timestamp'] @@ -554,7 +554,7 @@ const PackViewInDiscoverAction = React.memo(PackViewInDiscoverActionComponent); const PackViewInLensActionComponent: React.FC = ({ item, - logsIndexPattern, + logsDataView, packName, agentIds, }) => { @@ -563,7 +563,7 @@ const PackViewInLensActionComponent: React.FC = ({ const { data: lastResultsData } = usePackQueryLastResults({ actionId, interval, - logsIndexPattern, + logsDataView, }); const startDate = lastResultsData?.['@timestamp'] @@ -602,17 +602,17 @@ const PackQueriesStatusTableComponent: React.FC = ( Record> >({}); - const indexPatterns = useKibana().services.data.indexPatterns; - const [logsIndexPattern, setLogsIndexPattern] = useState(undefined); + const dataViews = useKibana().services.data.dataViews; + const [logsDataView, setLogsDataView] = useState(undefined); useEffect(() => { - const fetchLogsIndexPattern = async () => { - const indexPattern = await indexPatterns.find('logs-*'); + const fetchLogsDataView = async () => { + const dataView = await dataViews.find('logs-*'); - setLogsIndexPattern(indexPattern[0]); + setLogsDataView(dataView[0]); }; - fetchLogsIndexPattern(); - }, [indexPatterns]); + fetchLogsDataView(); + }, [dataViews]); const renderQueryColumn = useCallback( (query: string) => ( @@ -645,7 +645,7 @@ const PackQueriesStatusTableComponent: React.FC = ( const renderLastResultsColumn = useCallback( (item) => ( = ( expanded={!!itemIdToExpandedRowMap[item.id]} /> ), - [itemIdToExpandedRowMap, packName, toggleErrors, logsIndexPattern] + [itemIdToExpandedRowMap, packName, toggleErrors, logsDataView] ); const renderDiscoverResultsAction = useCallback( @@ -661,11 +661,11 @@ const PackQueriesStatusTableComponent: React.FC = ( ), - [agentIds, logsIndexPattern, packName] + [agentIds, logsDataView, packName] ); const renderLensResultsAction = useCallback( @@ -673,11 +673,11 @@ const PackQueriesStatusTableComponent: React.FC = ( ), - [agentIds, logsIndexPattern, packName] + [agentIds, logsDataView, packName] ); const getItemId = useCallback( diff --git a/x-pack/plugins/osquery/public/packs/pack_queries_table.tsx b/x-pack/plugins/osquery/public/packs/pack_queries_table.tsx index d23d5f6ffb06a..8a42aa9c28b72 100644 --- a/x-pack/plugins/osquery/public/packs/pack_queries_table.tsx +++ b/x-pack/plugins/osquery/public/packs/pack_queries_table.tsx @@ -13,7 +13,7 @@ import { i18n } from '@kbn/i18n'; import { PlatformIcons } from './queries/platforms'; import { OsqueryManagerPackagePolicyInputStream } from '../../common/types'; -interface PackQueriesTableProps { +export interface PackQueriesTableProps { data: OsqueryManagerPackagePolicyInputStream[]; onDeleteClick?: (item: OsqueryManagerPackagePolicyInputStream) => void; onEditClick?: (item: OsqueryManagerPackagePolicyInputStream) => void; @@ -184,3 +184,5 @@ const PackQueriesTableComponent: React.FC = ({ }; export const PackQueriesTable = React.memo(PackQueriesTableComponent); +// eslint-disable-next-line import/no-default-export +export default PackQueriesTable; diff --git a/x-pack/plugins/osquery/public/packs/packs_table.tsx b/x-pack/plugins/osquery/public/packs/packs_table.tsx index dcca0e2f56596..9bea07b7c234c 100644 --- a/x-pack/plugins/osquery/public/packs/packs_table.tsx +++ b/x-pack/plugins/osquery/public/packs/packs_table.tsx @@ -52,7 +52,7 @@ export const AgentPoliciesPopover = ({ agentPolicyIds }: { agentPolicyIds: strin const button = useMemo( () => ( - + <>{agentPolicyIds?.length ?? 0} ), diff --git a/x-pack/plugins/osquery/public/packs/queries/ecs_mapping_editor_field.tsx b/x-pack/plugins/osquery/public/packs/queries/ecs_mapping_editor_field.tsx index 85f4b3b3f0fad..76805c452bf98 100644 --- a/x-pack/plugins/osquery/public/packs/queries/ecs_mapping_editor_field.tsx +++ b/x-pack/plugins/osquery/public/packs/queries/ecs_mapping_editor_field.tsx @@ -6,7 +6,18 @@ */ import { produce } from 'immer'; -import { each, isEmpty, find, orderBy, sortedUniqBy, isArray, map, reduce, get } from 'lodash'; +import { + castArray, + each, + isEmpty, + find, + orderBy, + sortedUniqBy, + isArray, + map, + reduce, + get, +} from 'lodash'; import React, { forwardRef, useCallback, @@ -81,30 +92,24 @@ const typeMap = { }; const StyledEuiSuperSelect = styled(EuiSuperSelect)` - &.euiFormControlLayout__prepend { - padding-left: 8px; - padding-right: 24px; - box-shadow: none; - - .euiIcon { - padding: 0; - width: 18px; - background: none; - } + min-width: 70px; + border-radius: 6px 0 0 6px; + + .euiIcon { + padding: 0; + width: 18px; + background: none; } `; // @ts-expect-error update types const ResultComboBox = styled(EuiComboBox)` - &.euiComboBox--prepended .euiSuperSelect { - border-right: 1px solid ${(props) => props.theme.eui.euiBorderColor}; - - .euiFormControlLayout__childrenWrapper { - border-radius: 6px 0 0 6px; + &.euiComboBox { + position: relative; + left: -1px; - .euiFormControlLayoutIcons--right { - right: 6px; - } + .euiComboBox__inputWrap { + border-radius: 0 6px 6px 0; } } `; @@ -311,9 +316,11 @@ const OSQUERY_COLUMN_VALUE_TYPE_OPTIONS = [ }, ]; +const EMPTY_ARRAY: EuiComboBoxOptionOption[] = []; + interface OsqueryColumnFieldProps { resultType: FieldHook; - resultValue: FieldHook; + resultValue: FieldHook; euiFieldProps: EuiComboBoxProps; idAria?: string; } @@ -324,6 +331,7 @@ const OsqueryColumnFieldComponent: React.FC = ({ euiFieldProps = {}, idAria, }) => { + const inputRef = useRef(); const { setValue } = resultValue; const { setValue: setType } = resultType; const { isInvalid, errorMessage } = getFieldValidityAndErrorMessage(resultValue); @@ -367,16 +375,27 @@ const OsqueryColumnFieldComponent: React.FC = ({ (newType) => { if (newType !== resultType.value) { setType(newType); + setValue(newType === 'value' && euiFieldProps.singleSelection === false ? [] : ''); } }, - [setType, resultType.value] + [resultType.value, setType, setValue, euiFieldProps.singleSelection] ); const handleCreateOption = useCallback( - (newOption) => { - setValue(newOption); + (newOption: string) => { + if (euiFieldProps.singleSelection === false) { + setValue([newOption]); + if (resultValue.value.length) { + setValue([...castArray(resultValue.value), newOption]); + } else { + setValue([newOption]); + } + inputRef.current?.blur(); + } else { + setValue(newOption); + } }, - [setValue] + [euiFieldProps.singleSelection, resultValue.value, setValue] ); const Prepend = useMemo( @@ -400,6 +419,11 @@ const OsqueryColumnFieldComponent: React.FC = ({ setSelected(() => { if (!resultValue.value.length) return []; + // Static array values + if (isArray(resultValue.value)) { + return resultValue.value.map((value) => ({ label: value })); + } + const selectedOption = find(euiFieldProps?.options, ['label', resultValue.value]); return selectedOption ? [selectedOption] : [{ label: resultValue.value }]; @@ -416,18 +440,26 @@ const OsqueryColumnFieldComponent: React.FC = ({ describedByIds={describedByIds} isDisabled={euiFieldProps.isDisabled} > - + + {Prepend} + + { + inputRef.current = ref; + }} + fullWidth + selectedOptions={selectedOptions} + onChange={handleChange} + onCreateOption={handleCreateOption} + renderOption={renderOsqueryOption} + rowHeight={32} + isClearable + {...euiFieldProps} + options={(resultType.value === 'field' && euiFieldProps.options) || EMPTY_ARRAY} + /> + + ); }; @@ -497,7 +529,7 @@ const getOsqueryResultFieldValidator = ) => { const fieldRequiredError = fieldValidators.emptyField( i18n.translate('xpack.osquery.pack.queryFlyoutForm.osqueryResultFieldRequiredErrorMessage', { - defaultMessage: 'Osquery result is required.', + defaultMessage: 'Value is required.', }) )(args); @@ -551,6 +583,7 @@ interface ECSMappingEditorFormRef { export const ECSMappingEditorForm = forwardRef( ({ isDisabled, osquerySchemaOptions, defaultValue, onAdd, onChange, onDelete }, ref) => { const editForm = !!defaultValue; + const multipleValuesField = useRef(false); const currentFormData = useRef(defaultValue); const formSchema = { key: { @@ -595,13 +628,13 @@ export const ECSMappingEditorForm = forwardRef { validate(); - __validateFields(['result.value']); + validateFields(['result.value']); const { data, isValid } = await submit(); if (isValid) { @@ -619,7 +652,7 @@ export const ECSMappingEditorForm = forwardRef { if (defaultValue?.key && onDelete) { @@ -648,6 +681,8 @@ export const ECSMappingEditorForm = forwardRef )} @@ -666,7 +701,7 @@ export const ECSMappingEditorForm = forwardRef { if (!deepEqual(formData, currentFormData.current)) { currentFormData.current = formData; + const ecsOption = find(ECSSchemaOptions, ['label', formData.key]); + multipleValuesField.current = + ecsOption?.value?.normalization === 'array' && formData.result.type === 'value'; handleSubmit(); } }, [handleSubmit, formData, onAdd]); - // useEffect(() => { - // if (defaultValue) { - // validate(); - // __validateFields(['result.value']); - // } - // }, [defaultValue, osquerySchemaOptions, validate, __validateFields]); - return (
    diff --git a/x-pack/plugins/osquery/public/packs/queries/platforms/platform_icon.tsx b/x-pack/plugins/osquery/public/packs/queries/platforms/platform_icon.tsx index 1126dfd690c19..62adf4558e573 100644 --- a/x-pack/plugins/osquery/public/packs/queries/platforms/platform_icon.tsx +++ b/x-pack/plugins/osquery/public/packs/queries/platforms/platform_icon.tsx @@ -6,16 +6,27 @@ */ import { EuiIcon } from '@elastic/eui'; -import React from 'react'; +import React, { useEffect, useState } from 'react'; import { getPlatformIconModule } from './helpers'; -interface PlatformIconProps { +export interface PlatformIconProps { platform: string; } const PlatformIconComponent: React.FC = ({ platform }) => { - const platformIconModule = getPlatformIconModule(platform); - return ; + const [Icon, setIcon] = useState(null); + + // FIXME: This is a hack to force the icon to be loaded asynchronously. + useEffect(() => { + const interval = setInterval(() => { + const platformIconModule = getPlatformIconModule(platform); + setIcon(); + }, 0); + + return () => clearInterval(interval); + }, [platform, setIcon]); + + return Icon; }; export const PlatformIcon = React.memo(PlatformIconComponent); diff --git a/x-pack/plugins/osquery/public/packs/use_pack_query_errors.ts b/x-pack/plugins/osquery/public/packs/use_pack_query_errors.ts index b88bd8ce5709d..ba009b22a8d46 100644 --- a/x-pack/plugins/osquery/public/packs/use_pack_query_errors.ts +++ b/x-pack/plugins/osquery/public/packs/use_pack_query_errors.ts @@ -6,21 +6,21 @@ */ import { useQuery } from 'react-query'; -import { IndexPattern, SortDirection } from '../../../../../src/plugins/data/common'; +import { DataView, SortDirection } from '../../../../../src/plugins/data/common'; import { useKibana } from '../common/lib/kibana'; interface UsePackQueryErrorsProps { actionId: string; interval: number; - logsIndexPattern?: IndexPattern; + logsDataView?: DataView; skip?: boolean; } export const usePackQueryErrors = ({ actionId, interval, - logsIndexPattern, + logsDataView, skip = false, }: UsePackQueryErrorsProps) => { const data = useKibana().services.data; @@ -29,7 +29,7 @@ export const usePackQueryErrors = ({ ['scheduledQueryErrors', { actionId, interval }], async () => { const searchSource = await data.search.searchSource.create({ - index: logsIndexPattern, + index: logsDataView, fields: ['*'], sort: [ { @@ -73,7 +73,7 @@ export const usePackQueryErrors = ({ }, { keepPreviousData: true, - enabled: !!(!skip && actionId && interval && logsIndexPattern), + enabled: !!(!skip && actionId && interval && logsDataView), select: (response) => response.rawResponse.hits ?? [], refetchOnReconnect: false, refetchOnWindowFocus: false, diff --git a/x-pack/plugins/osquery/public/packs/use_pack_query_last_results.ts b/x-pack/plugins/osquery/public/packs/use_pack_query_last_results.ts index cb84386dbe3ea..f473ef14ccb7a 100644 --- a/x-pack/plugins/osquery/public/packs/use_pack_query_last_results.ts +++ b/x-pack/plugins/osquery/public/packs/use_pack_query_last_results.ts @@ -7,21 +7,21 @@ import { useQuery } from 'react-query'; import moment from 'moment-timezone'; -import { IndexPattern } from '../../../../../src/plugins/data/common'; +import { DataView, SortDirection } from '../../../../../src/plugins/data/common'; import { useKibana } from '../common/lib/kibana'; interface UsePackQueryLastResultsProps { actionId: string; agentIds?: string[]; interval: number; - logsIndexPattern?: IndexPattern; + logsDataView?: DataView; skip?: boolean; } export const usePackQueryLastResults = ({ actionId, interval, - logsIndexPattern, + logsDataView, skip = false, }: UsePackQueryLastResultsProps) => { const data = useKibana().services.data; @@ -30,8 +30,9 @@ export const usePackQueryLastResults = ({ ['scheduledQueryLastResults', { actionId }], async () => { const lastResultsSearchSource = await data.search.searchSource.create({ - index: logsIndexPattern, + index: logsDataView, size: 1, + sort: { '@timestamp': SortDirection.desc }, query: { // @ts-expect-error update types bool: { @@ -51,7 +52,7 @@ export const usePackQueryLastResults = ({ if (timestamp) { const aggsSearchSource = await data.search.searchSource.create({ - index: logsIndexPattern, + index: logsDataView, size: 1, aggs: { unique_agents: { cardinality: { field: 'agent.id' } }, @@ -92,7 +93,7 @@ export const usePackQueryLastResults = ({ }, { keepPreviousData: true, - enabled: !!(!skip && actionId && interval && logsIndexPattern), + enabled: !!(!skip && actionId && interval && logsDataView), refetchOnReconnect: false, refetchOnWindowFocus: false, } diff --git a/x-pack/plugins/osquery/public/results/results_table.tsx b/x-pack/plugins/osquery/public/results/results_table.tsx index d1d16730e7982..164d4fbdc878b 100644 --- a/x-pack/plugins/osquery/public/results/results_table.tsx +++ b/x-pack/plugins/osquery/public/results/results_table.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import { get, isEmpty, isEqual, keys, map, reduce } from 'lodash/fp'; +import { get, isEmpty, isArray, isObject, isEqual, keys, map, reduce } from 'lodash/fp'; import { EuiCallOut, EuiCode, @@ -123,6 +123,11 @@ const ResultsTableComponent: React.FC = ({ [visibleColumns, setVisibleColumns] ); + const ecsMappingColumns = useMemo( + () => keys(get('actionDetails._source.data.ecs_mapping', actionDetails) || {}), + [actionDetails] + ); + const renderCellValue: EuiDataGridProps['renderCellValue'] = useMemo( () => // eslint-disable-next-line react/display-name @@ -140,9 +145,22 @@ const ResultsTableComponent: React.FC = ({ return {value}; } + if (ecsMappingColumns.includes(columnId)) { + const ecsFieldValue = get(columnId, data[rowIndex % pagination.pageSize]?._source); + + if (isArray(ecsFieldValue) || isObject(ecsFieldValue)) { + try { + return JSON.stringify(ecsFieldValue, null, 2); + // eslint-disable-next-line no-empty + } catch (e) {} + } + + return ecsFieldValue ?? '-'; + } + return !isEmpty(value) ? value : '-'; }, - [getFleetAppUrl, pagination.pageSize] + [ecsMappingColumns, getFleetAppUrl, pagination.pageSize] ); const tableSorting = useMemo( @@ -218,12 +236,17 @@ const ResultsTableComponent: React.FC = ({ return; } - const newColumns = keys(allResultsData?.edges[0]?.fields) - .sort() - .reduce( - (acc, fieldName) => { - const { data, seen } = acc; - if (fieldName === 'agent.name') { + const fields = [ + 'agent.name', + ...ecsMappingColumns.sort(), + ...keys(allResultsData?.edges[0]?.fields || {}).sort(), + ]; + + const newColumns = fields.reduce( + (acc, fieldName) => { + const { data, seen } = acc; + if (fieldName === 'agent.name') { + if (!seen.has(fieldName)) { data.push({ id: fieldName, displayAsText: i18n.translate( @@ -234,34 +257,48 @@ const ResultsTableComponent: React.FC = ({ ), defaultSortDirection: Direction.asc, }); - - return acc; + seen.add(fieldName); } - if (fieldName.startsWith('osquery.')) { - const displayAsText = fieldName.split('.')[1]; - if (!seen.has(displayAsText)) { - data.push({ - id: fieldName, - displayAsText, - display: getHeaderDisplay(displayAsText), - defaultSortDirection: Direction.asc, - }); - seen.add(displayAsText); - } - return acc; + return acc; + } + + if (ecsMappingColumns.includes(fieldName)) { + if (!seen.has(fieldName)) { + data.push({ + id: fieldName, + displayAsText: fieldName, + defaultSortDirection: Direction.asc, + }); + seen.add(fieldName); } + return acc; + } + if (fieldName.startsWith('osquery.')) { + const displayAsText = fieldName.split('.')[1]; + if (!seen.has(displayAsText)) { + data.push({ + id: fieldName, + displayAsText, + display: getHeaderDisplay(displayAsText), + defaultSortDirection: Direction.asc, + }); + seen.add(displayAsText); + } return acc; - }, - { data: [], seen: new Set() } as { data: EuiDataGridColumn[]; seen: Set } - ).data; + } + + return acc; + }, + { data: [], seen: new Set() } as { data: EuiDataGridColumn[]; seen: Set } + ).data; setColumns((currentColumns) => !isEqual(map('id', currentColumns), map('id', newColumns)) ? newColumns : currentColumns ); setVisibleColumns(map('id', newColumns)); - }, [allResultsData?.edges, getHeaderDisplay]); + }, [allResultsData?.edges, ecsMappingColumns, getHeaderDisplay]); const toolbarVisibility = useMemo( () => ({ diff --git a/x-pack/plugins/osquery/public/shared_components/osquery_action/index.tsx b/x-pack/plugins/osquery/public/shared_components/osquery_action/index.tsx index 53b8ea436c124..8fc289b7ef36b 100644 --- a/x-pack/plugins/osquery/public/shared_components/osquery_action/index.tsx +++ b/x-pack/plugins/osquery/public/shared_components/osquery_action/index.tsx @@ -127,7 +127,7 @@ const OsqueryActionComponent: React.FC = ({ metadata }) => { ); } - return ; + return ; }; export const OsqueryAction = React.memo(OsqueryActionComponent); diff --git a/x-pack/plugins/osquery/server/lib/saved_query/saved_object_mappings.ts b/x-pack/plugins/osquery/server/lib/saved_query/saved_object_mappings.ts index fb2c834f3c74d..2990027ff8d97 100644 --- a/x-pack/plugins/osquery/server/lib/saved_query/saved_object_mappings.ts +++ b/x-pack/plugins/osquery/server/lib/saved_query/saved_object_mappings.ts @@ -5,8 +5,8 @@ * 2.0. */ +import { produce } from 'immer'; import { SavedObjectsType } from '../../../../../../src/core/server'; - import { savedQuerySavedObjectType, packSavedObjectType } from '../../../common/types'; export const savedQuerySavedObjectMappings: SavedObjectsType['mappings'] = { @@ -54,9 +54,13 @@ export const savedQueryType: SavedObjectsType = { namespaceType: 'multiple-isolated', mappings: savedQuerySavedObjectMappings, management: { - defaultSearchField: 'id', importableAndExportable: true, getTitle: (savedObject) => savedObject.attributes.id, + getEditUrl: (savedObject) => `/saved_queries/${savedObject.id}/edit`, + getInAppUrl: (savedObject) => ({ + path: `/app/saved_queries/${savedObject.id}`, + uiCapabilitiesPath: 'osquery.read', + }), }, }; @@ -117,6 +121,19 @@ export const packType: SavedObjectsType = { management: { defaultSearchField: 'name', importableAndExportable: true, - getTitle: (savedObject) => savedObject.attributes.name, + getTitle: (savedObject) => `Pack: ${savedObject.attributes.name}`, + getEditUrl: (savedObject) => `/packs/${savedObject.id}/edit`, + getInAppUrl: (savedObject) => ({ + path: `/app/packs/${savedObject.id}`, + uiCapabilitiesPath: 'osquery.read', + }), + onExport: (context, objects) => + produce(objects, (draft) => { + draft.forEach((packSO) => { + packSO.references = []; + }); + + return draft; + }), }, }; diff --git a/x-pack/plugins/osquery/server/plugin.ts b/x-pack/plugins/osquery/server/plugin.ts index 1bb394843e5b7..16f0b17cc10df 100644 --- a/x-pack/plugins/osquery/server/plugin.ts +++ b/x-pack/plugins/osquery/server/plugin.ts @@ -227,7 +227,7 @@ export class OsqueryPlugin implements Plugin !isEmpty(value) + ), }; const actionResponse = await esClient.index<{}, {}>({ index: '.fleet-actions', diff --git a/x-pack/plugins/osquery/server/routes/fleet_wrapper/get_agent_policies.ts b/x-pack/plugins/osquery/server/routes/fleet_wrapper/get_agent_policies.ts index accfc2d9ef4da..06641cc60e13d 100644 --- a/x-pack/plugins/osquery/server/routes/fleet_wrapper/get_agent_policies.ts +++ b/x-pack/plugins/osquery/server/routes/fleet_wrapper/get_agent_policies.ts @@ -5,7 +5,7 @@ * 2.0. */ -import bluebird from 'bluebird'; +import pMap from 'p-map'; import { schema } from '@kbn/config-schema'; import { filter, uniq, map } from 'lodash'; import { satisfies } from 'semver'; @@ -47,7 +47,7 @@ export const getAgentPoliciesRoute = (router: IRouter, osqueryContext: OsqueryAp const agentPolicies = await agentPolicyService?.getByIds(soClient, agentPolicyIds); if (agentPolicies?.length) { - await bluebird.map( + await pMap( agentPolicies, (agentPolicy: GetAgentPoliciesResponseItem) => agentService diff --git a/x-pack/plugins/osquery/server/routes/pack/create_pack_route.ts b/x-pack/plugins/osquery/server/routes/pack/create_pack_route.ts index 16710d578abb7..bdc307e36619f 100644 --- a/x-pack/plugins/osquery/server/routes/pack/create_pack_route.ts +++ b/x-pack/plugins/osquery/server/routes/pack/create_pack_route.ts @@ -43,7 +43,10 @@ export const createPackRoute = (router: IRouter, osqueryContext: OsqueryAppConte schema.recordOf( schema.string(), schema.object({ - field: schema.string(), + field: schema.maybe(schema.string()), + value: schema.maybe( + schema.oneOf([schema.string(), schema.arrayOf(schema.string())]) + ), }) ) ), @@ -68,8 +71,7 @@ export const createPackRoute = (router: IRouter, osqueryContext: OsqueryAppConte const conflictingEntries = await savedObjectsClient.find({ type: packSavedObjectType, - search: name, - searchFields: ['name'], + filter: `${packSavedObjectType}.attributes.name: "${name}"`, }); if (conflictingEntries.saved_objects.length) { diff --git a/x-pack/plugins/osquery/server/routes/pack/update_pack_route.ts b/x-pack/plugins/osquery/server/routes/pack/update_pack_route.ts index 1abdec17a922b..88af904088984 100644 --- a/x-pack/plugins/osquery/server/routes/pack/update_pack_route.ts +++ b/x-pack/plugins/osquery/server/routes/pack/update_pack_route.ts @@ -6,7 +6,19 @@ */ import moment from 'moment-timezone'; -import { set, unset, has, difference, filter, find, map, mapKeys, pickBy, uniq } from 'lodash'; +import { + isEmpty, + set, + unset, + has, + difference, + filter, + find, + map, + mapKeys, + pickBy, + uniq, +} from 'lodash'; import { schema } from '@kbn/config-schema'; import { produce } from 'immer'; import { @@ -51,7 +63,10 @@ export const updatePackRoute = (router: IRouter, osqueryContext: OsqueryAppConte schema.recordOf( schema.string(), schema.object({ - field: schema.string(), + field: schema.maybe(schema.string()), + value: schema.maybe( + schema.oneOf([schema.string(), schema.arrayOf(schema.string())]) + ), }) ) ), @@ -82,8 +97,7 @@ export const updatePackRoute = (router: IRouter, osqueryContext: OsqueryAppConte if (name) { const conflictingEntries = await savedObjectsClient.find({ type: packSavedObjectType, - search: name, - searchFields: ['name'], + filter: `${packSavedObjectType}.attributes.name: "${name}"`, }); if ( @@ -112,13 +126,16 @@ export const updatePackRoute = (router: IRouter, osqueryContext: OsqueryAppConte request.params.id, { enabled, - ...pickBy({ - name, - description, - queries: queries && convertPackQueriesToSO(queries), - updated_at: moment().toISOString(), - updated_by: currentUser, - }), + ...pickBy( + { + name, + description, + queries: queries && convertPackQueriesToSO(queries), + updated_at: moment().toISOString(), + updated_by: currentUser, + }, + (value) => !isEmpty(value) + ), }, policy_ids ? { diff --git a/x-pack/plugins/osquery/server/routes/saved_query/create_saved_query_route.ts b/x-pack/plugins/osquery/server/routes/saved_query/create_saved_query_route.ts index 5c65ebf1a701e..66c114012fd78 100644 --- a/x-pack/plugins/osquery/server/routes/saved_query/create_saved_query_route.ts +++ b/x-pack/plugins/osquery/server/routes/saved_query/create_saved_query_route.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { pickBy } from 'lodash'; +import { isEmpty, pickBy } from 'lodash'; import { IRouter } from '../../../../../../src/core/server'; import { PLUGIN_ID } from '../../../common'; import { @@ -39,8 +39,7 @@ export const createSavedQueryRoute = (router: IRouter, osqueryContext: OsqueryAp const conflictingEntries = await savedObjectsClient.find({ type: savedQuerySavedObjectType, - search: id, - searchFields: ['id'], + filter: `${savedQuerySavedObjectType}.attributes.id: "${id}"`, }); if (conflictingEntries.saved_objects.length) { @@ -49,26 +48,32 @@ export const createSavedQueryRoute = (router: IRouter, osqueryContext: OsqueryAp const savedQuerySO = await savedObjectsClient.create( savedQuerySavedObjectType, - pickBy({ - id, - description, - query, - platform, - version, - interval, - ecs_mapping: convertECSMappingToArray(ecs_mapping), - created_by: currentUser, - created_at: new Date().toISOString(), - updated_by: currentUser, - updated_at: new Date().toISOString(), - }) + pickBy( + { + id, + description, + query, + platform, + version, + interval, + ecs_mapping: convertECSMappingToArray(ecs_mapping), + created_by: currentUser, + created_at: new Date().toISOString(), + updated_by: currentUser, + updated_at: new Date().toISOString(), + }, + (value) => !isEmpty(value) + ) ); return response.ok({ - body: pickBy({ - ...savedQuerySO, - ecs_mapping, - }), + body: pickBy( + { + ...savedQuerySO, + ecs_mapping, + }, + (value) => !isEmpty(value) + ), }); } ); diff --git a/x-pack/plugins/osquery/server/routes/saved_query/update_saved_query_route.ts b/x-pack/plugins/osquery/server/routes/saved_query/update_saved_query_route.ts index b34999204b8a3..21cfd0bd43772 100644 --- a/x-pack/plugins/osquery/server/routes/saved_query/update_saved_query_route.ts +++ b/x-pack/plugins/osquery/server/routes/saved_query/update_saved_query_route.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { filter, pickBy } from 'lodash'; +import { isEmpty, filter, pickBy } from 'lodash'; import { schema } from '@kbn/config-schema'; import { PLUGIN_ID } from '../../../common'; @@ -35,7 +35,9 @@ export const updateSavedQueryRoute = (router: IRouter, osqueryContext: OsqueryAp schema.string(), schema.object({ field: schema.maybe(schema.string()), - value: schema.maybe(schema.string()), + value: schema.maybe( + schema.oneOf([schema.string(), schema.arrayOf(schema.string())]) + ), }) ) ), @@ -62,8 +64,7 @@ export const updateSavedQueryRoute = (router: IRouter, osqueryContext: OsqueryAp const conflictingEntries = await savedObjectsClient.find<{ id: string }>({ type: savedQuerySavedObjectType, - search: id, - searchFields: ['id'], + filter: `${savedQuerySavedObjectType}.attributes.id: "${id}"`, }); if ( @@ -76,17 +77,20 @@ export const updateSavedQueryRoute = (router: IRouter, osqueryContext: OsqueryAp const updatedSavedQuerySO = await savedObjectsClient.update( savedQuerySavedObjectType, request.params.id, - pickBy({ - id, - description, - platform, - query, - version, - interval, - ecs_mapping: convertECSMappingToArray(ecs_mapping), - updated_by: currentUser, - updated_at: new Date().toISOString(), - }), + pickBy( + { + id, + description, + platform, + query, + version, + interval, + ecs_mapping: convertECSMappingToArray(ecs_mapping), + updated_by: currentUser, + updated_at: new Date().toISOString(), + }, + (value) => !isEmpty(value) + ), { refresh: 'wait_for', } diff --git a/x-pack/plugins/osquery/server/routes/status/create_status_route.ts b/x-pack/plugins/osquery/server/routes/status/create_status_route.ts index 630ec8b3743c8..ae79ef851bed9 100644 --- a/x-pack/plugins/osquery/server/routes/status/create_status_route.ts +++ b/x-pack/plugins/osquery/server/routes/status/create_status_route.ts @@ -107,6 +107,50 @@ export const createStatusRoute = (router: IRouter, osqueryContext: OsqueryAppCon pkgName: OSQUERY_INTEGRATION_NAME, }); + const agentPolicyIds = uniq(map(policyPackages?.items, 'policy_id')); + const agentPolicies = mapKeys( + await agentPolicyService?.getByIds(internalSavedObjectsClient, agentPolicyIds), + 'id' + ); + + await Promise.all( + map(migrationObject.packs, async (packObject) => { + await internalSavedObjectsClient.create( + packSavedObjectType, + { + // @ts-expect-error update types + name: packObject.name, + // @ts-expect-error update types + description: packObject.description, + // @ts-expect-error update types + queries: convertPackQueriesToSO(packObject.queries), + // @ts-expect-error update types + enabled: packObject.enabled, + created_at: new Date().toISOString(), + created_by: 'system', + updated_at: new Date().toISOString(), + updated_by: 'system', + }, + { + // @ts-expect-error update types + references: packObject.policy_ids.map((policyId: string) => ({ + id: policyId, + name: agentPolicies[policyId].name, + type: AGENT_POLICY_SAVED_OBJECT_TYPE, + })), + refresh: 'wait_for', + } + ); + }) + ); + + // delete unnecessary package policies + await packagePolicyService?.delete( + internalSavedObjectsClient, + esClient, + migrationObject.packagePoliciesToDelete + ); + // updatePackagePolicies await Promise.all( map(migrationObject.agentPolicyToPackage, async (value, key) => { @@ -151,49 +195,6 @@ export const createStatusRoute = (router: IRouter, osqueryContext: OsqueryAppCon } }) ); - - const agentPolicyIds = uniq(map(policyPackages?.items, 'policy_id')); - const agentPolicies = mapKeys( - await agentPolicyService?.getByIds(internalSavedObjectsClient, agentPolicyIds), - 'id' - ); - - await Promise.all( - map(migrationObject.packs, async (packObject) => { - await internalSavedObjectsClient.create( - packSavedObjectType, - { - // @ts-expect-error update types - name: packObject.name, - // @ts-expect-error update types - description: packObject.description, - // @ts-expect-error update types - queries: convertPackQueriesToSO(packObject.queries), - // @ts-expect-error update types - enabled: packObject.enabled, - created_at: new Date().toISOString(), - created_by: 'system', - updated_at: new Date().toISOString(), - updated_by: 'system', - }, - { - // @ts-expect-error update types - references: packObject.policy_ids.map((policyId: string) => ({ - id: policyId, - name: agentPolicies[policyId].name, - type: AGENT_POLICY_SAVED_OBJECT_TYPE, - })), - refresh: 'wait_for', - } - ); - }) - ); - - await packagePolicyService?.delete( - internalSavedObjectsClient, - esClient, - migrationObject.packagePoliciesToDelete - ); // eslint-disable-next-line no-empty } catch (e) {} } diff --git a/x-pack/plugins/osquery/server/saved_objects.ts b/x-pack/plugins/osquery/server/saved_objects.ts index 27e7dd28ce664..16a1f2efb7e9d 100644 --- a/x-pack/plugins/osquery/server/saved_objects.ts +++ b/x-pack/plugins/osquery/server/saved_objects.ts @@ -7,24 +7,11 @@ import { CoreSetup } from '../../../../src/core/server'; -import { OsqueryAppContext } from './lib/osquery_app_context_services'; import { savedQueryType, packType } from './lib/saved_query/saved_object_mappings'; import { usageMetricType } from './routes/usage/saved_object_mappings'; -const types = [savedQueryType, packType]; - -export const savedObjectTypes = types.map((type) => type.name); - -export const initSavedObjects = ( - savedObjects: CoreSetup['savedObjects'], - osqueryContext: OsqueryAppContext -) => { - const config = osqueryContext.config(); - +export const initSavedObjects = (savedObjects: CoreSetup['savedObjects']) => { savedObjects.registerType(usageMetricType); savedObjects.registerType(savedQueryType); - - if (config.packs) { - savedObjects.registerType(packType); - } + savedObjects.registerType(packType); }; diff --git a/x-pack/plugins/reporting/common/constants.ts b/x-pack/plugins/reporting/common/constants.ts index cafab65677ee4..1eef032945e69 100644 --- a/x-pack/plugins/reporting/common/constants.ts +++ b/x-pack/plugins/reporting/common/constants.ts @@ -121,7 +121,7 @@ export const REPORTING_REDIRECT_LOCATOR_STORE_KEY = '__REPORTING_REDIRECT_LOCATO * be injected to the page */ export const getRedirectAppPath = () => { - return '/app/management/insightsAndAlerting/reporting/r'; + return '/app/reportingRedirect'; }; // Statuses diff --git a/x-pack/plugins/reporting/common/job_utils.ts b/x-pack/plugins/reporting/common/job_utils.ts index d8b4503cfefba..c1fd2eb5eb8c4 100644 --- a/x-pack/plugins/reporting/common/job_utils.ts +++ b/x-pack/plugins/reporting/common/job_utils.ts @@ -5,7 +5,32 @@ * 2.0. */ +import { + CSV_JOB_TYPE, + PDF_JOB_TYPE, + PNG_JOB_TYPE, + PDF_JOB_TYPE_V2, + PNG_JOB_TYPE_V2, + CSV_JOB_TYPE_DEPRECATED, +} from './constants'; + // TODO: Remove this code once everyone is using the new PDF format, then we can also remove the legacy // export type entirely export const isJobV2Params = ({ sharingData }: { sharingData: Record }): boolean => sharingData.locatorParams != null; + +export const prettyPrintJobType = (type: string) => { + switch (type) { + case PDF_JOB_TYPE: + case PDF_JOB_TYPE_V2: + return 'PDF'; + case CSV_JOB_TYPE: + case CSV_JOB_TYPE_DEPRECATED: + return 'CSV'; + case PNG_JOB_TYPE: + case PNG_JOB_TYPE_V2: + return 'PNG'; + default: + return type; + } +}; diff --git a/x-pack/plugins/reporting/common/types/base.ts b/x-pack/plugins/reporting/common/types/base.ts index 44960c57f61c1..a44378979ac3c 100644 --- a/x-pack/plugins/reporting/common/types/base.ts +++ b/x-pack/plugins/reporting/common/types/base.ts @@ -7,6 +7,7 @@ import type { Ensure, SerializableRecord } from '@kbn/utility-types'; import type { LayoutParams } from './layout'; +import { LocatorParams } from './url'; export type JobId = string; @@ -21,9 +22,19 @@ export type BaseParams = Ensure< SerializableRecord >; +export type BaseParamsV2 = BaseParams & { + locatorParams: LocatorParams[]; +}; + // base params decorated with encrypted headers that come into runJob functions export interface BasePayload extends BaseParams { headers: string; spaceId?: string; isDeprecated?: boolean; } + +export interface BasePayloadV2 extends BaseParamsV2 { + headers: string; + spaceId?: string; + isDeprecated?: boolean; +} diff --git a/x-pack/plugins/reporting/common/types/index.ts b/x-pack/plugins/reporting/common/types/index.ts index 75e8cb0af9698..8612400e8b390 100644 --- a/x-pack/plugins/reporting/common/types/index.ts +++ b/x-pack/plugins/reporting/common/types/index.ts @@ -6,9 +6,9 @@ */ import type { Size, LayoutParams } from './layout'; -import type { JobId, BaseParams, BasePayload } from './base'; +import type { JobId, BaseParams, BaseParamsV2, BasePayload, BasePayloadV2 } from './base'; -export type { JobId, BaseParams, BasePayload }; +export type { JobId, BaseParams, BaseParamsV2, BasePayload, BasePayloadV2 }; export type { Size, LayoutParams }; export type { DownloadReportFn, diff --git a/x-pack/plugins/reporting/common/types/url.ts b/x-pack/plugins/reporting/common/types/url.ts index dfb8ee9f908e3..28e935713c45e 100644 --- a/x-pack/plugins/reporting/common/types/url.ts +++ b/x-pack/plugins/reporting/common/types/url.ts @@ -14,9 +14,7 @@ export type DownloadReportFn = (jobId: JobId) => DownloadLink; type ManagementLink = string; export type ManagementLinkFn = () => ManagementLink; -export interface LocatorParams< - P extends SerializableRecord = SerializableRecord & { forceNow?: string } -> { +export interface LocatorParams

    { id: string; version: string; params: P; diff --git a/x-pack/plugins/reporting/public/constants.ts b/x-pack/plugins/reporting/public/constants.ts deleted file mode 100644 index c7e77fd44a780..0000000000000 --- a/x-pack/plugins/reporting/public/constants.ts +++ /dev/null @@ -1,8 +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 REACT_ROUTER_REDIRECT_APP_PATH = '/r'; diff --git a/x-pack/plugins/reporting/public/lib/job.tsx b/x-pack/plugins/reporting/public/lib/job.tsx index d5d0695aaefb9..d5d77ac18aa5c 100644 --- a/x-pack/plugins/reporting/public/lib/job.tsx +++ b/x-pack/plugins/reporting/public/lib/job.tsx @@ -12,6 +12,7 @@ import moment from 'moment'; import React from 'react'; import { JOB_STATUSES } from '../../common/constants'; import { + BaseParamsV2, JobId, ReportApiJSON, ReportOutput, @@ -34,6 +35,7 @@ export class Job { public objectType: ReportPayload['objectType']; public title: ReportPayload['title']; public isDeprecated: ReportPayload['isDeprecated']; + public spaceId: ReportPayload['spaceId']; public browserTimezone?: ReportPayload['browserTimezone']; public layout: ReportPayload['layout']; @@ -57,6 +59,8 @@ export class Job { public max_size_reached?: TaskRunResult['max_size_reached']; public warnings?: TaskRunResult['warnings']; + public locatorParams?: BaseParamsV2['locatorParams']; + constructor(report: ReportApiJSON) { this.id = report.id; this.index = report.index; @@ -82,9 +86,11 @@ export class Job { this.content_type = report.output?.content_type; this.isDeprecated = report.payload.isDeprecated || false; + this.spaceId = report.payload.spaceId; this.csv_contains_formulas = report.output?.csv_contains_formulas; this.max_size_reached = report.output?.max_size_reached; this.warnings = report.output?.warnings; + this.locatorParams = (report.payload as BaseParamsV2).locatorParams; } getStatusMessage() { @@ -167,6 +173,25 @@ export class Job { ); } + /** + * Returns a user friendly version of the report job creation date + */ + getCreatedAtDate(): string { + return this.formatDate(this.created_at); + } + + /** + * Returns a user friendly version of the user that created the report job + */ + getCreatedBy(): string { + return ( + this.created_by || + i18n.translate('xpack.reporting.jobCreatedBy.unknownUserPlaceholderText', { + defaultMessage: 'Unknown', + }) + ); + } + getCreatedAtLabel() { if (this.created_by) { return ( @@ -191,15 +216,20 @@ export class Job { } } + getDeprecatedMessage(): undefined | string { + if (this.isDeprecated) { + return i18n.translate('xpack.reporting.jobWarning.exportTypeDeprecated', { + defaultMessage: + 'This is a deprecated export type. Automation of this report will need to be re-created for compatibility with future versions of Kibana.', + }); + } + } + getWarnings() { const warnings: string[] = []; - if (this.isDeprecated) { - warnings.push( - i18n.translate('xpack.reporting.jobWarning.exportTypeDeprecated', { - defaultMessage: - 'This is a deprecated export type. Automation of this report will need to be re-created for compatibility with future versions of Kibana.', - }) - ); + const deprecatedMessage = this.getDeprecatedMessage(); + if (deprecatedMessage) { + warnings.push(deprecatedMessage); } if (this.csv_contains_formulas) { @@ -234,6 +264,10 @@ export class Job { } } + getPrettyStatusTimestamp() { + return this.formatDate(this.getStatusTimestamp()); + } + private formatDate(timestamp: string) { try { return moment(timestamp).format('YYYY-MM-DD @ hh:mm A'); diff --git a/x-pack/plugins/reporting/public/lib/reporting_api_client/reporting_api_client.ts b/x-pack/plugins/reporting/public/lib/reporting_api_client/reporting_api_client.ts index b27c2a65be963..c44427f3ca9e1 100644 --- a/x-pack/plugins/reporting/public/lib/reporting_api_client/reporting_api_client.ts +++ b/x-pack/plugins/reporting/public/lib/reporting_api_client/reporting_api_client.ts @@ -10,12 +10,14 @@ import { stringify } from 'query-string'; import rison from 'rison-node'; import type { HttpFetchQuery } from 'src/core/public'; import { HttpSetup, IUiSettingsClient } from 'src/core/public'; +import { buildKibanaPath } from '../../../common/build_kibana_path'; import { API_BASE_GENERATE, API_BASE_URL, API_GENERATE_IMMEDIATE, API_LIST_URL, API_MIGRATE_ILM_POLICY_URL, + getRedirectAppPath, REPORTING_MANAGEMENT_HOME, } from '../../../common/constants'; import { @@ -73,6 +75,19 @@ export class ReportingAPIClient implements IReportingAPI { private kibanaVersion: string ) {} + public getKibanaAppHref(job: Job): string { + const searchParams = stringify({ jobId: job.id }); + + const path = buildKibanaPath({ + basePath: this.http.basePath.serverBasePath, + spaceId: job.spaceId, + appPath: getRedirectAppPath(), + }); + + const href = `${path}?${searchParams}`; + return href; + } + public getReportURL(jobId: string) { const apiBaseUrl = this.http.basePath.prepend(API_LIST_URL); const downloadLink = `${apiBaseUrl}/download/${jobId}`; diff --git a/x-pack/plugins/reporting/public/management/__snapshots__/report_info_button.test.tsx.snap b/x-pack/plugins/reporting/public/management/__snapshots__/report_info_button.test.tsx.snap deleted file mode 100644 index e8b9362db7525..0000000000000 --- a/x-pack/plugins/reporting/public/management/__snapshots__/report_info_button.test.tsx.snap +++ /dev/null @@ -1,256 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`ReportInfoButton handles button click flyout on click 1`] = ` - -`; - -exports[`ReportInfoButton opens flyout with fetch error info 1`] = ` -Array [ - -

    -
    - , -
    -
    , -] -`; - -exports[`ReportInfoButton opens flyout with info 1`] = ` -Array [ - -
    - - - - - - - - - - - - - - - - -
    - -
    -
    - -
    - - -
    -`; diff --git a/x-pack/plugins/reporting/public/management/ilm_policy_link.tsx b/x-pack/plugins/reporting/public/management/components/ilm_policy_link.tsx similarity index 90% rename from x-pack/plugins/reporting/public/management/ilm_policy_link.tsx rename to x-pack/plugins/reporting/public/management/components/ilm_policy_link.tsx index 1dccb11dbbbc5..dfb884c24e917 100644 --- a/x-pack/plugins/reporting/public/management/ilm_policy_link.tsx +++ b/x-pack/plugins/reporting/public/management/components/ilm_policy_link.tsx @@ -11,8 +11,8 @@ import { i18n } from '@kbn/i18n'; import { EuiButtonEmpty } from '@elastic/eui'; import type { ApplicationStart } from 'src/core/public'; -import { ILM_POLICY_NAME } from '../../common/constants'; -import { LocatorPublic, SerializableRecord } from '../shared_imports'; +import { ILM_POLICY_NAME } from '../../../common/constants'; +import { LocatorPublic, SerializableRecord } from '../../shared_imports'; interface Props { navigateToUrl: ApplicationStart['navigateToUrl']; diff --git a/x-pack/plugins/reporting/public/management/components/index.ts b/x-pack/plugins/reporting/public/management/components/index.ts new file mode 100644 index 0000000000000..10c34ed628a15 --- /dev/null +++ b/x-pack/plugins/reporting/public/management/components/index.ts @@ -0,0 +1,13 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export { MigrateIlmPolicyCallOut } from './migrate_ilm_policy_callout'; +export { IlmPolicyLink } from './ilm_policy_link'; +export { ReportDeleteButton } from './report_delete_button'; +export { ReportDiagnostic } from './report_diagnostic'; +export { ReportStatusIndicator } from './report_status_indicator'; +export { ReportInfoFlyout } from './report_info_flyout'; diff --git a/x-pack/plugins/reporting/public/management/migrate_ilm_policy_callout/ilm_policy_migration_needed_callout.tsx b/x-pack/plugins/reporting/public/management/components/migrate_ilm_policy_callout/ilm_policy_migration_needed_callout.tsx similarity index 90% rename from x-pack/plugins/reporting/public/management/migrate_ilm_policy_callout/ilm_policy_migration_needed_callout.tsx rename to x-pack/plugins/reporting/public/management/components/migrate_ilm_policy_callout/ilm_policy_migration_needed_callout.tsx index e96cb842d55cf..7eb54049a15a9 100644 --- a/x-pack/plugins/reporting/public/management/migrate_ilm_policy_callout/ilm_policy_migration_needed_callout.tsx +++ b/x-pack/plugins/reporting/public/management/components/migrate_ilm_policy_callout/ilm_policy_migration_needed_callout.tsx @@ -9,13 +9,14 @@ import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import type { FunctionComponent } from 'react'; import React, { useState } from 'react'; +import useMountedState from 'react-use/lib/useMountedState'; import { EuiCallOut, EuiButton, EuiCode } from '@elastic/eui'; import type { NotificationsSetup } from 'src/core/public'; -import { ILM_POLICY_NAME } from '../../../common/constants'; +import { ILM_POLICY_NAME } from '../../../../common/constants'; -import { useInternalApiClient } from '../../lib/reporting_api_client'; +import { useInternalApiClient } from '../../../lib/reporting_api_client'; const i18nTexts = { title: i18n.translate('xpack.reporting.listing.ilmPolicyCallout.migrationNeededTitle', { @@ -63,6 +64,7 @@ export const IlmPolicyMigrationNeededCallOut: FunctionComponent = ({ onMigrationDone, }) => { const [isMigratingIndices, setIsMigratingIndices] = useState(false); + const isMounted = useMountedState(); const { apiClient } = useInternalApiClient(); @@ -78,7 +80,7 @@ export const IlmPolicyMigrationNeededCallOut: FunctionComponent = ({ toastMessage: e.body?.message, }); } finally { - setIsMigratingIndices(false); + if (isMounted()) setIsMigratingIndices(false); } }; diff --git a/x-pack/plugins/reporting/public/management/components/migrate_ilm_policy_callout/index.tsx b/x-pack/plugins/reporting/public/management/components/migrate_ilm_policy_callout/index.tsx new file mode 100644 index 0000000000000..0c2359acdb679 --- /dev/null +++ b/x-pack/plugins/reporting/public/management/components/migrate_ilm_policy_callout/index.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 type { FunctionComponent } from 'react'; +import React from 'react'; +import { EuiSpacer, EuiFlexItem } from '@elastic/eui'; + +import { NotificationsSetup } from 'src/core/public'; + +import { useIlmPolicyStatus } from '../../../lib/ilm_policy_status_context'; + +import { IlmPolicyMigrationNeededCallOut } from './ilm_policy_migration_needed_callout'; + +interface Props { + toasts: NotificationsSetup['toasts']; +} + +export const MigrateIlmPolicyCallOut: FunctionComponent = ({ toasts }) => { + const { isLoading, recheckStatus, status } = useIlmPolicyStatus(); + + if (isLoading || !status || status === 'ok') { + return null; + } + + return ( + <> + + + + + + ); +}; diff --git a/x-pack/plugins/reporting/public/management/components/report_delete_button.tsx b/x-pack/plugins/reporting/public/management/components/report_delete_button.tsx new file mode 100644 index 0000000000000..d91560ddd86f5 --- /dev/null +++ b/x-pack/plugins/reporting/public/management/components/report_delete_button.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 { EuiButton, EuiConfirmModal } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import React, { Fragment, PureComponent } from 'react'; +import { Job } from '../../lib/job'; +import { ListingProps } from '../'; + +type DeleteFn = () => Promise; +type Props = { jobsToDelete: Job[]; performDelete: DeleteFn } & ListingProps; +interface State { + showConfirm: boolean; +} + +export class ReportDeleteButton extends PureComponent { + constructor(props: Props) { + super(props); + this.state = { showConfirm: false }; + } + + private hideConfirm() { + this.setState({ showConfirm: false }); + } + + private showConfirm() { + this.setState({ showConfirm: true }); + } + + private renderConfirm() { + const { jobsToDelete } = this.props; + + const title = + jobsToDelete.length > 1 + ? i18n.translate('xpack.reporting.listing.table.deleteNumConfirmTitle', { + defaultMessage: `Delete {num} reports?`, + values: { num: jobsToDelete.length }, + }) + : i18n.translate('xpack.reporting.listing.table.deleteConfirmTitle', { + defaultMessage: `Delete the "{name}" report?`, + values: { name: jobsToDelete[0].title }, + }); + const message = i18n.translate('xpack.reporting.listing.table.deleteConfirmMessage', { + defaultMessage: `You can't recover deleted reports.`, + }); + const confirmButtonText = i18n.translate('xpack.reporting.listing.table.deleteConfirmButton', { + defaultMessage: `Delete`, + }); + const cancelButtonText = i18n.translate('xpack.reporting.listing.table.deleteCancelButton', { + defaultMessage: `Cancel`, + }); + + return ( + this.hideConfirm()} + onConfirm={() => this.props.performDelete()} + confirmButtonText={confirmButtonText} + cancelButtonText={cancelButtonText} + defaultFocusedButton="confirm" + buttonColor="danger" + > + {message} + + ); + } + + public render() { + const { jobsToDelete } = this.props; + if (jobsToDelete.length === 0) return null; + + return ( + + this.showConfirm()} + iconType="trash" + color={'danger'} + data-test-subj="deleteReportButton" + > + {i18n.translate('xpack.reporting.listing.table.deleteReportButton', { + defaultMessage: `Delete {num, plural, one {report} other {reports} }`, + values: { num: jobsToDelete.length }, + })} + + {this.state.showConfirm ? this.renderConfirm() : null} + + ); + } +} diff --git a/x-pack/plugins/reporting/public/management/report_diagnostic.tsx b/x-pack/plugins/reporting/public/management/components/report_diagnostic.tsx similarity index 98% rename from x-pack/plugins/reporting/public/management/report_diagnostic.tsx rename to x-pack/plugins/reporting/public/management/components/report_diagnostic.tsx index ce585fe427e6c..124ce9af891a3 100644 --- a/x-pack/plugins/reporting/public/management/report_diagnostic.tsx +++ b/x-pack/plugins/reporting/public/management/components/report_diagnostic.tsx @@ -21,7 +21,7 @@ import { EuiText, EuiTitle, } from '@elastic/eui'; -import { ReportingAPIClient, DiagnoseResponse } from '../lib/reporting_api_client'; +import { ReportingAPIClient, DiagnoseResponse } from '../../lib/reporting_api_client'; interface Props { apiClient: ReportingAPIClient; diff --git a/x-pack/plugins/reporting/public/management/components/report_info_button.tsx b/x-pack/plugins/reporting/public/management/components/report_info_button.tsx new file mode 100644 index 0000000000000..26e495e5908dc --- /dev/null +++ b/x-pack/plugins/reporting/public/management/components/report_info_button.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 { EuiButtonEmpty, EuiToolTip } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import React, { FunctionComponent } from 'react'; +import { Job } from '../../lib/job'; + +interface Props { + job: Job; + onClick: () => void; +} + +export const ReportInfoButton: FunctionComponent = ({ job, onClick }) => { + let message = i18n.translate('xpack.reporting.listing.table.reportInfoButtonTooltip', { + defaultMessage: 'See report info.', + }); + if (job.getError()) { + message = i18n.translate('xpack.reporting.listing.table.reportInfoAndErrorButtonTooltip', { + defaultMessage: 'See report info and error message.', + }); + } else if (job.getWarnings()) { + message = i18n.translate('xpack.reporting.listing.table.reportInfoAndWarningsButtonTooltip', { + defaultMessage: 'See report info and warnings.', + }); + } + + const showReportInfoCopy = i18n.translate( + 'xpack.reporting.listing.table.showReportInfoAriaLabel', + { + defaultMessage: 'Show report info', + } + ); + + return ( + + + {showReportInfoCopy} + + + ); +}; diff --git a/x-pack/plugins/reporting/public/management/components/report_info_flyout.tsx b/x-pack/plugins/reporting/public/management/components/report_info_flyout.tsx new file mode 100644 index 0000000000000..2a7d52cf9403b --- /dev/null +++ b/x-pack/plugins/reporting/public/management/components/report_info_flyout.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, { FunctionComponent, useState, useEffect } from 'react'; +import useMountedState from 'react-use/lib/useMountedState'; +import { + EuiFlyout, + EuiFlyoutBody, + EuiFlyoutHeader, + EuiPortal, + EuiText, + EuiTitle, + EuiLoadingSpinner, +} from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; + +import { Job } from '../../lib/job'; +import { useInternalApiClient } from '../../lib/reporting_api_client'; + +import { ReportInfoFlyoutContent } from './report_info_flyout_content'; + +interface Props { + onClose: () => void; + job: Job; +} + +export const ReportInfoFlyout: FunctionComponent = ({ onClose, job }) => { + const [isLoading, setIsLoading] = useState(true); + const [loadingError, setLoadingError] = useState(); + const [info, setInfo] = useState(); + const isMounted = useMountedState(); + const { apiClient } = useInternalApiClient(); + + useEffect(() => { + (async function loadInfo() { + if (isLoading) { + try { + const infoResponse = await apiClient.getInfo(job.id); + if (isMounted()) { + setInfo(infoResponse); + } + } catch (err) { + if (isMounted()) { + setLoadingError(err); + } + } finally { + if (isMounted()) { + setIsLoading(false); + } + } + } + })(); + }, [isLoading, apiClient, job.id, isMounted]); + + return ( + + + + +

    + {loadingError + ? i18n.translate('xpack.reporting.listing.table.reportInfoUnableToFetch', { + defaultMessage: 'Unable to fetch report info.', + }) + : i18n.translate('xpack.reporting.listing.table.reportCalloutTitle', { + defaultMessage: 'Report info', + })} +

    +
    +
    + + {isLoading ? ( + + ) : loadingError ? undefined : !!info ? ( + + + + ) : undefined} + +
    +
    + ); +}; diff --git a/x-pack/plugins/reporting/public/management/components/report_info_flyout_content.tsx b/x-pack/plugins/reporting/public/management/components/report_info_flyout_content.tsx new file mode 100644 index 0000000000000..25199c4abaa68 --- /dev/null +++ b/x-pack/plugins/reporting/public/management/components/report_info_flyout_content.tsx @@ -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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { FunctionComponent } from 'react'; +import { i18n } from '@kbn/i18n'; +import { EuiDescriptionList, EuiSpacer, EuiText } from '@elastic/eui'; + +import type { Job } from '../../lib/job'; +import { USES_HEADLESS_JOB_TYPES } from '../../../common/constants'; + +const NA = i18n.translate('xpack.reporting.listing.infoPanel.notApplicableLabel', { + defaultMessage: 'N/A', +}); + +const UNKNOWN = i18n.translate('xpack.reporting.listing.infoPanel.unknownLabel', { + defaultMessage: 'unknown', +}); + +const getDimensions = (info: Job): string => { + const defaultDimensions = { width: null, height: null }; + const { width, height } = info.layout?.dimensions || defaultDimensions; + if (width && height) { + return `Width: ${width} x Height: ${height}`; + } + return UNKNOWN; +}; + +interface Props { + info: Job; +} + +export const ReportInfoFlyoutContent: FunctionComponent = ({ info }) => { + const timeout = info.timeout ? info.timeout.toString() : NA; + + const jobInfo = [ + { + title: i18n.translate('xpack.reporting.listing.infoPanel.titleInfo', { + defaultMessage: 'Title', + }), + description: info.title || NA, + }, + { + title: i18n.translate('xpack.reporting.listing.infoPanel.createdAtInfo', { + defaultMessage: 'Created at', + }), + description: info.getCreatedAtLabel(), + }, + { + title: i18n.translate('xpack.reporting.listing.infoPanel.statusInfo', { + defaultMessage: 'Status', + }), + description: info.getStatus(), + }, + { + title: i18n.translate('xpack.reporting.listing.infoPanel.tzInfo', { + defaultMessage: 'Time zone', + }), + description: info.browserTimezone || NA, + }, + ]; + + const processingInfo = [ + { + title: i18n.translate('xpack.reporting.listing.infoPanel.startedAtInfo', { + defaultMessage: 'Started at', + }), + description: info.started_at || NA, + }, + { + title: i18n.translate('xpack.reporting.listing.infoPanel.completedAtInfo', { + defaultMessage: 'Completed at', + }), + description: info.completed_at || NA, + }, + { + title: i18n.translate('xpack.reporting.listing.infoPanel.processedByInfo', { + defaultMessage: 'Processed by', + }), + description: + info.kibana_name && info.kibana_id ? `${info.kibana_name} (${info.kibana_id})` : NA, + }, + { + title: i18n.translate('xpack.reporting.listing.infoPanel.contentTypeInfo', { + defaultMessage: 'Content type', + }), + description: info.content_type || NA, + }, + { + title: i18n.translate('xpack.reporting.listing.infoPanel.sizeInfo', { + defaultMessage: 'Size in bytes', + }), + description: info.size?.toString() || NA, + }, + { + title: i18n.translate('xpack.reporting.listing.infoPanel.attemptsInfo', { + defaultMessage: 'Attempts', + }), + description: info.attempts.toString(), + }, + { + title: i18n.translate('xpack.reporting.listing.infoPanel.maxAttemptsInfo', { + defaultMessage: 'Max attempts', + }), + description: info.max_attempts?.toString() || NA, + }, + { + title: i18n.translate('xpack.reporting.listing.infoPanel.timeoutInfo', { + defaultMessage: 'Timeout', + }), + description: timeout, + }, + { + title: i18n.translate('xpack.reporting.listing.infoPanel.exportTypeInfo', { + defaultMessage: 'Export type', + }), + description: info.isDeprecated + ? i18n.translate('xpack.reporting.listing.table.reportCalloutExportTypeDeprecated', { + defaultMessage: '{jobtype} (DEPRECATED)', + values: { jobtype: info.jobtype }, + }) + : info.jobtype, + }, + + // TODO: when https://github.com/elastic/kibana/pull/106137 is merged, add kibana version field + ]; + + const jobScreenshot = [ + { + title: i18n.translate('xpack.reporting.listing.infoPanel.dimensionsInfo', { + defaultMessage: 'Dimensions', + }), + description: getDimensions(info), + }, + { + title: i18n.translate('xpack.reporting.listing.infoPanel.layoutInfo', { + defaultMessage: 'Layout', + }), + description: info.layout?.id || UNKNOWN, + }, + { + title: i18n.translate('xpack.reporting.listing.infoPanel.browserTypeInfo', { + defaultMessage: 'Browser type', + }), + description: info.browser_type || NA, + }, + ]; + + const warnings = info.getWarnings(); + const warningsInfo = warnings && [ + { + title: Warnings, + description: {warnings}, + }, + ]; + + const errored = info.getError(); + const errorInfo = errored && [ + { + title: Error, + description: {errored}, + }, + ]; + + return ( + <> + + + + {USES_HEADLESS_JOB_TYPES.includes(info.jobtype) ? ( + <> + + + + ) : null} + {warningsInfo ? ( + <> + + + + ) : null} + {errorInfo ? ( + <> + + + + ) : null} + + ); +}; diff --git a/x-pack/plugins/reporting/public/management/components/report_status_indicator.tsx b/x-pack/plugins/reporting/public/management/components/report_status_indicator.tsx new file mode 100644 index 0000000000000..21fd0fc76745c --- /dev/null +++ b/x-pack/plugins/reporting/public/management/components/report_status_indicator.tsx @@ -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 React, { FC, useMemo } from 'react'; +import { i18n } from '@kbn/i18n'; +import { EuiFlexGroup, EuiFlexItem, EuiIcon, EuiLoadingSpinner, EuiToolTip } from '@elastic/eui'; + +import type { Job } from '../../lib/job'; +import { JOB_STATUSES } from '../../../common/constants'; +import { jobHasIssues } from '../utils'; + +interface Props { + job: Job; +} + +const i18nTexts = { + completed: i18n.translate('xpack.reporting.statusIndicator.completedLabel', { + defaultMessage: 'Done', + }), + completedWithWarnings: i18n.translate( + 'xpack.reporting.statusIndicator.completedWithWarningsLabel', + { + defaultMessage: 'Done, warnings detected', + } + ), + pending: i18n.translate('xpack.reporting.statusIndicator.pendingLabel', { + defaultMessage: 'Pending', + }), + processing: ({ attempt, of }: { attempt: number; of?: number }) => + of !== undefined + ? i18n.translate('xpack.reporting.statusIndicator.processingMaxAttemptsLabel', { + defaultMessage: `Processing, attempt {attempt} of {of}`, + values: { attempt, of }, + }) + : i18n.translate('xpack.reporting.statusIndicator.processingLabel', { + defaultMessage: `Processing, attempt {attempt}`, + values: { attempt }, + }), + failed: i18n.translate('xpack.reporting.statusIndicator.failedLabel', { + defaultMessage: 'Failed', + }), + unknown: i18n.translate('xpack.reporting.statusIndicator.unknownLabel', { + defaultMessage: 'Unknown', + }), + lastStatusUpdate: ({ date }: { date: string }) => + i18n.translate('xpack.reporting.statusIndicator.lastStatusUpdateLabel', { + defaultMessage: 'Updated at {date}', + values: { date }, + }), +}; + +export const ReportStatusIndicator: FC = ({ job }) => { + const hasIssues = useMemo(() => jobHasIssues(job), [job]); + + let icon: JSX.Element; + let statusText: string; + + switch (job.status) { + case JOB_STATUSES.COMPLETED: + if (hasIssues) { + icon = ; + statusText = i18nTexts.completedWithWarnings; + break; + } + icon = ; + statusText = i18nTexts.completed; + break; + case JOB_STATUSES.WARNINGS: + icon = ; + statusText = i18nTexts.completedWithWarnings; + break; + case JOB_STATUSES.PENDING: + icon = ; + statusText = i18nTexts.pending; + break; + case JOB_STATUSES.PROCESSING: + icon = ; + statusText = i18nTexts.processing({ attempt: job.attempts, of: job.max_attempts }); + break; + case JOB_STATUSES.FAILED: + icon = ; + statusText = i18nTexts.failed; + break; + default: + icon = ; + statusText = i18nTexts.unknown; + } + + return ( + + + {icon} + {statusText} + + + ); +}; diff --git a/x-pack/plugins/reporting/public/management/index.ts b/x-pack/plugins/reporting/public/management/index.ts index 4d324135288db..7bd7845051d2c 100644 --- a/x-pack/plugins/reporting/public/management/index.ts +++ b/x-pack/plugins/reporting/public/management/index.ts @@ -5,7 +5,6 @@ * 2.0. */ -import { InjectedIntl } from '@kbn/i18n/react'; import { ApplicationStart, ToastsSetup } from 'src/core/public'; import { LicensingPluginSetup } from '../../../licensing/public'; import { UseIlmPolicyStatusReturn } from '../lib/ilm_policy_status_context'; @@ -14,7 +13,6 @@ import { ClientConfigType } from '../plugin'; import type { SharePluginSetup } from '../shared_imports'; export interface ListingProps { - intl: InjectedIntl; apiClient: ReportingAPIClient; capabilities: ApplicationStart['capabilities']; license$: LicensingPluginSetup['license$']; // FIXME: license$ is deprecated diff --git a/x-pack/plugins/reporting/public/management/migrate_ilm_policy_callout/index.tsx b/x-pack/plugins/reporting/public/management/migrate_ilm_policy_callout/index.tsx deleted file mode 100644 index 892cbcdde5ede..0000000000000 --- a/x-pack/plugins/reporting/public/management/migrate_ilm_policy_callout/index.tsx +++ /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; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import type { FunctionComponent } from 'react'; -import React from 'react'; -import { EuiSpacer, EuiFlexItem } from '@elastic/eui'; - -import { NotificationsSetup } from 'src/core/public'; - -import { useIlmPolicyStatus } from '../../lib/ilm_policy_status_context'; - -import { IlmPolicyMigrationNeededCallOut } from './ilm_policy_migration_needed_callout'; - -interface Props { - toasts: NotificationsSetup['toasts']; -} - -export const MigrateIlmPolicyCallOut: FunctionComponent = ({ toasts }) => { - const { isLoading, recheckStatus, status } = useIlmPolicyStatus(); - - if (isLoading || !status || status === 'ok') { - return null; - } - - return ( - <> - - - - - - ); -}; diff --git a/x-pack/plugins/reporting/public/management/report_delete_button.tsx b/x-pack/plugins/reporting/public/management/report_delete_button.tsx deleted file mode 100644 index da1ce9dd9e1cb..0000000000000 --- a/x-pack/plugins/reporting/public/management/report_delete_button.tsx +++ /dev/null @@ -1,104 +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 { EuiButton, EuiConfirmModal } from '@elastic/eui'; -import React, { Fragment, PureComponent } from 'react'; -import { Job } from '../lib/job'; -import { ListingProps } from './'; - -type DeleteFn = () => Promise; -type Props = { jobsToDelete: Job[]; performDelete: DeleteFn } & ListingProps; -interface State { - showConfirm: boolean; -} - -export class ReportDeleteButton extends PureComponent { - constructor(props: Props) { - super(props); - this.state = { showConfirm: false }; - } - - private hideConfirm() { - this.setState({ showConfirm: false }); - } - - private showConfirm() { - this.setState({ showConfirm: true }); - } - - private renderConfirm() { - const { intl, jobsToDelete } = this.props; - - const title = - jobsToDelete.length > 1 - ? intl.formatMessage( - { - id: 'xpack.reporting.listing.table.deleteNumConfirmTitle', - defaultMessage: `Delete {num} reports?`, - }, - { num: jobsToDelete.length } - ) - : intl.formatMessage( - { - id: 'xpack.reporting.listing.table.deleteConfirmTitle', - defaultMessage: `Delete the "{name}" report?`, - }, - { name: jobsToDelete[0].title } - ); - const message = intl.formatMessage({ - id: 'xpack.reporting.listing.table.deleteConfirmMessage', - defaultMessage: `You can't recover deleted reports.`, - }); - const confirmButtonText = intl.formatMessage({ - id: 'xpack.reporting.listing.table.deleteConfirmButton', - defaultMessage: `Delete`, - }); - const cancelButtonText = intl.formatMessage({ - id: 'xpack.reporting.listing.table.deleteCancelButton', - defaultMessage: `Cancel`, - }); - - return ( - this.hideConfirm()} - onConfirm={() => this.props.performDelete()} - confirmButtonText={confirmButtonText} - cancelButtonText={cancelButtonText} - defaultFocusedButton="confirm" - buttonColor="danger" - > - {message} - - ); - } - - public render() { - const { jobsToDelete, intl } = this.props; - if (jobsToDelete.length === 0) return null; - - return ( - - this.showConfirm()} - iconType="trash" - color={'danger'} - data-test-subj="deleteReportButton" - > - {intl.formatMessage( - { - id: 'xpack.reporting.listing.table.deleteReportButton', - defaultMessage: `Delete {num, plural, one {report} other {reports} }`, - }, - { num: jobsToDelete.length } - )} - - {this.state.showConfirm ? this.renderConfirm() : null} - - ); - } -} diff --git a/x-pack/plugins/reporting/public/management/report_download_button.tsx b/x-pack/plugins/reporting/public/management/report_download_button.tsx deleted file mode 100644 index f21c83fbf42da..0000000000000 --- a/x-pack/plugins/reporting/public/management/report_download_button.tsx +++ /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; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { EuiButtonIcon, EuiToolTip } from '@elastic/eui'; -import { InjectedIntl } from '@kbn/i18n/react'; -import React, { FunctionComponent } from 'react'; -import { JOB_STATUSES } from '../../common/constants'; -import { Job as ListingJob } from '../lib/job'; -import { ReportingAPIClient } from '../lib/reporting_api_client'; - -interface Props { - intl: InjectedIntl; - apiClient: ReportingAPIClient; - job: ListingJob; -} - -export const ReportDownloadButton: FunctionComponent = (props: Props) => { - const { job, apiClient, intl } = props; - - if (job.status !== JOB_STATUSES.COMPLETED && job.status !== JOB_STATUSES.WARNINGS) { - return null; - } - - const button = ( - apiClient.downloadReport(job.id)} - iconType="importAction" - aria-label={intl.formatMessage({ - id: 'xpack.reporting.listing.table.downloadReportAriaLabel', - defaultMessage: 'Download report', - })} - /> - ); - - const warnings = job.getWarnings(); - if (warnings) { - return ( - - {button} - - ); - } - - return ( - - {button} - - ); -}; diff --git a/x-pack/plugins/reporting/public/management/report_info_button.test.tsx b/x-pack/plugins/reporting/public/management/report_info_button.test.tsx deleted file mode 100644 index c52027355ac5e..0000000000000 --- a/x-pack/plugins/reporting/public/management/report_info_button.test.tsx +++ /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 React from 'react'; -import { mountWithIntl } from '@kbn/test/jest'; -import { coreMock } from '../../../../../src/core/public/mocks'; -import { Job } from '../lib/job'; -import { ReportInfoButton } from './report_info_button'; - -jest.mock('../lib/reporting_api_client'); - -import { ReportingAPIClient } from '../lib/reporting_api_client'; - -const coreSetup = coreMock.createSetup(); -const apiClient = new ReportingAPIClient(coreSetup.http, coreSetup.uiSettings, '7.15.0'); - -const job = new Job({ - id: 'abc-123', - index: '.reporting-2020.04.12', - migration_version: '7.15.0', - attempts: 0, - browser_type: 'chromium', - created_at: '2020-04-14T21:01:13.064Z', - created_by: 'elastic', - jobtype: 'printable_pdf', - max_attempts: 1, - meta: { layout: 'preserve_layout', objectType: 'canvas workpad' }, - payload: { - browserTimezone: 'America/Phoenix', - version: '7.15.0-test', - layout: { dimensions: { height: 720, width: 1080 }, id: 'preserve_layout' }, - objectType: 'canvas workpad', - title: 'My Canvas Workpad', - }, - process_expiration: '1970-01-01T00:00:00.000Z', - status: 'pending', - timeout: 300000, -}); - -describe('ReportInfoButton', () => { - it('handles button click flyout on click', () => { - const wrapper = mountWithIntl(); - const input = wrapper.find('[data-test-subj="reportInfoButton"]').hostNodes(); - expect(input).toMatchSnapshot(); - }); - - it('opens flyout with info', async () => { - const wrapper = mountWithIntl(); - const input = wrapper.find('[data-test-subj="reportInfoButton"]').hostNodes(); - - input.simulate('click'); - - const flyout = wrapper.find('[data-test-subj="reportInfoFlyout"]'); - expect(flyout).toMatchSnapshot(); - - expect(apiClient.getInfo).toHaveBeenCalledTimes(1); - expect(apiClient.getInfo).toHaveBeenCalledWith('abc-123'); - }); - - it('opens flyout with fetch error info', () => { - // simulate fetch failure - apiClient.getInfo = jest.fn(() => { - throw new Error('Could not fetch the job info'); - }); - - const wrapper = mountWithIntl(); - const input = wrapper.find('[data-test-subj="reportInfoButton"]').hostNodes(); - - input.simulate('click'); - - const flyout = wrapper.find('[data-test-subj="reportInfoFlyout"]'); - expect(flyout).toMatchSnapshot(); - - expect(apiClient.getInfo).toHaveBeenCalledTimes(1); - expect(apiClient.getInfo).toHaveBeenCalledWith('abc-123'); - }); -}); diff --git a/x-pack/plugins/reporting/public/management/report_info_button.tsx b/x-pack/plugins/reporting/public/management/report_info_button.tsx deleted file mode 100644 index 7a70286785e4f..0000000000000 --- a/x-pack/plugins/reporting/public/management/report_info_button.tsx +++ /dev/null @@ -1,377 +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 { - EuiButtonIcon, - EuiDescriptionList, - EuiFlyout, - EuiFlyoutBody, - EuiFlyoutHeader, - EuiPortal, - EuiSpacer, - EuiText, - EuiTitle, - EuiToolTip, -} from '@elastic/eui'; -import { injectI18n } from '@kbn/i18n/react'; -import React, { Component } from 'react'; -import { USES_HEADLESS_JOB_TYPES } from '../../common/constants'; -import { Job } from '../lib/job'; -import { ReportingAPIClient } from '../lib/reporting_api_client'; -import { ListingProps } from '.'; - -interface Props extends Pick { - apiClient: ReportingAPIClient; - job: Job; -} - -interface State { - isLoading: boolean; - isFlyoutVisible: boolean; - calloutTitle: string; - info: Job | null; - error: Error | null; -} - -const NA = 'n/a'; -const UNKNOWN = 'unknown'; - -const getDimensions = (info: Job): string => { - const defaultDimensions = { width: null, height: null }; - const { width, height } = info.layout?.dimensions || defaultDimensions; - if (width && height) { - return `Width: ${width} x Height: ${height}`; - } - return UNKNOWN; -}; - -class ReportInfoButtonUi extends Component { - private mounted?: boolean; - - constructor(props: Props) { - super(props); - - this.state = { - isLoading: false, - isFlyoutVisible: false, - calloutTitle: props.intl.formatMessage({ - id: 'xpack.reporting.listing.table.reportCalloutTitle', - defaultMessage: 'Report info', - }), - info: null, - error: null, - }; - - this.closeFlyout = this.closeFlyout.bind(this); - this.showFlyout = this.showFlyout.bind(this); - } - - public renderInfo() { - const { info, error: err } = this.state; - if (err) { - return err.message; - } - if (!info) { - return null; - } - - const timeout = info.timeout ? info.timeout.toString() : NA; - - const jobInfo = [ - { - title: this.props.intl.formatMessage({ - id: 'xpack.reporting.listing.infoPanel.titleInfo', - defaultMessage: 'Title', - }), - description: info.title || NA, - }, - { - title: this.props.intl.formatMessage({ - id: 'xpack.reporting.listing.infoPanel.createdAtInfo', - defaultMessage: 'Created at', - }), - description: info.getCreatedAtLabel(), - }, - { - title: this.props.intl.formatMessage({ - id: 'xpack.reporting.listing.infoPanel.statusInfo', - defaultMessage: 'Status', - }), - description: info.getStatus(), - }, - { - title: this.props.intl.formatMessage({ - id: 'xpack.reporting.listing.infoPanel.tzInfo', - defaultMessage: 'Time zone', - }), - description: info.browserTimezone || NA, - }, - ]; - - const processingInfo = [ - { - title: this.props.intl.formatMessage({ - id: 'xpack.reporting.listing.infoPanel.startedAtInfo', - defaultMessage: 'Started at', - }), - description: info.started_at || NA, - }, - { - title: this.props.intl.formatMessage({ - id: 'xpack.reporting.listing.infoPanel.completedAtInfo', - defaultMessage: 'Completed at', - }), - description: info.completed_at || NA, - }, - { - title: this.props.intl.formatMessage({ - id: 'xpack.reporting.listing.infoPanel.processedByInfo', - defaultMessage: 'Processed by', - }), - description: - info.kibana_name && info.kibana_id ? `${info.kibana_name} (${info.kibana_id})` : NA, - }, - { - title: this.props.intl.formatMessage({ - id: 'xpack.reporting.listing.infoPanel.contentTypeInfo', - defaultMessage: 'Content type', - }), - description: info.content_type || NA, - }, - { - title: this.props.intl.formatMessage({ - id: 'xpack.reporting.listing.infoPanel.sizeInfo', - defaultMessage: 'Size in bytes', - }), - description: info.size?.toString() || NA, - }, - { - title: this.props.intl.formatMessage({ - id: 'xpack.reporting.listing.infoPanel.attemptsInfo', - defaultMessage: 'Attempts', - }), - description: info.attempts.toString(), - }, - { - title: this.props.intl.formatMessage({ - id: 'xpack.reporting.listing.infoPanel.maxAttemptsInfo', - defaultMessage: 'Max attempts', - }), - description: info.max_attempts?.toString() || NA, - }, - { - title: this.props.intl.formatMessage({ - id: 'xpack.reporting.listing.infoPanel.timeoutInfo', - defaultMessage: 'Timeout', - }), - description: timeout, - }, - { - title: this.props.intl.formatMessage({ - id: 'xpack.reporting.listing.infoPanel.exportTypeInfo', - defaultMessage: 'Export type', - }), - description: info.isDeprecated - ? this.props.intl.formatMessage( - { - id: 'xpack.reporting.listing.table.reportCalloutExportTypeDeprecated', - defaultMessage: '{jobtype} (DEPRECATED)', - }, - { jobtype: info.jobtype } - ) - : info.jobtype, - }, - - // TODO when https://github.com/elastic/kibana/pull/106137 is merged, add kibana version field - ]; - - const jobScreenshot = [ - { - title: this.props.intl.formatMessage({ - id: 'xpack.reporting.listing.infoPanel.dimensionsInfo', - defaultMessage: 'Dimensions', - }), - description: getDimensions(info), - }, - { - title: this.props.intl.formatMessage({ - id: 'xpack.reporting.listing.infoPanel.layoutInfo', - defaultMessage: 'Layout', - }), - description: info.layout?.id || UNKNOWN, - }, - { - title: this.props.intl.formatMessage({ - id: 'xpack.reporting.listing.infoPanel.browserTypeInfo', - defaultMessage: 'Browser type', - }), - description: info.browser_type || NA, - }, - ]; - - const warnings = info.getWarnings(); - const warningsInfo = warnings && [ - { - title: Warnings, - description: {warnings}, - }, - ]; - - const errored = info.getError(); - const errorInfo = errored && [ - { - title: Error, - description: {errored}, - }, - ]; - - return ( - <> - - - - {USES_HEADLESS_JOB_TYPES.includes(info.jobtype) ? ( - <> - - - - ) : null} - {warningsInfo ? ( - <> - - - - ) : null} - {errorInfo ? ( - <> - - - - ) : null} - - ); - } - - public componentWillUnmount() { - this.mounted = false; - } - - public componentDidMount() { - this.mounted = true; - } - - public render() { - const job = this.props.job; - let flyout; - - if (this.state.isFlyoutVisible) { - flyout = ( - - - - -

    {this.state.calloutTitle}

    -
    -
    - - {this.renderInfo()} - -
    -
    - ); - } - - let message = this.props.intl.formatMessage({ - id: 'xpack.reporting.listing.table.reportInfoButtonTooltip', - defaultMessage: 'See report info.', - }); - if (job.getError()) { - message = this.props.intl.formatMessage({ - id: 'xpack.reporting.listing.table.reportInfoAndErrorButtonTooltip', - defaultMessage: 'See report info and error message.', - }); - } else if (job.getWarnings()) { - message = this.props.intl.formatMessage({ - id: 'xpack.reporting.listing.table.reportInfoAndWarningsButtonTooltip', - defaultMessage: 'See report info and warnings.', - }); - } - - let buttonIconType = 'iInCircle'; - let buttonColor: 'primary' | 'danger' | 'warning' = 'primary'; - if (job.getWarnings() || job.getError()) { - buttonIconType = 'alert'; - buttonColor = 'danger'; - } - if (job.getWarnings()) { - buttonColor = 'warning'; - } - - return ( - <> - - - - {flyout} - - ); - } - - private loadInfo = async () => { - this.setState({ isLoading: true }); - try { - const info = await this.props.apiClient.getInfo(this.props.job.id); - if (this.mounted) { - this.setState({ isLoading: false, info }); - } - } catch (err) { - if (this.mounted) { - this.setState({ - isLoading: false, - calloutTitle: this.props.intl.formatMessage({ - id: 'xpack.reporting.listing.table.reportInfoUnableToFetch', - defaultMessage: 'Unable to fetch report info.', - }), - info: null, - error: err, - }); - } - } - }; - - private closeFlyout = () => { - this.setState({ - isFlyoutVisible: false, - info: null, // force re-read for next click - }); - }; - - private showFlyout = () => { - this.setState({ isFlyoutVisible: true }); - - if (!this.state.info) { - this.loadInfo(); - } - }; -} - -export const ReportInfoButton = injectI18n(ReportInfoButtonUi); diff --git a/x-pack/plugins/reporting/public/management/report_listing.scss b/x-pack/plugins/reporting/public/management/report_listing.scss new file mode 100644 index 0000000000000..7e85838ba09cd --- /dev/null +++ b/x-pack/plugins/reporting/public/management/report_listing.scss @@ -0,0 +1,7 @@ +.kbnReporting { + &__reportListing { + &__typeIcon { + padding-left: $euiSizeS; + } + } +} diff --git a/x-pack/plugins/reporting/public/management/report_listing.test.tsx b/x-pack/plugins/reporting/public/management/report_listing.test.tsx index 5e80c2d666c23..577d64be38a54 100644 --- a/x-pack/plugins/reporting/public/management/report_listing.test.tsx +++ b/x-pack/plugins/reporting/public/management/report_listing.test.tsx @@ -7,16 +7,17 @@ import { registerTestBed } from '@kbn/test/jest'; import type { SerializableRecord, UnwrapPromise } from '@kbn/utility-types'; -import type { DeeplyMockedKeys } from '@kbn/utility-types/jest'; import React from 'react'; import { act } from 'react-dom/test-utils'; import type { Observable } from 'rxjs'; +import type { IUiSettingsClient } from 'src/core/public'; import { ListingProps as Props, ReportListing } from '.'; import type { NotificationsSetup } from '../../../../../src/core/public'; import { applicationServiceMock, httpServiceMock, notificationServiceMock, + coreMock, } from '../../../../../src/core/public/mocks'; import type { LocatorPublic, SharePluginSetup } from '../../../../../src/plugins/share/public'; import type { ILicense } from '../../../licensing/public'; @@ -65,12 +66,18 @@ const mockJobs: ReportApiJSON[] = [ id: 'k90e51pk1ieucbae0c3t8wo2', attempts: 0, created_at: '2020-04-14T21:01:13.064Z', - jobtype: 'printable_pdf', + jobtype: 'printable_pdf_v2', meta: { layout: 'preserve_layout', objectType: 'canvas workpad' }, payload: { + spaceId: 'my-space', objectType: 'canvas workpad', title: 'My Canvas Workpad', - }, + locatorParams: [ + { + id: 'MY_APP', + }, + ], + } as any, status: 'pending', }), buildMockReport({ @@ -201,12 +208,6 @@ const mockJobs: ReportApiJSON[] = [ }), ]; -const reportingAPIClient = { - list: jest.fn(() => Promise.resolve(mockJobs.map((j) => new Job(j)))), - total: jest.fn(() => Promise.resolve(18)), - migrateReportingIndicesIlmPolicy: jest.fn(), -} as unknown as DeeplyMockedKeys; - const validCheck = { check: () => ({ state: 'VALID', @@ -233,11 +234,13 @@ const mockPollConfig = { describe('ReportListing', () => { let httpService: ReturnType; + let uiSettingsClient: IUiSettingsClient; let applicationService: ReturnType; let ilmLocator: undefined | LocatorPublic; let urlService: SharePluginSetup['url']; let testBed: UnwrapPromise>; let toasts: NotificationsSetup['toasts']; + let reportingAPIClient: ReportingAPIClient; const createTestBed = registerTestBed( (props?: Partial) => ( @@ -290,6 +293,7 @@ describe('ReportListing', () => { beforeEach(async () => { toasts = notificationServiceMock.createSetupContract().toasts; httpService = httpServiceMock.createSetupContract(); + uiSettingsClient = coreMock.createSetup().uiSettings; applicationService = applicationServiceMock.createStartContract(); applicationService.capabilities = { catalogue: {}, @@ -300,6 +304,16 @@ describe('ReportListing', () => { getUrl: jest.fn(), } as unknown as LocatorPublic; + reportingAPIClient = new ReportingAPIClient(httpService, uiSettingsClient, 'x.x.x'); + + jest + .spyOn(reportingAPIClient, 'list') + .mockImplementation(() => Promise.resolve(mockJobs.map((j) => new Job(j)))); + jest.spyOn(reportingAPIClient, 'total').mockImplementation(() => Promise.resolve(18)); + jest + .spyOn(reportingAPIClient, 'migrateReportingIndicesIlmPolicy') + .mockImplementation(jest.fn()); + urlService = { locators: { get: () => ilmLocator, @@ -312,10 +326,9 @@ describe('ReportListing', () => { jest.clearAllMocks(); }); - it('Report job listing with some items', () => { - const { actions } = testBed; - const table = actions.findListTable(); - expect(table).toMatchSnapshot(); + it('renders a listing with some items', () => { + const { find } = testBed; + expect(find('reportDownloadLink').length).toBe(mockJobs.length); }); it('subscribes to license changes, and unsubscribes on dismount', async () => { @@ -334,6 +347,21 @@ describe('ReportListing', () => { expect(unsubscribeMock).toHaveBeenCalled(); }); + it('navigates to a Kibana App in a new tab and is spaces aware', () => { + const { find } = testBed; + + jest.spyOn(window, 'open').mockImplementation(jest.fn()); + jest.spyOn(window, 'focus').mockImplementation(jest.fn()); + + find('euiCollapsedItemActionsButton').first().simulate('click'); + find('reportOpenInKibanaApp').first().simulate('click'); + + expect(window.open).toHaveBeenCalledWith( + '/s/my-space/app/reportingRedirect?jobId=k90e51pk1ieucbae0c3t8wo2', + '_blank' + ); + }); + describe('ILM policy', () => { beforeEach(async () => { httpService = httpServiceMock.createSetupContract(); @@ -414,7 +442,9 @@ describe('ReportListing', () => { it('informs users when migrations failed', async () => { const status: IlmPolicyMigrationStatus = 'indices-not-managed-by-policy'; httpService.get.mockResolvedValueOnce({ status }); - reportingAPIClient.migrateReportingIndicesIlmPolicy.mockRejectedValueOnce(new Error('oops!')); + (reportingAPIClient.migrateReportingIndicesIlmPolicy as jest.Mock).mockRejectedValueOnce( + new Error('oops!') + ); await runSetup(); const { actions } = testBed; diff --git a/x-pack/plugins/reporting/public/management/report_listing.tsx b/x-pack/plugins/reporting/public/management/report_listing.tsx index 6b46778011250..46c375cd8880f 100644 --- a/x-pack/plugins/reporting/public/management/report_listing.tsx +++ b/x-pack/plugins/reporting/public/management/report_listing.tsx @@ -12,15 +12,17 @@ import { EuiLoadingSpinner, EuiPageHeader, EuiSpacer, - EuiText, - EuiTextColor, + EuiBasicTableColumn, + EuiIconTip, + EuiLink, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { FormattedMessage, injectI18n } from '@kbn/i18n/react'; +import { FormattedMessage } from '@kbn/i18n/react'; import { Component, default as React, Fragment } from 'react'; import { Subscription } from 'rxjs'; import { ILicense } from '../../../licensing/public'; -import { REPORT_TABLE_ID, REPORT_TABLE_ROW_ID } from '../../common/constants'; +import { REPORT_TABLE_ID, REPORT_TABLE_ROW_ID, JOB_STATUSES } from '../../common/constants'; +import { prettyPrintJobType } from '../../common/job_utils'; import { Poller } from '../../common/poller'; import { durationToNumber } from '../../common/schema_utils'; import { useIlmPolicyStatus } from '../lib/ilm_policy_status_context'; @@ -28,13 +30,20 @@ import { Job } from '../lib/job'; import { checkLicense } from '../lib/license_check'; import { useInternalApiClient } from '../lib/reporting_api_client'; import { useKibana } from '../shared_imports'; -import { IlmPolicyLink } from './ilm_policy_link'; -import { MigrateIlmPolicyCallOut } from './migrate_ilm_policy_callout'; -import { ReportDeleteButton } from './report_delete_button'; -import { ReportDiagnostic } from './report_diagnostic'; -import { ReportDownloadButton } from './report_download_button'; -import { ReportInfoButton } from './report_info_button'; import { ListingProps as Props } from './'; +import { PDF_JOB_TYPE_V2, PNG_JOB_TYPE_V2 } from '../../common/constants'; +import { + IlmPolicyLink, + MigrateIlmPolicyCallOut, + ReportDeleteButton, + ReportDiagnostic, + ReportStatusIndicator, + ReportInfoFlyout, +} from './components'; +import { guessAppIconTypeFromObjectType } from './utils'; +import './report_listing.scss'; + +type TableColumn = EuiBasicTableColumn; interface State { page: number; @@ -45,6 +54,7 @@ interface State { showLinks: boolean; enableLinks: boolean; badLicenseMessage: string; + selectedJob: undefined | Job; } class ReportListingUi extends Component { @@ -65,6 +75,7 @@ class ReportListingUi extends Component { showLinks: false, enableLinks: false, badLicenseMessage: '', + selectedJob: undefined, }; this.isInitialJobsFetch = true; @@ -177,23 +188,19 @@ class ReportListingUi extends Component { await this.props.apiClient.deleteReport(job.id); this.removeJob(job); this.props.toasts.addSuccess( - this.props.intl.formatMessage( - { - id: 'xpack.reporting.listing.table.deleteConfim', - defaultMessage: `The {reportTitle} report was deleted`, + i18n.translate('xpack.reporting.listing.table.deleteConfim', { + defaultMessage: `The {reportTitle} report was deleted`, + values: { + reportTitle: job.title, }, - { reportTitle: job.title } - ) + }) ); } catch (error) { this.props.toasts.addDanger( - this.props.intl.formatMessage( - { - id: 'xpack.reporting.listing.table.deleteFailedErrorMessage', - defaultMessage: `The report was not deleted: {error}`, - }, - { error } - ) + i18n.translate('xpack.reporting.listing.table.deleteFailedErrorMessage', { + defaultMessage: `The report was not deleted: {error}`, + values: { error }, + }) ); throw error; } @@ -240,8 +247,7 @@ class ReportListingUi extends Component { if (fetchError.message === 'Failed to fetch') { this.props.toasts.addDanger( fetchError.message || - this.props.intl.formatMessage({ - id: 'xpack.reporting.listing.table.requestFailedErrorMessage', + i18n.translate('xpack.reporting.listing.table.requestFailedErrorMessage', { defaultMessage: 'Request failed', }) ); @@ -265,61 +271,176 @@ class ReportListingUi extends Component { return this.state.showLinks && this.state.enableLinks; }; - private renderTable() { - const { intl } = this.props; + /** + * Widths like this are not the best, but the auto-layout does not play well with text in links. We can update + * this with something that works better on all screen sizes. This works for desktop, mobile fallback is provided on a + * per column basis. + */ + private readonly tableColumnWidths = { + type: '5%', + title: '30%', + status: '20%', + createdAt: '25%', + content: '10%', + actions: '10%', + }; - const tableColumns = [ + private renderTable() { + const { tableColumnWidths } = this; + const tableColumns: TableColumn[] = [ + { + field: 'type', + width: tableColumnWidths.type, + name: i18n.translate('xpack.reporting.listing.tableColumns.typeTitle', { + defaultMessage: 'Type', + }), + render: (_type: string, job) => { + return ( +
    + +
    + ); + }, + mobileOptions: { + show: true, + render: (job) => { + return
    {job.objectType}
    ; + }, + }, + }, { field: 'title', - name: intl.formatMessage({ - id: 'xpack.reporting.listing.tableColumns.reportTitle', - defaultMessage: 'Report', + name: i18n.translate('xpack.reporting.listing.tableColumns.reportTitle', { + defaultMessage: 'Title', }), - render: (objectTitle: string, job: Job) => { + width: tableColumnWidths.title, + render: (objectTitle: string, job) => { return (
    -
    {objectTitle}
    - - {job.objectType} - + this.setState({ selectedJob: job })}> + {objectTitle || + i18n.translate('xpack.reporting.listing.table.noTitleLabel', { + defaultMessage: 'Untitled', + })} +
    ); }, + mobileOptions: { + header: false, + width: '100%', // This is not recognized by EUI types but has an effect, leaving for now + } as unknown as { header: boolean }, + }, + { + field: 'status', + width: tableColumnWidths.status, + name: i18n.translate('xpack.reporting.listing.tableColumns.statusTitle', { + defaultMessage: 'Status', + }), + render: (_status: string, job) => { + return ( + + + + ); + }, + mobileOptions: { + show: false, + }, }, { field: 'created_at', - name: intl.formatMessage({ - id: 'xpack.reporting.listing.tableColumns.createdAtTitle', + width: tableColumnWidths.createdAt, + name: i18n.translate('xpack.reporting.listing.tableColumns.createdAtTitle', { defaultMessage: 'Created at', }), - render: (_createdAt: string, job: Job) => ( -
    {job.getCreatedAtLabel()}
    + render: (_createdAt: string, job) => ( +
    {job.getCreatedAtDate()}
    ), + mobileOptions: { + show: false, + }, }, { - field: 'status', - name: intl.formatMessage({ - id: 'xpack.reporting.listing.tableColumns.statusTitle', - defaultMessage: 'Status', + field: 'content', + width: tableColumnWidths.content, + name: i18n.translate('xpack.reporting.listing.tableColumns.content', { + defaultMessage: 'Content', }), - render: (_status: string, job: Job) => ( -
    {job.getStatusLabel()}
    - ), + render: (_status: string, job) => prettyPrintJobType(job.jobtype), + mobileOptions: { + show: false, + }, }, { - name: intl.formatMessage({ - id: 'xpack.reporting.listing.tableColumns.actionsTitle', + name: i18n.translate('xpack.reporting.listing.tableColumns.actionsTitle', { defaultMessage: 'Actions', }), + width: tableColumnWidths.actions, actions: [ { - render: (job: Job) => { - return ( -
    - - -
    - ); + isPrimary: true, + 'data-test-subj': 'reportDownloadLink', + type: 'icon', + icon: 'download', + name: i18n.translate('xpack.reporting.listing.table.downloadReportButtonLabel', { + defaultMessage: 'Download report', + }), + description: i18n.translate('xpack.reporting.listing.table.downloadReportDescription', { + defaultMessage: 'Download this report in a new tab.', + }), + onClick: (job) => this.props.apiClient.downloadReport(job.id), + enabled: (job) => + job.status === JOB_STATUSES.COMPLETED || job.status === JOB_STATUSES.WARNINGS, + }, + { + name: i18n.translate( + 'xpack.reporting.listing.table.viewReportingInfoActionButtonLabel', + { + defaultMessage: 'View report info', + } + ), + description: i18n.translate( + 'xpack.reporting.listing.table.viewReportingInfoActionButtonDescription', + { + defaultMessage: 'View additional information about this report.', + } + ), + type: 'icon', + icon: 'iInCircle', + onClick: (job) => this.setState({ selectedJob: job }), + }, + { + name: i18n.translate('xpack.reporting.listing.table.openInKibanaAppLabel', { + defaultMessage: 'Open in Kibana App', + }), + 'data-test-subj': 'reportOpenInKibanaApp', + description: i18n.translate( + 'xpack.reporting.listing.table.openInKibanaAppDescription', + { + defaultMessage: 'Open the Kibana App where this report was generated.', + } + ), + available: (job) => + [PDF_JOB_TYPE_V2, PNG_JOB_TYPE_V2].some( + (linkableJobType) => linkableJobType === job.jobtype + ), + type: 'icon', + icon: 'popout', + onClick: (job) => { + const href = this.props.apiClient.getKibanaAppHref(job); + window.open(href, '_blank'); + window.focus(); }, }, ], @@ -358,12 +479,10 @@ class ReportListingUi extends Component { columns={tableColumns} noItemsMessage={ this.state.isLoading - ? intl.formatMessage({ - id: 'xpack.reporting.listing.table.loadingReportsDescription', + ? i18n.translate('xpack.reporting.listing.table.loadingReportsDescription', { defaultMessage: 'Loading reports', }) - : intl.formatMessage({ - id: 'xpack.reporting.listing.table.noCreatedReportsDescription', + : i18n.translate('xpack.reporting.listing.table.noCreatedReportsDescription', { defaultMessage: 'No reports have been created', }) } @@ -374,13 +493,17 @@ class ReportListingUi extends Component { data-test-subj={REPORT_TABLE_ID} rowProps={() => ({ 'data-test-subj': REPORT_TABLE_ROW_ID })} /> + {!!this.state.selectedJob && ( + this.setState({ selectedJob: undefined })} + job={this.state.selectedJob} + /> + )} ); } } -const PrivateReportListing = injectI18n(ReportListingUi); - export const ReportListing = ( props: Omit ) => { @@ -392,7 +515,7 @@ export const ReportListing = ( }, } = useKibana(); return ( - { + switch (type) { + case 'search': + return 'discoverApp'; + case 'dashboard': + return 'dashboardApp'; + case 'visualization': + return 'visualizeApp'; + case 'canvas workpad': + return 'canvasApp'; + default: + return 'apps'; + } +}; + +export const jobHasIssues = (job: Job): boolean => { + return ( + Boolean(job.getWarnings()) || + [JOB_STATUSES.WARNINGS, JOB_STATUSES.FAILED].some((status) => job.status === status) + ); +}; diff --git a/x-pack/plugins/reporting/public/plugin.ts b/x-pack/plugins/reporting/public/plugin.ts index 7fd6047470a0e..fe80ed679c8ed 100644 --- a/x-pack/plugins/reporting/public/plugin.ts +++ b/x-pack/plugins/reporting/public/plugin.ts @@ -41,9 +41,9 @@ import type { UiActionsSetup, UiActionsStart, } from './shared_imports'; +import { AppNavLinkStatus } from './shared_imports'; import { ReportingCsvShareProvider } from './share_context_menu/register_csv_reporting'; import { reportingScreenshotShareProvider } from './share_context_menu/register_pdf_png_reporting'; -import { isRedirectAppPath } from './utils'; export interface ClientConfigType { poll: { jobsRefresh: { interval: number; intervalErrorMultiplier: number } }; @@ -173,15 +173,6 @@ export class ReportingPublicPlugin title: this.title, order: 1, mount: async (params) => { - // The redirect app will be mounted if reporting is opened on a specific path. The redirect app expects a - // specific environment to be present so that it can navigate to a specific application. This is used by - // report generation to navigate to the correct place with full app state. - if (isRedirectAppPath(params.history.location.pathname)) { - const { mountRedirectApp } = await import('./redirect'); - return mountRedirectApp({ ...params, share, apiClient }); - } - - // Otherwise load the reporting management UI. params.setBreadcrumbs([{ text: this.breadcrumbText }]); const [[start], { mountManagementSection }] = await Promise.all([ getStartServices(), @@ -208,6 +199,19 @@ export class ReportingPublicPlugin }, }); + core.application.register({ + id: 'reportingRedirect', + mount: async (params) => { + const { mountRedirectApp } = await import('./redirect'); + return mountRedirectApp({ ...params, share, apiClient }); + }, + title: 'Reporting redirect app', + searchable: false, + chromeless: true, + exactRoute: true, + navLinkStatus: AppNavLinkStatus.hidden, + }); + uiActions.addTriggerAction( CONTEXT_MENU_TRIGGER, new ReportingCsvPanelAction({ core, apiClient, startServices$, license$, usesUiCapabilities }) diff --git a/x-pack/plugins/reporting/public/redirect/mount_redirect_app.tsx b/x-pack/plugins/reporting/public/redirect/mount_redirect_app.tsx index 4bf6d40acb170..eb34fc71cbf4e 100644 --- a/x-pack/plugins/reporting/public/redirect/mount_redirect_app.tsx +++ b/x-pack/plugins/reporting/public/redirect/mount_redirect_app.tsx @@ -9,12 +9,13 @@ import { render, unmountComponentAtNode } from 'react-dom'; import React from 'react'; import { EuiErrorBoundary } from '@elastic/eui'; -import type { ManagementAppMountParams, SharePluginSetup } from '../shared_imports'; +import type { AppMountParameters } from 'kibana/public'; +import type { SharePluginSetup } from '../shared_imports'; import type { ReportingAPIClient } from '../lib/reporting_api_client'; import { RedirectApp } from './redirect_app'; -interface MountParams extends ManagementAppMountParams { +interface MountParams extends AppMountParameters { apiClient: ReportingAPIClient; share: SharePluginSetup; } diff --git a/x-pack/plugins/reporting/public/redirect/redirect_app.scss b/x-pack/plugins/reporting/public/redirect/redirect_app.scss new file mode 100644 index 0000000000000..7e6b40e8231e8 --- /dev/null +++ b/x-pack/plugins/reporting/public/redirect/redirect_app.scss @@ -0,0 +1,8 @@ +.reportingRedirectApp { + &__interstitialPage { + /* + Create some padding above and below the page so that the errors (if any) display nicely. + */ + margin: $euiSizeXXL auto; + } +} diff --git a/x-pack/plugins/reporting/public/redirect/redirect_app.tsx b/x-pack/plugins/reporting/public/redirect/redirect_app.tsx index cf027e2a46196..4b271b17c5e85 100644 --- a/x-pack/plugins/reporting/public/redirect/redirect_app.tsx +++ b/x-pack/plugins/reporting/public/redirect/redirect_app.tsx @@ -7,8 +7,9 @@ import React, { useEffect, useState } from 'react'; import type { FunctionComponent } from 'react'; +import { parse } from 'query-string'; import { i18n } from '@kbn/i18n'; -import { EuiTitle, EuiCallOut, EuiCodeBlock } from '@elastic/eui'; +import { EuiCallOut, EuiCodeBlock } from '@elastic/eui'; import type { ScopedHistory } from 'src/core/public'; @@ -18,6 +19,8 @@ import { LocatorParams } from '../../common/types'; import { ReportingAPIClient } from '../lib/reporting_api_client'; import type { SharePluginSetup } from '../shared_imports'; +import './redirect_app.scss'; + interface Props { apiClient: ReportingAPIClient; history: ScopedHistory; @@ -28,9 +31,6 @@ const i18nTexts = { errorTitle: i18n.translate('xpack.reporting.redirectApp.errorTitle', { defaultMessage: 'Redirect error', }), - redirectingTitle: i18n.translate('xpack.reporting.redirectApp.redirectingMessage', { - defaultMessage: 'Redirecting...', - }), consoleMessagePrefix: i18n.translate( 'xpack.reporting.redirectApp.redirectConsoleErrorPrefixLabel', { @@ -39,36 +39,51 @@ const i18nTexts = { ), }; -export const RedirectApp: FunctionComponent = ({ share }) => { +export const RedirectApp: FunctionComponent = ({ share, apiClient }) => { const [error, setError] = useState(); useEffect(() => { - try { - const locatorParams = (window as unknown as Record)[ - REPORTING_REDIRECT_LOCATOR_STORE_KEY - ]; + (async () => { + try { + let locatorParams: undefined | LocatorParams; - if (!locatorParams) { - throw new Error('Could not find locator params for report'); - } + const { jobId } = parse(window.location.search); - share.navigate(locatorParams); - } catch (e) { - setError(e); - // eslint-disable-next-line no-console - console.error(i18nTexts.consoleMessagePrefix, e.message); - throw e; - } - }, [share]); + if (jobId) { + const result = await apiClient.getInfo(jobId as string); + locatorParams = result?.locatorParams?.[0]; + } else { + locatorParams = (window as unknown as Record)[ + REPORTING_REDIRECT_LOCATOR_STORE_KEY + ]; + } + + if (!locatorParams) { + throw new Error('Could not find locator params for report'); + } + + share.navigate(locatorParams); + } catch (e) { + setError(e); + // eslint-disable-next-line no-console + console.error(i18nTexts.consoleMessagePrefix, e.message); + throw e; + } + })(); + }, [share, apiClient]); - return error ? ( - -

    {error.message}

    - {error.stack && {error.stack}} -
    - ) : ( - -

    {i18nTexts.redirectingTitle}

    -
    + return ( +
    + {error ? ( + +

    {error.message}

    + {error.stack && {error.stack}} +
    + ) : ( + // We don't show anything on this page, the share service will handle showing any issues with + // using the locator +
    + )} +
    ); }; diff --git a/x-pack/plugins/reporting/public/share_context_menu/screen_capture_panel_content.test.tsx b/x-pack/plugins/reporting/public/share_context_menu/screen_capture_panel_content.test.tsx index f37aaea114cfa..0366c1c6d052c 100644 --- a/x-pack/plugins/reporting/public/share_context_menu/screen_capture_panel_content.test.tsx +++ b/x-pack/plugins/reporting/public/share_context_menu/screen_capture_panel_content.test.tsx @@ -63,6 +63,43 @@ test('ScreenCapturePanelContent properly renders a view with "canvas" layout opt expect(component.text()).toMatch('Full page layout'); }); +test('ScreenCapturePanelContent allows POST URL to be copied when objectId is provided', () => { + const component = mount( + + + + ); + expect(component.text()).toMatch('Copy POST URL'); + expect(component.text()).not.toMatch('Unsaved work'); +}); + +test('ScreenCapturePanelContent does not allow POST URL to be copied when objectId is not provided', () => { + const component = mount( + + + + ); + expect(component.text()).not.toMatch('Copy POST URL'); + expect(component.text()).toMatch('Unsaved work'); +}); + test('ScreenCapturePanelContent properly renders a view with "print" layout option', () => { const component = mount( diff --git a/x-pack/plugins/reporting/public/shared/get_shared_components.tsx b/x-pack/plugins/reporting/public/shared/get_shared_components.tsx index 623e06dd74462..b08036e8b1c80 100644 --- a/x-pack/plugins/reporting/public/shared/get_shared_components.tsx +++ b/x-pack/plugins/reporting/public/shared/get_shared_components.tsx @@ -16,7 +16,8 @@ interface IncludeOnCloseFn { onClose: () => void; } -type Props = Pick & IncludeOnCloseFn; +type Props = Pick & + IncludeOnCloseFn; /* * As of 7.14, the only shared component is a PDF report that is suited for Canvas integration. diff --git a/x-pack/plugins/reporting/public/shared_imports.ts b/x-pack/plugins/reporting/public/shared_imports.ts index 30e6cd12e3ed9..037dc5e374d25 100644 --- a/x-pack/plugins/reporting/public/shared_imports.ts +++ b/x-pack/plugins/reporting/public/shared_imports.ts @@ -7,6 +7,8 @@ export type { SharePluginSetup, SharePluginStart, LocatorPublic } from 'src/plugins/share/public'; +export { AppNavLinkStatus } from '../../../../src/core/public'; + export type { UseRequestResponse } from '../../../../src/plugins/es_ui_shared/public'; export { useRequest } from '../../../../src/plugins/es_ui_shared/public'; diff --git a/x-pack/plugins/reporting/public/utils.ts b/x-pack/plugins/reporting/public/utils.ts deleted file mode 100644 index f39c7ef2174ef..0000000000000 --- a/x-pack/plugins/reporting/public/utils.ts +++ /dev/null @@ -1,12 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { REACT_ROUTER_REDIRECT_APP_PATH } from './constants'; - -export const isRedirectAppPath = (pathname: string) => { - return pathname.startsWith(REACT_ROUTER_REDIRECT_APP_PATH); -}; diff --git a/x-pack/plugins/reporting/server/browsers/chromium/driver/chromium_driver.ts b/x-pack/plugins/reporting/server/browsers/chromium/driver/chromium_driver.ts index e7c2b68ba2712..0947d24f827c2 100644 --- a/x-pack/plugins/reporting/server/browsers/chromium/driver/chromium_driver.ts +++ b/x-pack/plugins/reporting/server/browsers/chromium/driver/chromium_driver.ts @@ -210,36 +210,16 @@ export class HeadlessChromiumDriver { return resp; } - public async waitFor( - { - fn, - args, - toEqual, - timeout, - }: { - fn: EvaluateFn; - args: SerializableOrJSHandle[]; - toEqual: number; - timeout: number; - }, - context: EvaluateMetaOpts, - logger: LevelLogger - ): Promise { - const startTime = Date.now(); - - while (true) { - const result = await this.evaluate({ fn, args }, context, logger); - if (result === toEqual) { - return; - } - - if (Date.now() - startTime > timeout) { - throw new Error( - `Timed out waiting for the items selected to equal ${toEqual}. Found: ${result}. Context: ${context.context}` - ); - } - await new Promise((r) => setTimeout(r, WAIT_FOR_DELAY_MS)); - } + public async waitFor({ + fn, + args, + timeout, + }: { + fn: EvaluateFn; + args: SerializableOrJSHandle[]; + timeout: number; + }): Promise { + await this.page.waitForFunction(fn, { timeout, polling: WAIT_FOR_DELAY_MS }, ...args); } public async setViewport( diff --git a/x-pack/plugins/reporting/server/browsers/chromium/driver_factory/index.test.ts b/x-pack/plugins/reporting/server/browsers/chromium/driver_factory/index.test.ts new file mode 100644 index 0000000000000..dae692fae8825 --- /dev/null +++ b/x-pack/plugins/reporting/server/browsers/chromium/driver_factory/index.test.ts @@ -0,0 +1,76 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import puppeteer from 'puppeteer'; +import * as Rx from 'rxjs'; +import { take } from 'rxjs/operators'; +import { HeadlessChromiumDriverFactory } from '.'; +import type { ReportingCore } from '../../..'; +import { + createMockConfigSchema, + createMockLevelLogger, + createMockReportingCore, +} from '../../../test_helpers'; + +jest.mock('puppeteer'); + +const mock = (browserDriverFactory: HeadlessChromiumDriverFactory) => { + browserDriverFactory.getBrowserLogger = jest.fn(() => new Rx.Observable()); + browserDriverFactory.getProcessLogger = jest.fn(() => new Rx.Observable()); + browserDriverFactory.getPageExit = jest.fn(() => new Rx.Observable()); + return browserDriverFactory; +}; + +describe('class HeadlessChromiumDriverFactory', () => { + let reporting: ReportingCore; + const logger = createMockLevelLogger(); + const path = 'path/to/headless_shell'; + + beforeEach(async () => { + (puppeteer as jest.Mocked).launch.mockResolvedValue({ + newPage: jest.fn().mockResolvedValue({ + target: jest.fn(() => ({ + createCDPSession: jest.fn().mockResolvedValue({ + send: jest.fn(), + }), + })), + emulateTimezone: jest.fn(), + setDefaultTimeout: jest.fn(), + }), + close: jest.fn(), + process: jest.fn(), + } as unknown as puppeteer.Browser); + + reporting = await createMockReportingCore( + createMockConfigSchema({ + capture: { + browser: { chromium: { proxy: {} } }, + timeouts: { openUrl: 50000 }, + }, + }) + ); + }); + + it('createPage returns browser driver and process exit observable', async () => { + const factory = mock(new HeadlessChromiumDriverFactory(reporting, path, logger)); + const utils = await factory.createPage({}).pipe(take(1)).toPromise(); + expect(utils).toHaveProperty('driver'); + expect(utils).toHaveProperty('exit$'); + }); + + it('createPage rejects if Puppeteer launch fails', async () => { + (puppeteer as jest.Mocked).launch.mockRejectedValue( + `Puppeteer Launch mock fail.` + ); + const factory = mock(new HeadlessChromiumDriverFactory(reporting, path, logger)); + expect(() => + factory.createPage({}).pipe(take(1)).toPromise() + ).rejects.toThrowErrorMatchingInlineSnapshot( + `"Error spawning Chromium browser! Puppeteer Launch mock fail."` + ); + }); +}); diff --git a/x-pack/plugins/reporting/server/browsers/chromium/driver_factory/index.ts b/x-pack/plugins/reporting/server/browsers/chromium/driver_factory/index.ts index 264e673d2bf74..2aef62f59985b 100644 --- a/x-pack/plugins/reporting/server/browsers/chromium/driver_factory/index.ts +++ b/x-pack/plugins/reporting/server/browsers/chromium/driver_factory/index.ts @@ -23,7 +23,7 @@ import { LevelLogger } from '../../../lib'; import { safeChildProcess } from '../../safe_child_process'; import { HeadlessChromiumDriver } from '../driver'; import { args } from './args'; -import { getMetrics, Metrics } from './metrics'; +import { getMetrics } from './metrics'; type BrowserConfig = CaptureConfig['browser']['chromium']; @@ -35,7 +35,7 @@ export class HeadlessChromiumDriverFactory { private getChromiumArgs: () => string[]; private core: ReportingCore; - constructor(core: ReportingCore, binaryPath: string, logger: LevelLogger) { + constructor(core: ReportingCore, binaryPath: string, private logger: LevelLogger) { this.core = core; this.binaryPath = binaryPath; const config = core.getConfig(); @@ -62,7 +62,7 @@ export class HeadlessChromiumDriverFactory { */ createPage( { browserTimezone }: { browserTimezone?: string }, - pLogger: LevelLogger + pLogger = this.logger ): Rx.Observable<{ driver: HeadlessChromiumDriver; exit$: Rx.Observable }> { // FIXME: 'create' is deprecated return Rx.Observable.create(async (observer: InnerSubscriber) => { @@ -72,10 +72,7 @@ export class HeadlessChromiumDriverFactory { const chromiumArgs = this.getChromiumArgs(); logger.debug(`Chromium launch args set to: ${chromiumArgs}`); - let browser: puppeteer.Browser; - let page: puppeteer.Page; - let devTools: puppeteer.CDPSession | undefined; - let startMetrics: Metrics | undefined; + let browser: puppeteer.Browser | null = null; try { browser = await puppeteer.launch({ @@ -89,29 +86,28 @@ export class HeadlessChromiumDriverFactory { TZ: browserTimezone, }, }); + } catch (err) { + observer.error(new Error(`Error spawning Chromium browser! ${err}`)); + return; + } - page = await browser.newPage(); - devTools = await page.target().createCDPSession(); + const page = await browser.newPage(); + const devTools = await page.target().createCDPSession(); - await devTools.send('Performance.enable', { timeDomain: 'timeTicks' }); - startMetrics = await devTools.send('Performance.getMetrics'); + await devTools.send('Performance.enable', { timeDomain: 'timeTicks' }); + const startMetrics = await devTools.send('Performance.getMetrics'); - // Log version info for debugging / maintenance - const versionInfo = await devTools.send('Browser.getVersion'); - logger.debug(`Browser version: ${JSON.stringify(versionInfo)}`); + // Log version info for debugging / maintenance + const versionInfo = await devTools.send('Browser.getVersion'); + logger.debug(`Browser version: ${JSON.stringify(versionInfo)}`); - await page.emulateTimezone(browserTimezone); + await page.emulateTimezone(browserTimezone); - // Set the default timeout for all navigation methods to the openUrl timeout (30 seconds) - // All waitFor methods have their own timeout config passed in to them - page.setDefaultTimeout(durationToNumber(this.captureConfig.timeouts.openUrl)); + // Set the default timeout for all navigation methods to the openUrl timeout + // All waitFor methods have their own timeout config passed in to them + page.setDefaultTimeout(durationToNumber(this.captureConfig.timeouts.openUrl)); - logger.debug(`Browser page driver created`); - } catch (err) { - observer.error(new Error(`Error spawning Chromium browser!`)); - observer.error(err); - throw err; - } + logger.debug(`Browser page driver created`); const childProcess = { async kill() { @@ -134,7 +130,7 @@ export class HeadlessChromiumDriverFactory { } try { - await browser.close(); + await browser?.close(); } catch (err) { // do not throw logger.error(err); diff --git a/x-pack/plugins/reporting/server/browsers/download/ensure_downloaded.test.ts b/x-pack/plugins/reporting/server/browsers/download/ensure_downloaded.test.ts index 955e8214af8fa..9db128c019ac0 100644 --- a/x-pack/plugins/reporting/server/browsers/download/ensure_downloaded.test.ts +++ b/x-pack/plugins/reporting/server/browsers/download/ensure_downloaded.test.ts @@ -17,7 +17,8 @@ import { LevelLogger } from '../../lib'; jest.mock('./checksum'); jest.mock('./download'); -describe('ensureBrowserDownloaded', () => { +// https://github.com/elastic/kibana/issues/115881 +describe.skip('ensureBrowserDownloaded', () => { let logger: jest.Mocked; beforeEach(() => { diff --git a/x-pack/plugins/reporting/server/core.ts b/x-pack/plugins/reporting/server/core.ts index e89ba6af3e28f..bc74f5463ba33 100644 --- a/x-pack/plugins/reporting/server/core.ts +++ b/x-pack/plugins/reporting/server/core.ts @@ -7,7 +7,7 @@ import Hapi from '@hapi/hapi'; import * as Rx from 'rxjs'; -import { first, map, take } from 'rxjs/operators'; +import { filter, first, map, take } from 'rxjs/operators'; import { ScreenshotModePluginSetup } from 'src/plugins/screenshot_mode/server'; import { BasePath, @@ -17,6 +17,8 @@ import { PluginInitializerContext, SavedObjectsClientContract, SavedObjectsServiceStart, + ServiceStatusLevels, + StatusServiceSetup, UiSettingsServiceStart, } from '../../../../src/core/server'; import { PluginStart as DataPluginStart } from '../../../../src/plugins/data/server'; @@ -44,6 +46,7 @@ export interface ReportingInternalSetup { taskManager: TaskManagerSetupContract; screenshotMode: ScreenshotModePluginSetup; logger: LevelLogger; + status: StatusServiceSetup; } export interface ReportingInternalStart { @@ -111,12 +114,25 @@ export class ReportingCore { this.pluginStart$.next(startDeps); // trigger the observer this.pluginStartDeps = startDeps; // cache + await this.assertKibanaIsAvailable(); + const { taskManager } = startDeps; const { executeTask, monitorTask } = this; // enable this instance to generate reports and to monitor for pending reports await Promise.all([executeTask.init(taskManager), monitorTask.init(taskManager)]); } + private async assertKibanaIsAvailable(): Promise { + const { status } = this.getPluginSetupDeps(); + + await status.overall$ + .pipe( + filter((current) => current.level === ServiceStatusLevels.available), + first() + ) + .toPromise(); + } + /* * Blocks the caller until setup is done */ diff --git a/x-pack/plugins/reporting/server/export_types/common/pdf/index.test.ts b/x-pack/plugins/reporting/server/export_types/common/pdf/index.test.ts index e336e5f124a2f..74a247d4568ab 100644 --- a/x-pack/plugins/reporting/server/export_types/common/pdf/index.test.ts +++ b/x-pack/plugins/reporting/server/export_types/common/pdf/index.test.ts @@ -14,7 +14,8 @@ const imageBase64 = Buffer.from( 'base64' ); -describe('PdfMaker', () => { +// FLAKY: https://github.com/elastic/kibana/issues/118484 +describe.skip('PdfMaker', () => { it('makes PDF using PrintLayout mode', async () => { const config = createMockConfig(createMockConfigSchema()); const layout = new PrintLayout(config.get('capture')); diff --git a/x-pack/plugins/reporting/server/export_types/common/v2/get_full_redirect_app_url.test.ts b/x-pack/plugins/reporting/server/export_types/common/v2/get_full_redirect_app_url.test.ts index 7a2ec5b83e7f4..69baf143156fd 100644 --- a/x-pack/plugins/reporting/server/export_types/common/v2/get_full_redirect_app_url.test.ts +++ b/x-pack/plugins/reporting/server/export_types/common/v2/get_full_redirect_app_url.test.ts @@ -33,13 +33,13 @@ describe('getFullRedirectAppUrl', () => { test('smoke test', () => { expect(getFullRedirectAppUrl(config, 'test', undefined)).toBe( - 'http://localhost:1234/test/s/test/app/management/insightsAndAlerting/reporting/r' + 'http://localhost:1234/test/s/test/app/reportingRedirect' ); }); test('adding forceNow', () => { expect(getFullRedirectAppUrl(config, 'test', 'TEST with a space')).toBe( - 'http://localhost:1234/test/s/test/app/management/insightsAndAlerting/reporting/r?forceNow=TEST%20with%20a%20space' + 'http://localhost:1234/test/s/test/app/reportingRedirect?forceNow=TEST%20with%20a%20space' ); }); }); diff --git a/x-pack/plugins/reporting/server/export_types/png_v2/execute_job.test.ts b/x-pack/plugins/reporting/server/export_types/png_v2/execute_job.test.ts index 3cf3c057e7b9c..ba076f98996b1 100644 --- a/x-pack/plugins/reporting/server/export_types/png_v2/execute_job.test.ts +++ b/x-pack/plugins/reporting/server/export_types/png_v2/execute_job.test.ts @@ -102,7 +102,7 @@ test(`passes browserTimezone to generatePng`, async () => { "warning": [Function], }, Array [ - "localhost:80undefined/app/management/insightsAndAlerting/reporting/r?forceNow=test", + "localhost:80undefined/app/reportingRedirect?forceNow=test", Object { "id": "test", "params": Object {}, diff --git a/x-pack/plugins/reporting/server/lib/screenshots/observable.test.ts b/x-pack/plugins/reporting/server/lib/screenshots/observable.test.ts index 3dc06996f0f04..3071ecb54dc26 100644 --- a/x-pack/plugins/reporting/server/lib/screenshots/observable.test.ts +++ b/x-pack/plugins/reporting/server/lib/screenshots/observable.test.ts @@ -353,7 +353,7 @@ describe('Screenshot Observable Pipeline', () => { }, }, ], - "error": [Error: An error occurred when trying to read the page for visualization panel info: Error: Mock error!], + "error": [Error: The "wait for elements" phase encountered an error: Error: An error occurred when trying to read the page for visualization panel info: Error: Mock error!], "screenshots": Array [ Object { "data": Object { diff --git a/x-pack/plugins/reporting/server/lib/screenshots/observable.ts b/x-pack/plugins/reporting/server/lib/screenshots/observable.ts index 48802eb5e5fbe..d400c423c5e04 100644 --- a/x-pack/plugins/reporting/server/lib/screenshots/observable.ts +++ b/x-pack/plugins/reporting/server/lib/screenshots/observable.ts @@ -58,9 +58,8 @@ export function getScreenshots$( const screen = new ScreenshotObservableHandler(driver, opts, getTimeouts(captureConfig)); return Rx.from(opts.urlsOrUrlLocatorTuples).pipe( - concatMap((urlOrUrlLocatorTuple, index) => { - return Rx.of(1).pipe( - screen.setupPage(index, urlOrUrlLocatorTuple, apmTrans), + concatMap((urlOrUrlLocatorTuple, index) => + screen.setupPage(index, urlOrUrlLocatorTuple, apmTrans).pipe( catchError((err) => { screen.checkPageIsOpen(); // this fails the job if the browser has closed @@ -69,8 +68,8 @@ export function getScreenshots$( }), takeUntil(exit$), screen.getScreenshots() - ); - }), + ) + ), take(opts.urlsOrUrlLocatorTuples.length), toArray() ); diff --git a/x-pack/plugins/reporting/server/lib/screenshots/observable_handler.test.ts b/x-pack/plugins/reporting/server/lib/screenshots/observable_handler.test.ts index 25a8bed370d86..cb0a513992722 100644 --- a/x-pack/plugins/reporting/server/lib/screenshots/observable_handler.test.ts +++ b/x-pack/plugins/reporting/server/lib/screenshots/observable_handler.test.ts @@ -95,7 +95,7 @@ describe('ScreenshotObservableHandler', () => { const testPipeline = () => test$.toPromise(); await expect(testPipeline).rejects.toMatchInlineSnapshot( - `[Error: The "Test Config" phase took longer than 0.2 seconds. You may need to increase "test.config.value": TimeoutError: Timeout has occurred]` + `[Error: The "Test Config" phase took longer than 0.2 seconds. You may need to increase "test.config.value"]` ); }); diff --git a/x-pack/plugins/reporting/server/lib/screenshots/observable_handler.ts b/x-pack/plugins/reporting/server/lib/screenshots/observable_handler.ts index 87c247273ef04..1db313b091025 100644 --- a/x-pack/plugins/reporting/server/lib/screenshots/observable_handler.ts +++ b/x-pack/plugins/reporting/server/lib/screenshots/observable_handler.ts @@ -7,7 +7,7 @@ import apm from 'elastic-apm-node'; import * as Rx from 'rxjs'; -import { catchError, mergeMap, timeout } from 'rxjs/operators'; +import { catchError, mergeMap, switchMapTo, timeoutWith } from 'rxjs/operators'; import { numberToDuration } from '../../../common/schema_utils'; import { UrlOrUrlLocatorTuple } from '../../../common/types'; import { HeadlessChromiumDriver } from '../../browsers'; @@ -33,7 +33,6 @@ export class ScreenshotObservableHandler { private conditionalHeaders: ScreenshotObservableOpts['conditionalHeaders']; private layout: ScreenshotObservableOpts['layout']; private logger: ScreenshotObservableOpts['logger']; - private waitErrorRegistered = false; constructor( private readonly driver: HeadlessChromiumDriver, @@ -50,35 +49,27 @@ export class ScreenshotObservableHandler { */ public waitUntil(phase: PhaseInstance) { const { timeoutValue, label, configValue } = phase; - return (source: Rx.Observable) => { - return source.pipe( - timeout(timeoutValue), - catchError((error: string | Error) => { - if (this.waitErrorRegistered) { - throw error; // do not create a stack of errors within the error - } - - this.logger.error(error); - let throwError = new Error(`The "${label}" phase encountered an error: ${error}`); - - if (error instanceof Rx.TimeoutError) { - throwError = new Error( - `The "${label}" phase took longer than` + - ` ${numberToDuration(timeoutValue).asSeconds()} seconds.` + - ` You may need to increase "${configValue}": ${error}` - ); - } - - this.waitErrorRegistered = true; - this.logger.error(throwError); - throw throwError; - }) + + return (source: Rx.Observable) => + source.pipe( + catchError((error) => { + throw new Error(`The "${label}" phase encountered an error: ${error}`); + }), + timeoutWith( + timeoutValue, + Rx.throwError( + new Error( + `The "${label}" phase took longer than ${numberToDuration( + timeoutValue + ).asSeconds()} seconds. You may need to increase "${configValue}"` + ) + ) + ) ); - }; } private openUrl(index: number, urlOrUrlLocatorTuple: UrlOrUrlLocatorTuple) { - return mergeMap(() => + return Rx.defer(() => openUrl( this.timeouts.openUrl.timeoutValue, this.driver, @@ -87,24 +78,25 @@ export class ScreenshotObservableHandler { this.conditionalHeaders, this.logger ) - ); + ).pipe(this.waitUntil(this.timeouts.openUrl)); } private waitForElements() { const driver = this.driver; const waitTimeout = this.timeouts.waitForElements.timeoutValue; - return (withPageOpen: Rx.Observable) => - withPageOpen.pipe( - mergeMap(() => getNumberOfItems(waitTimeout, driver, this.layout, this.logger)), - mergeMap(async (itemsCount) => { - // set the viewport to the dimentions from the job, to allow elements to flow into the expected layout - const viewport = this.layout.getViewport(itemsCount) || getDefaultViewPort(); - await Promise.all([ - driver.setViewport(viewport, this.logger), - waitForVisualizations(waitTimeout, driver, itemsCount, this.layout, this.logger), - ]); - }) - ); + + return Rx.defer(() => getNumberOfItems(waitTimeout, driver, this.layout, this.logger)).pipe( + mergeMap((itemsCount) => { + // set the viewport to the dimentions from the job, to allow elements to flow into the expected layout + const viewport = this.layout.getViewport(itemsCount) || getDefaultViewPort(); + + return Rx.forkJoin([ + driver.setViewport(viewport, this.logger), + waitForVisualizations(waitTimeout, driver, itemsCount, this.layout, this.logger), + ]); + }), + this.waitUntil(this.timeouts.waitForElements) + ); } private completeRender(apmTrans: apm.Transaction | null) { @@ -112,32 +104,27 @@ export class ScreenshotObservableHandler { const layout = this.layout; const logger = this.logger; - return (withElements: Rx.Observable) => - withElements.pipe( - mergeMap(async () => { - // Waiting till _after_ elements have rendered before injecting our CSS - // allows for them to be displayed properly in many cases - await injectCustomCss(driver, layout, logger); - - const apmPositionElements = apmTrans?.startSpan('position_elements', 'correction'); - // position panel elements for print layout - await layout.positionElements?.(driver, logger); - apmPositionElements?.end(); - - await waitForRenderComplete(this.timeouts.loadDelay, driver, layout, logger); - }), - mergeMap(() => - Promise.all([ - getTimeRange(driver, layout, logger), - getElementPositionAndAttributes(driver, layout, logger), - getRenderErrors(driver, layout, logger), - ]).then(([timeRange, elementsPositionAndAttributes, renderErrors]) => ({ - elementsPositionAndAttributes, - timeRange, - renderErrors, - })) - ) - ); + return Rx.defer(async () => { + // Waiting till _after_ elements have rendered before injecting our CSS + // allows for them to be displayed properly in many cases + await injectCustomCss(driver, layout, logger); + + const apmPositionElements = apmTrans?.startSpan('position_elements', 'correction'); + // position panel elements for print layout + await layout.positionElements?.(driver, logger); + apmPositionElements?.end(); + + await waitForRenderComplete(this.timeouts.loadDelay, driver, layout, logger); + }).pipe( + mergeMap(() => + Rx.forkJoin({ + timeRange: getTimeRange(driver, layout, logger), + elementsPositionAndAttributes: getElementPositionAndAttributes(driver, layout, logger), + renderErrors: getRenderErrors(driver, layout, logger), + }) + ), + this.waitUntil(this.timeouts.renderComplete) + ); } public setupPage( @@ -145,15 +132,10 @@ export class ScreenshotObservableHandler { urlOrUrlLocatorTuple: UrlOrUrlLocatorTuple, apmTrans: apm.Transaction | null ) { - return (initial: Rx.Observable) => - initial.pipe( - this.openUrl(index, urlOrUrlLocatorTuple), - this.waitUntil(this.timeouts.openUrl), - this.waitForElements(), - this.waitUntil(this.timeouts.waitForElements), - this.completeRender(apmTrans), - this.waitUntil(this.timeouts.renderComplete) - ); + return this.openUrl(index, urlOrUrlLocatorTuple).pipe( + switchMapTo(this.waitForElements()), + switchMapTo(this.completeRender(apmTrans)) + ); } public getScreenshots() { diff --git a/x-pack/plugins/reporting/server/lib/screenshots/wait_for_visualizations.ts b/x-pack/plugins/reporting/server/lib/screenshots/wait_for_visualizations.ts index d4bf1db2a0c5a..10a53b238d892 100644 --- a/x-pack/plugins/reporting/server/lib/screenshots/wait_for_visualizations.ts +++ b/x-pack/plugins/reporting/server/lib/screenshots/wait_for_visualizations.ts @@ -11,10 +11,23 @@ import { HeadlessChromiumDriver } from '../../browsers'; import { LayoutInstance } from '../layouts'; import { CONTEXT_WAITFORELEMENTSTOBEINDOM } from './constants'; -type SelectorArgs = Record; +interface CompletedItemsCountParameters { + context: string; + count: number; + renderCompleteSelector: string; +} -const getCompletedItemsCount = ({ renderCompleteSelector }: SelectorArgs) => { - return document.querySelectorAll(renderCompleteSelector).length; +const getCompletedItemsCount = ({ + context, + count, + renderCompleteSelector, +}: CompletedItemsCountParameters) => { + const { length } = document.querySelectorAll(renderCompleteSelector); + + // eslint-disable-next-line no-console + console.debug(`evaluate ${context}: waitng for ${count} elements, got ${length}.`); + + return length >= count; }; /* @@ -40,11 +53,11 @@ export const waitForVisualizations = async ( ); try { - await browser.waitFor( - { fn: getCompletedItemsCount, args: [{ renderCompleteSelector }], toEqual, timeout }, - { context: CONTEXT_WAITFORELEMENTSTOBEINDOM }, - logger - ); + await browser.waitFor({ + fn: getCompletedItemsCount, + args: [{ renderCompleteSelector, context: CONTEXT_WAITFORELEMENTSTOBEINDOM, count: toEqual }], + timeout, + }); logger.debug(`found ${toEqual} rendered elements in the DOM`); } catch (err) { diff --git a/x-pack/plugins/reporting/server/plugin.ts b/x-pack/plugins/reporting/server/plugin.ts index 07d61ff1630fc..8969a698a8ce4 100644 --- a/x-pack/plugins/reporting/server/plugin.ts +++ b/x-pack/plugins/reporting/server/plugin.ts @@ -52,7 +52,6 @@ export class ReportingPlugin const router = http.createRouter(); const basePath = http.basePath; - reportingCore.pluginSetup({ screenshotMode, features, @@ -63,6 +62,7 @@ export class ReportingPlugin spaces, taskManager, logger: this.logger, + status: core.status, }); registerUiSettings(core); diff --git a/x-pack/plugins/reporting/server/routes/deprecations.test.ts b/x-pack/plugins/reporting/server/routes/deprecations.test.ts index 5367b6bd531ed..63be2acf52c25 100644 --- a/x-pack/plugins/reporting/server/routes/deprecations.test.ts +++ b/x-pack/plugins/reporting/server/routes/deprecations.test.ts @@ -24,7 +24,8 @@ import { registerDeprecationsRoutes } from './deprecations'; type SetupServerReturn = UnwrapPromise>; -describe(`GET ${API_GET_ILM_POLICY_STATUS}`, () => { +// https://github.com/elastic/kibana/issues/115881 +describe.skip(`GET ${API_GET_ILM_POLICY_STATUS}`, () => { const reportingSymbol = Symbol('reporting'); let server: SetupServerReturn['server']; let httpSetup: SetupServerReturn['httpSetup']; diff --git a/x-pack/plugins/reporting/server/routes/diagnostic/browser.test.ts b/x-pack/plugins/reporting/server/routes/diagnostic/browser.test.ts index 7b4cc2008a676..a27ce6a49b1a2 100644 --- a/x-pack/plugins/reporting/server/routes/diagnostic/browser.test.ts +++ b/x-pack/plugins/reporting/server/routes/diagnostic/browser.test.ts @@ -10,6 +10,7 @@ import { spawn } from 'child_process'; import { createInterface } from 'readline'; import { setupServer } from 'src/core/server/test_utils'; import supertest from 'supertest'; +import * as Rx from 'rxjs'; import { ReportingCore } from '../..'; import { createMockConfigSchema, @@ -28,8 +29,10 @@ type SetupServerReturn = UnwrapPromise>; const devtoolMessage = 'DevTools listening on (ws://localhost:4000)'; const fontNotFoundMessage = 'Could not find the default font'; -// FLAKY: https://github.com/elastic/kibana/issues/89369 -describe.skip('POST /diagnose/browser', () => { +const wait = (ms: number): Rx.Observable<0> => + Rx.from(new Promise<0>((resolve) => setTimeout(() => resolve(0), ms))); + +describe('POST /diagnose/browser', () => { jest.setTimeout(6000); const reportingSymbol = Symbol('reporting'); const mockLogger = createMockLevelLogger(); @@ -53,6 +56,9 @@ describe.skip('POST /diagnose/browser', () => { () => ({ usesUiCapabilities: () => false }) ); + // Make all uses of 'Rx.timer' return an observable that completes in 50ms + jest.spyOn(Rx, 'timer').mockImplementation(() => wait(50)); + core = await createMockReportingCore( config, createMockPluginSetup({ @@ -79,6 +85,7 @@ describe.skip('POST /diagnose/browser', () => { }); afterEach(async () => { + jest.restoreAllMocks(); await server.stop(); }); diff --git a/x-pack/plugins/reporting/server/test_helpers/create_mock_reportingplugin.ts b/x-pack/plugins/reporting/server/test_helpers/create_mock_reportingplugin.ts index d62cc750ccfcc..c05b2c54aeabf 100644 --- a/x-pack/plugins/reporting/server/test_helpers/create_mock_reportingplugin.ts +++ b/x-pack/plugins/reporting/server/test_helpers/create_mock_reportingplugin.ts @@ -11,7 +11,7 @@ jest.mock('../browsers'); import _ from 'lodash'; import * as Rx from 'rxjs'; -import { coreMock, elasticsearchServiceMock } from 'src/core/server/mocks'; +import { coreMock, elasticsearchServiceMock, statusServiceMock } from 'src/core/server/mocks'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths import { dataPluginMock } from 'src/plugins/data/server/mocks'; import { FieldFormatsRegistry } from 'src/plugins/field_formats/common'; @@ -45,6 +45,7 @@ export const createMockPluginSetup = (setupMock?: any): ReportingInternalSetup = licensing: { license$: Rx.of({ isAvailable: true, isActive: true, type: 'basic' }) } as any, taskManager: taskManagerMock.createSetup(), logger: createMockLevelLogger(), + status: statusServiceMock.createSetupContract(), ...setupMock, }; }; diff --git a/x-pack/plugins/rule_registry/server/rule_data_plugin_service/resource_installer.ts b/x-pack/plugins/rule_registry/server/rule_data_plugin_service/resource_installer.ts index 3798506eeacd1..bfdec28a50987 100644 --- a/x-pack/plugins/rule_registry/server/rule_data_plugin_service/resource_installer.ts +++ b/x-pack/plugins/rule_registry/server/rule_data_plugin_service/resource_installer.ts @@ -309,6 +309,7 @@ export class ResourceInstaller { template: { settings: { + hidden: true, 'index.lifecycle': { name: ilmPolicyName, // TODO: fix the types in the ES package, they don't include rollover_alias??? diff --git a/x-pack/plugins/security/public/authentication/capture_url/capture_url_app.test.ts b/x-pack/plugins/security/public/authentication/capture_url/capture_url_app.test.ts index 44fd5ab195341..0283df88fdb87 100644 --- a/x-pack/plugins/security/public/authentication/capture_url/capture_url_app.test.ts +++ b/x-pack/plugins/security/public/authentication/capture_url/capture_url_app.test.ts @@ -59,7 +59,7 @@ describe('captureURLApp', () => { captureURLApp.create(coreSetupMock); const [[{ mount }]] = coreSetupMock.application.register.mock.calls; - await mount(coreMock.createAppMountParamters()); + await mount(coreMock.createAppMountParameters()); expect(mockLocationReplace).toHaveBeenCalledTimes(1); expect(mockLocationReplace).toHaveBeenCalledWith( @@ -77,7 +77,7 @@ describe('captureURLApp', () => { captureURLApp.create(coreSetupMock); const [[{ mount }]] = coreSetupMock.application.register.mock.calls; - await mount(coreMock.createAppMountParamters()); + await mount(coreMock.createAppMountParameters()); expect(mockLocationReplace).toHaveBeenCalledTimes(1); expect(mockLocationReplace).toHaveBeenCalledWith( diff --git a/x-pack/plugins/security/public/components/breadcrumb.test.tsx b/x-pack/plugins/security/public/components/breadcrumb.test.tsx new file mode 100644 index 0000000000000..00cd4be90a780 --- /dev/null +++ b/x-pack/plugins/security/public/components/breadcrumb.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 { render } from '@testing-library/react'; +import React from 'react'; + +import { coreMock } from 'src/core/public/mocks'; + +import { Breadcrumb, BreadcrumbsProvider, createBreadcrumbsChangeHandler } from './breadcrumb'; + +describe('security breadcrumbs', () => { + const setBreadcrumbs = jest.fn(); + const { chrome } = coreMock.createStart(); + + beforeEach(() => { + setBreadcrumbs.mockReset(); + chrome.docTitle.reset.mockReset(); + chrome.docTitle.change.mockReset(); + }); + + it('rendering one breadcrumb and it should NOT have an href attributes', async () => { + render( + + +
    {'Find'}
    +
    +
    + ); + + expect(setBreadcrumbs).toHaveBeenCalledTimes(1); + expect(setBreadcrumbs).toHaveBeenCalledWith([{ text: 'Find' }]); + }); + + it('rendering two breadcrumb and our last breadcrumb should NOT have an href attributes', async () => { + render( + + +
    {'Find'}
    + +
    {'Sandy is a sweet dog'}
    +
    +
    +
    + ); + + expect(setBreadcrumbs).toHaveBeenCalledTimes(1); + expect(setBreadcrumbs).toHaveBeenCalledWith([{ href: '/', text: 'Find' }, { text: 'Sandy' }]); + }); + + it('rendering three breadcrumb and our last breadcrumb should NOT have an href attributes', async () => { + render( + + +
    {'Find'}
    + +
    {'Sandy is a sweet dog'}
    + +
    {'Sandy is a mutts'}
    +
    +
    +
    +
    + ); + + expect(setBreadcrumbs).toHaveBeenCalledTimes(1); + expect(setBreadcrumbs).toHaveBeenCalledWith([ + { href: '/', text: 'Find' }, + { href: '/sandy', text: 'Sandy' }, + { text: 'Breed' }, + ]); + }); +}); diff --git a/x-pack/plugins/security/public/components/breadcrumb.tsx b/x-pack/plugins/security/public/components/breadcrumb.tsx index 353f738501cbe..4706f60712ad5 100644 --- a/x-pack/plugins/security/public/components/breadcrumb.tsx +++ b/x-pack/plugins/security/public/components/breadcrumb.tsx @@ -80,11 +80,17 @@ export const BreadcrumbsProvider: FunctionComponent = const breadcrumbsRef = useRef([]); const handleChange = (breadcrumbs: BreadcrumbProps[]) => { + const newBreadcrumbs = breadcrumbs.map((item, index) => { + if (index === breadcrumbs.length - 1) { + return { ...item, href: undefined }; + } + return item; + }); if (onChange) { - onChange(breadcrumbs); + onChange(newBreadcrumbs); } else if (services.chrome) { const setBreadcrumbs = createBreadcrumbsChangeHandler(services.chrome); - setBreadcrumbs(breadcrumbs); + setBreadcrumbs(newBreadcrumbs); } }; diff --git a/x-pack/plugins/security/public/management/api_keys/api_keys_grid/api_keys_grid_page.test.tsx b/x-pack/plugins/security/public/management/api_keys/api_keys_grid/api_keys_grid_page.test.tsx index 72fd79805f970..6e3de061fd191 100644 --- a/x-pack/plugins/security/public/management/api_keys/api_keys_grid/api_keys_grid_page.test.tsx +++ b/x-pack/plugins/security/public/management/api_keys/api_keys_grid/api_keys_grid_page.test.tsx @@ -5,13 +5,7 @@ * 2.0. */ -import { - fireEvent, - render, - waitFor, - waitForElementToBeRemoved, - within, -} from '@testing-library/react'; +import { render } from '@testing-library/react'; import { createMemoryHistory } from 'history'; import React from 'react'; @@ -22,60 +16,75 @@ import { Providers } from '../api_keys_management_app'; import { apiKeysAPIClientMock } from '../index.mock'; import { APIKeysGridPage } from './api_keys_grid_page'; -jest.mock('@elastic/eui/lib/services/accessibility/html_id_generator', () => ({ - htmlIdGenerator: () => () => `id-${Math.random()}`, -})); +/* + * Note to engineers + * we moved these 4 tests below to "x-pack/test/functional/apps/api_keys/home_page.ts": + * 1-"creates API key when submitting form, redirects back and displays base64" + * 2-"creates API key with optional expiration, redirects back and displays base64" + * 3-"deletes multiple api keys using bulk select" + * 4-"deletes api key using cta button" + * to functional tests to avoid flakyness + */ -jest.setTimeout(15000); +describe('APIKeysGridPage', () => { + // We are spying on the console.error to avoid react to throw error + // in our test "displays error when fetching API keys fails" + // since we are using EuiErrorBoundary and react will console.error any errors + const consoleWarnMock = jest.spyOn(console, 'error').mockImplementation(); -const coreStart = coreMock.createStart(); + const coreStart = coreMock.createStart(); + const apiClientMock = apiKeysAPIClientMock.create(); + const { authc } = securityMock.createSetup(); -const apiClientMock = apiKeysAPIClientMock.create(); -apiClientMock.checkPrivileges.mockResolvedValue({ - areApiKeysEnabled: true, - canManage: true, - isAdmin: true, -}); -apiClientMock.getApiKeys.mockResolvedValue({ - apiKeys: [ - { - creation: 1571322182082, - expiration: 1571408582082, - id: '0QQZ2m0BO2XZwgJFuWTT', - invalidated: false, - name: 'first-api-key', - realm: 'reserved', - username: 'elastic', - }, - { - creation: 1571322182082, - expiration: 1571408582082, - id: 'BO2XZwgJFuWTT0QQZ2m0', - invalidated: false, - name: 'second-api-key', - realm: 'reserved', - username: 'elastic', - }, - ], -}); + beforeEach(() => { + apiClientMock.checkPrivileges.mockClear(); + apiClientMock.getApiKeys.mockClear(); + coreStart.http.get.mockClear(); + coreStart.http.post.mockClear(); + authc.getCurrentUser.mockClear(); -const authc = securityMock.createSetup().authc; -authc.getCurrentUser.mockResolvedValue( - mockAuthenticatedUser({ - username: 'jdoe', - full_name: '', - email: '', - enabled: true, - roles: ['superuser'], - }) -); + apiClientMock.checkPrivileges.mockResolvedValue({ + areApiKeysEnabled: true, + canManage: true, + isAdmin: true, + }); + apiClientMock.getApiKeys.mockResolvedValue({ + apiKeys: [ + { + creation: 1571322182082, + expiration: 1571408582082, + id: '0QQZ2m0BO2XZwgJFuWTT', + invalidated: false, + name: 'first-api-key', + realm: 'reserved', + username: 'elastic', + }, + { + creation: 1571322182082, + expiration: 1571408582082, + id: 'BO2XZwgJFuWTT0QQZ2m0', + invalidated: false, + name: 'second-api-key', + realm: 'reserved', + username: 'elastic', + }, + ], + }); -// FLAKY: https://github.com/elastic/kibana/issues/97085 -describe.skip('APIKeysGridPage', () => { + authc.getCurrentUser.mockResolvedValue( + mockAuthenticatedUser({ + username: 'jdoe', + full_name: '', + email: '', + enabled: true, + roles: ['superuser'], + }) + ); + }); it('loads and displays API keys', async () => { const history = createMemoryHistory({ initialEntries: ['/'] }); - const { getByText } = render( + const { findByText } = render( { ); - await waitForElementToBeRemoved(() => getByText(/Loading API keys/)); - getByText(/first-api-key/); - getByText(/second-api-key/); + expect(await findByText(/Loading API keys/)).not.toBeInTheDocument(); + await findByText(/first-api-key/); + await findByText(/second-api-key/); + }); + + afterAll(() => { + // Let's make sure we restore everything just in case + consoleWarnMock.mockRestore(); }); it('displays callout when API keys are disabled', async () => { @@ -98,7 +112,7 @@ describe.skip('APIKeysGridPage', () => { isAdmin: true, }); - const { getByText } = render( + const { findByText } = render( { ); - await waitForElementToBeRemoved(() => getByText(/Loading API keys/)); - getByText(/API keys not enabled/); + expect(await findByText(/Loading API keys/)).not.toBeInTheDocument(); + await findByText(/API keys not enabled/); }); it('displays error when user does not have required permissions', async () => { @@ -120,7 +134,7 @@ describe.skip('APIKeysGridPage', () => { isAdmin: false, }); - const { getByText } = render( + const { findByText } = render( { ); - await waitForElementToBeRemoved(() => getByText(/Loading API keys/)); - getByText(/You need permission to manage API keys/); + expect(await findByText(/Loading API keys/)).not.toBeInTheDocument(); + await findByText(/You need permission to manage API keys/); }); it('displays error when fetching API keys fails', async () => { apiClientMock.getApiKeys.mockRejectedValueOnce({ - body: { error: 'Internal Server Error', message: '', statusCode: 500 }, - }); - const history = createMemoryHistory({ initialEntries: ['/'] }); - - const { getByText } = render( - - - - ); - - await waitForElementToBeRemoved(() => getByText(/Loading API keys/)); - getByText(/Could not load API keys/); - }); - - it('creates API key when submitting form, redirects back and displays base64', async () => { - const history = createMemoryHistory({ initialEntries: ['/create'] }); - coreStart.http.get.mockResolvedValue([{ name: 'superuser' }]); - coreStart.http.post.mockResolvedValue({ id: '1D', api_key: 'AP1_K3Y' }); - - const { findByRole, findByDisplayValue } = render( - - - - ); - expect(coreStart.http.get).toHaveBeenCalledWith('/api/security/role'); - - const dialog = await findByRole('dialog'); - - fireEvent.click(await findByRole('button', { name: 'Create API key' })); - - const alert = await findByRole('alert'); - within(alert).getByText(/Enter a name/i); - - fireEvent.change(await within(dialog).findByLabelText('Name'), { - target: { value: 'Test' }, - }); - - fireEvent.click(await findByRole('button', { name: 'Create API key' })); - - await waitFor(() => { - expect(coreStart.http.post).toHaveBeenLastCalledWith('/internal/security/api_key', { - body: JSON.stringify({ name: 'Test' }), - }); - expect(history.location.pathname).toBe('/'); - }); - - await findByDisplayValue(btoa('1D:AP1_K3Y')); - }); - - it('creates API key with optional expiration, redirects back and displays base64', async () => { - const history = createMemoryHistory({ initialEntries: ['/create'] }); - coreStart.http.get.mockResolvedValue([{ name: 'superuser' }]); - coreStart.http.post.mockResolvedValue({ id: '1D', api_key: 'AP1_K3Y' }); - - const { findByRole, findByDisplayValue } = render( - - - - ); - expect(coreStart.http.get).toHaveBeenCalledWith('/api/security/role'); - - const dialog = await findByRole('dialog'); - - fireEvent.change(await within(dialog).findByLabelText('Name'), { - target: { value: 'Test' }, - }); - - fireEvent.click(await within(dialog).findByLabelText('Expire after time')); - - fireEvent.click(await findByRole('button', { name: 'Create API key' })); - - const alert = await findByRole('alert'); - within(alert).getByText(/Enter a valid duration or disable this option\./i); - - fireEvent.change(await within(dialog).findByLabelText('Lifetime (days)'), { - target: { value: '12' }, - }); - - fireEvent.click(await findByRole('button', { name: 'Create API key' })); - - await waitFor(() => { - expect(coreStart.http.post).toHaveBeenLastCalledWith('/internal/security/api_key', { - body: JSON.stringify({ name: 'Test', expiration: '12d' }), - }); - expect(history.location.pathname).toBe('/'); + body: { + error: 'Internal Server Error', + message: 'Internal Server Error', + statusCode: 500, + }, }); - - await findByDisplayValue(btoa('1D:AP1_K3Y')); - }); - - it('deletes api key using cta button', async () => { - const history = createMemoryHistory({ initialEntries: ['/'] }); - - const { findByRole, findAllByLabelText } = render( - - - - ); - - const [deleteButton] = await findAllByLabelText(/Delete/i); - fireEvent.click(deleteButton); - - const dialog = await findByRole('dialog'); - fireEvent.click(await within(dialog).findByRole('button', { name: 'Delete API key' })); - - await waitFor(() => { - expect(apiClientMock.invalidateApiKeys).toHaveBeenLastCalledWith( - [{ id: '0QQZ2m0BO2XZwgJFuWTT', name: 'first-api-key' }], - true - ); - }); - }); - - it('deletes multiple api keys using bulk select', async () => { const history = createMemoryHistory({ initialEntries: ['/'] }); - const { findByRole, findAllByRole } = render( + const { findByText } = render( { ); - const deleteCheckboxes = await findAllByRole('checkbox', { name: 'Select this row' }); - deleteCheckboxes.forEach((checkbox) => fireEvent.click(checkbox)); - fireEvent.click(await findByRole('button', { name: 'Delete API keys' })); - - const dialog = await findByRole('dialog'); - fireEvent.click(await within(dialog).findByRole('button', { name: 'Delete API keys' })); - - await waitFor(() => { - expect(apiClientMock.invalidateApiKeys).toHaveBeenLastCalledWith( - [ - { id: '0QQZ2m0BO2XZwgJFuWTT', name: 'first-api-key' }, - { id: 'BO2XZwgJFuWTT0QQZ2m0', name: 'second-api-key' }, - ], - true - ); - }); + expect(await findByText(/Loading API keys/)).not.toBeInTheDocument(); + await findByText(/Could not load API keys/); }); }); diff --git a/x-pack/plugins/security/public/management/api_keys/api_keys_grid/api_keys_grid_page.tsx b/x-pack/plugins/security/public/management/api_keys/api_keys_grid/api_keys_grid_page.tsx index dcf2a7bfe5165..a4843e4637d8b 100644 --- a/x-pack/plugins/security/public/management/api_keys/api_keys_grid/api_keys_grid_page.tsx +++ b/x-pack/plugins/security/public/management/api_keys/api_keys_grid/api_keys_grid_page.tsx @@ -164,6 +164,7 @@ export class APIKeysGridPage extends Component { {...reactRouterNavigate(this.props.history, '/create')} fill iconType="plusInCircleFilled" + data-test-subj="apiKeysCreatePromptButton" > { {...reactRouterNavigate(this.props.history, '/create')} fill iconType="plusInCircleFilled" + data-test-subj="apiKeysCreateTableButton" > { color: 'danger', onClick: (item) => invalidateApiKeyPrompt([{ id: item.id, name: item.name }], this.onApiKeysInvalidated), + 'data-test-subj': 'apiKeysTableDeleteAction', }, ], }, diff --git a/x-pack/plugins/security/public/management/api_keys/api_keys_grid/create_api_key_flyout.tsx b/x-pack/plugins/security/public/management/api_keys/api_keys_grid/create_api_key_flyout.tsx index e1ffc3b4b3515..f2fa6f7de468e 100644 --- a/x-pack/plugins/security/public/management/api_keys/api_keys_grid/create_api_key_flyout.tsx +++ b/x-pack/plugins/security/public/management/api_keys/api_keys_grid/create_api_key_flyout.tsx @@ -202,6 +202,7 @@ export const CreateApiKeyFlyout: FunctionComponent = ({ isInvalid={form.touched.name && !!form.errors.name} inputRef={firstFieldRef} fullWidth + data-test-subj="apiKeyNameInput" /> @@ -258,6 +259,7 @@ export const CreateApiKeyFlyout: FunctionComponent = ({ )} checked={!!form.values.customExpiration} onChange={(e) => form.setValue('customExpiration', e.target.checked)} + data-test-subj="apiKeyCustomExpirationSwitch" /> {form.values.customExpiration && ( <> @@ -284,6 +286,7 @@ export const CreateApiKeyFlyout: FunctionComponent = ({ defaultValue={form.values.expiration} isInvalid={form.touched.expiration && !!form.errors.expiration} fullWidth + data-test-subj="apiKeyCustomExpirationInput" /> diff --git a/x-pack/plugins/security/public/management/api_keys/api_keys_management_app.test.tsx b/x-pack/plugins/security/public/management/api_keys/api_keys_management_app.test.tsx index d2611864e77a2..922fd59c56d1b 100644 --- a/x-pack/plugins/security/public/management/api_keys/api_keys_management_app.test.tsx +++ b/x-pack/plugins/security/public/management/api_keys/api_keys_management_app.test.tsx @@ -56,7 +56,7 @@ describe('apiKeysManagementApp', () => { }); expect(setBreadcrumbs).toHaveBeenCalledTimes(1); - expect(setBreadcrumbs).toHaveBeenCalledWith([{ href: '/', text: 'API keys' }]); + expect(setBreadcrumbs).toHaveBeenCalledWith([{ text: 'API keys' }]); expect(docTitle.change).toHaveBeenCalledWith(['API keys']); expect(docTitle.reset).not.toHaveBeenCalled(); expect(container).toMatchInlineSnapshot(` diff --git a/x-pack/plugins/security/public/management/role_mappings/role_mappings_management_app.test.tsx b/x-pack/plugins/security/public/management/role_mappings/role_mappings_management_app.test.tsx index e73abc3b1eeaf..f6d17327b7118 100644 --- a/x-pack/plugins/security/public/management/role_mappings/role_mappings_management_app.test.tsx +++ b/x-pack/plugins/security/public/management/role_mappings/role_mappings_management_app.test.tsx @@ -5,6 +5,14 @@ * 2.0. */ +import { act } from '@testing-library/react'; +import { noop } from 'lodash'; + +import { coreMock, scopedHistoryMock } from 'src/core/public/mocks'; +import type { Unmount } from 'src/plugins/management/public/types'; + +import { roleMappingsManagementApp } from './role_mappings_management_app'; + jest.mock('./role_mappings_grid', () => ({ RoleMappingsGridPage: (props: any) => // `docLinks` object is too big to include into test snapshot, so we just check its existence. @@ -23,24 +31,23 @@ jest.mock('./edit_role_mapping', () => ({ })}`, })); -import { coreMock, scopedHistoryMock } from 'src/core/public/mocks'; - -import { roleMappingsManagementApp } from './role_mappings_management_app'; - async function mountApp(basePath: string, pathname: string) { const container = document.createElement('div'); const setBreadcrumbs = jest.fn(); const startServices = await coreMock.createSetup().getStartServices(); - const unmount = await roleMappingsManagementApp - .create({ getStartServices: () => Promise.resolve(startServices) as any }) - .mount({ - basePath, - element: container, - setBreadcrumbs, - history: scopedHistoryMock.create({ pathname }), - }); + let unmount: Unmount = noop; + await act(async () => { + unmount = await roleMappingsManagementApp + .create({ getStartServices: () => Promise.resolve(startServices) as any }) + .mount({ + basePath, + element: container, + setBreadcrumbs, + history: scopedHistoryMock.create({ pathname }), + }); + }); return { unmount, container, setBreadcrumbs, docTitle: startServices[0].chrome.docTitle }; } @@ -65,7 +72,7 @@ describe('roleMappingsManagementApp', () => { const { setBreadcrumbs, container, unmount, docTitle } = await mountApp('/', '/'); expect(setBreadcrumbs).toHaveBeenCalledTimes(1); - expect(setBreadcrumbs).toHaveBeenCalledWith([{ href: `/`, text: 'Role Mappings' }]); + expect(setBreadcrumbs).toHaveBeenCalledWith([{ text: 'Role Mappings' }]); expect(docTitle.change).toHaveBeenCalledWith('Role Mappings'); expect(docTitle.reset).not.toHaveBeenCalled(); expect(container).toMatchInlineSnapshot(` @@ -114,8 +121,8 @@ describe('roleMappingsManagementApp', () => { expect(setBreadcrumbs).toHaveBeenCalledTimes(1); expect(setBreadcrumbs).toHaveBeenCalledWith([ - { href: `/`, text: 'Role Mappings' }, - { href: `/edit/${encodeURIComponent(roleMappingName)}`, text: roleMappingName }, + { href: '/', text: 'Role Mappings' }, + { text: roleMappingName }, ]); expect(docTitle.change).toHaveBeenCalledWith('Role Mappings'); expect(docTitle.reset).not.toHaveBeenCalled(); @@ -139,9 +146,8 @@ describe('roleMappingsManagementApp', () => { expect(setBreadcrumbs).toHaveBeenCalledTimes(1); expect(setBreadcrumbs).toHaveBeenCalledWith([ - { href: `/`, text: 'Role Mappings' }, + { href: '/', text: 'Role Mappings' }, { - href: '/edit/some%20%E5%AE%89%E5%85%A8%E6%80%A7%20role%20mapping', text: roleMappingName, }, ]); diff --git a/x-pack/plugins/security/public/management/role_mappings/role_mappings_management_app.tsx b/x-pack/plugins/security/public/management/role_mappings/role_mappings_management_app.tsx index 4dfc9b43642bf..22d09e9e2a678 100644 --- a/x-pack/plugins/security/public/management/role_mappings/role_mappings_management_app.tsx +++ b/x-pack/plugins/security/public/management/role_mappings/role_mappings_management_app.tsx @@ -7,13 +7,18 @@ import React from 'react'; import { render, unmountComponentAtNode } from 'react-dom'; -import { Route, Router, Switch, useParams } from 'react-router-dom'; +import { Route, Router, useParams } from 'react-router-dom'; import { i18n } from '@kbn/i18n'; import type { StartServicesAccessor } from 'src/core/public'; import type { RegisterManagementAppArgs } from 'src/plugins/management/public'; import { KibanaContextProvider } from '../../../../../../src/plugins/kibana_react/public'; +import { + Breadcrumb, + BreadcrumbsProvider, + createBreadcrumbsChangeHandler, +} from '../../components/breadcrumb'; import type { PluginStartDependencies } from '../../plugin'; import { tryDecodeURIComponent } from '../url_utils'; @@ -27,21 +32,12 @@ export const roleMappingsManagementApp = Object.freeze({ const title = i18n.translate('xpack.security.management.roleMappingsTitle', { defaultMessage: 'Role Mappings', }); + return { id: this.id, order: 40, title, async mount({ element, setBreadcrumbs, history }) { - const [coreStart] = await getStartServices(); - const roleMappingsBreadcrumbs = [ - { - text: title, - href: `/`, - }, - ]; - - coreStart.chrome.docTitle.change(title); - const [ [core], { RoleMappingsGridPage }, @@ -56,20 +52,9 @@ export const roleMappingsManagementApp = Object.freeze({ import('../roles'), ]); + core.chrome.docTitle.change(title); + const roleMappingsAPIClient = new RoleMappingsAPIClient(core.http); - const RoleMappingsGridPageWithBreadcrumbs = () => { - setBreadcrumbs(roleMappingsBreadcrumbs); - return ( - - ); - }; const EditRoleMappingsPageWithBreadcrumbs = () => { const { name } = useParams<{ name?: string }>(); @@ -78,26 +63,26 @@ export const roleMappingsManagementApp = Object.freeze({ // See https://github.com/elastic/kibana/issues/82440 const decodedName = name ? tryDecodeURIComponent(name) : undefined; - setBreadcrumbs([ - ...roleMappingsBreadcrumbs, - name + const breadcrumbObj = + name && decodedName ? { text: decodedName, href: `/edit/${encodeURIComponent(name)}` } : { text: i18n.translate('xpack.security.roleMappings.createBreadcrumb', { defaultMessage: 'Create', }), - }, - ]); + }; return ( - + + + ); }; @@ -105,14 +90,25 @@ export const roleMappingsManagementApp = Object.freeze({ - - - - - - - - + + + + + + + + + + , @@ -120,7 +116,6 @@ export const roleMappingsManagementApp = Object.freeze({ ); return () => { - coreStart.chrome.docTitle.reset(); unmountComponentAtNode(element); }; }, diff --git a/x-pack/plugins/security/public/management/roles/edit_role/spaces_popover_list/spaces_popover_list.test.tsx b/x-pack/plugins/security/public/management/roles/edit_role/spaces_popover_list/spaces_popover_list.test.tsx index fb21fac3006b8..031df26eb38f7 100644 --- a/x-pack/plugins/security/public/management/roles/edit_role/spaces_popover_list/spaces_popover_list.test.tsx +++ b/x-pack/plugins/security/public/management/roles/edit_role/spaces_popover_list/spaces_popover_list.test.tsx @@ -46,8 +46,7 @@ const spacesManager = spacesManagerMock.create(); const { getStartServices } = coreMock.createSetup(); const spacesApiUi = getUiApi({ spacesManager, getStartServices }); -// FLAKY: https://github.com/elastic/kibana/issues/101454 -describe.skip('SpacesPopoverList', () => { +describe('SpacesPopoverList', () => { async function setup(spaces: Space[]) { const wrapper = mountWithIntl( @@ -84,41 +83,16 @@ describe.skip('SpacesPopoverList', () => { const spaceAvatar = items.at(index).find(SpaceAvatarInternal); expect(spaceAvatar.props().space).toEqual(space); }); - - expect(wrapper.find(EuiFieldSearch)).toHaveLength(0); }); - it('renders a search box when there are 8 or more spaces', async () => { - const lotsOfSpaces = [1, 2, 3, 4, 5, 6, 7, 8].map((num) => ({ - id: `space-${num}`, - name: `Space ${num}`, - disabledFeatures: [], - })); - - const wrapper = await setup(lotsOfSpaces); + it('Should NOT render a search box when there is less than 8 spaces', async () => { + const wrapper = await setup(mockSpaces); await act(async () => { wrapper.find(EuiButtonEmpty).simulate('click'); }); wrapper.update(); - const menu = wrapper.find(EuiContextMenuPanel).first(); - const items = menu.find(EuiContextMenuItem); - expect(items).toHaveLength(lotsOfSpaces.length); - - const searchField = wrapper.find(EuiFieldSearch); - expect(searchField).toHaveLength(1); - - searchField.props().onSearch!('Space 6'); - await act(async () => {}); - wrapper.update(); - expect(wrapper.find(SpaceAvatarInternal)).toHaveLength(1); - - searchField.props().onSearch!('this does not match'); - wrapper.update(); - expect(wrapper.find(SpaceAvatarInternal)).toHaveLength(0); - - const updatedMenu = wrapper.find(EuiContextMenuPanel).first(); - expect(updatedMenu.text()).toMatchInlineSnapshot(`"Spaces no spaces found "`); + expect(wrapper.find(EuiFieldSearch)).toHaveLength(0); }); it('can close its popover', async () => { diff --git a/x-pack/plugins/security/public/management/roles/roles_management_app.test.tsx b/x-pack/plugins/security/public/management/roles/roles_management_app.test.tsx index 4b10c2d5cf13b..faab47a858d67 100644 --- a/x-pack/plugins/security/public/management/roles/roles_management_app.test.tsx +++ b/x-pack/plugins/security/public/management/roles/roles_management_app.test.tsx @@ -5,7 +5,11 @@ * 2.0. */ +import { act } from '@testing-library/react'; +import { noop } from 'lodash'; + import { coreMock, scopedHistoryMock } from 'src/core/public/mocks'; +import type { Unmount } from 'src/plugins/management/public/types'; import { featuresPluginMock } from '../../../../features/public/mocks'; import { licenseMock } from '../../../common/licensing/index.mock'; @@ -29,20 +33,23 @@ async function mountApp(basePath: string, pathname: string) { const featuresStart = featuresPluginMock.createStart(); const coreStart = coreMock.createStart(); - const unmount = await rolesManagementApp - .create({ - license: licenseMock.create(), - fatalErrors, - getStartServices: jest - .fn() - .mockResolvedValue([coreStart, { data: {}, features: featuresStart }]), - }) - .mount({ - basePath, - element: container, - setBreadcrumbs, - history: scopedHistoryMock.create({ pathname }), - }); + let unmount: Unmount = noop; + await act(async () => { + unmount = await rolesManagementApp + .create({ + license: licenseMock.create(), + fatalErrors, + getStartServices: jest + .fn() + .mockResolvedValue([coreStart, { data: {}, features: featuresStart }]), + }) + .mount({ + basePath, + element: container, + setBreadcrumbs, + history: scopedHistoryMock.create({ pathname }), + }); + }); return { unmount, container, setBreadcrumbs, docTitle: coreStart.chrome.docTitle }; } @@ -71,7 +78,7 @@ describe('rolesManagementApp', () => { const { setBreadcrumbs, container, unmount, docTitle } = await mountApp('/', '/'); expect(setBreadcrumbs).toHaveBeenCalledTimes(1); - expect(setBreadcrumbs).toHaveBeenCalledWith([{ href: `/`, text: 'Roles' }]); + expect(setBreadcrumbs).toHaveBeenCalledWith([{ text: 'Roles' }]); expect(docTitle.change).toHaveBeenCalledWith('Roles'); expect(docTitle.reset).not.toHaveBeenCalled(); expect(container).toMatchInlineSnapshot(` @@ -116,10 +123,7 @@ describe('rolesManagementApp', () => { ); expect(setBreadcrumbs).toHaveBeenCalledTimes(1); - expect(setBreadcrumbs).toHaveBeenCalledWith([ - { href: `/`, text: 'Roles' }, - { href: `/edit/${encodeURIComponent(roleName)}`, text: roleName }, - ]); + expect(setBreadcrumbs).toHaveBeenCalledWith([{ href: `/`, text: 'Roles' }, { text: roleName }]); expect(docTitle.change).toHaveBeenCalledWith('Roles'); expect(docTitle.reset).not.toHaveBeenCalled(); expect(container).toMatchInlineSnapshot(` @@ -169,7 +173,6 @@ describe('rolesManagementApp', () => { expect(setBreadcrumbs).toHaveBeenCalledWith([ { href: `/`, text: 'Roles' }, { - href: '/edit/some%20%E5%AE%89%E5%85%A8%E6%80%A7%20role', text: roleName, }, ]); diff --git a/x-pack/plugins/security/public/management/roles/roles_management_app.tsx b/x-pack/plugins/security/public/management/roles/roles_management_app.tsx index 8936f506066e0..fcd037a861ed0 100644 --- a/x-pack/plugins/security/public/management/roles/roles_management_app.tsx +++ b/x-pack/plugins/security/public/management/roles/roles_management_app.tsx @@ -7,7 +7,7 @@ import React from 'react'; import { render, unmountComponentAtNode } from 'react-dom'; -import { Route, Router, Switch, useParams } from 'react-router-dom'; +import { Route, Router, useParams } from 'react-router-dom'; import { i18n } from '@kbn/i18n'; import type { FatalErrorsSetup, StartServicesAccessor } from 'src/core/public'; @@ -15,6 +15,11 @@ import type { RegisterManagementAppArgs } from 'src/plugins/management/public'; import { KibanaContextProvider } from '../../../../../../src/plugins/kibana_react/public'; import type { SecurityLicense } from '../../../common/licensing'; +import { + Breadcrumb, + BreadcrumbsProvider, + createBreadcrumbsChangeHandler, +} from '../../components/breadcrumb'; import type { PluginStartDependencies } from '../../plugin'; import { tryDecodeURIComponent } from '../url_utils'; @@ -35,13 +40,6 @@ export const rolesManagementApp = Object.freeze({ order: 20, title, async mount({ element, setBreadcrumbs, history }) { - const rolesBreadcrumbs = [ - { - text: title, - href: `/`, - }, - ]; - const [ [startServices, { data, features, spaces }], { RolesGridPage }, @@ -72,16 +70,6 @@ export const rolesManagementApp = Object.freeze({ chrome.docTitle.change(title); const rolesAPIClient = new RolesAPIClient(http); - const RolesGridPageWithBreadcrumbs = () => { - setBreadcrumbs(rolesBreadcrumbs); - return ( - - ); - }; const EditRolePageWithBreadcrumbs = ({ action }: { action: 'edit' | 'clone' }) => { const { roleName } = useParams<{ roleName?: string }>(); @@ -90,38 +78,38 @@ export const rolesManagementApp = Object.freeze({ // See https://github.com/elastic/kibana/issues/82440 const decodedRoleName = roleName ? tryDecodeURIComponent(roleName) : undefined; - setBreadcrumbs([ - ...rolesBreadcrumbs, - action === 'edit' && roleName + const breadcrumbObj = + action === 'edit' && roleName && decodedRoleName ? { text: decodedRoleName, href: `/edit/${encodeURIComponent(roleName)}` } : { text: i18n.translate('xpack.security.roles.createBreadcrumb', { defaultMessage: 'Create', }), - }, - ]); + }; const spacesApiUi = spaces?.ui; return ( - + + + ); }; @@ -129,26 +117,32 @@ export const rolesManagementApp = Object.freeze({ - - - - - - - - - - - + + + + + + + + + + + + + , - element ); return () => { - chrome.docTitle.reset(); unmountComponentAtNode(element); }; }, diff --git a/x-pack/plugins/security/public/management/users/edit_user/change_password_flyout.test.tsx b/x-pack/plugins/security/public/management/users/edit_user/change_password_flyout.test.tsx new file mode 100644 index 0000000000000..b0b8ca2030fa0 --- /dev/null +++ b/x-pack/plugins/security/public/management/users/edit_user/change_password_flyout.test.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. + */ + +import type { ChangePasswordFormValues } from './change_password_flyout'; +import { validateChangePasswordForm } from './change_password_flyout'; + +describe('ChangePasswordFlyout', () => { + describe('#validateChangePasswordForm', () => { + describe('for current user', () => { + it('should show an error when it is current user with no current password', () => { + expect( + validateChangePasswordForm({ password: 'changeme', confirm_password: 'changeme' }, true) + ).toMatchInlineSnapshot(` + Object { + "current_password": "Enter your current password.", + } + `); + }); + + it('should show errors when there is no new password', () => { + expect( + validateChangePasswordForm( + { + password: undefined, + confirm_password: 'changeme', + } as unknown as ChangePasswordFormValues, + true + ) + ).toMatchInlineSnapshot(` + Object { + "current_password": "Enter your current password.", + "password": "Enter a new password.", + } + `); + }); + + it('should show errors when the new password is not at least 6 characters', () => { + expect(validateChangePasswordForm({ password: '12345', confirm_password: '12345' }, true)) + .toMatchInlineSnapshot(` + Object { + "current_password": "Enter your current password.", + "password": "Password must be at least 6 characters.", + } + `); + }); + + it('should show errors when new password does not match confirmation password', () => { + expect( + validateChangePasswordForm( + { + password: 'changeme', + confirm_password: 'notTheSame', + }, + true + ) + ).toMatchInlineSnapshot(` + Object { + "confirm_password": "Passwords do not match.", + "current_password": "Enter your current password.", + } + `); + }); + + it('should show NO errors', () => { + expect( + validateChangePasswordForm( + { + current_password: 'oldpassword', + password: 'changeme', + confirm_password: 'changeme', + }, + true + ) + ).toMatchInlineSnapshot(`Object {}`); + }); + }); + + describe('for another user', () => { + it('should show errors when there is no new password', () => { + expect( + validateChangePasswordForm( + { + password: undefined, + confirm_password: 'changeme', + } as unknown as ChangePasswordFormValues, + false + ) + ).toMatchInlineSnapshot(` + Object { + "password": "Enter a new password.", + } + `); + }); + + it('should show errors when the new password is not at least 6 characters', () => { + expect(validateChangePasswordForm({ password: '1234', confirm_password: '1234' }, false)) + .toMatchInlineSnapshot(` + Object { + "password": "Password must be at least 6 characters.", + } + `); + }); + + it('should show errors when new password does not match confirmation password', () => { + expect( + validateChangePasswordForm( + { + password: 'changeme', + confirm_password: 'notTheSame', + }, + false + ) + ).toMatchInlineSnapshot(` + Object { + "confirm_password": "Passwords do not match.", + } + `); + }); + + it('should show NO errors', () => { + expect( + validateChangePasswordForm( + { + password: 'changeme', + confirm_password: 'changeme', + }, + false + ) + ).toMatchInlineSnapshot(`Object {}`); + }); + }); + }); +}); diff --git a/x-pack/plugins/security/public/management/users/edit_user/change_password_flyout.tsx b/x-pack/plugins/security/public/management/users/edit_user/change_password_flyout.tsx index 445d424adb388..29282696ffdf1 100644 --- a/x-pack/plugins/security/public/management/users/edit_user/change_password_flyout.tsx +++ b/x-pack/plugins/security/public/management/users/edit_user/change_password_flyout.tsx @@ -44,6 +44,49 @@ export interface ChangePasswordFlyoutProps { onSuccess?(): void; } +export const validateChangePasswordForm = ( + values: ChangePasswordFormValues, + isCurrentUser: boolean +) => { + const errors: ValidationErrors = {}; + + if (isCurrentUser) { + if (!values.current_password) { + errors.current_password = i18n.translate( + 'xpack.security.management.users.changePasswordFlyout.currentPasswordRequiredError', + { + defaultMessage: 'Enter your current password.', + } + ); + } + } + + if (!values.password) { + errors.password = i18n.translate( + 'xpack.security.management.users.changePasswordFlyout.passwordRequiredError', + { + defaultMessage: 'Enter a new password.', + } + ); + } else if (values.password.length < 6) { + errors.password = i18n.translate( + 'xpack.security.management.users.changePasswordFlyout.passwordInvalidError', + { + defaultMessage: 'Password must be at least 6 characters.', + } + ); + } else if (values.password !== values.confirm_password) { + errors.confirm_password = i18n.translate( + 'xpack.security.management.users.changePasswordFlyout.confirmPasswordInvalidError', + { + defaultMessage: 'Passwords do not match.', + } + ); + } + + return errors; +}; + export const ChangePasswordFlyout: FunctionComponent = ({ username, defaultValues = { @@ -99,52 +142,7 @@ export const ChangePasswordFlyout: FunctionComponent } } }, - validate: async (values) => { - const errors: ValidationErrors = {}; - - if (isCurrentUser) { - if (!values.current_password) { - errors.current_password = i18n.translate( - 'xpack.security.management.users.changePasswordFlyout.currentPasswordRequiredError', - { - defaultMessage: 'Enter your current password.', - } - ); - } - } - - if (!values.password) { - errors.password = i18n.translate( - 'xpack.security.management.users.changePasswordFlyout.passwordRequiredError', - { - defaultMessage: 'Enter a new password.', - } - ); - } else if (values.password.length < 6) { - errors.password = i18n.translate( - 'xpack.security.management.users.changePasswordFlyout.passwordInvalidError', - { - defaultMessage: 'Password must be at least 6 characters.', - } - ); - } else if (!values.confirm_password) { - errors.confirm_password = i18n.translate( - 'xpack.security.management.users.changePasswordFlyout.confirmPasswordRequiredError', - { - defaultMessage: 'Passwords do not match.', - } - ); - } else if (values.password !== values.confirm_password) { - errors.confirm_password = i18n.translate( - 'xpack.security.management.users.changePasswordFlyout.confirmPasswordInvalidError', - { - defaultMessage: 'Passwords do not match.', - } - ); - } - - return errors; - }, + validate: async (values) => validateChangePasswordForm(values, isCurrentUser), defaultValues, }); @@ -246,6 +244,7 @@ export const ChangePasswordFlyout: FunctionComponent isInvalid={form.touched.current_password && !!form.errors.current_password} autoComplete="current-password" inputRef={firstFieldRef} + data-test-subj="editUserChangePasswordCurrentPasswordInput" /> ) : null} @@ -268,6 +267,7 @@ export const ChangePasswordFlyout: FunctionComponent isInvalid={form.touched.password && !!form.errors.password} autoComplete="new-password" inputRef={isCurrentUser ? undefined : firstFieldRef} + data-test-subj="editUserChangePasswordNewPasswordInput" /> defaultValue={form.values.confirm_password} isInvalid={form.touched.confirm_password && !!form.errors.confirm_password} autoComplete="new-password" + data-test-subj="editUserChangePasswordConfirmPasswordInput" /> diff --git a/x-pack/plugins/security/public/management/users/edit_user/edit_user_page.test.tsx b/x-pack/plugins/security/public/management/users/edit_user/edit_user_page.test.tsx index b8e98042b1cff..66a73d9c6aa87 100644 --- a/x-pack/plugins/security/public/management/users/edit_user/edit_user_page.test.tsx +++ b/x-pack/plugins/security/public/management/users/edit_user/edit_user_page.test.tsx @@ -5,21 +5,16 @@ * 2.0. */ -import { fireEvent, render, waitFor, within } from '@testing-library/react'; +import { fireEvent, render } from '@testing-library/react'; import { createMemoryHistory } from 'history'; import React from 'react'; import { coreMock } from 'src/core/public/mocks'; -import { mockAuthenticatedUser } from '../../../../common/model/authenticated_user.mock'; import { securityMock } from '../../../mocks'; import { Providers } from '../users_management_app'; import { EditUserPage } from './edit_user_page'; -jest.mock('@elastic/eui/lib/services/accessibility/html_id_generator', () => ({ - htmlIdGenerator: () => () => `id-${Math.random()}`, -})); - const userMock = { username: 'jdoe', full_name: '', @@ -28,13 +23,7 @@ const userMock = { roles: ['superuser'], }; -// FLAKY: https://github.com/elastic/kibana/issues/115473 -// FLAKY: https://github.com/elastic/kibana/issues/115474 -// FLAKY: https://github.com/elastic/kibana/issues/116889 -// FLAKY: https://github.com/elastic/kibana/issues/117081 -// FLAKY: https://github.com/elastic/kibana/issues/116891 -// FLAKY: https://github.com/elastic/kibana/issues/116890 -describe.skip('EditUserPage', () => { +describe('EditUserPage', () => { const coreStart = coreMock.createStart(); let history = createMemoryHistory({ initialEntries: ['/edit/jdoe'] }); const authc = securityMock.createSetup().authc; @@ -135,263 +124,4 @@ describe.skip('EditUserPage', () => { await findByText(/Role .deprecated_role. is deprecated. Use .new_role. instead/i); }); - - it('updates user when submitting form and redirects back', async () => { - coreStart.http.get.mockResolvedValueOnce(userMock); - coreStart.http.get.mockResolvedValueOnce([]); - coreStart.http.post.mockResolvedValueOnce({}); - - const { findByRole, findByLabelText } = render( - - - - ); - - fireEvent.change(await findByLabelText('Full name'), { target: { value: 'John Doe' } }); - fireEvent.change(await findByLabelText('Email address'), { - target: { value: 'jdoe@elastic.co' }, - }); - fireEvent.click(await findByRole('button', { name: 'Update user' })); - - await waitFor(() => { - expect(coreStart.http.get).toHaveBeenCalledWith('/internal/security/users/jdoe'); - expect(coreStart.http.get).toHaveBeenCalledWith('/api/security/role'); - expect(coreStart.http.post).toHaveBeenLastCalledWith('/internal/security/users/jdoe', { - body: JSON.stringify({ - ...userMock, - full_name: 'John Doe', - email: 'jdoe@elastic.co', - }), - }); - expect(history.location.pathname).toBe('/'); - }); - }); - - it('warns when user form submission fails', async () => { - coreStart.http.get.mockResolvedValueOnce(userMock); - coreStart.http.get.mockResolvedValueOnce([]); - coreStart.http.post.mockRejectedValueOnce(new Error('Error message')); - - const { findByRole, findByLabelText } = render( - - - - ); - - fireEvent.change(await findByLabelText('Full name'), { target: { value: 'John Doe' } }); - fireEvent.change(await findByLabelText('Email address'), { - target: { value: 'jdoe@elastic.co' }, - }); - fireEvent.click(await findByRole('button', { name: 'Update user' })); - - await waitFor(() => { - expect(coreStart.http.get).toHaveBeenCalledWith('/internal/security/users/jdoe'); - expect(coreStart.http.get).toHaveBeenCalledWith('/api/security/role'); - expect(coreStart.http.post).toHaveBeenLastCalledWith('/internal/security/users/jdoe', { - body: JSON.stringify({ - ...userMock, - full_name: 'John Doe', - email: 'jdoe@elastic.co', - }), - }); - expect(coreStart.notifications.toasts.addDanger).toHaveBeenCalledWith({ - text: 'Error message', - title: "Could not update user 'jdoe'", - }); - expect(history.location.pathname).toBe('/edit/jdoe'); - }); - }); - - it('changes password of other user when submitting form and closes dialog', async () => { - coreStart.http.get.mockResolvedValueOnce(userMock); - coreStart.http.get.mockResolvedValueOnce([]); - authc.getCurrentUser.mockResolvedValueOnce( - mockAuthenticatedUser({ ...userMock, username: 'elastic' }) - ); - coreStart.http.post.mockResolvedValueOnce({}); - - const { findByRole } = render( - - - - ); - - fireEvent.click(await findByRole('button', { name: 'Change password' })); - const dialog = await findByRole('dialog'); - fireEvent.change(await within(dialog).findByLabelText('New password'), { - target: { value: 'changeme' }, - }); - fireEvent.change(await within(dialog).findByLabelText('Confirm password'), { - target: { value: 'changeme' }, - }); - fireEvent.click(await within(dialog).findByRole('button', { name: 'Change password' })); - - expect(await findByRole('dialog')).not.toBeInTheDocument(); - expect(coreStart.http.post).toHaveBeenLastCalledWith('/internal/security/users/jdoe/password', { - body: JSON.stringify({ - newPassword: 'changeme', - }), - }); - }); - - it('changes password of current user when submitting form and closes dialog', async () => { - coreStart.http.get.mockResolvedValueOnce(userMock); - coreStart.http.get.mockResolvedValueOnce([]); - authc.getCurrentUser.mockResolvedValueOnce(mockAuthenticatedUser(userMock)); - coreStart.http.post.mockResolvedValueOnce({}); - - const { findByRole } = render( - - - - ); - - fireEvent.click(await findByRole('button', { name: 'Change password' })); - const dialog = await findByRole('dialog'); - fireEvent.change(await within(dialog).findByLabelText('Current password'), { - target: { value: '123456' }, - }); - fireEvent.change(await within(dialog).findByLabelText('New password'), { - target: { value: 'changeme' }, - }); - fireEvent.change(await within(dialog).findByLabelText('Confirm password'), { - target: { value: 'changeme' }, - }); - fireEvent.click(await within(dialog).findByRole('button', { name: 'Change password' })); - - expect(await findByRole('dialog')).not.toBeInTheDocument(); - expect(coreStart.http.post).toHaveBeenLastCalledWith('/internal/security/users/jdoe/password', { - body: JSON.stringify({ - newPassword: 'changeme', - password: '123456', - }), - }); - }); - - it('warns when change password form submission fails', async () => { - coreStart.http.get.mockResolvedValueOnce(userMock); - coreStart.http.get.mockResolvedValueOnce([]); - authc.getCurrentUser.mockResolvedValueOnce( - mockAuthenticatedUser({ ...userMock, username: 'elastic' }) - ); - coreStart.http.post.mockRejectedValueOnce(new Error('Error message')); - - const { findByRole } = render( - - - - ); - - fireEvent.click(await findByRole('button', { name: 'Change password' })); - const dialog = await findByRole('dialog'); - fireEvent.change(await within(dialog).findByLabelText('New password'), { - target: { value: 'changeme' }, - }); - fireEvent.change(await within(dialog).findByLabelText('Confirm password'), { - target: { value: 'changeme' }, - }); - fireEvent.click(await within(dialog).findByRole('button', { name: 'Change password' })); - - await waitFor(() => { - expect(coreStart.notifications.toasts.addDanger).toHaveBeenCalledWith({ - text: 'Error message', - title: 'Could not change password', - }); - }); - }); - - it('validates change password form', async () => { - coreStart.http.get.mockResolvedValueOnce(userMock); - coreStart.http.get.mockResolvedValueOnce([]); - authc.getCurrentUser.mockResolvedValueOnce(mockAuthenticatedUser(userMock)); - coreStart.http.post.mockResolvedValueOnce({}); - - const { findByRole } = render( - - - - ); - - fireEvent.click(await findByRole('button', { name: 'Change password' })); - const dialog = await findByRole('dialog'); - fireEvent.click(await within(dialog).findByRole('button', { name: 'Change password' })); - await within(dialog).findByText(/Enter your current password/i); - await within(dialog).findByText(/Enter a new password/i); - - fireEvent.change(await within(dialog).findByLabelText('Current password'), { - target: { value: 'changeme' }, - }); - fireEvent.change(await within(dialog).findByLabelText('New password'), { - target: { value: '111' }, - }); - await within(dialog).findAllByText(/Password must be at least 6 characters/i); - - fireEvent.change(await within(dialog).findByLabelText('New password'), { - target: { value: '123456' }, - }); - fireEvent.change(await within(dialog).findByLabelText('Confirm password'), { - target: { value: '111' }, - }); - await within(dialog).findAllByText(/Passwords do not match/i); - }); - - it('deactivates user when confirming and closes dialog', async () => { - coreStart.http.get.mockResolvedValueOnce(userMock); - coreStart.http.get.mockResolvedValueOnce([]); - coreStart.http.post.mockResolvedValueOnce({}); - - const { findByRole } = render( - - - - ); - - fireEvent.click(await findByRole('button', { name: 'Deactivate user' })); - const dialog = await findByRole('dialog'); - fireEvent.click(await within(dialog).findByRole('button', { name: 'Deactivate user' })); - - expect(await findByRole('dialog')).not.toBeInTheDocument(); - expect(coreStart.http.post).toHaveBeenLastCalledWith('/internal/security/users/jdoe/_disable'); - }); - - it('activates user when confirming and closes dialog', async () => { - coreStart.http.get.mockResolvedValueOnce({ ...userMock, enabled: false }); - coreStart.http.get.mockResolvedValueOnce([]); - coreStart.http.post.mockResolvedValueOnce({}); - - const { findByRole, findAllByRole } = render( - - - - ); - - const [enableButton] = await findAllByRole('button', { name: 'Activate user' }); - fireEvent.click(enableButton); - const dialog = await findByRole('dialog'); - fireEvent.click(await within(dialog).findByRole('button', { name: 'Activate user' })); - - expect(await findByRole('dialog')).not.toBeInTheDocument(); - expect(coreStart.http.post).toHaveBeenLastCalledWith('/internal/security/users/jdoe/_enable'); - }); - - it('deletes user when confirming and redirects back', async () => { - coreStart.http.get.mockResolvedValueOnce(userMock); - coreStart.http.get.mockResolvedValueOnce([]); - coreStart.http.delete.mockResolvedValueOnce({}); - - const { findByRole } = render( - - - - ); - - fireEvent.click(await findByRole('button', { name: 'Delete user' })); - const dialog = await findByRole('dialog'); - fireEvent.click(await within(dialog).findByRole('button', { name: 'Delete user' })); - - await waitFor(() => { - expect(coreStart.http.delete).toHaveBeenLastCalledWith('/internal/security/users/jdoe'); - expect(history.location.pathname).toBe('/'); - }); - }); }); diff --git a/x-pack/plugins/security/public/management/users/edit_user/edit_user_page.tsx b/x-pack/plugins/security/public/management/users/edit_user/edit_user_page.tsx index 66216da49be95..8887ec93ff58d 100644 --- a/x-pack/plugins/security/public/management/users/edit_user/edit_user_page.tsx +++ b/x-pack/plugins/security/public/management/users/edit_user/edit_user_page.tsx @@ -211,7 +211,11 @@ export const EditUserPage: FunctionComponent = ({ username }) - setAction('changePassword')} size="s"> + setAction('changePassword')} + size="s" + data-test-subj="editUserChangePasswordButton" + > = ({ username }) - setAction('enableUser')} size="s"> + setAction('enableUser')} + size="s" + data-test-subj="editUserEnableUserButton" + > = ({ username }) - setAction('disableUser')} size="s"> + setAction('disableUser')} + size="s" + data-test-subj="editUserDisableUserButton" + > = ({ username }) - setAction('deleteUser')} size="s" color="danger"> + setAction('deleteUser')} + size="s" + color="danger" + data-test-subj="editUserDeleteUserButton" + > { const setBreadcrumbs = jest.fn(); const history = scopedHistoryMock.create({ pathname: '/create' }); - const unmount = await usersManagementApp.create({ authc, getStartServices }).mount({ - basePath: '/', - element, - setBreadcrumbs, - history, + let unmount: Unmount = noop; + await act(async () => { + unmount = await usersManagementApp.create({ authc, getStartServices }).mount({ + basePath: '/', + element, + setBreadcrumbs, + history, + }); }); expect(setBreadcrumbs).toHaveBeenLastCalledWith([ { href: '/', text: 'Users' }, - { href: '/create', text: 'Create' }, + { text: 'Create' }, ]); unmount(); diff --git a/x-pack/plugins/security/public/management/users/users_management_app.tsx b/x-pack/plugins/security/public/management/users/users_management_app.tsx index f6a2956c7ad43..7957599da7f57 100644 --- a/x-pack/plugins/security/public/management/users/users_management_app.tsx +++ b/x-pack/plugins/security/public/management/users/users_management_app.tsx @@ -119,7 +119,6 @@ export const usersManagementApp = Object.freeze({ ); return () => { - coreStart.chrome.docTitle.reset(); unmountComponentAtNode(element); }; }, diff --git a/x-pack/plugins/security/server/config.test.ts b/x-pack/plugins/security/server/config.test.ts index feadbbab5a4ca..9ebdcb5e4d05f 100644 --- a/x-pack/plugins/security/server/config.test.ts +++ b/x-pack/plugins/security/server/config.test.ts @@ -11,7 +11,7 @@ jest.mock('crypto', () => ({ })); jest.mock('@kbn/utils', () => ({ - getDataPath: () => '/mock/kibana/data/path', + getLogsPath: () => '/mock/kibana/logs/path', })); import { loggingSystemMock } from 'src/core/server/mocks'; @@ -1720,7 +1720,7 @@ describe('createConfig()', () => { ).audit.appender ).toMatchInlineSnapshot(` Object { - "fileName": "/mock/kibana/data/path/audit.log", + "fileName": "/mock/kibana/logs/path/audit.log", "layout": Object { "type": "json", }, diff --git a/x-pack/plugins/security/server/config.ts b/x-pack/plugins/security/server/config.ts index ba0d0d35d8ddd..f993707bd8d9e 100644 --- a/x-pack/plugins/security/server/config.ts +++ b/x-pack/plugins/security/server/config.ts @@ -12,7 +12,7 @@ import path from 'path'; import type { Type, TypeOf } from '@kbn/config-schema'; import { schema } from '@kbn/config-schema'; import { i18n } from '@kbn/i18n'; -import { getDataPath } from '@kbn/utils'; +import { getLogsPath } from '@kbn/utils'; import type { AppenderConfigType, Logger } from 'src/core/server'; import { config as coreConfig } from '../../../../src/core/server'; @@ -378,7 +378,7 @@ export function createConfig( config.audit.appender ?? ({ type: 'rolling-file', - fileName: path.join(getDataPath(), 'audit.log'), + fileName: path.join(getLogsPath(), 'audit.log'), layout: { type: 'json', }, diff --git a/x-pack/plugins/security_solution/common/constants.ts b/x-pack/plugins/security_solution/common/constants.ts index 2772c3de51065..071d01a1bd557 100644 --- a/x-pack/plugins/security_solution/common/constants.ts +++ b/x-pack/plugins/security_solution/common/constants.ts @@ -5,14 +5,20 @@ * 2.0. */ -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { ENABLE_ITOM } from '../../actions/server/constants/connectors'; import type { TransformConfigSchema } from './transforms/types'; import { ENABLE_CASE_CONNECTOR } from '../../cases/common'; import { METADATA_TRANSFORMS_PATTERN } from './endpoint/constants'; +/** + * as const + * + * The const assertion ensures that type widening does not occur + * https://mariusschulz.com/blog/literal-type-widening-in-typescript + * Please follow this convention when adding to this file + */ + export const APP_ID = 'securitySolution' as const; -export const APP_UI_ID = 'securitySolutionUI'; +export const APP_UI_ID = 'securitySolutionUI' as const; export const CASES_FEATURE_ID = 'securitySolutionCases' as const; export const SERVER_APP_ID = 'siem' as const; export const APP_NAME = 'Security' as const; @@ -26,6 +32,8 @@ export const DEFAULT_DATE_FORMAT_TZ = 'dateFormat:tz' as const; export const DEFAULT_DARK_MODE = 'theme:darkMode' as const; export const DEFAULT_INDEX_KEY = 'securitySolution:defaultIndex' as const; export const DEFAULT_NUMBER_FORMAT = 'format:number:defaultPattern' as const; +export const DEFAULT_DATA_VIEW_ID = 'security-solution' as const; +export const DEFAULT_TIME_FIELD = '@timestamp' as const; export const DEFAULT_TIME_RANGE = 'timepicker:timeDefaults' as const; export const DEFAULT_REFRESH_RATE_INTERVAL = 'timepicker:refreshIntervalDefaults' as const; export const DEFAULT_APP_TIME_RANGE = 'securitySolution:timeDefaults' as const; @@ -51,7 +59,6 @@ export const DEFAULT_TIMEPICKER_QUICK_RANGES = 'timepicker:quickRanges' as const export const DEFAULT_TRANSFORMS = 'securitySolution:transforms' as const; export const SCROLLING_DISABLED_CLASS_NAME = 'scrolling-disabled' as const; export const GLOBAL_HEADER_HEIGHT = 96 as const; // px -export const GLOBAL_HEADER_HEIGHT_WITH_GLOBAL_BANNER = 128 as const; // px export const FILTERS_GLOBAL_HEIGHT = 109 as const; // px export const FULL_SCREEN_TOGGLED_CLASS_NAME = 'fullScreenToggled' as const; export const NO_ALERT_INDEX = 'no-alert-index-049FC71A-4C2C-446F-9901-37XMC5024C51' as const; @@ -62,6 +69,7 @@ export const DEFAULT_RULE_REFRESH_IDLE_VALUE = 2700000 as const; // ms export const DEFAULT_RULE_NOTIFICATION_QUERY_SIZE = 100 as const; export const SECURITY_FEATURE_ID = 'Security' as const; export const DEFAULT_SPACE_ID = 'default' as const; +export const DEFAULT_RELATIVE_DATE_THRESHOLD = 24 as const; // Document path where threat indicator fields are expected. Fields are used // to enrich signals, and are copied to threat.enrichments. @@ -268,6 +276,7 @@ export const TIMELINE_PREPACKAGED_URL = `${TIMELINE_URL}/_prepackaged` as const; export const NOTE_URL = '/api/note' as const; export const PINNED_EVENT_URL = '/api/pinned_event' as const; +export const SOURCERER_API_URL = '/api/sourcerer' as const; /** * Default signals index key for kibana.dev.yml @@ -316,6 +325,7 @@ export const NOTIFICATION_SUPPORTED_ACTION_TYPES_IDS = [ '.resilient', '.servicenow', '.servicenow-sir', + '.servicenow-itom', '.slack', '.swimlane', '.teams', @@ -326,11 +336,6 @@ if (ENABLE_CASE_CONNECTOR) { NOTIFICATION_SUPPORTED_ACTION_TYPES_IDS.push('.case'); } -// TODO: Remove when ITOM is ready -if (ENABLE_ITOM) { - NOTIFICATION_SUPPORTED_ACTION_TYPES_IDS.push('.servicenow-itom'); -} - export const NOTIFICATION_THROTTLE_NO_ACTIONS = 'no_actions' as const; export const NOTIFICATION_THROTTLE_RULE = 'rule' as const; @@ -355,7 +360,7 @@ export const ELASTIC_NAME = 'estc' as const; export const METADATA_TRANSFORM_STATS_URL = `/api/transform/transforms/${METADATA_TRANSFORMS_PATTERN}/_stats`; -export const RISKY_HOSTS_INDEX_PREFIX = 'ml_host_risk_score_latest_'; +export const RISKY_HOSTS_INDEX_PREFIX = 'ml_host_risk_score_latest_' as const; export const TRANSFORM_STATES = { ABORTING: 'aborting', diff --git a/x-pack/plugins/security_solution/common/detection_engine/get_query_filter.ts b/x-pack/plugins/security_solution/common/detection_engine/get_query_filter.ts index 033e979d2814c..42c10614975eb 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/get_query_filter.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/get_query_filter.ts @@ -11,7 +11,7 @@ import type { CreateExceptionListItemSchema, } from '@kbn/securitysolution-io-ts-list-types'; import { buildExceptionFilter } from '@kbn/securitysolution-list-utils'; -import { Filter, EsQueryConfig, IndexPatternBase, buildEsQuery } from '@kbn/es-query'; +import { Filter, EsQueryConfig, DataViewBase, buildEsQuery } from '@kbn/es-query'; import { ESBoolQuery } from '../typed_json'; import { Query, Index, TimestampOverrideOrUndefined } from './schemas/common/schemas'; @@ -24,7 +24,7 @@ export const getQueryFilter = ( lists: Array, excludeExceptions: boolean = true ): ESBoolQuery => { - const indexPattern: IndexPatternBase = { + const indexPattern: DataViewBase = { fields: [], title: index.join(), }; diff --git a/x-pack/plugins/security_solution/common/endpoint/constants.ts b/x-pack/plugins/security_solution/common/endpoint/constants.ts index 178a2b68a4aab..fc418df95602b 100644 --- a/x-pack/plugins/security_solution/common/endpoint/constants.ts +++ b/x-pack/plugins/security_solution/common/endpoint/constants.ts @@ -15,6 +15,9 @@ export const ENDPOINT_ACTION_RESPONSES_INDEX = `${ENDPOINT_ACTION_RESPONSES_DS}- export const eventsIndexPattern = 'logs-endpoint.events.*'; export const alertsIndexPattern = 'logs-endpoint.alerts-*'; +// metadata datastream +export const METADATA_DATASTREAM = 'metrics-endpoint.metadata-default'; + /** index pattern for the data source index (data stream) that the Endpoint streams documents to */ export const metadataIndexPattern = 'metrics-endpoint.metadata-*'; diff --git a/x-pack/plugins/security_solution/common/endpoint/data_loaders/index_endpoint_hosts.ts b/x-pack/plugins/security_solution/common/endpoint/data_loaders/index_endpoint_hosts.ts index de564019db6d0..ed75823cd30d3 100644 --- a/x-pack/plugins/security_solution/common/endpoint/data_loaders/index_endpoint_hosts.ts +++ b/x-pack/plugins/security_solution/common/endpoint/data_loaders/index_endpoint_hosts.ts @@ -8,6 +8,7 @@ import { Client } from '@elastic/elasticsearch'; import { cloneDeep, merge } from 'lodash'; import { AxiosResponse } from 'axios'; +import uuid from 'uuid'; // eslint-disable-next-line import/no-extraneous-dependencies import { KbnClient } from '@kbn/test'; import { DeleteByQueryResponse } from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; @@ -139,12 +140,13 @@ export async function indexEndpointHostDocs({ if (enrollFleet) { const { id: appliedPolicyId, name: appliedPolicyName } = hostMetadata.Endpoint.policy.applied; + const uniqueAppliedPolicyName = `${appliedPolicyName}-${uuid.v4()}`; // If we don't yet have a "real" policy record, then create it now in ingest (package config) if (!realPolicies[appliedPolicyId]) { const createdPolicies = await indexFleetEndpointPolicy( kbnClient, - appliedPolicyName, + uniqueAppliedPolicyName, epmEndpointPackage.version ); diff --git a/x-pack/plugins/security_solution/common/jest.config.js b/x-pack/plugins/security_solution/common/jest.config.js new file mode 100644 index 0000000000000..ca6f7cd368f2b --- /dev/null +++ b/x-pack/plugins/security_solution/common/jest.config.js @@ -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. + */ + +module.exports = { + preset: '@kbn/test', + rootDir: '../../../..', + roots: ['/x-pack/plugins/security_solution/common'], + coverageDirectory: + '/target/kibana-coverage/jest/x-pack/plugins/security_solution/common', + coverageReporters: ['text', 'html'], + collectCoverageFrom: ['/x-pack/plugins/security_solution/common/**/*.{ts,tsx}'], +}; diff --git a/x-pack/plugins/security_solution/common/search_strategy/index_fields/index.ts b/x-pack/plugins/security_solution/common/search_strategy/index_fields/index.ts index be5fd3b5c4dc5..86bc11f7a596d 100644 --- a/x-pack/plugins/security_solution/common/search_strategy/index_fields/index.ts +++ b/x-pack/plugins/security_solution/common/search_strategy/index_fields/index.ts @@ -5,79 +5,17 @@ * 2.0. */ -import type { IFieldSubType } from '@kbn/es-query'; - -import type { - IEsSearchRequest, - IEsSearchResponse, - IIndexPattern, -} from '../../../../../../src/plugins/data/common'; -import type { DocValueFields, Maybe } from '../common'; - -interface FieldInfo { - category: string; - description?: string; - example?: string | number; - format?: string; - name: string; - type?: string; -} - -export interface IndexField { - /** Where the field belong */ - category: string; - /** Example of field's value */ - example?: Maybe; - /** whether the field's belong to an alias index */ - indexes: Array>; - /** The name of the field */ - name: string; - /** The type of the field's values as recognized by Kibana */ - type: string; - /** Whether the field's values can be efficiently searched for */ - searchable: boolean; - /** Whether the field's values can be aggregated */ - aggregatable: boolean; - /** Description of the field */ - description?: Maybe; - format?: Maybe; - /** the elastic type as mapped in the index */ - esTypes?: string[]; - subType?: IFieldSubType; - readFromDocValues: boolean; -} - -export type BeatFields = Record; - -export interface IndexFieldsStrategyRequest extends IEsSearchRequest { - indices: string[]; - onlyCheckIfIndicesExist: boolean; -} - -export interface IndexFieldsStrategyResponse extends IEsSearchResponse { - indexFields: IndexField[]; - indicesExist: string[]; -} - -export interface BrowserField { - aggregatable: boolean; - category: string; - description: string | null; - example: string | number | null; - fields: Readonly>>; - format: string; - indexes: string[]; - name: string; - searchable: boolean; - type: string; - subType?: IFieldSubType; -} - -export type BrowserFields = Readonly>>; - -export const EMPTY_BROWSER_FIELDS = {}; -export const EMPTY_DOCVALUE_FIELD: DocValueFields[] = []; -export const EMPTY_INDEX_PATTERN: IIndexPattern = { - fields: [], - title: '', -}; +export type { + FieldInfo, + IndexField, + BeatFields, + IndexFieldsStrategyRequest, + IndexFieldsStrategyResponse, + BrowserField, + BrowserFields, +} from '../../../../timelines/common'; +export { + EMPTY_BROWSER_FIELDS, + EMPTY_DOCVALUE_FIELD, + EMPTY_INDEX_FIELDS, +} from '../../../../timelines/common'; diff --git a/x-pack/plugins/security_solution/common/search_strategy/timeline/events/last_event_time/index.ts b/x-pack/plugins/security_solution/common/search_strategy/timeline/events/last_event_time/index.ts index 39f23a63c8afe..d6735b59c229d 100644 --- a/x-pack/plugins/security_solution/common/search_strategy/timeline/events/last_event_time/index.ts +++ b/x-pack/plugins/security_solution/common/search_strategy/timeline/events/last_event_time/index.ts @@ -10,6 +10,7 @@ export { LastEventIndexKey } from '../../../../../../timelines/common'; export type { LastTimeDetails, TimelineEventsLastEventTimeStrategyResponse, + TimelineKpiStrategyRequest, TimelineKpiStrategyResponse, TimelineEventsLastEventTimeRequestOptions, } from '../../../../../../timelines/common'; diff --git a/x-pack/plugins/security_solution/common/search_strategy/timeline/index.ts b/x-pack/plugins/security_solution/common/search_strategy/timeline/index.ts index 548560ac5cb8c..2d94a36a937d5 100644 --- a/x-pack/plugins/security_solution/common/search_strategy/timeline/index.ts +++ b/x-pack/plugins/security_solution/common/search_strategy/timeline/index.ts @@ -5,6 +5,7 @@ * 2.0. */ +import { MappingRuntimeFields } from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; import { IEsSearchRequest } from '../../../../../../src/plugins/data/common'; import { ESQuery } from '../../typed_json'; import { @@ -41,6 +42,7 @@ export interface TimelineRequestBasicOptions extends IEsSearchRequest { defaultIndex: string[]; docValueFields?: DocValueFields[]; factoryQueryType?: TimelineFactoryQueryTypes; + runtimeMappings: MappingRuntimeFields; } export interface TimelineRequestSortField extends SortField { @@ -171,6 +173,7 @@ export interface SortTimelineInput { export interface TimelineInput { columns?: Maybe; dataProviders?: Maybe; + dataViewId?: Maybe; description?: Maybe; eqlOptions?: Maybe; eventType?: Maybe; diff --git a/x-pack/plugins/security_solution/common/test/index.ts b/x-pack/plugins/security_solution/common/test/index.ts index 6d5df76b306a3..53261d54e84b0 100644 --- a/x-pack/plugins/security_solution/common/test/index.ts +++ b/x-pack/plugins/security_solution/common/test/index.ts @@ -7,12 +7,12 @@ // For the source of these roles please consult the PR these were introduced https://github.com/elastic/kibana/pull/81866#issue-511165754 export enum ROLES { + soc_manager = 'soc_manager', reader = 'reader', t1_analyst = 't1_analyst', t2_analyst = 't2_analyst', hunter = 'hunter', rule_author = 'rule_author', - soc_manager = 'soc_manager', platform_engineer = 'platform_engineer', detections_admin = 'detections_admin', } diff --git a/x-pack/plugins/security_solution/common/types/timeline/index.ts b/x-pack/plugins/security_solution/common/types/timeline/index.ts index c0046f7535db8..60fd126e6fd85 100644 --- a/x-pack/plugins/security_solution/common/types/timeline/index.ts +++ b/x-pack/plugins/security_solution/common/types/timeline/index.ts @@ -272,6 +272,7 @@ export type TimelineTypeLiteralWithNull = runtimeTypes.TypeOf; +export type TimelineWithoutExternalRefs = Omit; /* * Timeline IDs @@ -719,6 +720,7 @@ export interface TimelineResult { created?: Maybe; createdBy?: Maybe; dataProviders?: Maybe; + dataViewId?: Maybe; dateRange?: Maybe; description?: Maybe; eqlOptions?: Maybe; diff --git a/x-pack/plugins/security_solution/common/types/timeline/store.ts b/x-pack/plugins/security_solution/common/types/timeline/store.ts index 03cf0c39378e5..75cd44ba2b7d7 100644 --- a/x-pack/plugins/security_solution/common/types/timeline/store.ts +++ b/x-pack/plugins/security_solution/common/types/timeline/store.ts @@ -38,19 +38,20 @@ export interface SortColumnTimeline { } export interface TimelinePersistInput { - id: string; + columns: ColumnHeaderOptions[]; dataProviders?: DataProvider[]; + dataViewId: string; dateRange?: { start: string; end: string; }; + defaultColumns?: ColumnHeaderOptions[]; excludedRowRendererIds?: RowRendererId[]; expandedDetail?: TimelineExpandedDetail; filters?: Filter[]; - columns: ColumnHeaderOptions[]; - defaultColumns?: ColumnHeaderOptions[]; - itemsPerPage?: number; + id: string; indexNames: string[]; + itemsPerPage?: number; kqlQuery?: { filterQuery: SerializedFilterQuery | null; }; diff --git a/x-pack/plugins/security_solution/cypress/cypress.json b/x-pack/plugins/security_solution/cypress/cypress.json index 6a9a240af5873..8c27309becf08 100644 --- a/x-pack/plugins/security_solution/cypress/cypress.json +++ b/x-pack/plugins/security_solution/cypress/cypress.json @@ -12,5 +12,10 @@ "video": false, "videosFolder": "../../../target/kibana-security-solution/cypress/videos", "viewportHeight": 900, - "viewportWidth": 1440 + "viewportWidth": 1440, + "env": { + "protocol": "http", + "hostname": "localhost", + "configport": "5601" + } } diff --git a/x-pack/plugins/security_solution/cypress/downloads/timelines_export.ndjson b/x-pack/plugins/security_solution/cypress/downloads/timelines_export.ndjson new file mode 100644 index 0000000000000..8cf76734ad876 --- /dev/null +++ b/x-pack/plugins/security_solution/cypress/downloads/timelines_export.ndjson @@ -0,0 +1 @@ +{"savedObjectId":"46cca0e0-2580-11ec-8e56-9dafa0b0343b","version":"WzIyNjIzNCwxXQ==","columns":[{"id":"@timestamp"},{"id":"user.name"},{"id":"event.category"},{"id":"event.action"},{"id":"host.name"}],"kqlMode":"filter","kqlQuery":{"filterQuery":{"kuery":{"expression":"host.name: *","kind":"kuery"}}},"dateRange":{"start":"1514809376000","end":"1577881376000"},"description":"This is the best timeline","title":"Security Timeline","created":1633399341550,"createdBy":"elastic","updated":1633399341550,"updatedBy":"elastic","savedQueryId":null,"dataViewId":null,"timelineType":"default","sort":[],"eventNotes":[],"globalNotes":[],"pinnedEventIds":[]} diff --git a/x-pack/plugins/security_solution/cypress/helpers/rules.ts b/x-pack/plugins/security_solution/cypress/helpers/rules.ts index ebe357c382770..63542f9a78f84 100644 --- a/x-pack/plugins/security_solution/cypress/helpers/rules.ts +++ b/x-pack/plugins/security_solution/cypress/helpers/rules.ts @@ -20,3 +20,18 @@ export const formatMitreAttackDescription = (mitre: Mitre[]) => { ) .join(''); }; + +export const elementsOverlap = ($element1: JQuery, $element2: JQuery) => { + const rectA = $element1[0].getBoundingClientRect(); + const rectB = $element2[0].getBoundingClientRect(); + + // If they don't overlap horizontally, they don't overlap + if (rectA.right < rectB.left || rectB.right < rectA.left) { + return false; + } else if (rectA.bottom < rectB.top || rectB.bottom < rectA.top) { + // If they don't overlap vertically, they don't overlap + return false; + } else { + return true; + } +}; diff --git a/x-pack/plugins/security_solution/cypress/integration/cases/privileges.spec.ts b/x-pack/plugins/security_solution/cypress/integration/cases/privileges.spec.ts index 23016ecc512b1..0337cd3bd6e17 100644 --- a/x-pack/plugins/security_solution/cypress/integration/cases/privileges.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/cases/privileges.spec.ts @@ -18,184 +18,28 @@ import { filterStatusOpen, } from '../../tasks/create_new_case'; import { - constructUrlWithUser, - getEnvAuth, + loginAndWaitForHostDetailsPage, loginWithUserAndWaitForPageWithoutDateRange, + logout, } from '../../tasks/login'; +import { + createUsersAndRoles, + deleteUsersAndRoles, + secAll, + secAllUser, + secReadCasesAllUser, + secReadCasesAll, +} from '../../tasks/privileges'; import { CASES_URL } from '../../urls/navigation'; - -interface User { - username: string; - password: string; - description?: string; - roles: string[]; -} - -interface UserInfo { - username: string; - full_name: string; - email: string; -} - -interface FeaturesPrivileges { - [featureId: string]: string[]; -} - -interface ElasticsearchIndices { - names: string[]; - privileges: string[]; -} - -interface ElasticSearchPrivilege { - cluster?: string[]; - indices?: ElasticsearchIndices[]; -} - -interface KibanaPrivilege { - spaces: string[]; - base?: string[]; - feature?: FeaturesPrivileges; -} - -interface Role { - name: string; - privileges: { - elasticsearch?: ElasticSearchPrivilege; - kibana?: KibanaPrivilege[]; - }; -} - -const secAll: Role = { - name: 'sec_all_role', - privileges: { - elasticsearch: { - indices: [ - { - names: ['*'], - privileges: ['all'], - }, - ], - }, - kibana: [ - { - feature: { - siem: ['all'], - securitySolutionCases: ['all'], - actions: ['all'], - actionsSimulators: ['all'], - }, - spaces: ['*'], - }, - ], - }, -}; - -const secAllUser: User = { - username: 'sec_all_user', - password: 'password', - roles: [secAll.name], -}; - -const secReadCasesAll: Role = { - name: 'sec_read_cases_all_role', - privileges: { - elasticsearch: { - indices: [ - { - names: ['*'], - privileges: ['all'], - }, - ], - }, - kibana: [ - { - feature: { - siem: ['read'], - securitySolutionCases: ['all'], - actions: ['all'], - actionsSimulators: ['all'], - }, - spaces: ['*'], - }, - ], - }, -}; - -const secReadCasesAllUser: User = { - username: 'sec_read_cases_all_user', - password: 'password', - roles: [secReadCasesAll.name], -}; - +import { openSourcerer } from '../../tasks/sourcerer'; const usersToCreate = [secAllUser, secReadCasesAllUser]; const rolesToCreate = [secAll, secReadCasesAll]; - -const getUserInfo = (user: User): UserInfo => ({ - username: user.username, - full_name: user.username.replace('_', ' '), - email: `${user.username}@elastic.co`, -}); - -const createUsersAndRoles = (users: User[], roles: Role[]) => { - const envUser = getEnvAuth(); - for (const role of roles) { - cy.log(`Creating role: ${JSON.stringify(role)}`); - cy.request({ - body: role.privileges, - headers: { 'kbn-xsrf': 'cypress-creds-via-config' }, - method: 'PUT', - url: constructUrlWithUser(envUser, `/api/security/role/${role.name}`), - }) - .its('status') - .should('eql', 204); - } - - for (const user of users) { - const userInfo = getUserInfo(user); - cy.log(`Creating user: ${JSON.stringify(user)}`); - cy.request({ - body: { - username: user.username, - password: user.password, - roles: user.roles, - full_name: userInfo.full_name, - email: userInfo.email, - }, - headers: { 'kbn-xsrf': 'cypress-creds-via-config' }, - method: 'POST', - url: constructUrlWithUser(envUser, `/internal/security/users/${user.username}`), - }) - .its('status') - .should('eql', 200); - } -}; - -const deleteUsersAndRoles = (users: User[], roles: Role[]) => { - const envUser = getEnvAuth(); - for (const user of users) { - cy.log(`Deleting user: ${JSON.stringify(user)}`); - cy.request({ - headers: { 'kbn-xsrf': 'cypress-creds-via-config' }, - method: 'DELETE', - url: constructUrlWithUser(envUser, `/internal/security/users/${user.username}`), - failOnStatusCode: false, - }) - .its('status') - .should('oneOf', [204, 404]); - } - - for (const role of roles) { - cy.log(`Deleting role: ${JSON.stringify(role)}`); - cy.request({ - headers: { 'kbn-xsrf': 'cypress-creds-via-config' }, - method: 'DELETE', - url: constructUrlWithUser(envUser, `/api/security/role/${role.name}`), - failOnStatusCode: false, - }) - .its('status') - .should('oneOf', [204, 404]); - } +// needed to generate index pattern +const visitSecuritySolution = () => { + loginAndWaitForHostDetailsPage(); + openSourcerer(); + logout(); }; const testCase: TestCaseWithoutTimeline = { @@ -205,11 +49,11 @@ const testCase: TestCaseWithoutTimeline = { reporter: 'elastic', owner: 'securitySolution', }; - describe('Cases privileges', () => { before(() => { cleanKibana(); createUsersAndRoles(usersToCreate, rolesToCreate); + visitSecuritySolution(); }); after(() => { diff --git a/x-pack/plugins/security_solution/cypress/integration/data_sources/create_runtime_field.spec.ts b/x-pack/plugins/security_solution/cypress/integration/data_sources/create_runtime_field.spec.ts new file mode 100644 index 0000000000000..1f2ca36c5a3d7 --- /dev/null +++ b/x-pack/plugins/security_solution/cypress/integration/data_sources/create_runtime_field.spec.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 { cleanKibana } from '../../tasks/common'; + +import { loginAndWaitForPage } from '../../tasks/login'; +import { openTimelineUsingToggle } from '../../tasks/security_main'; +import { openTimelineFieldsBrowser, populateTimeline } from '../../tasks/timeline'; + +import { HOSTS_URL, ALERTS_URL } from '../../urls/navigation'; + +import { waitForAlertsIndexToBeCreated, waitForAlertsPanelToBeLoaded } from '../../tasks/alerts'; +import { createCustomRuleActivated } from '../../tasks/api_calls/rules'; + +import { getNewRule } from '../../objects/rule'; +import { refreshPage } from '../../tasks/security_header'; +import { waitForAlertsToPopulate } from '../../tasks/create_new_rule'; +import { openEventsViewerFieldsBrowser } from '../../tasks/hosts/events'; + +describe('Create DataView runtime field', () => { + before(() => { + cleanKibana(); + }); + + it('adds field to alert table', () => { + const fieldName = 'field.name.alert.page'; + loginAndWaitForPage(ALERTS_URL); + waitForAlertsPanelToBeLoaded(); + waitForAlertsIndexToBeCreated(); + createCustomRuleActivated(getNewRule()); + refreshPage(); + waitForAlertsToPopulate(500); + openEventsViewerFieldsBrowser(); + + cy.get('[data-test-subj="create-field"]').click(); + cy.get('.indexPatternFieldEditorMaskOverlay').find('[data-test-subj="input"]').type(fieldName); + cy.get('[data-test-subj="fieldSaveButton"]').click(); + + cy.get( + `[data-test-subj="events-viewer-panel"] [data-test-subj="dataGridHeaderCell-${fieldName}"]` + ).should('exist'); + }); + + it('adds field to timeline', () => { + const fieldName = 'field.name.timeline'; + + loginAndWaitForPage(HOSTS_URL); + openTimelineUsingToggle(); + populateTimeline(); + openTimelineFieldsBrowser(); + + cy.get('[data-test-subj="create-field"]').click(); + cy.get('.indexPatternFieldEditorMaskOverlay').find('[data-test-subj="input"]').type(fieldName); + cy.get('[data-test-subj="fieldSaveButton"]').click(); + + cy.get(`[data-test-subj="timeline"] [data-test-subj="header-text-${fieldName}"]`).should( + 'exist' + ); + }); +}); diff --git a/x-pack/plugins/security_solution/cypress/integration/data_sources/sourcerer.spec.ts b/x-pack/plugins/security_solution/cypress/integration/data_sources/sourcerer.spec.ts index 26c366e981d44..bd7acc38c1021 100644 --- a/x-pack/plugins/security_solution/cypress/integration/data_sources/sourcerer.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/data_sources/sourcerer.spec.ts @@ -5,7 +5,10 @@ * 2.0. */ -import { loginAndWaitForPage } from '../../tasks/login'; +import { + loginAndWaitForPage, + loginWithUserAndWaitForPageWithoutDateRange, +} from '../../tasks/login'; import { HOSTS_URL } from '../../urls/navigation'; import { waitForAllHostsToBeLoaded } from '../../tasks/hosts/all_hosts'; @@ -28,20 +31,34 @@ import { openTimelineUsingToggle } from '../../tasks/security_main'; import { populateTimeline } from '../../tasks/timeline'; import { SERVER_SIDE_EVENT_COUNT } from '../../screens/timeline'; import { cleanKibana } from '../../tasks/common'; +import { createUsersAndRoles, secReadCasesAll, secReadCasesAllUser } from '../../tasks/privileges'; +import { TOASTER } from '../../screens/configure_cases'; +const usersToCreate = [secReadCasesAllUser]; +const rolesToCreate = [secReadCasesAll]; // Skipped at the moment as this has flake due to click handler issues. This has been raised with team members // and the code is being re-worked and then these tests will be unskipped -describe.skip('Sourcerer', () => { - before(() => { +describe('Sourcerer', () => { + beforeEach(() => { cleanKibana(); }); - - beforeEach(() => { - cy.clearLocalStorage(); - loginAndWaitForPage(HOSTS_URL); + describe('permissions', () => { + before(() => { + createUsersAndRoles(usersToCreate, rolesToCreate); + }); + it(`role(s) ${secReadCasesAllUser.roles.join()} shows error when user does not have permissions`, () => { + loginWithUserAndWaitForPageWithoutDateRange(HOSTS_URL, secReadCasesAllUser); + cy.get(TOASTER).should('have.text', 'Write role required to generate data'); + }); }); + // Originially written in December 2020, flakey from day1 + // has always been skipped with intentions to fix, see note at top of file + describe.skip('Default scope', () => { + beforeEach(() => { + cy.clearLocalStorage(); + loginAndWaitForPage(HOSTS_URL); + }); - describe('Default scope', () => { it('has SIEM index patterns selected on initial load', () => { openSourcerer(); isSourcererSelection(`auditbeat-*`); @@ -52,7 +69,7 @@ describe.skip('Sourcerer', () => { isSourcererOptions([`metrics-*`, `logs-*`]); }); - it('selected KIP gets added to sourcerer', () => { + it('selected DATA_VIEW gets added to sourcerer', () => { setSourcererOption(`metrics-*`); openSourcerer(); isSourcererSelection(`metrics-*`); @@ -75,8 +92,14 @@ describe.skip('Sourcerer', () => { isNotSourcererSelection(`metrics-*`); }); }); + // Originially written in December 2020, flakey from day1 + // has always been skipped with intentions to fix + describe.skip('Timeline scope', () => { + beforeEach(() => { + cy.clearLocalStorage(); + loginAndWaitForPage(HOSTS_URL); + }); - describe('Timeline scope', () => { const alertPatterns = ['.siem-signals-default']; const rawPatterns = ['auditbeat-*']; const allPatterns = [...alertPatterns, ...rawPatterns]; diff --git a/x-pack/plugins/security_solution/cypress/integration/detection_alerts/alerts_details.spec.ts b/x-pack/plugins/security_solution/cypress/integration/detection_alerts/alerts_details.spec.ts index 803ff4b4d0d80..116ee4d3820f9 100644 --- a/x-pack/plugins/security_solution/cypress/integration/detection_alerts/alerts_details.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/detection_alerts/alerts_details.spec.ts @@ -5,10 +5,17 @@ * 2.0. */ -import { ALERT_FLYOUT, CELL_TEXT, JSON_TEXT, TABLE_ROWS } from '../../screens/alerts_details'; +import { + ALERT_FLYOUT, + CELL_TEXT, + JSON_TEXT, + TABLE_CONTAINER, + TABLE_ROWS, +} from '../../screens/alerts_details'; import { expandFirstAlert, + refreshAlerts, waitForAlertsIndexToBeCreated, waitForAlertsPanelToBeLoaded, } from '../../tasks/alerts'; @@ -32,6 +39,7 @@ describe('Alert details with unmapped fields', () => { createCustomRuleActivated(getUnmappedRule()); loginAndWaitForPageWithoutDateRange(ALERTS_URL); waitForAlertsPanelToBeLoaded(); + refreshAlerts(); expandFirstAlert(); }); @@ -63,4 +71,20 @@ describe('Alert details with unmapped fields', () => { cy.get(CELL_TEXT).eq(4).should('have.text', expectedUnmmappedField.text); }); }); + + // This test makes sure that the table does not overflow horizontally + it('Table does not scroll horizontally', () => { + openTable(); + + cy.get(ALERT_FLYOUT) + .find(TABLE_CONTAINER) + .within(($tableContainer) => { + expect($tableContainer[0].scrollLeft).to.equal(0); + + // Try to scroll left and make sure that the table hasn't actually scrolled + $tableContainer[0].scroll({ left: 1000 }); + + expect($tableContainer[0].scrollLeft).to.equal(0); + }); + }); }); diff --git a/x-pack/plugins/security_solution/cypress/integration/detection_rules/event_correlation_rule.spec.ts b/x-pack/plugins/security_solution/cypress/integration/detection_rules/event_correlation_rule.spec.ts index 10f556a11bf60..171d224cc32d3 100644 --- a/x-pack/plugins/security_solution/cypress/integration/detection_rules/event_correlation_rule.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/detection_rules/event_correlation_rule.spec.ts @@ -70,7 +70,7 @@ import { loginAndWaitForPageWithoutDateRange } from '../../tasks/login'; import { ALERTS_URL } from '../../urls/navigation'; -describe.skip('Detection rules, EQL', () => { +describe('Detection rules, EQL', () => { const expectedUrls = getEqlRule().referenceUrls.join(''); const expectedFalsePositives = getEqlRule().falsePositivesExamples.join(''); const expectedTags = getEqlRule().tags.join(''); @@ -169,7 +169,7 @@ describe.skip('Detection rules, EQL', () => { }); }); -describe.skip('Detection rules, sequence EQL', () => { +describe('Detection rules, sequence EQL', () => { const expectedNumberOfRules = 1; const expectedNumberOfSequenceAlerts = '1 alert'; 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 02621ea49e906..378de8f0bc593 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 @@ -114,7 +114,7 @@ import { loginAndWaitForPageWithoutDateRange } from '../../tasks/login'; import { goBackToAllRulesTable } from '../../tasks/rule_details'; import { ALERTS_URL, RULE_CREATION } from '../../urls/navigation'; -import { DEFAULT_THREAT_MATCH_QUERY } from '../../../common/constants'; +const DEFAULT_THREAT_MATCH_QUERY = '@timestamp >= "now-30d"'; describe('indicator match', () => { describe('Detection rules, Indicator Match', () => { diff --git a/x-pack/plugins/security_solution/cypress/integration/detection_rules/sorting.spec.ts b/x-pack/plugins/security_solution/cypress/integration/detection_rules/sorting.spec.ts index ef3d3a82d40bd..92f9e8180d50c 100644 --- a/x-pack/plugins/security_solution/cypress/integration/detection_rules/sorting.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/detection_rules/sorting.spec.ts @@ -34,7 +34,6 @@ import { waitForRuleToChangeStatus, } from '../../tasks/alerts_detection_rules'; import { loginAndWaitForPageWithoutDateRange } from '../../tasks/login'; -import { DEFAULT_RULE_REFRESH_INTERVAL_VALUE } from '../../../common/constants'; import { ALERTS_URL } from '../../urls/navigation'; import { createCustomRule } from '../../tasks/api_calls/rules'; @@ -46,6 +45,8 @@ import { getNewThresholdRule, } from '../../objects/rule'; +const DEFAULT_RULE_REFRESH_INTERVAL_VALUE = 60000; + describe('Alerts detection rules', () => { beforeEach(() => { cleanKibana(); diff --git a/x-pack/plugins/security_solution/cypress/integration/detection_rules/threshold_rule.spec.ts b/x-pack/plugins/security_solution/cypress/integration/detection_rules/threshold_rule.spec.ts index 4c76fdcb18ca7..02d8837261f2f 100644 --- a/x-pack/plugins/security_solution/cypress/integration/detection_rules/threshold_rule.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/detection_rules/threshold_rule.spec.ts @@ -99,7 +99,7 @@ describe('Detection rules, threshold', () => { waitForAlertsIndexToBeCreated(); }); - it.skip('Creates and activates a new threshold rule', () => { + it('Creates and activates a new threshold rule', () => { goToManageAlertsDetectionRules(); waitForRulesTableToBeLoaded(); goToCreateNewRule(); @@ -171,9 +171,7 @@ describe('Detection rules, threshold', () => { waitForAlertsToPopulate(); cy.get(NUMBER_OF_ALERTS).should(($count) => expect(+$count.text().split(' ')[0]).to.be.lt(100)); - cy.get(ALERT_GRID_CELL).eq(3).contains(rule.name); - cy.get(ALERT_GRID_CELL).eq(4).contains(rule.severity.toLowerCase()); - cy.get(ALERT_GRID_CELL).eq(5).contains(rule.riskScore); + cy.get(ALERT_GRID_CELL).contains(rule.name); }); it('Preview results of keyword using "host.name"', () => { diff --git a/x-pack/plugins/security_solution/cypress/integration/ml/ml_conditional_links.spec.ts b/x-pack/plugins/security_solution/cypress/integration/ml/ml_conditional_links.spec.ts index 89a0d5a660b97..f9d78ba12a5ea 100644 --- a/x-pack/plugins/security_solution/cypress/integration/ml/ml_conditional_links.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/ml/ml_conditional_links.spec.ts @@ -98,7 +98,7 @@ describe('ml conditional links', () => { loginAndWaitForPageWithoutDateRange(mlNetworkSingleIpNullKqlQuery); cy.url().should( 'include', - 'app/security/network/ip/127.0.0.1/source?timerange=(global:(linkTo:!(timeline),timerange:(from:%272019-08-28T11:00:00.000Z%27,kind:absolute,to:%272019-08-28T13:59:59.999Z%27)),timeline:(linkTo:!(global),timerange:(from:%272019-08-28T11:00:00.000Z%27,kind:absolute,to:%272019-08-28T13:59:59.999Z%27)))&sourcerer=(default:!(%27auditbeat-*%27))' + 'app/security/network/ip/127.0.0.1/source?timerange=(global:(linkTo:!(timeline),timerange:(from:%272019-08-28T11:00:00.000Z%27,kind:absolute,to:%272019-08-28T13:59:59.999Z%27)),timeline:(linkTo:!(global),timerange:(from:%272019-08-28T11:00:00.000Z%27,kind:absolute,to:%272019-08-28T13:59:59.999Z%27)))&sourcerer=(default:(id:security-solution-default,selectedPatterns:!(%27auditbeat-*%27)))' ); }); @@ -106,7 +106,7 @@ describe('ml conditional links', () => { loginAndWaitForPageWithoutDateRange(mlNetworkSingleIpKqlQuery); cy.url().should( 'include', - '/app/security/network/ip/127.0.0.1/source?query=(language:kuery,query:%27(process.name:%20%22conhost.exe%22%20or%20process.name:%20%22sc.exe%22)%27)&timerange=(global:(linkTo:!(timeline),timerange:(from:%272019-08-28T11:00:00.000Z%27,kind:absolute,to:%272019-08-28T13:59:59.999Z%27)),timeline:(linkTo:!(global),timerange:(from:%272019-08-28T11:00:00.000Z%27,kind:absolute,to:%272019-08-28T13:59:59.999Z%27)))&sourcerer=(default:!(%27auditbeat-*%27))' + '/app/security/network/ip/127.0.0.1/source?query=(language:kuery,query:%27(process.name:%20%22conhost.exe%22%20or%20process.name:%20%22sc.exe%22)%27)&timerange=(global:(linkTo:!(timeline),timerange:(from:%272019-08-28T11:00:00.000Z%27,kind:absolute,to:%272019-08-28T13:59:59.999Z%27)),timeline:(linkTo:!(global),timerange:(from:%272019-08-28T11:00:00.000Z%27,kind:absolute,to:%272019-08-28T13:59:59.999Z%27)))&sourcerer=(default:(id:security-solution-default,selectedPatterns:!(%27auditbeat-*%27)))' ); }); @@ -114,7 +114,7 @@ describe('ml conditional links', () => { loginAndWaitForPageWithoutDateRange(mlNetworkMultipleIpNullKqlQuery); cy.url().should( 'include', - 'app/security/network/flows?query=(language:kuery,query:%27((source.ip:%20%22127.0.0.1%22%20or%20destination.ip:%20%22127.0.0.1%22)%20or%20(source.ip:%20%22127.0.0.2%22%20or%20destination.ip:%20%22127.0.0.2%22))%27)&timerange=(global:(linkTo:!(timeline),timerange:(from:%272019-08-28T11:00:00.000Z%27,kind:absolute,to:%272019-08-28T13:59:59.999Z%27)),timeline:(linkTo:!(global),timerange:(from:%272019-08-28T11:00:00.000Z%27,kind:absolute,to:%272019-08-28T13:59:59.999Z%27)))&sourcerer=(default:!(%27auditbeat-*%27))' + 'app/security/network/flows?query=(language:kuery,query:%27((source.ip:%20%22127.0.0.1%22%20or%20destination.ip:%20%22127.0.0.1%22)%20or%20(source.ip:%20%22127.0.0.2%22%20or%20destination.ip:%20%22127.0.0.2%22))%27)&timerange=(global:(linkTo:!(timeline),timerange:(from:%272019-08-28T11:00:00.000Z%27,kind:absolute,to:%272019-08-28T13:59:59.999Z%27)),timeline:(linkTo:!(global),timerange:(from:%272019-08-28T11:00:00.000Z%27,kind:absolute,to:%272019-08-28T13:59:59.999Z%27)))&sourcerer=(default:(id:security-solution-default,selectedPatterns:!(%27auditbeat-*%27)))' ); }); @@ -122,7 +122,7 @@ describe('ml conditional links', () => { loginAndWaitForPageWithoutDateRange(mlNetworkMultipleIpKqlQuery); cy.url().should( 'include', - '/app/security/network/flows?query=(language:kuery,query:%27((source.ip:%20%22127.0.0.1%22%20or%20destination.ip:%20%22127.0.0.1%22)%20or%20(source.ip:%20%22127.0.0.2%22%20or%20destination.ip:%20%22127.0.0.2%22))%20and%20((process.name:%20%22conhost.exe%22%20or%20process.name:%20%22sc.exe%22))%27)&timerange=(global:(linkTo:!(timeline),timerange:(from:%272019-08-28T11:00:00.000Z%27,kind:absolute,to:%272019-08-28T13:59:59.999Z%27)),timeline:(linkTo:!(global),timerange:(from:%272019-08-28T11:00:00.000Z%27,kind:absolute,to:%272019-08-28T13:59:59.999Z%27)))&sourcerer=(default:!(%27auditbeat-*%27))' + '/app/security/network/flows?query=(language:kuery,query:%27((source.ip:%20%22127.0.0.1%22%20or%20destination.ip:%20%22127.0.0.1%22)%20or%20(source.ip:%20%22127.0.0.2%22%20or%20destination.ip:%20%22127.0.0.2%22))%20and%20((process.name:%20%22conhost.exe%22%20or%20process.name:%20%22sc.exe%22))%27)&timerange=(global:(linkTo:!(timeline),timerange:(from:%272019-08-28T11:00:00.000Z%27,kind:absolute,to:%272019-08-28T13:59:59.999Z%27)),timeline:(linkTo:!(global),timerange:(from:%272019-08-28T11:00:00.000Z%27,kind:absolute,to:%272019-08-28T13:59:59.999Z%27)))&sourcerer=(default:(id:security-solution-default,selectedPatterns:!(%27auditbeat-*%27)))' ); }); @@ -130,15 +130,16 @@ describe('ml conditional links', () => { loginAndWaitForPageWithoutDateRange(mlNetworkNullKqlQuery); cy.url().should( 'include', - '/app/security/network/flows?timerange=(global:(linkTo:!(timeline),timerange:(from:%272019-08-28T11:00:00.000Z%27,kind:absolute,to:%272019-08-28T13:59:59.999Z%27)),timeline:(linkTo:!(global),timerange:(from:%272019-08-28T11:00:00.000Z%27,kind:absolute,to:%272019-08-28T13:59:59.999Z%27)))&sourcerer=(default:!(%27auditbeat-*%27))' + '/app/security/network/flows?timerange=(global:(linkTo:!(timeline),timerange:(from:%272019-08-28T11:00:00.000Z%27,kind:absolute,to:%272019-08-28T13:59:59.999Z%27)),timeline:(linkTo:!(global),timerange:(from:%272019-08-28T11:00:00.000Z%27,kind:absolute,to:%272019-08-28T13:59:59.999Z%27)))&sourcerer=(default:(id:security-solution-default,selectedPatterns:!(%27auditbeat-*%27)))' ); }); it('redirects from a $ip$ with a value for the query', () => { loginAndWaitForPageWithoutDateRange(mlNetworkKqlQuery); + cy.url().should( 'include', - '/app/security/network/flows?query=(language:kuery,query:%27(process.name:%20%22conhost.exe%22%20or%20process.name:%20%22sc.exe%22)%27)&timerange=(global:(linkTo:!(timeline),timerange:(from:%272019-08-28T11:00:00.000Z%27,kind:absolute,to:%272019-08-28T13:59:59.999Z%27)),timeline:(linkTo:!(global),timerange:(from:%272019-08-28T11:00:00.000Z%27,kind:absolute,to:%272019-08-28T13:59:59.999Z%27)))&sourcerer=(default:!(%27auditbeat-*%27))' + `/app/security/network/flows?query=(language:kuery,query:%27(process.name:%20%22conhost.exe%22%20or%20process.name:%20%22sc.exe%22)%27)&timerange=(global:(linkTo:!(timeline),timerange:(from:%272019-08-28T11:00:00.000Z%27,kind:absolute,to:%272019-08-28T13:59:59.999Z%27)),timeline:(linkTo:!(global),timerange:(from:%272019-08-28T11:00:00.000Z%27,kind:absolute,to:%272019-08-28T13:59:59.999Z%27)))&sourcerer=(default:(id:security-solution-default,selectedPatterns:!(%27auditbeat-*%27)))` ); }); @@ -146,7 +147,7 @@ describe('ml conditional links', () => { loginAndWaitForPageWithoutDateRange(mlHostSingleHostNullKqlQuery); cy.url().should( 'include', - '/app/security/hosts/siem-windows/anomalies?timerange=(global:(linkTo:!(timeline),timerange:(from:%272019-06-06T06:00:00.000Z%27,kind:absolute,to:%272019-06-07T05:59:59.999Z%27)),timeline:(linkTo:!(global),timerange:(from:%272019-06-06T06:00:00.000Z%27,kind:absolute,to:%272019-06-07T05:59:59.999Z%27)))&sourcerer=(default:!(%27auditbeat-*%27))' + '/app/security/hosts/siem-windows/anomalies?timerange=(global:(linkTo:!(timeline),timerange:(from:%272019-06-06T06:00:00.000Z%27,kind:absolute,to:%272019-06-07T05:59:59.999Z%27)),timeline:(linkTo:!(global),timerange:(from:%272019-06-06T06:00:00.000Z%27,kind:absolute,to:%272019-06-07T05:59:59.999Z%27)))&sourcerer=(default:(id:security-solution-default,selectedPatterns:!(%27auditbeat-*%27)))' ); }); @@ -154,7 +155,7 @@ describe('ml conditional links', () => { loginAndWaitForPageWithoutDateRange(mlHostSingleHostKqlQueryVariable); cy.url().should( 'include', - '/app/security/hosts/siem-windows/anomalies?timerange=(global:(linkTo:!(timeline),timerange:(from:%272019-06-06T06:00:00.000Z%27,kind:absolute,to:%272019-06-07T05:59:59.999Z%27)),timeline:(linkTo:!(global),timerange:(from:%272019-06-06T06:00:00.000Z%27,kind:absolute,to:%272019-06-07T05:59:59.999Z%27)))&sourcerer=(default:!(%27auditbeat-*%27))' + '/app/security/hosts/siem-windows/anomalies?timerange=(global:(linkTo:!(timeline),timerange:(from:%272019-06-06T06:00:00.000Z%27,kind:absolute,to:%272019-06-07T05:59:59.999Z%27)),timeline:(linkTo:!(global),timerange:(from:%272019-06-06T06:00:00.000Z%27,kind:absolute,to:%272019-06-07T05:59:59.999Z%27)))&sourcerer=(default:(id:security-solution-default,selectedPatterns:!(%27auditbeat-*%27)))' ); }); @@ -162,7 +163,7 @@ describe('ml conditional links', () => { loginAndWaitForPageWithoutDateRange(mlHostSingleHostKqlQuery); cy.url().should( 'include', - '/app/security/hosts/siem-windows/anomalies?query=(language:kuery,query:%27(process.name:%20%22conhost.exe%22%20or%20process.name:%20%22sc.exe%22)%27)&timerange=(global:(linkTo:!(timeline),timerange:(from:%272019-06-06T06:00:00.000Z%27,kind:absolute,to:%272019-06-07T05:59:59.999Z%27)),timeline:(linkTo:!(global),timerange:(from:%272019-06-06T06:00:00.000Z%27,kind:absolute,to:%272019-06-07T05:59:59.999Z%27)))&sourcerer=(default:!(%27auditbeat-*%27))' + '/app/security/hosts/siem-windows/anomalies?query=(language:kuery,query:%27(process.name:%20%22conhost.exe%22%20or%20process.name:%20%22sc.exe%22)%27)&timerange=(global:(linkTo:!(timeline),timerange:(from:%272019-06-06T06:00:00.000Z%27,kind:absolute,to:%272019-06-07T05:59:59.999Z%27)),timeline:(linkTo:!(global),timerange:(from:%272019-06-06T06:00:00.000Z%27,kind:absolute,to:%272019-06-07T05:59:59.999Z%27)))&sourcerer=(default:(id:security-solution-default,selectedPatterns:!(%27auditbeat-*%27)))' ); }); @@ -170,7 +171,7 @@ describe('ml conditional links', () => { loginAndWaitForPageWithoutDateRange(mlHostMultiHostNullKqlQuery); cy.url().should( 'include', - '/app/security/hosts/anomalies?query=(language:kuery,query:%27(host.name:%20%22siem-windows%22%20or%20host.name:%20%22siem-suricata%22)%27)&timerange=(global:(linkTo:!(timeline),timerange:(from:%272019-06-06T06:00:00.000Z%27,kind:absolute,to:%272019-06-07T05:59:59.999Z%27)),timeline:(linkTo:!(global),timerange:(from:%272019-06-06T06:00:00.000Z%27,kind:absolute,to:%272019-06-07T05:59:59.999Z%27)))&sourcerer=(default:!(%27auditbeat-*%27))' + '/app/security/hosts/anomalies?query=(language:kuery,query:%27(host.name:%20%22siem-windows%22%20or%20host.name:%20%22siem-suricata%22)%27)&timerange=(global:(linkTo:!(timeline),timerange:(from:%272019-06-06T06:00:00.000Z%27,kind:absolute,to:%272019-06-07T05:59:59.999Z%27)),timeline:(linkTo:!(global),timerange:(from:%272019-06-06T06:00:00.000Z%27,kind:absolute,to:%272019-06-07T05:59:59.999Z%27)))&sourcerer=(default:(id:security-solution-default,selectedPatterns:!(%27auditbeat-*%27)))' ); }); @@ -178,7 +179,7 @@ describe('ml conditional links', () => { loginAndWaitForPageWithoutDateRange(mlHostMultiHostKqlQuery); cy.url().should( 'include', - '/app/security/hosts/anomalies?query=(language:kuery,query:%27(host.name:%20%22siem-windows%22%20or%20host.name:%20%22siem-suricata%22)%20and%20((process.name:%20%22conhost.exe%22%20or%20process.name:%20%22sc.exe%22))%27)&timerange=(global:(linkTo:!(timeline),timerange:(from:%272019-06-06T06:00:00.000Z%27,kind:absolute,to:%272019-06-07T05:59:59.999Z%27)),timeline:(linkTo:!(global),timerange:(from:%272019-06-06T06:00:00.000Z%27,kind:absolute,to:%272019-06-07T05:59:59.999Z%27)))&sourcerer=(default:!(%27auditbeat-*%27))' + '/app/security/hosts/anomalies?query=(language:kuery,query:%27(host.name:%20%22siem-windows%22%20or%20host.name:%20%22siem-suricata%22)%20and%20((process.name:%20%22conhost.exe%22%20or%20process.name:%20%22sc.exe%22))%27)&timerange=(global:(linkTo:!(timeline),timerange:(from:%272019-06-06T06:00:00.000Z%27,kind:absolute,to:%272019-06-07T05:59:59.999Z%27)),timeline:(linkTo:!(global),timerange:(from:%272019-06-06T06:00:00.000Z%27,kind:absolute,to:%272019-06-07T05:59:59.999Z%27)))&sourcerer=(default:(id:security-solution-default,selectedPatterns:!(%27auditbeat-*%27)))' ); }); @@ -186,7 +187,7 @@ describe('ml conditional links', () => { loginAndWaitForPageWithoutDateRange(mlHostVariableHostNullKqlQuery); cy.url().should( 'include', - '/app/security/hosts/anomalies?timerange=(global:(linkTo:!(timeline),timerange:(from:%272019-06-06T06:00:00.000Z%27,kind:absolute,to:%272019-06-07T05:59:59.999Z%27)),timeline:(linkTo:!(global),timerange:(from:%272019-06-06T06:00:00.000Z%27,kind:absolute,to:%272019-06-07T05:59:59.999Z%27)))&sourcerer=(default:!(%27auditbeat-*%27))' + '/app/security/hosts/anomalies?timerange=(global:(linkTo:!(timeline),timerange:(from:%272019-06-06T06:00:00.000Z%27,kind:absolute,to:%272019-06-07T05:59:59.999Z%27)),timeline:(linkTo:!(global),timerange:(from:%272019-06-06T06:00:00.000Z%27,kind:absolute,to:%272019-06-07T05:59:59.999Z%27)))&sourcerer=(default:(id:security-solution-default,selectedPatterns:!(%27auditbeat-*%27)))' ); }); @@ -194,7 +195,7 @@ describe('ml conditional links', () => { loginAndWaitForPageWithoutDateRange(mlHostVariableHostKqlQuery); cy.url().should( 'include', - '/app/security/hosts/anomalies?query=(language:kuery,query:%27(process.name:%20%22conhost.exe%22%20or%20process.name:%20%22sc.exe%22)%27)&timerange=(global:(linkTo:!(timeline),timerange:(from:%272019-06-06T06:00:00.000Z%27,kind:absolute,to:%272019-06-07T05:59:59.999Z%27)),timeline:(linkTo:!(global),timerange:(from:%272019-06-06T06:00:00.000Z%27,kind:absolute,to:%272019-06-07T05:59:59.999Z%27)))&sourcerer=(default:!(%27auditbeat-*%27))' + '/app/security/hosts/anomalies?query=(language:kuery,query:%27(process.name:%20%22conhost.exe%22%20or%20process.name:%20%22sc.exe%22)%27)&timerange=(global:(linkTo:!(timeline),timerange:(from:%272019-06-06T06:00:00.000Z%27,kind:absolute,to:%272019-06-07T05:59:59.999Z%27)),timeline:(linkTo:!(global),timerange:(from:%272019-06-06T06:00:00.000Z%27,kind:absolute,to:%272019-06-07T05:59:59.999Z%27)))&sourcerer=(default:(id:security-solution-default,selectedPatterns:!(%27auditbeat-*%27)))' ); }); }); diff --git a/x-pack/plugins/security_solution/cypress/integration/timelines/creation.spec.ts b/x-pack/plugins/security_solution/cypress/integration/timelines/creation.spec.ts index fb41aec91b6c4..cbff911e5d982 100644 --- a/x-pack/plugins/security_solution/cypress/integration/timelines/creation.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/timelines/creation.spec.ts @@ -121,7 +121,6 @@ describe('Create a timeline from a template', () => { loginAndWaitForPageWithoutDateRange(TIMELINE_TEMPLATES_URL); waitForTimelinesPanelToBeLoaded(); }); - it('Should have the same query and open the timeline modal', () => { selectCustomTemplates(); cy.wait('@timeline', { timeout: 100000 }); @@ -132,5 +131,6 @@ describe('Create a timeline from a template', () => { cy.get(TIMELINE_FLYOUT_WRAPPER).should('have.css', 'visibility', 'visible'); cy.get(TIMELINE_DESCRIPTION).should('have.text', getTimeline().description); cy.get(TIMELINE_QUERY).should('have.text', getTimeline().query); + closeTimeline(); }); }); diff --git a/x-pack/plugins/security_solution/cypress/integration/timelines/row_renderers.spec.ts b/x-pack/plugins/security_solution/cypress/integration/timelines/row_renderers.spec.ts index 0755142fbdc58..2219339d0577d 100644 --- a/x-pack/plugins/security_solution/cypress/integration/timelines/row_renderers.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/timelines/row_renderers.spec.ts @@ -5,12 +5,16 @@ * 2.0. */ +import { elementsOverlap } from '../../helpers/rules'; import { TIMELINE_ROW_RENDERERS_DISABLE_ALL_BTN, TIMELINE_ROW_RENDERERS_MODAL_CLOSE_BUTTON, TIMELINE_ROW_RENDERERS_MODAL_ITEMS_CHECKBOX, TIMELINE_ROW_RENDERERS_SEARCHBOX, TIMELINE_SHOW_ROW_RENDERERS_GEAR, + TIMELINE_ROW_RENDERERS_SURICATA_SIGNATURE, + TIMELINE_ROW_RENDERERS_SURICATA_SIGNATURE_TOOLTIP, + TIMELINE_ROW_RENDERERS_SURICATA_LINK_TOOLTIP, } from '../../screens/timeline'; import { cleanKibana } from '../../tasks/common'; @@ -81,4 +85,22 @@ describe('Row renderers', () => { cy.wait('@updateTimeline').its('response.statusCode').should('eq', 200); }); + + describe('Suricata', () => { + it('Signature tooltips do not overlap', () => { + // Hover the signature to show the tooltips + cy.get(TIMELINE_ROW_RENDERERS_SURICATA_SIGNATURE) + .parents('.euiPopover__anchor') + .trigger('mouseover'); + + cy.get(TIMELINE_ROW_RENDERERS_SURICATA_LINK_TOOLTIP).then(($googleLinkTooltip) => { + cy.get(TIMELINE_ROW_RENDERERS_SURICATA_SIGNATURE_TOOLTIP).then(($signatureTooltip) => { + expect( + elementsOverlap($googleLinkTooltip, $signatureTooltip), + 'tooltips do not overlap' + ).to.equal(false); + }); + }); + }); + }); }); diff --git a/x-pack/plugins/security_solution/cypress/integration/urls/state.spec.ts b/x-pack/plugins/security_solution/cypress/integration/urls/state.spec.ts index 73eb141f1ce3d..28fe1294e6f01 100644 --- a/x-pack/plugins/security_solution/cypress/integration/urls/state.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/urls/state.spec.ts @@ -182,11 +182,10 @@ describe('url state', () => { loginAndWaitForPageWithoutDateRange(ABSOLUTE_DATE_RANGE.url); kqlSearch('source.ip: "10.142.0.9" {enter}'); navigateFromHeaderTo(HOSTS); - cy.get(NETWORK).should( 'have.attr', 'href', - `/app/security/network?query=(language:kuery,query:'source.ip:%20%2210.142.0.9%22%20')&sourcerer=(default:!(\'auditbeat-*\'))&timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-08-01T20:03:29.186Z',kind:absolute,to:'2019-08-01T20:33:29.186Z')),timeline:(linkTo:!(global),timerange:(from:'2019-08-01T20:03:29.186Z',kind:absolute,to:'2019-08-01T20:33:29.186Z')))` + `/app/security/network?query=(language:kuery,query:'source.ip:%20%2210.142.0.9%22%20')&sourcerer=(default:(id:security-solution-default,selectedPatterns:!('auditbeat-*')))&timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-08-01T20:03:29.186Z',kind:absolute,to:'2019-08-01T20:33:29.186Z')),timeline:(linkTo:!(global),timerange:(from:'2019-08-01T20:03:29.186Z',kind:absolute,to:'2019-08-01T20:33:29.186Z')))` ); }); @@ -199,12 +198,12 @@ describe('url state', () => { cy.get(HOSTS).should( 'have.attr', 'href', - `/app/security/hosts?query=(language:kuery,query:'host.name:%20%22siem-kibana%22%20')&sourcerer=(default:!(\'auditbeat-*\'))&timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-08-01T20:03:29.186Z',kind:absolute,to:'2020-01-01T21:33:29.186Z')),timeline:(linkTo:!(global),timerange:(from:'2019-08-01T20:03:29.186Z',kind:absolute,to:'2020-01-01T21:33:29.186Z')))` + `/app/security/hosts?query=(language:kuery,query:'host.name:%20%22siem-kibana%22%20')&sourcerer=(default:(id:security-solution-default,selectedPatterns:!('auditbeat-*')))&timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-08-01T20:03:29.186Z',kind:absolute,to:'2020-01-01T21:33:29.186Z')),timeline:(linkTo:!(global),timerange:(from:'2019-08-01T20:03:29.186Z',kind:absolute,to:'2020-01-01T21:33:29.186Z')))` ); cy.get(NETWORK).should( 'have.attr', 'href', - `/app/security/network?query=(language:kuery,query:'host.name:%20%22siem-kibana%22%20')&sourcerer=(default:!(\'auditbeat-*\'))&timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-08-01T20:03:29.186Z',kind:absolute,to:'2020-01-01T21:33:29.186Z')),timeline:(linkTo:!(global),timerange:(from:'2019-08-01T20:03:29.186Z',kind:absolute,to:'2020-01-01T21:33:29.186Z')))` + `/app/security/network?query=(language:kuery,query:'host.name:%20%22siem-kibana%22%20')&sourcerer=(default:(id:security-solution-default,selectedPatterns:!('auditbeat-*')))&timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-08-01T20:03:29.186Z',kind:absolute,to:'2020-01-01T21:33:29.186Z')),timeline:(linkTo:!(global),timerange:(from:'2019-08-01T20:03:29.186Z',kind:absolute,to:'2020-01-01T21:33:29.186Z')))` ); cy.get(HOSTS_NAMES).first().should('have.text', 'siem-kibana'); @@ -215,21 +214,21 @@ describe('url state', () => { cy.get(ANOMALIES_TAB).should( 'have.attr', 'href', - "/app/security/hosts/siem-kibana/anomalies?sourcerer=(default:!('auditbeat-*'))&timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-08-01T20:03:29.186Z',kind:absolute,to:'2020-01-01T21:33:29.186Z')),timeline:(linkTo:!(global),timerange:(from:'2019-08-01T20:03:29.186Z',kind:absolute,to:'2020-01-01T21:33:29.186Z')))&query=(language:kuery,query:'agent.type:%20%22auditbeat%22%20')" + "/app/security/hosts/siem-kibana/anomalies?sourcerer=(default:(id:security-solution-default,selectedPatterns:!('auditbeat-*')))&timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-08-01T20:03:29.186Z',kind:absolute,to:'2020-01-01T21:33:29.186Z')),timeline:(linkTo:!(global),timerange:(from:'2019-08-01T20:03:29.186Z',kind:absolute,to:'2020-01-01T21:33:29.186Z')))&query=(language:kuery,query:'agent.type:%20%22auditbeat%22%20')" ); cy.get(BREADCRUMBS) .eq(1) .should( 'have.attr', 'href', - `/app/security/hosts?query=(language:kuery,query:'agent.type:%20%22auditbeat%22%20')&sourcerer=(default:!(\'auditbeat-*\'))&timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-08-01T20:03:29.186Z',kind:absolute,to:'2020-01-01T21:33:29.186Z')),timeline:(linkTo:!(global),timerange:(from:'2019-08-01T20:03:29.186Z',kind:absolute,to:'2020-01-01T21:33:29.186Z')))` + `/app/security/hosts?query=(language:kuery,query:'agent.type:%20%22auditbeat%22%20')&sourcerer=(default:(id:security-solution-default,selectedPatterns:!('auditbeat-*')))&timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-08-01T20:03:29.186Z',kind:absolute,to:'2020-01-01T21:33:29.186Z')),timeline:(linkTo:!(global),timerange:(from:'2019-08-01T20:03:29.186Z',kind:absolute,to:'2020-01-01T21:33:29.186Z')))` ); cy.get(BREADCRUMBS) .eq(2) .should( 'have.attr', 'href', - `/app/security/hosts/siem-kibana?query=(language:kuery,query:'agent.type:%20%22auditbeat%22%20')&sourcerer=(default:!(\'auditbeat-*\'))&timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-08-01T20:03:29.186Z',kind:absolute,to:'2020-01-01T21:33:29.186Z')),timeline:(linkTo:!(global),timerange:(from:'2019-08-01T20:03:29.186Z',kind:absolute,to:'2020-01-01T21:33:29.186Z')))` + `/app/security/hosts/siem-kibana?query=(language:kuery,query:'agent.type:%20%22auditbeat%22%20')&sourcerer=(default:(id:security-solution-default,selectedPatterns:!('auditbeat-*')))&timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-08-01T20:03:29.186Z',kind:absolute,to:'2020-01-01T21:33:29.186Z')),timeline:(linkTo:!(global),timerange:(from:'2019-08-01T20:03:29.186Z',kind:absolute,to:'2020-01-01T21:33:29.186Z')))` ); }); diff --git a/x-pack/plugins/security_solution/cypress/objects/timeline.ts b/x-pack/plugins/security_solution/cypress/objects/timeline.ts index f3d9bc1b9ef1a..70b8c1b400d51 100644 --- a/x-pack/plugins/security_solution/cypress/objects/timeline.ts +++ b/x-pack/plugins/security_solution/cypress/objects/timeline.ts @@ -87,6 +87,7 @@ export const expectedExportedTimelineTemplate = ( }, }, }, + dataViewId: timelineTemplateBody.dataViewId, dateRange: { start: timelineTemplateBody.dateRange?.start, end: timelineTemplateBody.dateRange?.end, @@ -127,6 +128,7 @@ export const expectedExportedTimeline = (timelineResponse: Cypress.Response