diff --git a/.buildkite/pipelines/es_snapshots/verify.yml b/.buildkite/pipelines/es_snapshots/verify.yml index aa49ba5d2641f..9af2e938db49d 100755 --- a/.buildkite/pipelines/es_snapshots/verify.yml +++ b/.buildkite/pipelines/es_snapshots/verify.yml @@ -29,6 +29,7 @@ steps: agents: queue: ci-group-6 depends_on: build + timeout_in_minutes: 150 key: default-cigroup retry: automatic: @@ -40,6 +41,7 @@ steps: agents: queue: ci-group-6 depends_on: build + timeout_in_minutes: 120 key: default-cigroup-docker retry: automatic: @@ -52,6 +54,7 @@ steps: agents: queue: ci-group-4d depends_on: build + timeout_in_minutes: 120 key: oss-cigroup retry: automatic: @@ -61,7 +64,8 @@ steps: - command: .buildkite/scripts/steps/test/jest_integration.sh label: 'Jest Integration Tests' agents: - queue: jest + queue: n2-4 + timeout_in_minutes: 120 key: jest-integration retry: automatic: @@ -72,6 +76,7 @@ steps: label: 'API Integration Tests' agents: queue: jest + timeout_in_minutes: 120 key: api-integration - command: .buildkite/scripts/steps/es_snapshots/trigger_promote.sh diff --git a/.buildkite/pipelines/hourly.yml b/.buildkite/pipelines/hourly.yml index 0dc8ad3e08d08..89541023be8e2 100644 --- a/.buildkite/pipelines/hourly.yml +++ b/.buildkite/pipelines/hourly.yml @@ -1,3 +1,5 @@ +env: + REPORT_FAILED_TESTS_TO_GITHUB: 'true' steps: - command: .buildkite/scripts/lifecycle/pre_build.sh label: Pre-Build @@ -17,7 +19,7 @@ steps: agents: queue: ci-group-6 depends_on: build - timeout_in_minutes: 120 + timeout_in_minutes: 150 key: default-cigroup retry: automatic: @@ -118,14 +120,14 @@ steps: - command: .buildkite/scripts/steps/test/jest_integration.sh label: 'Jest Integration Tests' agents: - queue: jest + queue: n2-4 timeout_in_minutes: 120 key: jest-integration - 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/scripts/lifecycle/post_command.sh b/.buildkite/scripts/lifecycle/post_command.sh index 14391fdd1e698..23f44a586e978 100755 --- a/.buildkite/scripts/lifecycle/post_command.sh +++ b/.buildkite/scripts/lifecycle/post_command.sh @@ -22,6 +22,5 @@ if [[ "$IS_TEST_EXECUTION_STEP" == "true" ]]; then buildkite-agent artifact upload 'x-pack/test/functional/failure_debug/html/*.html' buildkite-agent artifact upload '.es/**/*.hprof' - # TODO - re-enable when Jenkins is disabled - # node scripts/report_failed_tests --build-url="${BUILDKITE_BUILD_URL}#${BUILDKITE_JOB_ID}" 'target/junit/**/*.xml' + node scripts/report_failed_tests --build-url="${BUILDKITE_BUILD_URL}#${BUILDKITE_JOB_ID}" 'target/junit/**/*.xml' fi diff --git a/.buildkite/scripts/steps/storybooks/build_and_upload.js b/.buildkite/scripts/steps/storybooks/build_and_upload.js index 9d72f518837e9..49e36d2126cd4 100644 --- a/.buildkite/scripts/steps/storybooks/build_and_upload.js +++ b/.buildkite/scripts/steps/storybooks/build_and_upload.js @@ -6,13 +6,21 @@ const path = require('path'); const STORYBOOKS = [ 'apm', 'canvas', + 'codeeditor', 'ci_composite', 'url_template_editor', - 'codeeditor', 'dashboard', 'dashboard_enhanced', 'data_enhanced', 'embeddable', + 'expression_error', + 'expression_image', + 'expression_metric', + 'expression_repeat_image', + 'expression_reveal_image', + 'expression_shape', + 'expression_tagcloud', + 'fleet', 'infra', 'security_solution', 'ui_actions_enhanced', diff --git a/.buildkite/scripts/steps/test/jest_integration.sh b/.buildkite/scripts/steps/test/jest_integration.sh index 9f0228fd910bf..458651df2df2a 100755 --- a/.buildkite/scripts/steps/test/jest_integration.sh +++ b/.buildkite/scripts/steps/test/jest_integration.sh @@ -10,4 +10,4 @@ is_test_execution_step echo '--- Jest Integration Tests' checks-reporter-with-killswitch "Jest Integration Tests" \ - node scripts/jest_integration --ci --verbose + node --max-old-space-size=5120 scripts/jest_integration --ci diff --git a/.eslintrc.js b/.eslintrc.js index 27a7b38d4810a..213d602345b12 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -1506,7 +1506,7 @@ module.exports = { * TSVB overrides */ { - files: ['src/plugins/vis_type_timeseries/**/*.{js,mjs,ts,tsx}'], + files: ['src/plugins/vis_types/timeseries/**/*.{js,mjs,ts,tsx}'], rules: { 'import/no-default-export': 'error', }, diff --git a/.i18nrc.json b/.i18nrc.json index 5c1fafb093358..0fb32af4c5806 100644 --- a/.i18nrc.json +++ b/.i18nrc.json @@ -65,7 +65,7 @@ "visTypeMetric": "src/plugins/vis_types/metric", "visTypeTable": "src/plugins/vis_types/table", "visTypeTagCloud": "src/plugins/vis_types/tagcloud", - "visTypeTimeseries": "src/plugins/vis_type_timeseries", + "visTypeTimeseries": "src/plugins/vis_types/timeseries", "visTypeVega": "src/plugins/vis_types/vega", "visTypeVislib": "src/plugins/vis_types/vislib", "visTypeXy": "src/plugins/vis_types/xy", diff --git a/Jenkinsfile b/Jenkinsfile index db5ae306e6e2e..7dd3d0f41d27a 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -1,5 +1,10 @@ #!/bin/groovy +if (!env.ghprbPullId) { + print "Non-PR builds are now in Buildkite." + return +} + library 'kibana-pipeline-library' kibanaLibrary.load() diff --git a/api_docs/data.json b/api_docs/data.json index e5f1a3219d9e4..f333bbf80ec97 100644 --- a/api_docs/data.json +++ b/api_docs/data.json @@ -2716,19 +2716,19 @@ "references": [ { "plugin": "visTypeTimeseries", - "path": "src/plugins/vis_type_timeseries/server/lib/search_strategies/lib/cached_index_pattern_fetcher.test.ts" + "path": "src/plugins/vis_types/timeseries/server/lib/search_strategies/lib/cached_index_pattern_fetcher.test.ts" }, { "plugin": "visTypeTimeseries", - "path": "src/plugins/vis_type_timeseries/server/lib/search_strategies/lib/cached_index_pattern_fetcher.test.ts" + "path": "src/plugins/vis_types/timeseries/server/lib/search_strategies/lib/cached_index_pattern_fetcher.test.ts" }, { "plugin": "visTypeTimeseries", - "path": "src/plugins/vis_type_timeseries/server/lib/search_strategies/lib/cached_index_pattern_fetcher.test.ts" + "path": "src/plugins/vis_types/timeseries/server/lib/search_strategies/lib/cached_index_pattern_fetcher.test.ts" }, { "plugin": "visTypeTimeseries", - "path": "src/plugins/vis_type_timeseries/server/lib/search_strategies/lib/cached_index_pattern_fetcher.test.ts" + "path": "src/plugins/vis_types/timeseries/server/lib/search_strategies/lib/cached_index_pattern_fetcher.test.ts" }, { "plugin": "reporting", @@ -3068,11 +3068,11 @@ }, { "plugin": "visTypeTimeseries", - "path": "src/plugins/vis_type_timeseries/common/types/index.ts" + "path": "src/plugins/vis_types/timeseries/common/types/index.ts" }, { "plugin": "visTypeTimeseries", - "path": "src/plugins/vis_type_timeseries/common/types/index.ts" + "path": "src/plugins/vis_types/timeseries/common/types/index.ts" }, { "plugin": "osquery", @@ -3228,43 +3228,43 @@ }, { "plugin": "visTypeTimeseries", - "path": "src/plugins/vis_type_timeseries/common/index_patterns_utils.test.ts" + "path": "src/plugins/vis_types/timeseries/common/index_patterns_utils.test.ts" }, { "plugin": "visTypeTimeseries", - "path": "src/plugins/vis_type_timeseries/common/index_patterns_utils.test.ts" + "path": "src/plugins/vis_types/timeseries/common/index_patterns_utils.test.ts" }, { "plugin": "visTypeTimeseries", - "path": "src/plugins/vis_type_timeseries/common/index_patterns_utils.test.ts" + "path": "src/plugins/vis_types/timeseries/common/index_patterns_utils.test.ts" }, { "plugin": "visTypeTimeseries", - "path": "src/plugins/vis_type_timeseries/common/index_patterns_utils.test.ts" + "path": "src/plugins/vis_types/timeseries/common/index_patterns_utils.test.ts" }, { "plugin": "visTypeTimeseries", - "path": "src/plugins/vis_type_timeseries/public/application/components/lib/index_pattern_select/types.ts" + "path": "src/plugins/vis_types/timeseries/public/application/components/lib/index_pattern_select/types.ts" }, { "plugin": "visTypeTimeseries", - "path": "src/plugins/vis_type_timeseries/public/application/components/lib/index_pattern_select/types.ts" + "path": "src/plugins/vis_types/timeseries/public/application/components/lib/index_pattern_select/types.ts" }, { "plugin": "visTypeTimeseries", - "path": "src/plugins/vis_type_timeseries/public/application/components/lib/index_pattern_select/index_pattern_select.tsx" + "path": "src/plugins/vis_types/timeseries/public/application/components/lib/index_pattern_select/index_pattern_select.tsx" }, { "plugin": "visTypeTimeseries", - "path": "src/plugins/vis_type_timeseries/public/application/components/lib/index_pattern_select/index_pattern_select.tsx" + "path": "src/plugins/vis_types/timeseries/public/application/components/lib/index_pattern_select/index_pattern_select.tsx" }, { "plugin": "visTypeTimeseries", - "path": "src/plugins/vis_type_timeseries/public/application/components/timeseries_visualization.tsx" + "path": "src/plugins/vis_types/timeseries/public/application/components/timeseries_visualization.tsx" }, { "plugin": "visTypeTimeseries", - "path": "src/plugins/vis_type_timeseries/public/application/components/timeseries_visualization.tsx" + "path": "src/plugins/vis_types/timeseries/public/application/components/timeseries_visualization.tsx" }, { "plugin": "discover", @@ -5132,15 +5132,15 @@ }, { "plugin": "visTypeTimeseries", - "path": "src/plugins/vis_type_timeseries/public/application/components/lib/convert_series_to_datatable.ts" + "path": "src/plugins/vis_types/timeseries/public/application/components/lib/convert_series_to_datatable.ts" }, { "plugin": "visTypeTimeseries", - "path": "src/plugins/vis_type_timeseries/public/application/components/lib/convert_series_to_datatable.ts" + "path": "src/plugins/vis_types/timeseries/public/application/components/lib/convert_series_to_datatable.ts" }, { "plugin": "visTypeTimeseries", - "path": "src/plugins/vis_type_timeseries/public/application/components/lib/convert_series_to_datatable.ts" + "path": "src/plugins/vis_types/timeseries/public/application/components/lib/convert_series_to_datatable.ts" }, { "plugin": "inputControlVis", @@ -5304,15 +5304,15 @@ }, { "plugin": "visTypeTimeseries", - "path": "src/plugins/vis_type_timeseries/public/application/components/lib/convert_series_to_datatable.test.ts" + "path": "src/plugins/vis_types/timeseries/public/application/components/lib/convert_series_to_datatable.test.ts" }, { "plugin": "visTypeTimeseries", - "path": "src/plugins/vis_type_timeseries/public/application/components/lib/convert_series_to_datatable.test.ts" + "path": "src/plugins/vis_types/timeseries/public/application/components/lib/convert_series_to_datatable.test.ts" }, { "plugin": "visTypeTimeseries", - "path": "src/plugins/vis_type_timeseries/public/application/components/lib/convert_series_to_datatable.test.ts" + "path": "src/plugins/vis_types/timeseries/public/application/components/lib/convert_series_to_datatable.test.ts" }, { "plugin": "discover", @@ -5336,15 +5336,15 @@ }, { "plugin": "visTypeTimeseries", - "path": "src/plugins/vis_type_timeseries/target/types/public/application/components/lib/convert_series_to_datatable.d.ts" + "path": "src/plugins/vis_types/timeseries/target/types/public/application/components/lib/convert_series_to_datatable.d.ts" }, { "plugin": "visTypeTimeseries", - "path": "src/plugins/vis_type_timeseries/target/types/public/application/components/lib/convert_series_to_datatable.d.ts" + "path": "src/plugins/vis_types/timeseries/target/types/public/application/components/lib/convert_series_to_datatable.d.ts" }, { "plugin": "visTypeTimeseries", - "path": "src/plugins/vis_type_timeseries/target/types/public/application/components/lib/convert_series_to_datatable.d.ts" + "path": "src/plugins/vis_types/timeseries/target/types/public/application/components/lib/convert_series_to_datatable.d.ts" }, { "plugin": "discover", @@ -6729,27 +6729,27 @@ }, { "plugin": "visTypeTimeseries", - "path": "src/plugins/vis_type_timeseries/public/application/components/lib/convert_series_to_datatable.test.ts" + "path": "src/plugins/vis_types/timeseries/public/application/components/lib/convert_series_to_datatable.test.ts" }, { "plugin": "visTypeTimeseries", - "path": "src/plugins/vis_type_timeseries/public/application/components/lib/convert_series_to_datatable.test.ts" + "path": "src/plugins/vis_types/timeseries/public/application/components/lib/convert_series_to_datatable.test.ts" }, { "plugin": "visTypeTimeseries", - "path": "src/plugins/vis_type_timeseries/public/application/components/lib/convert_series_to_datatable.test.ts" + "path": "src/plugins/vis_types/timeseries/public/application/components/lib/convert_series_to_datatable.test.ts" }, { "plugin": "visTypeTimeseries", - "path": "src/plugins/vis_type_timeseries/public/application/components/lib/convert_series_to_datatable.test.ts" + "path": "src/plugins/vis_types/timeseries/public/application/components/lib/convert_series_to_datatable.test.ts" }, { "plugin": "visTypeTimeseries", - "path": "src/plugins/vis_type_timeseries/public/application/components/lib/convert_series_to_datatable.test.ts" + "path": "src/plugins/vis_types/timeseries/public/application/components/lib/convert_series_to_datatable.test.ts" }, { "plugin": "visTypeTimeseries", - "path": "src/plugins/vis_type_timeseries/public/application/components/lib/convert_series_to_datatable.test.ts" + "path": "src/plugins/vis_types/timeseries/public/application/components/lib/convert_series_to_datatable.test.ts" }, { "plugin": "discover", @@ -7030,51 +7030,51 @@ "references": [ { "plugin": "visTypeTimeseries", - "path": "src/plugins/vis_type_timeseries/server/lib/search_strategies/strategies/abstract_search_strategy.ts" + "path": "src/plugins/vis_types/timeseries/server/lib/search_strategies/strategies/abstract_search_strategy.ts" }, { "plugin": "visTypeTimeseries", - "path": "src/plugins/vis_type_timeseries/server/lib/search_strategies/strategies/abstract_search_strategy.ts" + "path": "src/plugins/vis_types/timeseries/server/lib/search_strategies/strategies/abstract_search_strategy.ts" }, { "plugin": "visTypeTimeseries", - "path": "src/plugins/vis_type_timeseries/server/lib/search_strategies/strategies/default_search_strategy.ts" + "path": "src/plugins/vis_types/timeseries/server/lib/search_strategies/strategies/default_search_strategy.ts" }, { "plugin": "visTypeTimeseries", - "path": "src/plugins/vis_type_timeseries/server/lib/search_strategies/strategies/default_search_strategy.ts" + "path": "src/plugins/vis_types/timeseries/server/lib/search_strategies/strategies/default_search_strategy.ts" }, { "plugin": "visTypeTimeseries", - "path": "src/plugins/vis_type_timeseries/server/lib/search_strategies/lib/cached_index_pattern_fetcher.ts" + "path": "src/plugins/vis_types/timeseries/server/lib/search_strategies/lib/cached_index_pattern_fetcher.ts" }, { "plugin": "visTypeTimeseries", - "path": "src/plugins/vis_type_timeseries/server/lib/search_strategies/lib/cached_index_pattern_fetcher.ts" + "path": "src/plugins/vis_types/timeseries/server/lib/search_strategies/lib/cached_index_pattern_fetcher.ts" }, { "plugin": "visTypeTimeseries", - "path": "src/plugins/vis_type_timeseries/server/lib/search_strategies/strategies/rollup_search_strategy.ts" + "path": "src/plugins/vis_types/timeseries/server/lib/search_strategies/strategies/rollup_search_strategy.ts" }, { "plugin": "visTypeTimeseries", - "path": "src/plugins/vis_type_timeseries/server/lib/search_strategies/strategies/rollup_search_strategy.ts" + "path": "src/plugins/vis_types/timeseries/server/lib/search_strategies/strategies/rollup_search_strategy.ts" }, { "plugin": "visTypeTimeseries", - "path": "src/plugins/vis_type_timeseries/server/types.ts" + "path": "src/plugins/vis_types/timeseries/server/types.ts" }, { "plugin": "visTypeTimeseries", - "path": "src/plugins/vis_type_timeseries/server/types.ts" + "path": "src/plugins/vis_types/timeseries/server/types.ts" }, { "plugin": "visTypeTimeseries", - "path": "src/plugins/vis_type_timeseries/server/lib/search_strategies/lib/cached_index_pattern_fetcher.test.ts" + "path": "src/plugins/vis_types/timeseries/server/lib/search_strategies/lib/cached_index_pattern_fetcher.test.ts" }, { "plugin": "visTypeTimeseries", - "path": "src/plugins/vis_type_timeseries/server/lib/search_strategies/lib/cached_index_pattern_fetcher.test.ts" + "path": "src/plugins/vis_types/timeseries/server/lib/search_strategies/lib/cached_index_pattern_fetcher.test.ts" }, { "plugin": "maps", @@ -7102,39 +7102,39 @@ }, { "plugin": "visTypeTimeseries", - "path": "src/plugins/vis_type_timeseries/common/index_patterns_utils.ts" + "path": "src/plugins/vis_types/timeseries/common/index_patterns_utils.ts" }, { "plugin": "visTypeTimeseries", - "path": "src/plugins/vis_type_timeseries/common/index_patterns_utils.ts" + "path": "src/plugins/vis_types/timeseries/common/index_patterns_utils.ts" }, { "plugin": "visTypeTimeseries", - "path": "src/plugins/vis_type_timeseries/server/lib/search_strategies/lib/fields_fetcher.ts" + "path": "src/plugins/vis_types/timeseries/server/lib/search_strategies/lib/fields_fetcher.ts" }, { "plugin": "visTypeTimeseries", - "path": "src/plugins/vis_type_timeseries/server/lib/search_strategies/lib/fields_fetcher.ts" + "path": "src/plugins/vis_types/timeseries/server/lib/search_strategies/lib/fields_fetcher.ts" }, { "plugin": "visTypeTimeseries", - "path": "src/plugins/vis_type_timeseries/server/plugin.ts" + "path": "src/plugins/vis_types/timeseries/server/plugin.ts" }, { "plugin": "visTypeTimeseries", - "path": "src/plugins/vis_type_timeseries/server/plugin.ts" + "path": "src/plugins/vis_types/timeseries/server/plugin.ts" }, { "plugin": "visTypeTimeseries", - "path": "src/plugins/vis_type_timeseries/common/index_patterns_utils.test.ts" + "path": "src/plugins/vis_types/timeseries/common/index_patterns_utils.test.ts" }, { "plugin": "visTypeTimeseries", - "path": "src/plugins/vis_type_timeseries/common/index_patterns_utils.test.ts" + "path": "src/plugins/vis_types/timeseries/common/index_patterns_utils.test.ts" }, { "plugin": "visTypeTimeseries", - "path": "src/plugins/vis_type_timeseries/common/index_patterns_utils.test.ts" + "path": "src/plugins/vis_types/timeseries/common/index_patterns_utils.test.ts" }, { "plugin": "discover", @@ -7146,27 +7146,27 @@ }, { "plugin": "visTypeTimeseries", - "path": "src/plugins/vis_type_timeseries/server/lib/search_strategies/strategies/abstract_search_strategy.test.ts" + "path": "src/plugins/vis_types/timeseries/server/lib/search_strategies/strategies/abstract_search_strategy.test.ts" }, { "plugin": "visTypeTimeseries", - "path": "src/plugins/vis_type_timeseries/server/lib/search_strategies/strategies/abstract_search_strategy.test.ts" + "path": "src/plugins/vis_types/timeseries/server/lib/search_strategies/strategies/abstract_search_strategy.test.ts" }, { "plugin": "visTypeTimeseries", - "path": "src/plugins/vis_type_timeseries/server/lib/search_strategies/strategies/rollup_search_strategy.test.ts" + "path": "src/plugins/vis_types/timeseries/server/lib/search_strategies/strategies/rollup_search_strategy.test.ts" }, { "plugin": "visTypeTimeseries", - "path": "src/plugins/vis_type_timeseries/server/lib/search_strategies/strategies/rollup_search_strategy.test.ts" + "path": "src/plugins/vis_types/timeseries/server/lib/search_strategies/strategies/rollup_search_strategy.test.ts" }, { "plugin": "visTypeTimeseries", - "path": "src/plugins/vis_type_timeseries/public/application/components/lib/index_pattern_select/combo_box_select.tsx" + "path": "src/plugins/vis_types/timeseries/public/application/components/lib/index_pattern_select/combo_box_select.tsx" }, { "plugin": "visTypeTimeseries", - "path": "src/plugins/vis_type_timeseries/public/application/components/lib/index_pattern_select/combo_box_select.tsx" + "path": "src/plugins/vis_types/timeseries/public/application/components/lib/index_pattern_select/combo_box_select.tsx" }, { "plugin": "discover", @@ -15122,11 +15122,11 @@ "references": [ { "plugin": "visTypeTimeseries", - "path": "src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/table/types.ts" + "path": "src/plugins/vis_types/timeseries/server/lib/vis_data/request_processors/table/types.ts" }, { "plugin": "visTypeTimeseries", - "path": "src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/table/types.ts" + "path": "src/plugins/vis_types/timeseries/server/lib/vis_data/request_processors/table/types.ts" } ], "initialIsOpen": false @@ -17024,11 +17024,11 @@ }, { "plugin": "visTypeTimeseries", - "path": "src/plugins/vis_type_timeseries/common/types/index.ts" + "path": "src/plugins/vis_types/timeseries/common/types/index.ts" }, { "plugin": "visTypeTimeseries", - "path": "src/plugins/vis_type_timeseries/common/types/index.ts" + "path": "src/plugins/vis_types/timeseries/common/types/index.ts" }, { "plugin": "securitySolution", @@ -24538,31 +24538,31 @@ }, { "plugin": "visTypeTimeseries", - "path": "src/plugins/vis_type_timeseries/public/application/lib/fetch_fields.ts" + "path": "src/plugins/vis_types/timeseries/public/application/lib/fetch_fields.ts" }, { "plugin": "visTypeTimeseries", - "path": "src/plugins/vis_type_timeseries/public/application/components/lib/index_pattern_select/combo_box_select.tsx" + "path": "src/plugins/vis_types/timeseries/public/application/components/lib/index_pattern_select/combo_box_select.tsx" }, { "plugin": "visTypeTimeseries", - "path": "src/plugins/vis_type_timeseries/public/application/components/query_bar_wrapper.tsx" + "path": "src/plugins/vis_types/timeseries/public/application/components/query_bar_wrapper.tsx" }, { "plugin": "visTypeTimeseries", - "path": "src/plugins/vis_type_timeseries/public/application/components/annotation_row.tsx" + "path": "src/plugins/vis_types/timeseries/public/application/components/annotation_row.tsx" }, { "plugin": "visTypeTimeseries", - "path": "src/plugins/vis_type_timeseries/public/metrics_type.ts" + "path": "src/plugins/vis_types/timeseries/public/metrics_type.ts" }, { "plugin": "visTypeTimeseries", - "path": "src/plugins/vis_type_timeseries/public/application/components/lib/convert_series_to_datatable.ts" + "path": "src/plugins/vis_types/timeseries/public/application/components/lib/convert_series_to_datatable.ts" }, { "plugin": "visTypeTimeseries", - "path": "src/plugins/vis_type_timeseries/public/timeseries_vis_renderer.tsx" + "path": "src/plugins/vis_types/timeseries/public/timeseries_vis_renderer.tsx" }, { "plugin": "visTypeVega", @@ -24760,7 +24760,7 @@ }, { "plugin": "visTypeTimeseries", - "path": "src/plugins/vis_type_timeseries/public/plugin.ts" + "path": "src/plugins/vis_types/timeseries/public/plugin.ts" }, { "plugin": "visTypeMetric", @@ -25295,19 +25295,19 @@ "references": [ { "plugin": "visTypeTimeseries", - "path": "src/plugins/vis_type_timeseries/server/lib/search_strategies/lib/cached_index_pattern_fetcher.test.ts" + "path": "src/plugins/vis_types/timeseries/server/lib/search_strategies/lib/cached_index_pattern_fetcher.test.ts" }, { "plugin": "visTypeTimeseries", - "path": "src/plugins/vis_type_timeseries/server/lib/search_strategies/lib/cached_index_pattern_fetcher.test.ts" + "path": "src/plugins/vis_types/timeseries/server/lib/search_strategies/lib/cached_index_pattern_fetcher.test.ts" }, { "plugin": "visTypeTimeseries", - "path": "src/plugins/vis_type_timeseries/server/lib/search_strategies/lib/cached_index_pattern_fetcher.test.ts" + "path": "src/plugins/vis_types/timeseries/server/lib/search_strategies/lib/cached_index_pattern_fetcher.test.ts" }, { "plugin": "visTypeTimeseries", - "path": "src/plugins/vis_type_timeseries/server/lib/search_strategies/lib/cached_index_pattern_fetcher.test.ts" + "path": "src/plugins/vis_types/timeseries/server/lib/search_strategies/lib/cached_index_pattern_fetcher.test.ts" }, { "plugin": "reporting", @@ -25647,11 +25647,11 @@ }, { "plugin": "visTypeTimeseries", - "path": "src/plugins/vis_type_timeseries/common/types/index.ts" + "path": "src/plugins/vis_types/timeseries/common/types/index.ts" }, { "plugin": "visTypeTimeseries", - "path": "src/plugins/vis_type_timeseries/common/types/index.ts" + "path": "src/plugins/vis_types/timeseries/common/types/index.ts" }, { "plugin": "osquery", @@ -25807,43 +25807,43 @@ }, { "plugin": "visTypeTimeseries", - "path": "src/plugins/vis_type_timeseries/common/index_patterns_utils.test.ts" + "path": "src/plugins/vis_types/timeseries/common/index_patterns_utils.test.ts" }, { "plugin": "visTypeTimeseries", - "path": "src/plugins/vis_type_timeseries/common/index_patterns_utils.test.ts" + "path": "src/plugins/vis_types/timeseries/common/index_patterns_utils.test.ts" }, { "plugin": "visTypeTimeseries", - "path": "src/plugins/vis_type_timeseries/common/index_patterns_utils.test.ts" + "path": "src/plugins/vis_types/timeseries/common/index_patterns_utils.test.ts" }, { "plugin": "visTypeTimeseries", - "path": "src/plugins/vis_type_timeseries/common/index_patterns_utils.test.ts" + "path": "src/plugins/vis_types/timeseries/common/index_patterns_utils.test.ts" }, { "plugin": "visTypeTimeseries", - "path": "src/plugins/vis_type_timeseries/public/application/components/lib/index_pattern_select/types.ts" + "path": "src/plugins/vis_types/timeseries/public/application/components/lib/index_pattern_select/types.ts" }, { "plugin": "visTypeTimeseries", - "path": "src/plugins/vis_type_timeseries/public/application/components/lib/index_pattern_select/types.ts" + "path": "src/plugins/vis_types/timeseries/public/application/components/lib/index_pattern_select/types.ts" }, { "plugin": "visTypeTimeseries", - "path": "src/plugins/vis_type_timeseries/public/application/components/lib/index_pattern_select/index_pattern_select.tsx" + "path": "src/plugins/vis_types/timeseries/public/application/components/lib/index_pattern_select/index_pattern_select.tsx" }, { "plugin": "visTypeTimeseries", - "path": "src/plugins/vis_type_timeseries/public/application/components/lib/index_pattern_select/index_pattern_select.tsx" + "path": "src/plugins/vis_types/timeseries/public/application/components/lib/index_pattern_select/index_pattern_select.tsx" }, { "plugin": "visTypeTimeseries", - "path": "src/plugins/vis_type_timeseries/public/application/components/timeseries_visualization.tsx" + "path": "src/plugins/vis_types/timeseries/public/application/components/timeseries_visualization.tsx" }, { "plugin": "visTypeTimeseries", - "path": "src/plugins/vis_type_timeseries/public/application/components/timeseries_visualization.tsx" + "path": "src/plugins/vis_types/timeseries/public/application/components/timeseries_visualization.tsx" }, { "plugin": "discover", @@ -27711,15 +27711,15 @@ }, { "plugin": "visTypeTimeseries", - "path": "src/plugins/vis_type_timeseries/public/application/components/lib/convert_series_to_datatable.ts" + "path": "src/plugins/vis_types/timeseries/public/application/components/lib/convert_series_to_datatable.ts" }, { "plugin": "visTypeTimeseries", - "path": "src/plugins/vis_type_timeseries/public/application/components/lib/convert_series_to_datatable.ts" + "path": "src/plugins/vis_types/timeseries/public/application/components/lib/convert_series_to_datatable.ts" }, { "plugin": "visTypeTimeseries", - "path": "src/plugins/vis_type_timeseries/public/application/components/lib/convert_series_to_datatable.ts" + "path": "src/plugins/vis_types/timeseries/public/application/components/lib/convert_series_to_datatable.ts" }, { "plugin": "inputControlVis", @@ -27883,15 +27883,15 @@ }, { "plugin": "visTypeTimeseries", - "path": "src/plugins/vis_type_timeseries/public/application/components/lib/convert_series_to_datatable.test.ts" + "path": "src/plugins/vis_types/timeseries/public/application/components/lib/convert_series_to_datatable.test.ts" }, { "plugin": "visTypeTimeseries", - "path": "src/plugins/vis_type_timeseries/public/application/components/lib/convert_series_to_datatable.test.ts" + "path": "src/plugins/vis_types/timeseries/public/application/components/lib/convert_series_to_datatable.test.ts" }, { "plugin": "visTypeTimeseries", - "path": "src/plugins/vis_type_timeseries/public/application/components/lib/convert_series_to_datatable.test.ts" + "path": "src/plugins/vis_types/timeseries/public/application/components/lib/convert_series_to_datatable.test.ts" }, { "plugin": "discover", @@ -27915,15 +27915,15 @@ }, { "plugin": "visTypeTimeseries", - "path": "src/plugins/vis_type_timeseries/target/types/public/application/components/lib/convert_series_to_datatable.d.ts" + "path": "src/plugins/vis_types/timeseries/target/types/public/application/components/lib/convert_series_to_datatable.d.ts" }, { "plugin": "visTypeTimeseries", - "path": "src/plugins/vis_type_timeseries/target/types/public/application/components/lib/convert_series_to_datatable.d.ts" + "path": "src/plugins/vis_types/timeseries/target/types/public/application/components/lib/convert_series_to_datatable.d.ts" }, { "plugin": "visTypeTimeseries", - "path": "src/plugins/vis_type_timeseries/target/types/public/application/components/lib/convert_series_to_datatable.d.ts" + "path": "src/plugins/vis_types/timeseries/target/types/public/application/components/lib/convert_series_to_datatable.d.ts" }, { "plugin": "discover", @@ -29308,27 +29308,27 @@ }, { "plugin": "visTypeTimeseries", - "path": "src/plugins/vis_type_timeseries/public/application/components/lib/convert_series_to_datatable.test.ts" + "path": "src/plugins/vis_types/timeseries/public/application/components/lib/convert_series_to_datatable.test.ts" }, { "plugin": "visTypeTimeseries", - "path": "src/plugins/vis_type_timeseries/public/application/components/lib/convert_series_to_datatable.test.ts" + "path": "src/plugins/vis_types/timeseries/public/application/components/lib/convert_series_to_datatable.test.ts" }, { "plugin": "visTypeTimeseries", - "path": "src/plugins/vis_type_timeseries/public/application/components/lib/convert_series_to_datatable.test.ts" + "path": "src/plugins/vis_types/timeseries/public/application/components/lib/convert_series_to_datatable.test.ts" }, { "plugin": "visTypeTimeseries", - "path": "src/plugins/vis_type_timeseries/public/application/components/lib/convert_series_to_datatable.test.ts" + "path": "src/plugins/vis_types/timeseries/public/application/components/lib/convert_series_to_datatable.test.ts" }, { "plugin": "visTypeTimeseries", - "path": "src/plugins/vis_type_timeseries/public/application/components/lib/convert_series_to_datatable.test.ts" + "path": "src/plugins/vis_types/timeseries/public/application/components/lib/convert_series_to_datatable.test.ts" }, { "plugin": "visTypeTimeseries", - "path": "src/plugins/vis_type_timeseries/public/application/components/lib/convert_series_to_datatable.test.ts" + "path": "src/plugins/vis_types/timeseries/public/application/components/lib/convert_series_to_datatable.test.ts" }, { "plugin": "discover", @@ -29609,51 +29609,51 @@ "references": [ { "plugin": "visTypeTimeseries", - "path": "src/plugins/vis_type_timeseries/server/lib/search_strategies/strategies/abstract_search_strategy.ts" + "path": "src/plugins/vis_types/timeseries/server/lib/search_strategies/strategies/abstract_search_strategy.ts" }, { "plugin": "visTypeTimeseries", - "path": "src/plugins/vis_type_timeseries/server/lib/search_strategies/strategies/abstract_search_strategy.ts" + "path": "src/plugins/vis_types/timeseries/server/lib/search_strategies/strategies/abstract_search_strategy.ts" }, { "plugin": "visTypeTimeseries", - "path": "src/plugins/vis_type_timeseries/server/lib/search_strategies/strategies/default_search_strategy.ts" + "path": "src/plugins/vis_types/timeseries/server/lib/search_strategies/strategies/default_search_strategy.ts" }, { "plugin": "visTypeTimeseries", - "path": "src/plugins/vis_type_timeseries/server/lib/search_strategies/strategies/default_search_strategy.ts" + "path": "src/plugins/vis_types/timeseries/server/lib/search_strategies/strategies/default_search_strategy.ts" }, { "plugin": "visTypeTimeseries", - "path": "src/plugins/vis_type_timeseries/server/lib/search_strategies/lib/cached_index_pattern_fetcher.ts" + "path": "src/plugins/vis_types/timeseries/server/lib/search_strategies/lib/cached_index_pattern_fetcher.ts" }, { "plugin": "visTypeTimeseries", - "path": "src/plugins/vis_type_timeseries/server/lib/search_strategies/lib/cached_index_pattern_fetcher.ts" + "path": "src/plugins/vis_types/timeseries/server/lib/search_strategies/lib/cached_index_pattern_fetcher.ts" }, { "plugin": "visTypeTimeseries", - "path": "src/plugins/vis_type_timeseries/server/lib/search_strategies/strategies/rollup_search_strategy.ts" + "path": "src/plugins/vis_types/timeseries/server/lib/search_strategies/strategies/rollup_search_strategy.ts" }, { "plugin": "visTypeTimeseries", - "path": "src/plugins/vis_type_timeseries/server/lib/search_strategies/strategies/rollup_search_strategy.ts" + "path": "src/plugins/vis_types/timeseries/server/lib/search_strategies/strategies/rollup_search_strategy.ts" }, { "plugin": "visTypeTimeseries", - "path": "src/plugins/vis_type_timeseries/server/types.ts" + "path": "src/plugins/vis_types/timeseries/server/types.ts" }, { "plugin": "visTypeTimeseries", - "path": "src/plugins/vis_type_timeseries/server/types.ts" + "path": "src/plugins/vis_types/timeseries/server/types.ts" }, { "plugin": "visTypeTimeseries", - "path": "src/plugins/vis_type_timeseries/server/lib/search_strategies/lib/cached_index_pattern_fetcher.test.ts" + "path": "src/plugins/vis_types/timeseries/server/lib/search_strategies/lib/cached_index_pattern_fetcher.test.ts" }, { "plugin": "visTypeTimeseries", - "path": "src/plugins/vis_type_timeseries/server/lib/search_strategies/lib/cached_index_pattern_fetcher.test.ts" + "path": "src/plugins/vis_types/timeseries/server/lib/search_strategies/lib/cached_index_pattern_fetcher.test.ts" }, { "plugin": "maps", @@ -29681,39 +29681,39 @@ }, { "plugin": "visTypeTimeseries", - "path": "src/plugins/vis_type_timeseries/common/index_patterns_utils.ts" + "path": "src/plugins/vis_types/timeseries/common/index_patterns_utils.ts" }, { "plugin": "visTypeTimeseries", - "path": "src/plugins/vis_type_timeseries/common/index_patterns_utils.ts" + "path": "src/plugins/vis_types/timeseries/common/index_patterns_utils.ts" }, { "plugin": "visTypeTimeseries", - "path": "src/plugins/vis_type_timeseries/server/lib/search_strategies/lib/fields_fetcher.ts" + "path": "src/plugins/vis_types/timeseries/server/lib/search_strategies/lib/fields_fetcher.ts" }, { "plugin": "visTypeTimeseries", - "path": "src/plugins/vis_type_timeseries/server/lib/search_strategies/lib/fields_fetcher.ts" + "path": "src/plugins/vis_types/timeseries/server/lib/search_strategies/lib/fields_fetcher.ts" }, { "plugin": "visTypeTimeseries", - "path": "src/plugins/vis_type_timeseries/server/plugin.ts" + "path": "src/plugins/vis_types/timeseries/server/plugin.ts" }, { "plugin": "visTypeTimeseries", - "path": "src/plugins/vis_type_timeseries/server/plugin.ts" + "path": "src/plugins/vis_types/timeseries/server/plugin.ts" }, { "plugin": "visTypeTimeseries", - "path": "src/plugins/vis_type_timeseries/common/index_patterns_utils.test.ts" + "path": "src/plugins/vis_types/timeseries/common/index_patterns_utils.test.ts" }, { "plugin": "visTypeTimeseries", - "path": "src/plugins/vis_type_timeseries/common/index_patterns_utils.test.ts" + "path": "src/plugins/vis_types/timeseries/common/index_patterns_utils.test.ts" }, { "plugin": "visTypeTimeseries", - "path": "src/plugins/vis_type_timeseries/common/index_patterns_utils.test.ts" + "path": "src/plugins/vis_types/timeseries/common/index_patterns_utils.test.ts" }, { "plugin": "discover", @@ -29725,27 +29725,27 @@ }, { "plugin": "visTypeTimeseries", - "path": "src/plugins/vis_type_timeseries/server/lib/search_strategies/strategies/abstract_search_strategy.test.ts" + "path": "src/plugins/vis_types/timeseries/server/lib/search_strategies/strategies/abstract_search_strategy.test.ts" }, { "plugin": "visTypeTimeseries", - "path": "src/plugins/vis_type_timeseries/server/lib/search_strategies/strategies/abstract_search_strategy.test.ts" + "path": "src/plugins/vis_types/timeseries/server/lib/search_strategies/strategies/abstract_search_strategy.test.ts" }, { "plugin": "visTypeTimeseries", - "path": "src/plugins/vis_type_timeseries/server/lib/search_strategies/strategies/rollup_search_strategy.test.ts" + "path": "src/plugins/vis_types/timeseries/server/lib/search_strategies/strategies/rollup_search_strategy.test.ts" }, { "plugin": "visTypeTimeseries", - "path": "src/plugins/vis_type_timeseries/server/lib/search_strategies/strategies/rollup_search_strategy.test.ts" + "path": "src/plugins/vis_types/timeseries/server/lib/search_strategies/strategies/rollup_search_strategy.test.ts" }, { "plugin": "visTypeTimeseries", - "path": "src/plugins/vis_type_timeseries/public/application/components/lib/index_pattern_select/combo_box_select.tsx" + "path": "src/plugins/vis_types/timeseries/public/application/components/lib/index_pattern_select/combo_box_select.tsx" }, { "plugin": "visTypeTimeseries", - "path": "src/plugins/vis_type_timeseries/public/application/components/lib/index_pattern_select/combo_box_select.tsx" + "path": "src/plugins/vis_types/timeseries/public/application/components/lib/index_pattern_select/combo_box_select.tsx" }, { "plugin": "discover", @@ -29806,51 +29806,51 @@ "references": [ { "plugin": "visTypeTimeseries", - "path": "src/plugins/vis_type_timeseries/server/lib/search_strategies/strategies/abstract_search_strategy.ts" + "path": "src/plugins/vis_types/timeseries/server/lib/search_strategies/strategies/abstract_search_strategy.ts" }, { "plugin": "visTypeTimeseries", - "path": "src/plugins/vis_type_timeseries/server/lib/search_strategies/strategies/abstract_search_strategy.ts" + "path": "src/plugins/vis_types/timeseries/server/lib/search_strategies/strategies/abstract_search_strategy.ts" }, { "plugin": "visTypeTimeseries", - "path": "src/plugins/vis_type_timeseries/server/lib/search_strategies/strategies/default_search_strategy.ts" + "path": "src/plugins/vis_types/timeseries/server/lib/search_strategies/strategies/default_search_strategy.ts" }, { "plugin": "visTypeTimeseries", - "path": "src/plugins/vis_type_timeseries/server/lib/search_strategies/strategies/default_search_strategy.ts" + "path": "src/plugins/vis_types/timeseries/server/lib/search_strategies/strategies/default_search_strategy.ts" }, { "plugin": "visTypeTimeseries", - "path": "src/plugins/vis_type_timeseries/server/lib/search_strategies/lib/cached_index_pattern_fetcher.ts" + "path": "src/plugins/vis_types/timeseries/server/lib/search_strategies/lib/cached_index_pattern_fetcher.ts" }, { "plugin": "visTypeTimeseries", - "path": "src/plugins/vis_type_timeseries/server/lib/search_strategies/lib/cached_index_pattern_fetcher.ts" + "path": "src/plugins/vis_types/timeseries/server/lib/search_strategies/lib/cached_index_pattern_fetcher.ts" }, { "plugin": "visTypeTimeseries", - "path": "src/plugins/vis_type_timeseries/server/lib/search_strategies/strategies/rollup_search_strategy.ts" + "path": "src/plugins/vis_types/timeseries/server/lib/search_strategies/strategies/rollup_search_strategy.ts" }, { "plugin": "visTypeTimeseries", - "path": "src/plugins/vis_type_timeseries/server/lib/search_strategies/strategies/rollup_search_strategy.ts" + "path": "src/plugins/vis_types/timeseries/server/lib/search_strategies/strategies/rollup_search_strategy.ts" }, { "plugin": "visTypeTimeseries", - "path": "src/plugins/vis_type_timeseries/server/types.ts" + "path": "src/plugins/vis_types/timeseries/server/types.ts" }, { "plugin": "visTypeTimeseries", - "path": "src/plugins/vis_type_timeseries/server/types.ts" + "path": "src/plugins/vis_types/timeseries/server/types.ts" }, { "plugin": "visTypeTimeseries", - "path": "src/plugins/vis_type_timeseries/server/lib/search_strategies/lib/cached_index_pattern_fetcher.test.ts" + "path": "src/plugins/vis_types/timeseries/server/lib/search_strategies/lib/cached_index_pattern_fetcher.test.ts" }, { "plugin": "visTypeTimeseries", - "path": "src/plugins/vis_type_timeseries/server/lib/search_strategies/lib/cached_index_pattern_fetcher.test.ts" + "path": "src/plugins/vis_types/timeseries/server/lib/search_strategies/lib/cached_index_pattern_fetcher.test.ts" }, { "plugin": "maps", @@ -29878,39 +29878,39 @@ }, { "plugin": "visTypeTimeseries", - "path": "src/plugins/vis_type_timeseries/common/index_patterns_utils.ts" + "path": "src/plugins/vis_types/timeseries/common/index_patterns_utils.ts" }, { "plugin": "visTypeTimeseries", - "path": "src/plugins/vis_type_timeseries/common/index_patterns_utils.ts" + "path": "src/plugins/vis_types/timeseries/common/index_patterns_utils.ts" }, { "plugin": "visTypeTimeseries", - "path": "src/plugins/vis_type_timeseries/server/lib/search_strategies/lib/fields_fetcher.ts" + "path": "src/plugins/vis_types/timeseries/server/lib/search_strategies/lib/fields_fetcher.ts" }, { "plugin": "visTypeTimeseries", - "path": "src/plugins/vis_type_timeseries/server/lib/search_strategies/lib/fields_fetcher.ts" + "path": "src/plugins/vis_types/timeseries/server/lib/search_strategies/lib/fields_fetcher.ts" }, { "plugin": "visTypeTimeseries", - "path": "src/plugins/vis_type_timeseries/server/plugin.ts" + "path": "src/plugins/vis_types/timeseries/server/plugin.ts" }, { "plugin": "visTypeTimeseries", - "path": "src/plugins/vis_type_timeseries/server/plugin.ts" + "path": "src/plugins/vis_types/timeseries/server/plugin.ts" }, { "plugin": "visTypeTimeseries", - "path": "src/plugins/vis_type_timeseries/common/index_patterns_utils.test.ts" + "path": "src/plugins/vis_types/timeseries/common/index_patterns_utils.test.ts" }, { "plugin": "visTypeTimeseries", - "path": "src/plugins/vis_type_timeseries/common/index_patterns_utils.test.ts" + "path": "src/plugins/vis_types/timeseries/common/index_patterns_utils.test.ts" }, { "plugin": "visTypeTimeseries", - "path": "src/plugins/vis_type_timeseries/common/index_patterns_utils.test.ts" + "path": "src/plugins/vis_types/timeseries/common/index_patterns_utils.test.ts" }, { "plugin": "discover", @@ -29922,27 +29922,27 @@ }, { "plugin": "visTypeTimeseries", - "path": "src/plugins/vis_type_timeseries/server/lib/search_strategies/strategies/abstract_search_strategy.test.ts" + "path": "src/plugins/vis_types/timeseries/server/lib/search_strategies/strategies/abstract_search_strategy.test.ts" }, { "plugin": "visTypeTimeseries", - "path": "src/plugins/vis_type_timeseries/server/lib/search_strategies/strategies/abstract_search_strategy.test.ts" + "path": "src/plugins/vis_types/timeseries/server/lib/search_strategies/strategies/abstract_search_strategy.test.ts" }, { "plugin": "visTypeTimeseries", - "path": "src/plugins/vis_type_timeseries/server/lib/search_strategies/strategies/rollup_search_strategy.test.ts" + "path": "src/plugins/vis_types/timeseries/server/lib/search_strategies/strategies/rollup_search_strategy.test.ts" }, { "plugin": "visTypeTimeseries", - "path": "src/plugins/vis_type_timeseries/server/lib/search_strategies/strategies/rollup_search_strategy.test.ts" + "path": "src/plugins/vis_types/timeseries/server/lib/search_strategies/strategies/rollup_search_strategy.test.ts" }, { "plugin": "visTypeTimeseries", - "path": "src/plugins/vis_type_timeseries/public/application/components/lib/index_pattern_select/combo_box_select.tsx" + "path": "src/plugins/vis_types/timeseries/public/application/components/lib/index_pattern_select/combo_box_select.tsx" }, { "plugin": "visTypeTimeseries", - "path": "src/plugins/vis_type_timeseries/public/application/components/lib/index_pattern_select/combo_box_select.tsx" + "path": "src/plugins/vis_types/timeseries/public/application/components/lib/index_pattern_select/combo_box_select.tsx" }, { "plugin": "discover", @@ -31186,11 +31186,11 @@ "references": [ { "plugin": "visTypeTimeseries", - "path": "src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/table/types.ts" + "path": "src/plugins/vis_types/timeseries/server/lib/vis_data/request_processors/table/types.ts" }, { "plugin": "visTypeTimeseries", - "path": "src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/table/types.ts" + "path": "src/plugins/vis_types/timeseries/server/lib/vis_data/request_processors/table/types.ts" } ], "initialIsOpen": false @@ -32849,11 +32849,11 @@ }, { "plugin": "visTypeTimeseries", - "path": "src/plugins/vis_type_timeseries/common/types/index.ts" + "path": "src/plugins/vis_types/timeseries/common/types/index.ts" }, { "plugin": "visTypeTimeseries", - "path": "src/plugins/vis_type_timeseries/common/types/index.ts" + "path": "src/plugins/vis_types/timeseries/common/types/index.ts" }, { "plugin": "securitySolution", @@ -34893,7 +34893,7 @@ }, { "plugin": "visTypeTimeseries", - "path": "src/plugins/vis_type_timeseries/server/plugin.ts" + "path": "src/plugins/vis_types/timeseries/server/plugin.ts" } ] }, @@ -37872,11 +37872,11 @@ "references": [ { "plugin": "visTypeTimeseries", - "path": "src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/table/types.ts" + "path": "src/plugins/vis_types/timeseries/server/lib/vis_data/request_processors/table/types.ts" }, { "plugin": "visTypeTimeseries", - "path": "src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/table/types.ts" + "path": "src/plugins/vis_types/timeseries/server/lib/vis_data/request_processors/table/types.ts" } ], "initialIsOpen": false @@ -39565,11 +39565,11 @@ }, { "plugin": "visTypeTimeseries", - "path": "src/plugins/vis_type_timeseries/common/types/index.ts" + "path": "src/plugins/vis_types/timeseries/common/types/index.ts" }, { "plugin": "visTypeTimeseries", - "path": "src/plugins/vis_type_timeseries/common/types/index.ts" + "path": "src/plugins/vis_types/timeseries/common/types/index.ts" }, { "plugin": "securitySolution", diff --git a/api_docs/data_index_patterns.json b/api_docs/data_index_patterns.json index c249796c9f3e7..21b11f425de4c 100644 --- a/api_docs/data_index_patterns.json +++ b/api_docs/data_index_patterns.json @@ -1111,7 +1111,7 @@ "references": [ { "plugin": "visTypeTimeseries", - "path": "src/plugins/vis_type_timeseries/server/lib/search_strategies/strategies/abstract_search_strategy.ts" + "path": "src/plugins/vis_types/timeseries/server/lib/search_strategies/strategies/abstract_search_strategy.ts" }, { "plugin": "graph", @@ -1147,7 +1147,7 @@ }, { "plugin": "visTypeTimeseries", - "path": "src/plugins/vis_type_timeseries/public/application/lib/fetch_fields.ts" + "path": "src/plugins/vis_types/timeseries/public/application/lib/fetch_fields.ts" } ], "children": [], @@ -3727,19 +3727,19 @@ "references": [ { "plugin": "visTypeTimeseries", - "path": "src/plugins/vis_type_timeseries/server/lib/search_strategies/lib/cached_index_pattern_fetcher.test.ts" + "path": "src/plugins/vis_types/timeseries/server/lib/search_strategies/lib/cached_index_pattern_fetcher.test.ts" }, { "plugin": "visTypeTimeseries", - "path": "src/plugins/vis_type_timeseries/server/lib/search_strategies/lib/cached_index_pattern_fetcher.test.ts" + "path": "src/plugins/vis_types/timeseries/server/lib/search_strategies/lib/cached_index_pattern_fetcher.test.ts" }, { "plugin": "visTypeTimeseries", - "path": "src/plugins/vis_type_timeseries/server/lib/search_strategies/lib/cached_index_pattern_fetcher.test.ts" + "path": "src/plugins/vis_types/timeseries/server/lib/search_strategies/lib/cached_index_pattern_fetcher.test.ts" }, { "plugin": "visTypeTimeseries", - "path": "src/plugins/vis_type_timeseries/server/lib/search_strategies/lib/cached_index_pattern_fetcher.test.ts" + "path": "src/plugins/vis_types/timeseries/server/lib/search_strategies/lib/cached_index_pattern_fetcher.test.ts" }, { "plugin": "reporting", @@ -4079,11 +4079,11 @@ }, { "plugin": "visTypeTimeseries", - "path": "src/plugins/vis_type_timeseries/common/types/index.ts" + "path": "src/plugins/vis_types/timeseries/common/types/index.ts" }, { "plugin": "visTypeTimeseries", - "path": "src/plugins/vis_type_timeseries/common/types/index.ts" + "path": "src/plugins/vis_types/timeseries/common/types/index.ts" }, { "plugin": "osquery", @@ -4239,35 +4239,35 @@ }, { "plugin": "visTypeTimeseries", - "path": "src/plugins/vis_type_timeseries/common/index_patterns_utils.test.ts" + "path": "src/plugins/vis_types/timeseries/common/index_patterns_utils.test.ts" }, { "plugin": "visTypeTimeseries", - "path": "src/plugins/vis_type_timeseries/common/index_patterns_utils.test.ts" + "path": "src/plugins/vis_types/timeseries/common/index_patterns_utils.test.ts" }, { "plugin": "visTypeTimeseries", - "path": "src/plugins/vis_type_timeseries/common/index_patterns_utils.test.ts" + "path": "src/plugins/vis_types/timeseries/common/index_patterns_utils.test.ts" }, { "plugin": "visTypeTimeseries", - "path": "src/plugins/vis_type_timeseries/common/index_patterns_utils.test.ts" + "path": "src/plugins/vis_types/timeseries/common/index_patterns_utils.test.ts" }, { "plugin": "visTypeTimeseries", - "path": "src/plugins/vis_type_timeseries/public/application/components/lib/index_pattern_select/types.ts" + "path": "src/plugins/vis_types/timeseries/public/application/components/lib/index_pattern_select/types.ts" }, { "plugin": "visTypeTimeseries", - "path": "src/plugins/vis_type_timeseries/public/application/components/lib/index_pattern_select/types.ts" + "path": "src/plugins/vis_types/timeseries/public/application/components/lib/index_pattern_select/types.ts" }, { "plugin": "visTypeTimeseries", - "path": "src/plugins/vis_type_timeseries/public/application/components/lib/index_pattern_select/index_pattern_select.tsx" + "path": "src/plugins/vis_types/timeseries/public/application/components/lib/index_pattern_select/index_pattern_select.tsx" }, { "plugin": "visTypeTimeseries", - "path": "src/plugins/vis_type_timeseries/public/application/components/lib/index_pattern_select/index_pattern_select.tsx" + "path": "src/plugins/vis_types/timeseries/public/application/components/lib/index_pattern_select/index_pattern_select.tsx" }, { "plugin": "visTypeTimeseries", @@ -6143,15 +6143,15 @@ }, { "plugin": "visTypeTimeseries", - "path": "src/plugins/vis_type_timeseries/public/application/components/lib/convert_series_to_datatable.ts" + "path": "src/plugins/vis_types/timeseries/public/application/components/lib/convert_series_to_datatable.ts" }, { "plugin": "visTypeTimeseries", - "path": "src/plugins/vis_type_timeseries/public/application/components/lib/convert_series_to_datatable.ts" + "path": "src/plugins/vis_types/timeseries/public/application/components/lib/convert_series_to_datatable.ts" }, { "plugin": "visTypeTimeseries", - "path": "src/plugins/vis_type_timeseries/public/application/components/lib/convert_series_to_datatable.ts" + "path": "src/plugins/vis_types/timeseries/public/application/components/lib/convert_series_to_datatable.ts" }, { "plugin": "inputControlVis", @@ -6315,15 +6315,15 @@ }, { "plugin": "visTypeTimeseries", - "path": "src/plugins/vis_type_timeseries/public/application/components/lib/convert_series_to_datatable.test.ts" + "path": "src/plugins/vis_types/timeseries/public/application/components/lib/convert_series_to_datatable.test.ts" }, { "plugin": "visTypeTimeseries", - "path": "src/plugins/vis_type_timeseries/public/application/components/lib/convert_series_to_datatable.test.ts" + "path": "src/plugins/vis_types/timeseries/public/application/components/lib/convert_series_to_datatable.test.ts" }, { "plugin": "visTypeTimeseries", - "path": "src/plugins/vis_type_timeseries/public/application/components/lib/convert_series_to_datatable.test.ts" + "path": "src/plugins/vis_types/timeseries/public/application/components/lib/convert_series_to_datatable.test.ts" }, { "plugin": "discover", @@ -6347,15 +6347,15 @@ }, { "plugin": "visTypeTimeseries", - "path": "src/plugins/vis_type_timeseries/target/types/public/application/components/lib/convert_series_to_datatable.d.ts" + "path": "src/plugins/vis_types/timeseries/target/types/public/application/components/lib/convert_series_to_datatable.d.ts" }, { "plugin": "visTypeTimeseries", - "path": "src/plugins/vis_type_timeseries/target/types/public/application/components/lib/convert_series_to_datatable.d.ts" + "path": "src/plugins/vis_types/timeseries/target/types/public/application/components/lib/convert_series_to_datatable.d.ts" }, { "plugin": "visTypeTimeseries", - "path": "src/plugins/vis_type_timeseries/target/types/public/application/components/lib/convert_series_to_datatable.d.ts" + "path": "src/plugins/vis_types/timeseries/target/types/public/application/components/lib/convert_series_to_datatable.d.ts" }, { "plugin": "discover", @@ -7740,27 +7740,27 @@ }, { "plugin": "visTypeTimeseries", - "path": "src/plugins/vis_type_timeseries/public/application/components/lib/convert_series_to_datatable.test.ts" + "path": "src/plugins/vis_types/timeseries/public/application/components/lib/convert_series_to_datatable.test.ts" }, { "plugin": "visTypeTimeseries", - "path": "src/plugins/vis_type_timeseries/public/application/components/lib/convert_series_to_datatable.test.ts" + "path": "src/plugins/vis_types/timeseries/public/application/components/lib/convert_series_to_datatable.test.ts" }, { "plugin": "visTypeTimeseries", - "path": "src/plugins/vis_type_timeseries/public/application/components/lib/convert_series_to_datatable.test.ts" + "path": "src/plugins/vis_types/timeseries/public/application/components/lib/convert_series_to_datatable.test.ts" }, { "plugin": "visTypeTimeseries", - "path": "src/plugins/vis_type_timeseries/public/application/components/lib/convert_series_to_datatable.test.ts" + "path": "src/plugins/vis_types/timeseries/public/application/components/lib/convert_series_to_datatable.test.ts" }, { "plugin": "visTypeTimeseries", - "path": "src/plugins/vis_type_timeseries/public/application/components/lib/convert_series_to_datatable.test.ts" + "path": "src/plugins/vis_types/timeseries/public/application/components/lib/convert_series_to_datatable.test.ts" }, { "plugin": "visTypeTimeseries", - "path": "src/plugins/vis_type_timeseries/public/application/components/lib/convert_series_to_datatable.test.ts" + "path": "src/plugins/vis_types/timeseries/public/application/components/lib/convert_series_to_datatable.test.ts" }, { "plugin": "discover", @@ -8041,51 +8041,51 @@ "references": [ { "plugin": "visTypeTimeseries", - "path": "src/plugins/vis_type_timeseries/server/lib/search_strategies/strategies/abstract_search_strategy.ts" + "path": "src/plugins/vis_types/timeseries/server/lib/search_strategies/strategies/abstract_search_strategy.ts" }, { "plugin": "visTypeTimeseries", - "path": "src/plugins/vis_type_timeseries/server/lib/search_strategies/strategies/abstract_search_strategy.ts" + "path": "src/plugins/vis_types/timeseries/server/lib/search_strategies/strategies/abstract_search_strategy.ts" }, { "plugin": "visTypeTimeseries", - "path": "src/plugins/vis_type_timeseries/server/lib/search_strategies/strategies/default_search_strategy.ts" + "path": "src/plugins/vis_types/timeseries/server/lib/search_strategies/strategies/default_search_strategy.ts" }, { "plugin": "visTypeTimeseries", - "path": "src/plugins/vis_type_timeseries/server/lib/search_strategies/strategies/default_search_strategy.ts" + "path": "src/plugins/vis_types/timeseries/server/lib/search_strategies/strategies/default_search_strategy.ts" }, { "plugin": "visTypeTimeseries", - "path": "src/plugins/vis_type_timeseries/server/lib/search_strategies/lib/cached_index_pattern_fetcher.ts" + "path": "src/plugins/vis_types/timeseries/server/lib/search_strategies/lib/cached_index_pattern_fetcher.ts" }, { "plugin": "visTypeTimeseries", - "path": "src/plugins/vis_type_timeseries/server/lib/search_strategies/lib/cached_index_pattern_fetcher.ts" + "path": "src/plugins/vis_types/timeseries/server/lib/search_strategies/lib/cached_index_pattern_fetcher.ts" }, { "plugin": "visTypeTimeseries", - "path": "src/plugins/vis_type_timeseries/server/lib/search_strategies/strategies/rollup_search_strategy.ts" + "path": "src/plugins/vis_types/timeseries/server/lib/search_strategies/strategies/rollup_search_strategy.ts" }, { "plugin": "visTypeTimeseries", - "path": "src/plugins/vis_type_timeseries/server/lib/search_strategies/strategies/rollup_search_strategy.ts" + "path": "src/plugins/vis_types/timeseries/server/lib/search_strategies/strategies/rollup_search_strategy.ts" }, { "plugin": "visTypeTimeseries", - "path": "src/plugins/vis_type_timeseries/server/types.ts" + "path": "src/plugins/vis_types/timeseries/server/types.ts" }, { "plugin": "visTypeTimeseries", - "path": "src/plugins/vis_type_timeseries/server/types.ts" + "path": "src/plugins/vis_types/timeseries/server/types.ts" }, { "plugin": "visTypeTimeseries", - "path": "src/plugins/vis_type_timeseries/server/lib/search_strategies/lib/cached_index_pattern_fetcher.test.ts" + "path": "src/plugins/vis_types/timeseries/server/lib/search_strategies/lib/cached_index_pattern_fetcher.test.ts" }, { "plugin": "visTypeTimeseries", - "path": "src/plugins/vis_type_timeseries/server/lib/search_strategies/lib/cached_index_pattern_fetcher.test.ts" + "path": "src/plugins/vis_types/timeseries/server/lib/search_strategies/lib/cached_index_pattern_fetcher.test.ts" }, { "plugin": "maps", @@ -8113,39 +8113,39 @@ }, { "plugin": "visTypeTimeseries", - "path": "src/plugins/vis_type_timeseries/common/index_patterns_utils.ts" + "path": "src/plugins/vis_types/timeseries/common/index_patterns_utils.ts" }, { "plugin": "visTypeTimeseries", - "path": "src/plugins/vis_type_timeseries/common/index_patterns_utils.ts" + "path": "src/plugins/vis_types/timeseries/common/index_patterns_utils.ts" }, { "plugin": "visTypeTimeseries", - "path": "src/plugins/vis_type_timeseries/server/lib/search_strategies/lib/fields_fetcher.ts" + "path": "src/plugins/vis_types/timeseries/server/lib/search_strategies/lib/fields_fetcher.ts" }, { "plugin": "visTypeTimeseries", - "path": "src/plugins/vis_type_timeseries/server/lib/search_strategies/lib/fields_fetcher.ts" + "path": "src/plugins/vis_types/timeseries/server/lib/search_strategies/lib/fields_fetcher.ts" }, { "plugin": "visTypeTimeseries", - "path": "src/plugins/vis_type_timeseries/server/plugin.ts" + "path": "src/plugins/vis_types/timeseries/server/plugin.ts" }, { "plugin": "visTypeTimeseries", - "path": "src/plugins/vis_type_timeseries/server/plugin.ts" + "path": "src/plugins/vis_types/timeseries/server/plugin.ts" }, { "plugin": "visTypeTimeseries", - "path": "src/plugins/vis_type_timeseries/common/index_patterns_utils.test.ts" + "path": "src/plugins/vis_types/timeseries/common/index_patterns_utils.test.ts" }, { "plugin": "visTypeTimeseries", - "path": "src/plugins/vis_type_timeseries/common/index_patterns_utils.test.ts" + "path": "src/plugins/vis_types/timeseries/common/index_patterns_utils.test.ts" }, { "plugin": "visTypeTimeseries", - "path": "src/plugins/vis_type_timeseries/common/index_patterns_utils.test.ts" + "path": "src/plugins/vis_types/timeseries/common/index_patterns_utils.test.ts" }, { "plugin": "discover", @@ -8157,27 +8157,27 @@ }, { "plugin": "visTypeTimeseries", - "path": "src/plugins/vis_type_timeseries/server/lib/search_strategies/strategies/abstract_search_strategy.test.ts" + "path": "src/plugins/vis_types/timeseries/server/lib/search_strategies/strategies/abstract_search_strategy.test.ts" }, { "plugin": "visTypeTimeseries", - "path": "src/plugins/vis_type_timeseries/server/lib/search_strategies/strategies/abstract_search_strategy.test.ts" + "path": "src/plugins/vis_types/timeseries/server/lib/search_strategies/strategies/abstract_search_strategy.test.ts" }, { "plugin": "visTypeTimeseries", - "path": "src/plugins/vis_type_timeseries/server/lib/search_strategies/strategies/rollup_search_strategy.test.ts" + "path": "src/plugins/vis_types/timeseries/server/lib/search_strategies/strategies/rollup_search_strategy.test.ts" }, { "plugin": "visTypeTimeseries", - "path": "src/plugins/vis_type_timeseries/server/lib/search_strategies/strategies/rollup_search_strategy.test.ts" + "path": "src/plugins/vis_types/timeseries/server/lib/search_strategies/strategies/rollup_search_strategy.test.ts" }, { "plugin": "visTypeTimeseries", - "path": "src/plugins/vis_type_timeseries/public/application/components/lib/index_pattern_select/combo_box_select.tsx" + "path": "src/plugins/vis_types/timeseries/public/application/components/lib/index_pattern_select/combo_box_select.tsx" }, { "plugin": "visTypeTimeseries", - "path": "src/plugins/vis_type_timeseries/public/application/components/lib/index_pattern_select/combo_box_select.tsx" + "path": "src/plugins/vis_types/timeseries/public/application/components/lib/index_pattern_select/combo_box_select.tsx" }, { "plugin": "discover", diff --git a/api_docs/deprecations_by_plugin.mdx b/api_docs/deprecations_by_plugin.mdx index 8180ea9d03267..30273efee57f6 100644 --- a/api_docs/deprecations_by_plugin.mdx +++ b/api_docs/deprecations_by_plugin.mdx @@ -958,25 +958,25 @@ warning: This document is auto-generated and is meant to be viewed inside our ex | Deprecated API | Reference location(s) | Remove By | | ---------------|-----------|-----------| -| | [abstract_search_strategy.ts](https://github.com/elastic/kibana/tree/master/src/plugins/vis_type_timeseries/server/lib/search_strategies/strategies/abstract_search_strategy.ts#:~:text=IndexPatternsService), [abstract_search_strategy.ts](https://github.com/elastic/kibana/tree/master/src/plugins/vis_type_timeseries/server/lib/search_strategies/strategies/abstract_search_strategy.ts#:~:text=IndexPatternsService), [default_search_strategy.ts](https://github.com/elastic/kibana/tree/master/src/plugins/vis_type_timeseries/server/lib/search_strategies/strategies/default_search_strategy.ts#:~:text=IndexPatternsService), [default_search_strategy.ts](https://github.com/elastic/kibana/tree/master/src/plugins/vis_type_timeseries/server/lib/search_strategies/strategies/default_search_strategy.ts#:~:text=IndexPatternsService), [cached_index_pattern_fetcher.ts](https://github.com/elastic/kibana/tree/master/src/plugins/vis_type_timeseries/server/lib/search_strategies/lib/cached_index_pattern_fetcher.ts#:~:text=IndexPatternsService), [cached_index_pattern_fetcher.ts](https://github.com/elastic/kibana/tree/master/src/plugins/vis_type_timeseries/server/lib/search_strategies/lib/cached_index_pattern_fetcher.ts#:~:text=IndexPatternsService), [rollup_search_strategy.ts](https://github.com/elastic/kibana/tree/master/src/plugins/vis_type_timeseries/server/lib/search_strategies/strategies/rollup_search_strategy.ts#:~:text=IndexPatternsService), [rollup_search_strategy.ts](https://github.com/elastic/kibana/tree/master/src/plugins/vis_type_timeseries/server/lib/search_strategies/strategies/rollup_search_strategy.ts#:~:text=IndexPatternsService), [types.ts](https://github.com/elastic/kibana/tree/master/src/plugins/vis_type_timeseries/server/types.ts#:~:text=IndexPatternsService), [types.ts](https://github.com/elastic/kibana/tree/master/src/plugins/vis_type_timeseries/server/types.ts#:~:text=IndexPatternsService)+ 17 more | - | -| | [cached_index_pattern_fetcher.test.ts](https://github.com/elastic/kibana/tree/master/src/plugins/vis_type_timeseries/server/lib/search_strategies/lib/cached_index_pattern_fetcher.test.ts#:~:text=IndexPattern), [cached_index_pattern_fetcher.test.ts](https://github.com/elastic/kibana/tree/master/src/plugins/vis_type_timeseries/server/lib/search_strategies/lib/cached_index_pattern_fetcher.test.ts#:~:text=IndexPattern), [cached_index_pattern_fetcher.test.ts](https://github.com/elastic/kibana/tree/master/src/plugins/vis_type_timeseries/server/lib/search_strategies/lib/cached_index_pattern_fetcher.test.ts#:~:text=IndexPattern), [cached_index_pattern_fetcher.test.ts](https://github.com/elastic/kibana/tree/master/src/plugins/vis_type_timeseries/server/lib/search_strategies/lib/cached_index_pattern_fetcher.test.ts#:~:text=IndexPattern), [index.ts](https://github.com/elastic/kibana/tree/master/src/plugins/vis_type_timeseries/common/types/index.ts#:~:text=IndexPattern), [index.ts](https://github.com/elastic/kibana/tree/master/src/plugins/vis_type_timeseries/common/types/index.ts#:~:text=IndexPattern), [index_patterns_utils.test.ts](https://github.com/elastic/kibana/tree/master/src/plugins/vis_type_timeseries/common/index_patterns_utils.test.ts#:~:text=IndexPattern), [index_patterns_utils.test.ts](https://github.com/elastic/kibana/tree/master/src/plugins/vis_type_timeseries/common/index_patterns_utils.test.ts#:~:text=IndexPattern), [index_patterns_utils.test.ts](https://github.com/elastic/kibana/tree/master/src/plugins/vis_type_timeseries/common/index_patterns_utils.test.ts#:~:text=IndexPattern), [index_patterns_utils.test.ts](https://github.com/elastic/kibana/tree/master/src/plugins/vis_type_timeseries/common/index_patterns_utils.test.ts#:~:text=IndexPattern)+ 15 more | - | -| | [convert_series_to_datatable.test.ts](https://github.com/elastic/kibana/tree/master/src/plugins/vis_type_timeseries/public/application/components/lib/convert_series_to_datatable.test.ts#:~:text=IndexPatternField), [convert_series_to_datatable.test.ts](https://github.com/elastic/kibana/tree/master/src/plugins/vis_type_timeseries/public/application/components/lib/convert_series_to_datatable.test.ts#:~:text=IndexPatternField), [convert_series_to_datatable.test.ts](https://github.com/elastic/kibana/tree/master/src/plugins/vis_type_timeseries/public/application/components/lib/convert_series_to_datatable.test.ts#:~:text=IndexPatternField), [convert_series_to_datatable.test.ts](https://github.com/elastic/kibana/tree/master/src/plugins/vis_type_timeseries/public/application/components/lib/convert_series_to_datatable.test.ts#:~:text=IndexPatternField), [convert_series_to_datatable.test.ts](https://github.com/elastic/kibana/tree/master/src/plugins/vis_type_timeseries/public/application/components/lib/convert_series_to_datatable.test.ts#:~:text=IndexPatternField), [convert_series_to_datatable.test.ts](https://github.com/elastic/kibana/tree/master/src/plugins/vis_type_timeseries/public/application/components/lib/convert_series_to_datatable.test.ts#:~:text=IndexPatternField) | - | -| | [fetch_fields.ts](https://github.com/elastic/kibana/tree/master/src/plugins/vis_type_timeseries/public/application/lib/fetch_fields.ts#:~:text=indexPatterns), [combo_box_select.tsx](https://github.com/elastic/kibana/tree/master/src/plugins/vis_type_timeseries/public/application/components/lib/index_pattern_select/combo_box_select.tsx#:~:text=indexPatterns), [query_bar_wrapper.tsx](https://github.com/elastic/kibana/tree/master/src/plugins/vis_type_timeseries/public/application/components/query_bar_wrapper.tsx#:~:text=indexPatterns), [annotation_row.tsx](https://github.com/elastic/kibana/tree/master/src/plugins/vis_type_timeseries/public/application/components/annotation_row.tsx#:~:text=indexPatterns), [metrics_type.ts](https://github.com/elastic/kibana/tree/master/src/plugins/vis_type_timeseries/public/metrics_type.ts#:~:text=indexPatterns), [convert_series_to_datatable.ts](https://github.com/elastic/kibana/tree/master/src/plugins/vis_type_timeseries/public/application/components/lib/convert_series_to_datatable.ts#:~:text=indexPatterns), [timeseries_vis_renderer.tsx](https://github.com/elastic/kibana/tree/master/src/plugins/vis_type_timeseries/public/timeseries_vis_renderer.tsx#:~:text=indexPatterns) | - | -| | [plugin.ts](https://github.com/elastic/kibana/tree/master/src/plugins/vis_type_timeseries/public/plugin.ts#:~:text=fieldFormats) | - | -| | [types.ts](https://github.com/elastic/kibana/tree/master/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/table/types.ts#:~:text=EsQueryConfig), [types.ts](https://github.com/elastic/kibana/tree/master/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/table/types.ts#:~:text=EsQueryConfig) | 8.1 | -| | [index.ts](https://github.com/elastic/kibana/tree/master/src/plugins/vis_type_timeseries/common/types/index.ts#:~:text=Filter), [index.ts](https://github.com/elastic/kibana/tree/master/src/plugins/vis_type_timeseries/common/types/index.ts#:~:text=Filter) | 8.1 | -| | [index.ts](https://github.com/elastic/kibana/tree/master/src/plugins/vis_type_timeseries/common/types/index.ts#:~:text=Filter), [index.ts](https://github.com/elastic/kibana/tree/master/src/plugins/vis_type_timeseries/common/types/index.ts#:~:text=Filter) | 8.1 | -| | [types.ts](https://github.com/elastic/kibana/tree/master/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/table/types.ts#:~:text=EsQueryConfig), [types.ts](https://github.com/elastic/kibana/tree/master/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/table/types.ts#:~:text=EsQueryConfig) | 8.1 | -| | [abstract_search_strategy.ts](https://github.com/elastic/kibana/tree/master/src/plugins/vis_type_timeseries/server/lib/search_strategies/strategies/abstract_search_strategy.ts#:~:text=IndexPatternsService), [abstract_search_strategy.ts](https://github.com/elastic/kibana/tree/master/src/plugins/vis_type_timeseries/server/lib/search_strategies/strategies/abstract_search_strategy.ts#:~:text=IndexPatternsService), [default_search_strategy.ts](https://github.com/elastic/kibana/tree/master/src/plugins/vis_type_timeseries/server/lib/search_strategies/strategies/default_search_strategy.ts#:~:text=IndexPatternsService), [default_search_strategy.ts](https://github.com/elastic/kibana/tree/master/src/plugins/vis_type_timeseries/server/lib/search_strategies/strategies/default_search_strategy.ts#:~:text=IndexPatternsService), [cached_index_pattern_fetcher.ts](https://github.com/elastic/kibana/tree/master/src/plugins/vis_type_timeseries/server/lib/search_strategies/lib/cached_index_pattern_fetcher.ts#:~:text=IndexPatternsService), [cached_index_pattern_fetcher.ts](https://github.com/elastic/kibana/tree/master/src/plugins/vis_type_timeseries/server/lib/search_strategies/lib/cached_index_pattern_fetcher.ts#:~:text=IndexPatternsService), [rollup_search_strategy.ts](https://github.com/elastic/kibana/tree/master/src/plugins/vis_type_timeseries/server/lib/search_strategies/strategies/rollup_search_strategy.ts#:~:text=IndexPatternsService), [rollup_search_strategy.ts](https://github.com/elastic/kibana/tree/master/src/plugins/vis_type_timeseries/server/lib/search_strategies/strategies/rollup_search_strategy.ts#:~:text=IndexPatternsService), [types.ts](https://github.com/elastic/kibana/tree/master/src/plugins/vis_type_timeseries/server/types.ts#:~:text=IndexPatternsService), [types.ts](https://github.com/elastic/kibana/tree/master/src/plugins/vis_type_timeseries/server/types.ts#:~:text=IndexPatternsService)+ 17 more | - | -| | [cached_index_pattern_fetcher.test.ts](https://github.com/elastic/kibana/tree/master/src/plugins/vis_type_timeseries/server/lib/search_strategies/lib/cached_index_pattern_fetcher.test.ts#:~:text=IndexPattern), [cached_index_pattern_fetcher.test.ts](https://github.com/elastic/kibana/tree/master/src/plugins/vis_type_timeseries/server/lib/search_strategies/lib/cached_index_pattern_fetcher.test.ts#:~:text=IndexPattern), [cached_index_pattern_fetcher.test.ts](https://github.com/elastic/kibana/tree/master/src/plugins/vis_type_timeseries/server/lib/search_strategies/lib/cached_index_pattern_fetcher.test.ts#:~:text=IndexPattern), [cached_index_pattern_fetcher.test.ts](https://github.com/elastic/kibana/tree/master/src/plugins/vis_type_timeseries/server/lib/search_strategies/lib/cached_index_pattern_fetcher.test.ts#:~:text=IndexPattern), [index.ts](https://github.com/elastic/kibana/tree/master/src/plugins/vis_type_timeseries/common/types/index.ts#:~:text=IndexPattern), [index.ts](https://github.com/elastic/kibana/tree/master/src/plugins/vis_type_timeseries/common/types/index.ts#:~:text=IndexPattern), [index_patterns_utils.test.ts](https://github.com/elastic/kibana/tree/master/src/plugins/vis_type_timeseries/common/index_patterns_utils.test.ts#:~:text=IndexPattern), [index_patterns_utils.test.ts](https://github.com/elastic/kibana/tree/master/src/plugins/vis_type_timeseries/common/index_patterns_utils.test.ts#:~:text=IndexPattern), [index_patterns_utils.test.ts](https://github.com/elastic/kibana/tree/master/src/plugins/vis_type_timeseries/common/index_patterns_utils.test.ts#:~:text=IndexPattern), [index_patterns_utils.test.ts](https://github.com/elastic/kibana/tree/master/src/plugins/vis_type_timeseries/common/index_patterns_utils.test.ts#:~:text=IndexPattern)+ 15 more | - | -| | [abstract_search_strategy.ts](https://github.com/elastic/kibana/tree/master/src/plugins/vis_type_timeseries/server/lib/search_strategies/strategies/abstract_search_strategy.ts#:~:text=getNonScriptedFields), [fetch_fields.ts](https://github.com/elastic/kibana/tree/master/src/plugins/vis_type_timeseries/public/application/lib/fetch_fields.ts#:~:text=getNonScriptedFields) | 8.1 | -| | [convert_series_to_datatable.test.ts](https://github.com/elastic/kibana/tree/master/src/plugins/vis_type_timeseries/public/application/components/lib/convert_series_to_datatable.test.ts#:~:text=IndexPatternField), [convert_series_to_datatable.test.ts](https://github.com/elastic/kibana/tree/master/src/plugins/vis_type_timeseries/public/application/components/lib/convert_series_to_datatable.test.ts#:~:text=IndexPatternField), [convert_series_to_datatable.test.ts](https://github.com/elastic/kibana/tree/master/src/plugins/vis_type_timeseries/public/application/components/lib/convert_series_to_datatable.test.ts#:~:text=IndexPatternField), [convert_series_to_datatable.test.ts](https://github.com/elastic/kibana/tree/master/src/plugins/vis_type_timeseries/public/application/components/lib/convert_series_to_datatable.test.ts#:~:text=IndexPatternField), [convert_series_to_datatable.test.ts](https://github.com/elastic/kibana/tree/master/src/plugins/vis_type_timeseries/public/application/components/lib/convert_series_to_datatable.test.ts#:~:text=IndexPatternField), [convert_series_to_datatable.test.ts](https://github.com/elastic/kibana/tree/master/src/plugins/vis_type_timeseries/public/application/components/lib/convert_series_to_datatable.test.ts#:~:text=IndexPatternField) | - | -| | [convert_series_to_datatable.test.ts](https://github.com/elastic/kibana/tree/master/src/plugins/vis_type_timeseries/public/application/components/lib/convert_series_to_datatable.test.ts#:~:text=IndexPatternField), [convert_series_to_datatable.test.ts](https://github.com/elastic/kibana/tree/master/src/plugins/vis_type_timeseries/public/application/components/lib/convert_series_to_datatable.test.ts#:~:text=IndexPatternField), [convert_series_to_datatable.test.ts](https://github.com/elastic/kibana/tree/master/src/plugins/vis_type_timeseries/public/application/components/lib/convert_series_to_datatable.test.ts#:~:text=IndexPatternField), [convert_series_to_datatable.test.ts](https://github.com/elastic/kibana/tree/master/src/plugins/vis_type_timeseries/public/application/components/lib/convert_series_to_datatable.test.ts#:~:text=IndexPatternField), [convert_series_to_datatable.test.ts](https://github.com/elastic/kibana/tree/master/src/plugins/vis_type_timeseries/public/application/components/lib/convert_series_to_datatable.test.ts#:~:text=IndexPatternField), [convert_series_to_datatable.test.ts](https://github.com/elastic/kibana/tree/master/src/plugins/vis_type_timeseries/public/application/components/lib/convert_series_to_datatable.test.ts#:~:text=IndexPatternField) | - | -| | [cached_index_pattern_fetcher.test.ts](https://github.com/elastic/kibana/tree/master/src/plugins/vis_type_timeseries/server/lib/search_strategies/lib/cached_index_pattern_fetcher.test.ts#:~:text=IndexPattern), [cached_index_pattern_fetcher.test.ts](https://github.com/elastic/kibana/tree/master/src/plugins/vis_type_timeseries/server/lib/search_strategies/lib/cached_index_pattern_fetcher.test.ts#:~:text=IndexPattern), [cached_index_pattern_fetcher.test.ts](https://github.com/elastic/kibana/tree/master/src/plugins/vis_type_timeseries/server/lib/search_strategies/lib/cached_index_pattern_fetcher.test.ts#:~:text=IndexPattern), [cached_index_pattern_fetcher.test.ts](https://github.com/elastic/kibana/tree/master/src/plugins/vis_type_timeseries/server/lib/search_strategies/lib/cached_index_pattern_fetcher.test.ts#:~:text=IndexPattern), [index.ts](https://github.com/elastic/kibana/tree/master/src/plugins/vis_type_timeseries/common/types/index.ts#:~:text=IndexPattern), [index.ts](https://github.com/elastic/kibana/tree/master/src/plugins/vis_type_timeseries/common/types/index.ts#:~:text=IndexPattern), [index_patterns_utils.test.ts](https://github.com/elastic/kibana/tree/master/src/plugins/vis_type_timeseries/common/index_patterns_utils.test.ts#:~:text=IndexPattern), [index_patterns_utils.test.ts](https://github.com/elastic/kibana/tree/master/src/plugins/vis_type_timeseries/common/index_patterns_utils.test.ts#:~:text=IndexPattern), [index_patterns_utils.test.ts](https://github.com/elastic/kibana/tree/master/src/plugins/vis_type_timeseries/common/index_patterns_utils.test.ts#:~:text=IndexPattern), [index_patterns_utils.test.ts](https://github.com/elastic/kibana/tree/master/src/plugins/vis_type_timeseries/common/index_patterns_utils.test.ts#:~:text=IndexPattern)+ 15 more | - | -| | [abstract_search_strategy.ts](https://github.com/elastic/kibana/tree/master/src/plugins/vis_type_timeseries/server/lib/search_strategies/strategies/abstract_search_strategy.ts#:~:text=IndexPatternsService), [abstract_search_strategy.ts](https://github.com/elastic/kibana/tree/master/src/plugins/vis_type_timeseries/server/lib/search_strategies/strategies/abstract_search_strategy.ts#:~:text=IndexPatternsService), [default_search_strategy.ts](https://github.com/elastic/kibana/tree/master/src/plugins/vis_type_timeseries/server/lib/search_strategies/strategies/default_search_strategy.ts#:~:text=IndexPatternsService), [default_search_strategy.ts](https://github.com/elastic/kibana/tree/master/src/plugins/vis_type_timeseries/server/lib/search_strategies/strategies/default_search_strategy.ts#:~:text=IndexPatternsService), [cached_index_pattern_fetcher.ts](https://github.com/elastic/kibana/tree/master/src/plugins/vis_type_timeseries/server/lib/search_strategies/lib/cached_index_pattern_fetcher.ts#:~:text=IndexPatternsService), [cached_index_pattern_fetcher.ts](https://github.com/elastic/kibana/tree/master/src/plugins/vis_type_timeseries/server/lib/search_strategies/lib/cached_index_pattern_fetcher.ts#:~:text=IndexPatternsService), [rollup_search_strategy.ts](https://github.com/elastic/kibana/tree/master/src/plugins/vis_type_timeseries/server/lib/search_strategies/strategies/rollup_search_strategy.ts#:~:text=IndexPatternsService), [rollup_search_strategy.ts](https://github.com/elastic/kibana/tree/master/src/plugins/vis_type_timeseries/server/lib/search_strategies/strategies/rollup_search_strategy.ts#:~:text=IndexPatternsService), [types.ts](https://github.com/elastic/kibana/tree/master/src/plugins/vis_type_timeseries/server/types.ts#:~:text=IndexPatternsService), [types.ts](https://github.com/elastic/kibana/tree/master/src/plugins/vis_type_timeseries/server/types.ts#:~:text=IndexPatternsService)+ 44 more | - | -| | [plugin.ts](https://github.com/elastic/kibana/tree/master/src/plugins/vis_type_timeseries/server/plugin.ts#:~:text=fieldFormats) | - | -| | [index.ts](https://github.com/elastic/kibana/tree/master/src/plugins/vis_type_timeseries/common/types/index.ts#:~:text=Filter), [index.ts](https://github.com/elastic/kibana/tree/master/src/plugins/vis_type_timeseries/common/types/index.ts#:~:text=Filter) | 8.1 | -| | [types.ts](https://github.com/elastic/kibana/tree/master/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/table/types.ts#:~:text=EsQueryConfig), [types.ts](https://github.com/elastic/kibana/tree/master/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/table/types.ts#:~:text=EsQueryConfig) | 8.1 | +| | [abstract_search_strategy.ts](https://github.com/elastic/kibana/tree/master/src/plugins/vis_types/timeseries/server/lib/search_strategies/strategies/abstract_search_strategy.ts#:~:text=IndexPatternsService), [abstract_search_strategy.ts](https://github.com/elastic/kibana/tree/master/src/plugins/vis_types/timeseries/server/lib/search_strategies/strategies/abstract_search_strategy.ts#:~:text=IndexPatternsService), [default_search_strategy.ts](https://github.com/elastic/kibana/tree/master/src/plugins/vis_types/timeseries/server/lib/search_strategies/strategies/default_search_strategy.ts#:~:text=IndexPatternsService), [default_search_strategy.ts](https://github.com/elastic/kibana/tree/master/src/plugins/vis_types/timeseries/server/lib/search_strategies/strategies/default_search_strategy.ts#:~:text=IndexPatternsService), [cached_index_pattern_fetcher.ts](https://github.com/elastic/kibana/tree/master/src/plugins/vis_types/timeseries/server/lib/search_strategies/lib/cached_index_pattern_fetcher.ts#:~:text=IndexPatternsService), [cached_index_pattern_fetcher.ts](https://github.com/elastic/kibana/tree/master/src/plugins/vis_types/timeseries/server/lib/search_strategies/lib/cached_index_pattern_fetcher.ts#:~:text=IndexPatternsService), [rollup_search_strategy.ts](https://github.com/elastic/kibana/tree/master/src/plugins/vis_types/timeseries/server/lib/search_strategies/strategies/rollup_search_strategy.ts#:~:text=IndexPatternsService), [rollup_search_strategy.ts](https://github.com/elastic/kibana/tree/master/src/plugins/vis_types/timeseries/server/lib/search_strategies/strategies/rollup_search_strategy.ts#:~:text=IndexPatternsService), [types.ts](https://github.com/elastic/kibana/tree/master/src/plugins/vis_types/timeseries/server/types.ts#:~:text=IndexPatternsService), [types.ts](https://github.com/elastic/kibana/tree/master/src/plugins/vis_types/timeseries/server/types.ts#:~:text=IndexPatternsService)+ 17 more | - | +| | [cached_index_pattern_fetcher.test.ts](https://github.com/elastic/kibana/tree/master/src/plugins/vis_types/timeseries/server/lib/search_strategies/lib/cached_index_pattern_fetcher.test.ts#:~:text=IndexPattern), [cached_index_pattern_fetcher.test.ts](https://github.com/elastic/kibana/tree/master/src/plugins/vis_types/timeseries/server/lib/search_strategies/lib/cached_index_pattern_fetcher.test.ts#:~:text=IndexPattern), [cached_index_pattern_fetcher.test.ts](https://github.com/elastic/kibana/tree/master/src/plugins/vis_types/timeseries/server/lib/search_strategies/lib/cached_index_pattern_fetcher.test.ts#:~:text=IndexPattern), [cached_index_pattern_fetcher.test.ts](https://github.com/elastic/kibana/tree/master/src/plugins/vis_types/timeseries/server/lib/search_strategies/lib/cached_index_pattern_fetcher.test.ts#:~:text=IndexPattern), [index.ts](https://github.com/elastic/kibana/tree/master/src/plugins/vis_types/timeseries/common/types/index.ts#:~:text=IndexPattern), [index.ts](https://github.com/elastic/kibana/tree/master/src/plugins/vis_types/timeseries/common/types/index.ts#:~:text=IndexPattern), [index_patterns_utils.test.ts](https://github.com/elastic/kibana/tree/master/src/plugins/vis_types/timeseries/common/index_patterns_utils.test.ts#:~:text=IndexPattern), [index_patterns_utils.test.ts](https://github.com/elastic/kibana/tree/master/src/plugins/vis_types/timeseries/common/index_patterns_utils.test.ts#:~:text=IndexPattern), [index_patterns_utils.test.ts](https://github.com/elastic/kibana/tree/master/src/plugins/vis_types/timeseries/common/index_patterns_utils.test.ts#:~:text=IndexPattern), [index_patterns_utils.test.ts](https://github.com/elastic/kibana/tree/master/src/plugins/vis_types/timeseries/common/index_patterns_utils.test.ts#:~:text=IndexPattern)+ 15 more | - | +| | [convert_series_to_datatable.test.ts](https://github.com/elastic/kibana/tree/master/src/plugins/vis_types/timeseries/public/application/components/lib/convert_series_to_datatable.test.ts#:~:text=IndexPatternField), [convert_series_to_datatable.test.ts](https://github.com/elastic/kibana/tree/master/src/plugins/vis_types/timeseries/public/application/components/lib/convert_series_to_datatable.test.ts#:~:text=IndexPatternField), [convert_series_to_datatable.test.ts](https://github.com/elastic/kibana/tree/master/src/plugins/vis_types/timeseries/public/application/components/lib/convert_series_to_datatable.test.ts#:~:text=IndexPatternField), [convert_series_to_datatable.test.ts](https://github.com/elastic/kibana/tree/master/src/plugins/vis_types/timeseries/public/application/components/lib/convert_series_to_datatable.test.ts#:~:text=IndexPatternField), [convert_series_to_datatable.test.ts](https://github.com/elastic/kibana/tree/master/src/plugins/vis_types/timeseries/public/application/components/lib/convert_series_to_datatable.test.ts#:~:text=IndexPatternField), [convert_series_to_datatable.test.ts](https://github.com/elastic/kibana/tree/master/src/plugins/vis_types/timeseries/public/application/components/lib/convert_series_to_datatable.test.ts#:~:text=IndexPatternField) | - | +| | [fetch_fields.ts](https://github.com/elastic/kibana/tree/master/src/plugins/vis_types/timeseries/public/application/lib/fetch_fields.ts#:~:text=indexPatterns), [combo_box_select.tsx](https://github.com/elastic/kibana/tree/master/src/plugins/vis_types/timeseries/public/application/components/lib/index_pattern_select/combo_box_select.tsx#:~:text=indexPatterns), [query_bar_wrapper.tsx](https://github.com/elastic/kibana/tree/master/src/plugins/vis_types/timeseries/public/application/components/query_bar_wrapper.tsx#:~:text=indexPatterns), [annotation_row.tsx](https://github.com/elastic/kibana/tree/master/src/plugins/vis_types/timeseries/public/application/components/annotation_row.tsx#:~:text=indexPatterns), [metrics_type.ts](https://github.com/elastic/kibana/tree/master/src/plugins/vis_types/timeseries/public/metrics_type.ts#:~:text=indexPatterns), [convert_series_to_datatable.ts](https://github.com/elastic/kibana/tree/master/src/plugins/vis_types/timeseries/public/application/components/lib/convert_series_to_datatable.ts#:~:text=indexPatterns), [timeseries_vis_renderer.tsx](https://github.com/elastic/kibana/tree/master/src/plugins/vis_types/timeseries/public/timeseries_vis_renderer.tsx#:~:text=indexPatterns) | - | +| | [plugin.ts](https://github.com/elastic/kibana/tree/master/src/plugins/vis_types/timeseries/public/plugin.ts#:~:text=fieldFormats) | - | +| | [types.ts](https://github.com/elastic/kibana/tree/master/src/plugins/vis_types/timeseries/server/lib/vis_data/request_processors/table/types.ts#:~:text=EsQueryConfig), [types.ts](https://github.com/elastic/kibana/tree/master/src/plugins/vis_types/timeseries/server/lib/vis_data/request_processors/table/types.ts#:~:text=EsQueryConfig) | 8.1 | +| | [index.ts](https://github.com/elastic/kibana/tree/master/src/plugins/vis_types/timeseries/common/types/index.ts#:~:text=Filter), [index.ts](https://github.com/elastic/kibana/tree/master/src/plugins/vis_types/timeseries/common/types/index.ts#:~:text=Filter) | 8.1 | +| | [index.ts](https://github.com/elastic/kibana/tree/master/src/plugins/vis_types/timeseries/common/types/index.ts#:~:text=Filter), [index.ts](https://github.com/elastic/kibana/tree/master/src/plugins/vis_types/timeseries/common/types/index.ts#:~:text=Filter) | 8.1 | +| | [types.ts](https://github.com/elastic/kibana/tree/master/src/plugins/vis_types/timeseries/server/lib/vis_data/request_processors/table/types.ts#:~:text=EsQueryConfig), [types.ts](https://github.com/elastic/kibana/tree/master/src/plugins/vis_types/timeseries/server/lib/vis_data/request_processors/table/types.ts#:~:text=EsQueryConfig) | 8.1 | +| | [abstract_search_strategy.ts](https://github.com/elastic/kibana/tree/master/src/plugins/vis_types/timeseries/server/lib/search_strategies/strategies/abstract_search_strategy.ts#:~:text=IndexPatternsService), [abstract_search_strategy.ts](https://github.com/elastic/kibana/tree/master/src/plugins/vis_types/timeseries/server/lib/search_strategies/strategies/abstract_search_strategy.ts#:~:text=IndexPatternsService), [default_search_strategy.ts](https://github.com/elastic/kibana/tree/master/src/plugins/vis_types/timeseries/server/lib/search_strategies/strategies/default_search_strategy.ts#:~:text=IndexPatternsService), [default_search_strategy.ts](https://github.com/elastic/kibana/tree/master/src/plugins/vis_types/timeseries/server/lib/search_strategies/strategies/default_search_strategy.ts#:~:text=IndexPatternsService), [cached_index_pattern_fetcher.ts](https://github.com/elastic/kibana/tree/master/src/plugins/vis_types/timeseries/server/lib/search_strategies/lib/cached_index_pattern_fetcher.ts#:~:text=IndexPatternsService), [cached_index_pattern_fetcher.ts](https://github.com/elastic/kibana/tree/master/src/plugins/vis_types/timeseries/server/lib/search_strategies/lib/cached_index_pattern_fetcher.ts#:~:text=IndexPatternsService), [rollup_search_strategy.ts](https://github.com/elastic/kibana/tree/master/src/plugins/vis_types/timeseries/server/lib/search_strategies/strategies/rollup_search_strategy.ts#:~:text=IndexPatternsService), [rollup_search_strategy.ts](https://github.com/elastic/kibana/tree/master/src/plugins/vis_types/timeseries/server/lib/search_strategies/strategies/rollup_search_strategy.ts#:~:text=IndexPatternsService), [types.ts](https://github.com/elastic/kibana/tree/master/src/plugins/vis_types/timeseries/server/types.ts#:~:text=IndexPatternsService), [types.ts](https://github.com/elastic/kibana/tree/master/src/plugins/vis_types/timeseries/server/types.ts#:~:text=IndexPatternsService)+ 17 more | - | +| | [cached_index_pattern_fetcher.test.ts](https://github.com/elastic/kibana/tree/master/src/plugins/vis_types/timeseries/server/lib/search_strategies/lib/cached_index_pattern_fetcher.test.ts#:~:text=IndexPattern), [cached_index_pattern_fetcher.test.ts](https://github.com/elastic/kibana/tree/master/src/plugins/vis_types/timeseries/server/lib/search_strategies/lib/cached_index_pattern_fetcher.test.ts#:~:text=IndexPattern), [cached_index_pattern_fetcher.test.ts](https://github.com/elastic/kibana/tree/master/src/plugins/vis_types/timeseries/server/lib/search_strategies/lib/cached_index_pattern_fetcher.test.ts#:~:text=IndexPattern), [cached_index_pattern_fetcher.test.ts](https://github.com/elastic/kibana/tree/master/src/plugins/vis_types/timeseries/server/lib/search_strategies/lib/cached_index_pattern_fetcher.test.ts#:~:text=IndexPattern), [index.ts](https://github.com/elastic/kibana/tree/master/src/plugins/vis_types/timeseries/common/types/index.ts#:~:text=IndexPattern), [index.ts](https://github.com/elastic/kibana/tree/master/src/plugins/vis_types/timeseries/common/types/index.ts#:~:text=IndexPattern), [index_patterns_utils.test.ts](https://github.com/elastic/kibana/tree/master/src/plugins/vis_types/timeseries/common/index_patterns_utils.test.ts#:~:text=IndexPattern), [index_patterns_utils.test.ts](https://github.com/elastic/kibana/tree/master/src/plugins/vis_types/timeseries/common/index_patterns_utils.test.ts#:~:text=IndexPattern), [index_patterns_utils.test.ts](https://github.com/elastic/kibana/tree/master/src/plugins/vis_types/timeseries/common/index_patterns_utils.test.ts#:~:text=IndexPattern), [index_patterns_utils.test.ts](https://github.com/elastic/kibana/tree/master/src/plugins/vis_types/timeseries/common/index_patterns_utils.test.ts#:~:text=IndexPattern)+ 15 more | - | +| | [abstract_search_strategy.ts](https://github.com/elastic/kibana/tree/master/src/plugins/vis_types/timeseries/server/lib/search_strategies/strategies/abstract_search_strategy.ts#:~:text=getNonScriptedFields), [fetch_fields.ts](https://github.com/elastic/kibana/tree/master/src/plugins/vis_types/timeseries/public/application/lib/fetch_fields.ts#:~:text=getNonScriptedFields) | 8.1 | +| | [convert_series_to_datatable.test.ts](https://github.com/elastic/kibana/tree/master/src/plugins/vis_types/timeseries/public/application/components/lib/convert_series_to_datatable.test.ts#:~:text=IndexPatternField), [convert_series_to_datatable.test.ts](https://github.com/elastic/kibana/tree/master/src/plugins/vis_types/timeseries/public/application/components/lib/convert_series_to_datatable.test.ts#:~:text=IndexPatternField), [convert_series_to_datatable.test.ts](https://github.com/elastic/kibana/tree/master/src/plugins/vis_types/timeseries/public/application/components/lib/convert_series_to_datatable.test.ts#:~:text=IndexPatternField), [convert_series_to_datatable.test.ts](https://github.com/elastic/kibana/tree/master/src/plugins/vis_types/timeseries/public/application/components/lib/convert_series_to_datatable.test.ts#:~:text=IndexPatternField), [convert_series_to_datatable.test.ts](https://github.com/elastic/kibana/tree/master/src/plugins/vis_types/timeseries/public/application/components/lib/convert_series_to_datatable.test.ts#:~:text=IndexPatternField), [convert_series_to_datatable.test.ts](https://github.com/elastic/kibana/tree/master/src/plugins/vis_types/timeseries/public/application/components/lib/convert_series_to_datatable.test.ts#:~:text=IndexPatternField) | - | +| | [convert_series_to_datatable.test.ts](https://github.com/elastic/kibana/tree/master/src/plugins/vis_types/timeseries/public/application/components/lib/convert_series_to_datatable.test.ts#:~:text=IndexPatternField), [convert_series_to_datatable.test.ts](https://github.com/elastic/kibana/tree/master/src/plugins/vis_types/timeseries/public/application/components/lib/convert_series_to_datatable.test.ts#:~:text=IndexPatternField), [convert_series_to_datatable.test.ts](https://github.com/elastic/kibana/tree/master/src/plugins/vis_types/timeseries/public/application/components/lib/convert_series_to_datatable.test.ts#:~:text=IndexPatternField), [convert_series_to_datatable.test.ts](https://github.com/elastic/kibana/tree/master/src/plugins/vis_types/timeseries/public/application/components/lib/convert_series_to_datatable.test.ts#:~:text=IndexPatternField), [convert_series_to_datatable.test.ts](https://github.com/elastic/kibana/tree/master/src/plugins/vis_types/timeseries/public/application/components/lib/convert_series_to_datatable.test.ts#:~:text=IndexPatternField), [convert_series_to_datatable.test.ts](https://github.com/elastic/kibana/tree/master/src/plugins/vis_types/timeseries/public/application/components/lib/convert_series_to_datatable.test.ts#:~:text=IndexPatternField) | - | +| | [cached_index_pattern_fetcher.test.ts](https://github.com/elastic/kibana/tree/master/src/plugins/vis_types/timeseries/server/lib/search_strategies/lib/cached_index_pattern_fetcher.test.ts#:~:text=IndexPattern), [cached_index_pattern_fetcher.test.ts](https://github.com/elastic/kibana/tree/master/src/plugins/vis_types/timeseries/server/lib/search_strategies/lib/cached_index_pattern_fetcher.test.ts#:~:text=IndexPattern), [cached_index_pattern_fetcher.test.ts](https://github.com/elastic/kibana/tree/master/src/plugins/vis_types/timeseries/server/lib/search_strategies/lib/cached_index_pattern_fetcher.test.ts#:~:text=IndexPattern), [cached_index_pattern_fetcher.test.ts](https://github.com/elastic/kibana/tree/master/src/plugins/vis_types/timeseries/server/lib/search_strategies/lib/cached_index_pattern_fetcher.test.ts#:~:text=IndexPattern), [index.ts](https://github.com/elastic/kibana/tree/master/src/plugins/vis_types/timeseries/common/types/index.ts#:~:text=IndexPattern), [index.ts](https://github.com/elastic/kibana/tree/master/src/plugins/vis_types/timeseries/common/types/index.ts#:~:text=IndexPattern), [index_patterns_utils.test.ts](https://github.com/elastic/kibana/tree/master/src/plugins/vis_types/timeseries/common/index_patterns_utils.test.ts#:~:text=IndexPattern), [index_patterns_utils.test.ts](https://github.com/elastic/kibana/tree/master/src/plugins/vis_types/timeseries/common/index_patterns_utils.test.ts#:~:text=IndexPattern), [index_patterns_utils.test.ts](https://github.com/elastic/kibana/tree/master/src/plugins/vis_types/timeseries/common/index_patterns_utils.test.ts#:~:text=IndexPattern), [index_patterns_utils.test.ts](https://github.com/elastic/kibana/tree/master/src/plugins/vis_types/timeseries/common/index_patterns_utils.test.ts#:~:text=IndexPattern)+ 15 more | - | +| | [abstract_search_strategy.ts](https://github.com/elastic/kibana/tree/master/src/plugins/vis_types/timeseries/server/lib/search_strategies/strategies/abstract_search_strategy.ts#:~:text=IndexPatternsService), [abstract_search_strategy.ts](https://github.com/elastic/kibana/tree/master/src/plugins/vis_types/timeseries/server/lib/search_strategies/strategies/abstract_search_strategy.ts#:~:text=IndexPatternsService), [default_search_strategy.ts](https://github.com/elastic/kibana/tree/master/src/plugins/vis_types/timeseries/server/lib/search_strategies/strategies/default_search_strategy.ts#:~:text=IndexPatternsService), [default_search_strategy.ts](https://github.com/elastic/kibana/tree/master/src/plugins/vis_types/timeseries/server/lib/search_strategies/strategies/default_search_strategy.ts#:~:text=IndexPatternsService), [cached_index_pattern_fetcher.ts](https://github.com/elastic/kibana/tree/master/src/plugins/vis_types/timeseries/server/lib/search_strategies/lib/cached_index_pattern_fetcher.ts#:~:text=IndexPatternsService), [cached_index_pattern_fetcher.ts](https://github.com/elastic/kibana/tree/master/src/plugins/vis_types/timeseries/server/lib/search_strategies/lib/cached_index_pattern_fetcher.ts#:~:text=IndexPatternsService), [rollup_search_strategy.ts](https://github.com/elastic/kibana/tree/master/src/plugins/vis_types/timeseries/server/lib/search_strategies/strategies/rollup_search_strategy.ts#:~:text=IndexPatternsService), [rollup_search_strategy.ts](https://github.com/elastic/kibana/tree/master/src/plugins/vis_types/timeseries/server/lib/search_strategies/strategies/rollup_search_strategy.ts#:~:text=IndexPatternsService), [types.ts](https://github.com/elastic/kibana/tree/master/src/plugins/vis_types/timeseries/server/types.ts#:~:text=IndexPatternsService), [types.ts](https://github.com/elastic/kibana/tree/master/src/plugins/vis_types/timeseries/server/types.ts#:~:text=IndexPatternsService)+ 44 more | - | +| | [plugin.ts](https://github.com/elastic/kibana/tree/master/src/plugins/vis_types/timeseries/server/plugin.ts#:~:text=fieldFormats) | - | +| | [index.ts](https://github.com/elastic/kibana/tree/master/src/plugins/vis_types/timeseries/common/types/index.ts#:~:text=Filter), [index.ts](https://github.com/elastic/kibana/tree/master/src/plugins/vis_types/timeseries/common/types/index.ts#:~:text=Filter) | 8.1 | +| | [types.ts](https://github.com/elastic/kibana/tree/master/src/plugins/vis_types/timeseries/server/lib/vis_data/request_processors/table/types.ts#:~:text=EsQueryConfig), [types.ts](https://github.com/elastic/kibana/tree/master/src/plugins/vis_types/timeseries/server/lib/vis_data/request_processors/table/types.ts#:~:text=EsQueryConfig) | 8.1 | diff --git a/api_docs/vis_type_timeseries.json b/api_docs/vis_type_timeseries.json index 01b211e3da90b..885f037b00a25 100644 --- a/api_docs/vis_type_timeseries.json +++ b/api_docs/vis_type_timeseries.json @@ -24,7 +24,7 @@ ") => data is ", "SeriesData" ], - "path": "src/plugins/vis_type_timeseries/common/vis_data_utils.ts", + "path": "src/plugins/vis_types/timeseries/common/vis_data_utils.ts", "deprecated": false, "children": [ { @@ -37,7 +37,7 @@ "signature": [ "TimeseriesVisData" ], - "path": "src/plugins/vis_type_timeseries/common/vis_data_utils.ts", + "path": "src/plugins/vis_types/timeseries/common/vis_data_utils.ts", "deprecated": false, "isRequired": true } @@ -58,7 +58,7 @@ ") => data is ", "TableData" ], - "path": "src/plugins/vis_type_timeseries/common/vis_data_utils.ts", + "path": "src/plugins/vis_types/timeseries/common/vis_data_utils.ts", "deprecated": false, "children": [ { @@ -71,7 +71,7 @@ "signature": [ "TimeseriesVisData" ], - "path": "src/plugins/vis_type_timeseries/common/vis_data_utils.ts", + "path": "src/plugins/vis_types/timeseries/common/vis_data_utils.ts", "deprecated": false, "isRequired": true } @@ -95,7 +95,7 @@ " | ", "TableData" ], - "path": "src/plugins/vis_type_timeseries/common/types/vis_data.ts", + "path": "src/plugins/vis_types/timeseries/common/types/vis_data.ts", "deprecated": false, "initialIsOpen": false } @@ -108,7 +108,7 @@ "tags": [], "label": "VisTypeTimeseriesSetup", "description": [], - "path": "src/plugins/vis_type_timeseries/server/plugin.ts", + "path": "src/plugins/vis_types/timeseries/server/plugin.ts", "deprecated": false, "children": [ { @@ -133,7 +133,7 @@ "TimeseriesVisData", ">" ], - "path": "src/plugins/vis_type_timeseries/server/plugin.ts", + "path": "src/plugins/vis_types/timeseries/server/plugin.ts", "deprecated": false, "children": [ { @@ -146,7 +146,7 @@ "signature": [ "DataRequestHandlerContext" ], - "path": "src/plugins/vis_type_timeseries/server/plugin.ts", + "path": "src/plugins/vis_types/timeseries/server/plugin.ts", "deprecated": false, "isRequired": true }, @@ -167,7 +167,7 @@ }, "" ], - "path": "src/plugins/vis_type_timeseries/server/plugin.ts", + "path": "src/plugins/vis_types/timeseries/server/plugin.ts", "deprecated": false, "isRequired": true }, @@ -181,7 +181,7 @@ "signature": [ "any" ], - "path": "src/plugins/vis_type_timeseries/server/plugin.ts", + "path": "src/plugins/vis_types/timeseries/server/plugin.ts", "deprecated": false, "isRequired": true } diff --git a/dev_docs/key_concepts/persistable_state.mdx b/dev_docs/key_concepts/persistable_state.mdx new file mode 100644 index 0000000000000..77c7e41ff2e53 --- /dev/null +++ b/dev_docs/key_concepts/persistable_state.mdx @@ -0,0 +1,83 @@ +--- +id: kibDevDocsPersistableStateIntro +slug: /kibana-dev-docs/persistable-state-intro +title: Persistable State +summary: Persitable state is a key concept to understand when building a Kibana plugin. +date: 2021-02-02 +tags: ['kibana','dev', 'contributor', 'api docs'] +--- + + “Persistable state” is developer-defined state that supports being persisted by a plugin other than the one defining it. Persistable State needs to be serializable and the owner can/should provide utilities to migrate it, extract and inject any it may contain, as well as telemetry collection utilities. + +## Exposing state that can be persisted + +Any plugin that exposes state that another plugin might persist should implement interface on their `setup` contract. This will allow plugins persisting the state to easily access migrations and other utilities. + +Example: Data plugin allows you to generate filters. Those filters can be persisted by applications in their saved +objects or in the URL. In order to allow apps to migrate the filters in case the structure changes in the future, the Data plugin implements `PersistableStateService` on . + +note: There is currently no obvious way for a plugin to know which state is safe to persist. The developer must manually look for a matching `PersistableStateService`, or ad-hoc provided migration utilities (as is the case with Rule Type Parameters). +In the future, we hope to make it easier for producers of state to understand when they need to write a migration with changes, and also make it easier for consumers of such state, to understand whether it is safe to persist. + +## Exposing state that can be persisted but is not owned by plugin exposing it (registry) + +Any plugin that owns collection of items (registry) whose state/configuration can be persisted should implement `PersistableStateService` +interface on their `setup` contract and each item in the collection should implement interface. + +Example: Embeddable plugin owns the registry of embeddable factories to which other plugins can register new embeddable factories. Dashboard plugin +stores a bunch of embeddable panels input in its saved object and URL. Embeddable plugin setup contract implements `PersistableStateService` +interface and each `EmbeddableFactory` needs to implement `PersistableStateDefinition` interface. + +Embeddable plugin exposes this interfaces: +``` +// EmbeddableInput implements Serializable + +export interface EmbeddableRegistryDefinition extends PersistableStateDefinition { + id: string; + ... +} + +export interface EmbeddableSetup extends PersistableStateService; +``` + +Note: if your plugin doesn't expose the state (it is the only one storing state), the plugin doesn't need to implement the `PersistableStateService` interface. +If the state your plugin is storing can be provided by other plugins (your plugin owns a registry) items in that registry still need to implement `PersistableStateDefinition` interface. + +## Storing persistable state as part of saved object + +Any plugin that stores any persistable state as part of their saved object should make sure that its saved object migration +and reference extraction and injection methods correctly use the matching `PersistableStateService` implementation for the state they are storing. + +Take a look at [example saved object](https://github.com/elastic/kibana/blob/master/examples/embeddable_examples/server/searchable_list_saved_object.ts#L32) which stores an embeddable state. Note how the `migrations`, `extractReferences` and `injectReferences` are defined. + +## Storing persistable state as part of URL + +When storing persistable state as part of URL you must make sure your URL is versioned. When loading the state `migrateToLatest` method +of `PersistableStateService` should be called, which will migrate the state from its original version to latest. + +note: Currently there is no recommended way on how to store version in url and its up to every application to decide on how to implement that. + +## Available state operations + +### Extraction/Injection of References + +In order to support import and export, and space-sharing capabilities, Saved Objects need to explicitly list any references they contain to other Saved Objects. +To support persisting your state in saved objects owned by another plugin, the and methods of Persistable State interface should be implemented. + + + +[See example embeddable providing extract/inject functions](https://github.com/elastic/kibana/blob/master/examples/embeddable_examples/public/migrations/migrations_embeddable_factory.ts) + +### Migrations and Backward compatibility + +As your plugin evolves, you may need to change your state in a breaking way. If that happens, you should write a migration to upgrade the state that existed prior to the change. + +. + +[See an example saved object storing embeddable state implementing saved object migration function](https://github.com/elastic/kibana/blob/master/examples/embeddable_examples/server/searchable_list_saved_object.ts) + +[See example embeddable providing migration functions](https://github.com/elastic/kibana/blob/master/examples/embeddable_examples/public/migrations/migrations_embeddable_factory.ts) + +## Telemetry + +You might want to collect statistics about how your state is used. If that is the case you should implement the telemetry method of Persistable State interface. diff --git a/docs/CHANGELOG.asciidoc b/docs/CHANGELOG.asciidoc index f9c65b2c4965c..12c9751d73a85 100644 --- a/docs/CHANGELOG.asciidoc +++ b/docs/CHANGELOG.asciidoc @@ -13,6 +13,8 @@ Review important information about the {kib} 7.x releases. // Best practices: // * When there are changes to kibana.yml settings, include the links to the new settings. +* <> +* <> * <> * <> * <> @@ -61,6 +63,493 @@ Review important information about the {kib} 7.x releases. * <> -- +[[release-notes-7.15.0]] +== {kib} 7.15.0 + +For information about the {kib} 7.15.0 release, review the following information. + +[float] +[[known-issue-7.15.0]] +=== Known issues + +.Upgrade Elastic Agents to use Osquery Manager integration +[%collapsible] +==== +*Details* + +You must upgrade your Elastic Agents to the latest version to use the Osquery Manager integration. + +*Impact* + +To upgrade, refer to {fleet-guide}/upgrade-elastic-agent.html[Upgrade Elastic Agent]. +==== + +.APM: Metrics-powered throughput is incorrect +[%collapsible] +==== +*Details* + +In some cases, users who have enabled APM aggregated transactions (metrics-powered UI), will see throughput incorrectly stuck on 1tpm. + +*Impact* + +Disable metrics powered transactions with `xpack.apm.searchAggregatedTransactions: 'never'` until 7.15.1. See {kibana-pull}112240[#112240] for more information. +==== + +[float] +[[breaking-changes-7.15.0]] +=== Breaking changes + +Breaking changes can prevent your application from optimal operation and performance. +Before you upgrade to 7.15.0, review the breaking changes, then mitigate the impact to your application. + +// tag::notable-breaking-changes[] + +[discrete] +[[breaking-osquery]] +.New scheduled query action ID format for Osquery Manager +[%collapsible] +==== +*Details* + +Action IDs for scheduled queries have been changed, which may break dashboards, alerts, or other features that depend on Osquery action IDs. + +*Impact* + +Previously, scheduled query action IDs used the query ID. For example, if you scheduled a query with the name `rpm_packages`, the action ID would be `rpm_packages`. Now, scheduled query action IDs use the `pack__` format. For example, if you have a scheduled query with the name `rpm_packages` in a group named `system_monitoring`, the action ID would be `pack_system_monitoring_rpm_packages`. +==== + +[discrete] +[[deprecation-105055]] +.Removes support for legacy charts library +[%collapsible] +==== +*Details* + +The legacy area, line, and charts library are deprecated in 7.15.0. In 7.16.0, support for the legacy charts library will be removed. For more information, refer to {kibana-pull}105055[#105055]. + +*Impact* + +When you upgrade, {kib} uses the new charts library by default, which includes improved performance, color palettes, fill capacity, and more. + +If you have changed the default {kibana-ref}/advanced-options.html#kibana-visualization-settings[Advanced Settings], you must disable the legacy charts library to use the new charts library: + +. Open the main menu, then click *Stack Management > Advanced Settings*. + +. Deselect *Legacy charts library*. +==== + +// end::notable-breaking-changes[] + +[float] +[[deprecations-7.15.0]] +=== Deprecations + +The following functionality is deprecated in 7.15.0, and will be removed in 8.0.0. +Deprecated functionality does not have an immediate impact on your application, but we strongly recommend +you make the necessary updates after you upgrade to 7.15.0. + +[discrete] +[[deprecation-108826]] +.Deprecates the dashboard APIs +[%collapsible] +==== +*Details* + +The import and export dashboard APIs are deprecated. For more information, refer to {kibana-pull}108826[#108826]. + +*Impact* + +Use the <> and <> APIs. +==== + +[discrete] +[[deprecation-108281]] +.Deprecates alerting and action settings +[%collapsible] +==== +*Details* + +The ability to disable alerts, actions, task manager, stack alerts, and event log plugins is deprecated. For more information, refer to {kibana-pull}108281[#108281]. + +*Impact* + +Use the supported <>. +==== + +[discrete] +[[deprecation-106566]] +.Moves filter utils to package and cleanup API +[%collapsible] +==== +*Details* + +The following filter-related utilities are deprecated when imported from the data plugin. + +On the client side, `esFilters` is now deprecated: +* `FilterLabel`, `FilterItem`, `getDisplayValueFromFilter`, `generateFilters`, `extractTimeRange` can be imported from `data\public` (not using `esFilters`). + +* The following utility functions are available for import from `@kbn/es-query`: +** `COMPARE_ALL_OPTIONS` +** `compareFilters` +** `enableFilter` +** `pinFilter` +** `toggleFilterDisabled` +** `dedupFilters` +** `onlyDisabledFiltersChanged` +** `uniqFilters` + +* The following functions are deprecated and won't be part of the 8.0.0 public API: +** `convertRangeFilterToTimeRangeString` +** `extractTimeFilter` +** `changeTimeFilter` +** `mapAndFlattenFilters` + +On the server side, `esFilters` is now deprecated. All utility functions are available for import from `@kbn/es-query`. + +For more information, refer to {kibana-pull}106566[#106566]. + +*Impact* + +Import all utility functions from `@kbn/es-query`. +==== + +[discrete] +[[deprecation-106232]] +.Refactors `textarea` UI argument +[%collapsible] +==== +*Details* + +Based on the `x-pack/plugins/canvas/CONTRIBUTING.md`, `recompose` has been removed in favor of React hooks at `textarea`. For more information, refer to {kibana-pull}106232[#106232]. + +*Impact* + +There is no user-facing impact. +==== + +[discrete] +[[deprecation-105981]] +.Deprecates `xpack.maps.showMapVisualizationTypes` +[%collapsible] +==== +*Details* + +Deprecates the `xpack.maps.showMapVisualizationTypes` *Maps* setting. For more information, refer to {kibana-pull}105981[#105981]. + +*Impact* + +When you upgrade, remove `xpack.maps.showMapVisualizationTypes` from your kibana.yml file. +==== + +[discrete] +[[deprecation-105742]] +.Refactors `string` UI argument +[%collapsible] +==== +*Details* + +Based on the `x-pack/plugins/canvas/CONTRIBUTING.md`, `recompose` has been removed in favor of React hooks at `string`. For more information, refer to {kibana-pull}105742[#105742]. + +*Impact* + +There is no user-facing impact. +==== + +[discrete] +[[deprecation-104685]] +.Deprecates legacy audit logger +[%collapsible] +==== +*Details* + +The legacy audit logger is deprecated. For more information, refer to {kibana-pull}104685[#104685]. + +*Impact* + +Use the ECS-comliant audit logger. For more information, refer to <>. +==== + +[discrete] +[[deprecation-100781]] +.Deprecates scripted fields +[%collapsible] +==== +*Details* + +Adding scripted fields to index patterns is deprecated. For more information, refer to {kibana-pull}100781[#100781]. + +*Impact* + +Use runtime fields. For more information, refer to <>. +==== + +[float] +[[features-7.15.0]] +=== Features +{kib} 7.15.0 adds the following new and notable features. + +APM:: +* Adds uninstrumented dependencies view {kibana-pull}106223[#106223] +* Replaces error rate table with failed transactions correlations {kibana-pull}108441[#108441] +* Moves latency correlations from flyout to transactions page {kibana-pull}107266[#107266] +Data ingest:: +* Adds copy_from to set processor {kibana-pull}104070[#104070] +* Adds community id processor {kibana-pull}103863[#103863] +* Adds network direction processor {kibana-pull}103436[#103436] +Elastic Security:: +For the Elastic Security 7.15.0 release information, refer to {security-guide}/release-notes.html[_Elastic Security Solution Release Notes_]. +Lens & Visualizations:: +* Synchronizes cursor position for x-axis across all *Lens* visualizations on a dashboard {kibana-pull}106845[#106845] +* Client Side caching in *Visualize* {kibana-pull}105589[#105589] +Machine Learning:: +* Checks for error messages in the anomaly detection jobs health rule type {kibana-pull}108701[#108701] +* Adds reset anomaly detection jobs link to jobs list {kibana-pull}108039[#108039] +* Delayed data test for anomaly detection jobs health rule type {kibana-pull}107183[#107183] +* Model memory state test for anomaly detection jobs health rule type {kibana-pull}106763[#106763] +* Alerting rule for anomaly detection jobs monitoring {kibana-pull}106084[#106084] +* Jobs import and export {kibana-pull}101037[#101037] +Osquery Manager:: +* Adds RBAC for the Osquery plugin {kibana-pull}106669[#106669] +* Adds an ECS mapping editor for scheduled queries {kibana-pull}107706[#107706] +Querying & Filtering:: +* Adds scoring support to KQL {kibana-pull}103727[#103727] +Sharing:: +* Redirect endpoint {kibana-pull}103899[#103899] + +For more information about the features introduced in 7.15.0, refer to <>. + +[[enhancements-and-bug-fixes-v7.15.0]] +=== Enhancements and bug fixes + +For detailed information about the 7.15.0 release, review the enhancements and bug fixes. + +[float] +[[enhancement-v7.15.0]] +=== Enhancements +Alerting:: +* Changed rules table to support visual indication for disabled and muted alerts {kibana-pull}104190[#104190] +APM:: +* Enables JVM metrics for the Ruby Agent running on a JVM (jRuby) {kibana-pull}108933[#108933] +* Adds APM agent instrumentation instructions in Fleet {kibana-pull}108242[#108242] +* Renames "Error rate" to "Failed transaction rate" {kibana-pull}107895[#107895] +* Fixes service inventory responsive design {kibana-pull}107690[#107690] +* Adds a logs tab for services {kibana-pull}107664[#107664] +* Adds time comparison to the Transactions page {kibana-pull}107299[#107299] +* Adds time comparison to the services inventory {kibana-pull}107094[#107094] +* Adds backend info and icons to flyouts {kibana-pull}107089[#107089] +* Supports visualizing composite spans {kibana-pull}106862[#106862] +* Allows editing of APM rules {kibana-pull}106598[#106598] +* Redesigns the APM Integration settings {kibana-pull}106535[#106535] +* Adds bulk update route to rule registry and bulk update function to alerts client {kibana-pull}106297[#106297] +* Replaces error rate table with failed transactions correlations {kibana-pull}108441[#108441] +* Moves latency correlations from flyout to transactions page {kibana-pull}107266[#107266] +* Latency correlations: Field/value candidates prioritization {kibana-pull}107370[#107370] +Canvas:: +* Expression progress {kibana-pull}104457[#104457] +* Expression metric {kibana-pull}104390[#104390] +* Expression image {kibana-pull}104318[#104318] +* Expression repeat image {kibana-pull}104255[#104255] +* Expression reveal image. Async libs and images loading {kibana-pull}103399[#103399] +* Expression shape {kibana-pull}103219[#103219] +* Expression error {kibana-pull}103048[#103048] +* Expression reveal image {kibana-pull}101987[#101987] +Data ingest:: +* Adds copy_from to set processor {kibana-pull}104070[#104070] +* Adds community id processor {kibana-pull}103863[#103863] +* Support output_format in date processor {kibana-pull}103729[#103729] +* Adds network direction processor {kibana-pull}103436[#103436] +Discover:: +* Split single query into 2 queries for faster results {kibana-pull}104818[#104818] +* Improves empty state page {kibana-pull}103602[#103602] +Elastic Security:: +For the Elastic Security 7.15.0 release information, refer to {security-guide}/release-notes.html[_Elastic Security Solution Release Notes_]. +Enterprise Search:: +For the Elastic Enterprise Search 7.15.0 release information, refer to {enterprise-search-ref}/release-notes-7.15.0.html[7.15.0 release notes]. +Fleet:: +* Updates Package Policy UI to support upgrading package policies {kibana-pull}107171[#107171] +* Removes subseconds from `event.ingested` {kibana-pull}104044[#104044] +* Adds package policy upgrade API {kibana-pull}103017[#103017] +Kibana Home & Add Data:: +* Use *Lens* in ecommerce data {kibana-pull}106039[#106039] +* Use *Lens* in flights dashboard {kibana-pull}104780[#104780] +Lens & Visualizations:: +* Long legend values support in *Aggregation based* visualizations {kibana-pull}108365[#108365] +* Long legend values support in *TSVB* {kibana-pull}108023[#108023] +* Supports long legend values in *Lens* {kibana-pull}107894[#107894] +* Adds a color picker in percentiles and percentiles rank aggs in *TSVB* {kibana-pull}107390[#107390] +* Synchronize cursor position for x-axis across all *Lens* visualizations on a dashboard {kibana-pull}106845[#106845] +* Allows the users to change the axis orientation in *Lens* {kibana-pull}106369[#106369] +* Improve network error message in *Lens* {kibana-pull}106246[#106246] +* Cleanup bar value labels configuration in *Lens* {kibana-pull}106231[#106231] +* Cleanup bar value labels configuration in *Aggregation based* visualizations {kibana-pull}106198[#106198] +* Adds client-side caching in *Visualize Library* to eliminate unnecessary data fetching when editing visualizations {kibana-pull}105589[#105589] +* Display legend inside chart in *Lens* {kibana-pull}105571[#105571] +* Adds a deprecation notice in the UI and a docs section {kibana-pull}105055[#105055] +* Replace flot with elastic-chart in *Timelion* {kibana-pull}81565[#81565] +Logs:: +* Refactor breadcrumbs {kibana-pull}103249[#103249] +Machine Learning:: +* Adds support for model_prune_window in job wizard {kibana-pull}108734[#108734] +* Checks for error messages in the anomaly detection jobs health rule type {kibana-pull}108701[#108701] +* Adds initial record score to the anomalies table expanded row content {kibana-pull}108216[#108216] +* Adds reset anomaly detection jobs link to jobs list {kibana-pull}108039[#108039] +* Adds evaluation quality metrics to classification exploration view {kibana-pull}107862[#107862] +* Adds a 30 day model prune window to non-rare security jobs {kibana-pull}107752[#107752] +* Delayed data test for anomaly detection jobs health rule type {kibana-pull}107183[#107183] +* Model memory state test for anomaly detection jobs health rule type {kibana-pull}106763[#106763] +* Enables index data visualizer document count chart to update time range query {kibana-pull}106438[#106438] +* Alerting rule for anomaly detection jobs monitoring {kibana-pull}106084[#106084] +* Edits rare anomaly detection job summaries {kibana-pull}105694[#105694] +* Jobs import and export {kibana-pull}101037[#101037] +Management:: +* Adds index templates flyout to the edit policy form {kibana-pull}108362[#108362] +* Refactored policies list to use EuiInMemoryTable {kibana-pull}107510[#107510] +* Transforms: Adds a type column to the transforms management table {kibana-pull}106990[#106990] +* Adds a flyout with linked index templates {kibana-pull}106734[#106734] +* Adds es version field support {kibana-pull}104870[#104870] +* Adds preview for runtime fields {kibana-pull}100198[#100198] +Maps:: +* 'show this layer only' layer action {kibana-pull}107947[#107947] +* Adds indication in layer TOC when layer is filtered by map bounds {kibana-pull}107662[#107662] +* Show actionable message when term joins have no matches {kibana-pull}105161[#105161] +* Adds edit tools defaults for user and timestamp {kibana-pull}103588[#103588] +* Auto generate legends and styles from mvt data {kibana-pull}94811[#94811] +Metrics:: +* Moves saved views button to page header {kibana-pull}107951[#107951] +* Adds manage rules link to alerts dropdown {kibana-pull}107950[#107950] +* Adds checkbox to optionally drop partial buckets from threshold alerts {kibana-pull}107676[#107676] +* Adds system.cpu.total.norm.pct to default metrics {kibana-pull}102428[#102428] +Monitoring:: +* Enables OOTB alerts in RAC page and multiple rules of a rule type {kibana-pull}106457[#106457] +Osquery Manager:: +* Adds status info for scheduled queries {kibana-pull}106600[#106600] +Platform:: +* Improves not found response handling in the saved objects repository {kibana-pull}108749[#108749] +* Updates `esaggs` expressions function to return partial results {kibana-pull}105620[#105620] +* Updates expressions public API to expose partial results support {kibana-pull}102403[#102403] +* Changes execution of alerts from async to sync {kibana-pull}97311[#97311] +Querying & Filtering:: +* Filter FilterBar suggestions by time (according to flag) {kibana-pull}107192[#107192] +Reporting:: +* Adds support of chunked export {kibana-pull}108485[#108485] +* Consolidate report job warnings and add warning for deprecated types {kibana-pull}106184[#106184] +* Adds warning logs about CSV export type being deprecated {kibana-pull}104025[#104025] +* New UI for migrating reporting indices ILM policy {kibana-pull}103853[#103853] +Security:: +* Support authenticating to Elasticsearch via service account tokens {kibana-pull}102121[#102121] +* Space management page UX improvements {kibana-pull}100448[#100448] +Sharing:: +* Redirect endpoint {kibana-pull}103899[#103899] +* Adds *Lens* markdown plugin {kibana-pull}96703[#96703] +Uptime:: +* Improve dedupe client performance {kibana-pull}103979[#103979] +* Multi Series View {kibana-pull}103855[#103855] +* Adds browser monitors configuration options {kibana-pull}102928[#102928] +Other:: +* Adds Thumbnails to Search UI {kibana-pull}104199[#104199] + +[float] +[[fixes-v7.15.0]] +=== Bug Fixes +APM:: +* Custom links creation don't work {kibana-pull}110676[#110676] +* Show relevant nodes in focused service map {kibana-pull}108028[#108028] +* Display throughput as tps (instead of tpm) when bucket size < 60 seconds {kibana-pull}107850[#107850] +Canvas:: +* `Flyout` refactor {kibana-pull}106728[#106728] +* Register `expression_functions` in `{expression}/public/plugin.ts` {kibana-pull}106636[#106636] +Discover:: +* Hide "Manage Searches" if user has insufficient permissions {kibana-pull}109099[#109099] +* Don't give write permissions to index patterns via Discover write permissions {kibana-pull}108376[#108376] +* Fixes multi-field display when parent field is not indexed {kibana-pull}102938[#102938] +Elastic Security:: +For the Elastic Security 7.15.0 release information, refer to {security-guide}/release-notes.html[_Elastic Security Solution Release Notes_]. +Fleet:: +* Fixes Fleet settings and HostInput error handling {kibana-pull}109418[#109418] +* Fixes Agent policy search to support simple text filters {kibana-pull}107306[#107306] +Kibana Home & Add Data:: +* Fixes `tabindex` and collapsible functionality {kibana-pull}107462[#107462] +Lens & Visualizations:: +* Fixes filters reappearing in the saved object when saving in *Lens* {kibana-pull}110460[#110460] +* Fixes small multiple title in dark mode {kibana-pull}110008[#110008] +* Markdown variables should be clickable in *TSVB* {kibana-pull}108844[#108844] +* Fixes Markdown variables are not available on the first rendering in *TSVB* {kibana-pull}108836[#108836] +* Switching between some aggregations in bucket section for sibling aggregations breaks the visualization {kibana-pull}108693[#108693] +* Fixes when clicking "Save and Return" on a Lens visualization the visualization's description gets erased in *Lens* {kibana-pull}108669[#108669] +* [Accessibility] Take into account background color for non opaque colors in *Lens* {kibana-pull}107877[#107877] +* Adds the ability to override runtime_mappings in *Vega* {kibana-pull}107875[#107875] +* Fixes behavior for points and bars in *timelion* {kibana-pull}107398[#107398] +* Formats correctly the falsy values on the x axis in *Lens* {kibana-pull}107134[#107134] +* Fixes send data request twice when opening visualizations {kibana-pull}106398[#106398] +* Filter button in legend keyword accessible {kibana-pull}106374[#106374] +* Annotations support runtime fields in *TSVB* {kibana-pull}104287[#104287] +* Top_hit supports runtime fields in *TSVB* {kibana-pull}103401[#103401] +* Saved visualization with search string confuse altering of search string {kibana-pull}103396[#103396] +Machine Learning:: +* Fixes "Show charts" control state {kibana-pull}110602[#110602] +* Fixes alignment of sorting arrow when histogram charts are enabled in data grid {kibana-pull}110053[#110053] +* Removes hardcoded datafeed indices for security auth and network modules {kibana-pull}109692[#109692] +* Fixes missing final new line character issue {kibana-pull}109274[#109274] +* Ensures cloning retains hyperparameters and results field is correct in data frame analytics wizard {kibana-pull}107811[#107811] +* Disables query delay editing for non-admin users {kibana-pull}107517[#107517] +* Ensures results view loads correctly for data frame analytics job created in Dev Tools {kibana-pull}107024[#107024] +Management:: +* Removes index pattern placeholder advanced setting {kibana-pull}110334[#110334] +* Fixes suffix field format leaks to index pattern management {kibana-pull}107139[#107139] +* Enable inspector to display multiple requests for multiple layers {kibana-pull}105224[#105224] +* Unified check for CSV cells for known formula characters (and value escaping more in general) {kibana-pull}105221[#105221] +Maps:: +* Abort full screen in dashboard and maps when user clicks back button {kibana-pull}108747[#108747] +* Include caused_by field for import failures {kibana-pull}107907[#107907] +Metrics:: +* Fixes refresh button for node details page {kibana-pull}108666[#108666] +* Removes alert previews {kibana-pull}107978[#107978] +* Ensures alert dropdown closes properly {kibana-pull}106343[#106343] +* Increase number of saved views fetched to 1000 {kibana-pull}106310[#106310] +* Drop partial buckets from ALL Metrics UI queries {kibana-pull}104784[#104784] +Platform:: +* Migrations: limit batch sizes to migrations.batchSizeBytes (= 100mb by default) {kibana-pull}109540[#109540] +Reporting:: +* Fixes ability to export CSV on searched data with frozen indices {kibana-pull}109976[#109976] + +[[release-notes-7.14.2]] +== {kib} 7.14.2 + +Review the following information about the 7.14.2 release. + +[float] +[[known-issue-v7.14.2]] +=== Known issue +{kib} is unable to restore *Discover* search sessions with a relative time range. When you restore the *Discover* search session, then run a new search, {kib} displays a `Your search session is still running` message. For more information, refer to {kibana-issue}101430[#101430]. + +[float] +[[breaking-changes-v7.14.2]] +=== Breaking changes +Breaking changes can prevent your application from optimal operation and performance. Before you upgrade to 7.14.2, review the <>. + +To review the breaking changes in previous versions, refer to the following: + +{kibana-ref-all}/7.13/release-notes-7.13.0.html#breaking-changes-7.13.0[7.13] | {kibana-ref-all}/7.12/release-notes-7.12.0.html#breaking-changes-7.12.0[7.12] | {kibana-ref-all}/7.11/breaking-changes-7.11.html[7.11] | {kibana-ref-all}/7.10/breaking-changes-7.10.html[7.10] | +{kibana-ref-all}/7.9/breaking-changes-7.9.html[7.9] | {kibana-ref-all}/7.8/breaking-changes-7.8.html[7.8] | {kibana-ref-all}/7.7/breaking-changes-7.7.html[7.7] | +{kibana-ref-all}/7.6/breaking-changes-7.6.html[7.6] | {kibana-ref-all}/7.5/breaking-changes-7.5.html[7.5] | +{kibana-ref-all}/7.4/breaking-changes-7.4.html[7.4] | {kibana-ref-all}/7.3/breaking-changes-7.3.html[7.3] | {kibana-ref-all}/7.2/breaking-changes-7.2.html[7.2] +| {kibana-ref-all}/7.1/breaking-changes-7.1.html[7.1] | {kibana-ref-all}/7.0/breaking-changes-7.0.html[7.0] + +[float] +[[enhancement-v7.14.2]] +=== Enhancements +Platform:: +* Provide better diagnostics when task manager performance is degraded {kibana-pull}109741[#109741] + +[float] +[[fixes-v7.14.2]] +=== Bug fixes +Canvas:: +* Fixes several minor workpad layout and interaction bugs {kibana-pull}110385[#110385] +* Fixes element stats {kibana-pull}109770[#109770] +* Fixes image argument form issues {kibana-pull}109767[#109767] +* Handle Timelion errors gracefully {kibana-pull}109761[#109761] +Dashboard:: +* Retain Tags on Quicksave {kibana-pull}111015[#111015] +* Read App State from URL on Soft Refresh {kibana-pull}109354[#109354] +Discover:: +* Fixes opening the same saved search {kibana-pull}111127[#111127] +* Fixes export that does not contain relative time filter {kibana-pull}110459[#110459] +* Fixes cleaning error state in 7.14 {kibana-pull}110036[#110036] +* Fixes performance regression in sidebar {kibana-pull}109999[#109999] +Elastic Security:: +For the Elastic Security 7.14.2 release information, refer to {security-guide}/release-notes.html[_Elastic Security Solution Release Notes_]. +Fleet:: +* Fixes config migration from ingestManager to support both xpack.ingestManager and xpack.fleet {kibana-pull}111612[#111612] +Lens & Visualizations:: +* Adds migration script for 'drop_last_bucket' value in *TSVB* {kibana-pull}110782[#110782] +Machine Learning:: +* Fixes Anomaly Explorer data points not showing up when index pattern has multiple indices {kibana-pull}110899[#110899] +Maps:: +* Ensure draw tools updates by index name, not index pattern title {kibana-pull}108394[#108394] +Platform:: +* Catch errors from individual providers {kibana-pull}111093[#111093] +Security:: +* Respect `auth_provider_hint` if session is not authenticated {kibana-pull}111521[#111521] +Uptime:: +* Removes blurring on URL filter selections {kibana-pull}110314[#110314] [[release-notes-7.14.1]] == {kib} 7.14.1 @@ -195,8 +684,6 @@ For information about the {kib} 7.14.0 release, review the following information Breaking changes can prevent your application from optimal operation and performance. Before you upgrade to 7.14.0, review the breaking changes, then mitigate the impact to your application. -// tag::notable-breaking-changes[] - [discrete] [[breaking-102263]] .Changes audit event terminology @@ -253,8 +740,6 @@ For more information, refer to {kibana-pull}99078[#99078]. When you upgrade to 7.14.0, {kib} automatically reflects the changes. No action is needed. ==== -// end::notable-breaking-changes[] - [float] [[deprecations-7.14.0]] === Deprecations @@ -957,8 +1442,6 @@ You should export the used index patterns separately. Breaking changes can prevent your application from optimal operation and performance. Before you upgrade to 7.13.0, review the breaking changes, then mitigate the impact to your application. -// tag::notable-breaking-changes[] - [discrete] [[breaking-97206]] .Remove Elastic Agent routes and related services @@ -995,9 +1478,6 @@ The *Explore underlying data* context menu on dashboards is now disabled by defa To enable the *Explore underlying data* context menu, set `xpack.discoverEnhanced.actions.exploreDataInContextMenu.enabled` to `true` in kibana.yml. ==== -// end::notable-breaking-changes[] - - [float] [[deprecations-7.13.0]] === Deprecations @@ -1527,8 +2007,6 @@ For information about the {kib} 7.12.0 release, review the following information Breaking changes can prevent your application from optimal operation and performance. Before you upgrade to 7.12.0, review the breaking changes, then mitigate the impact to your application. -// tag::notable-breaking-changes[] - [discrete] [[breaking-89632]] .Removes geo threshold alert type @@ -1572,8 +2050,6 @@ To display the cluster data in *Discover*, load documents directly from `_source . Go to `discover:searchFieldsFromSource`, then select *On*. ==== -// end::notable-breaking-changes[] - [float] [[known-issues-v7.12.0]] === Known issues @@ -2123,9 +2599,6 @@ Breaking changes can prevent your application from optimal operation and perform // The following section is re-used in the Installation and Upgrade Guide -// tag::notable-breaking-changes[] - - [discrete] [[ingestManager_renamed_fleet]] ==== Ingest Manager plugin renamed Fleet @@ -2163,8 +2636,6 @@ which drops support for glibc `2.12`-based operating systems. *via https://github.com/elastic/kibana/pull/83425[#83425]* -// end::notable-breaking-changes[] - [float] [[deprecation-v7.11.0]] === Deprecations @@ -2671,9 +3142,6 @@ Breaking changes can prevent your application from optimal operation and perform // The following section is re-used in the Installation and Upgrade Guide -// tag::notable-breaking-changes[] - - [discrete] [[breaking_kibana_legacy_plugins]] ===== Legacy plugins support removed @@ -2708,8 +3176,6 @@ more information about this property. *via https://github.com/elastic/kibana/pull/73805[#73805]* -// end::notable-breaking-changes[] - [discrete] [[general-plugin-API-changes-7-10]] ==== Breaking changes for plugin developers @@ -4043,8 +4509,6 @@ Breaking changes can prevent your application from optimal operation and perform // The following section is re-used in the Installation and Upgrade Guide -// tag::notable-breaking-changes[] - [float] [[breaking_kibana_keystore]] ===== `kibana.keystore` moved from the data folder to the config folder @@ -4056,8 +4520,6 @@ that path will continue to be used. *via https://github.com/elastic/kibana/pull/57856[#57856]* -// end::notable-breaking-changes[] - [float] [[general-plugin-API-changes-79]] ==== Breaking changes for plugin developers @@ -5143,9 +5605,6 @@ Breaking changes can prevent your application from optimal operation and perform // The following section is re-used in the Installation and Upgrade Guide - -// tag::notable-breaking-changes[] - [float] [[user-facing-changes-78]] ==== Breaking changes for users @@ -5183,10 +5642,6 @@ This fixes the Back button when navigating between dashboards using drilldowns. *via https://github.com/elastic/kibana/pull/62415[#62415]* ==== - -// end::notable-breaking-changes[] - - [float] [[general-plugin-API-changes-78]] ==== Breaking changes for plugin developers @@ -6006,9 +6461,6 @@ For information about the Kibana 7.7.0 release, review the following information Breaking changes can prevent your application from optimal operation and performance. Before you upgrade to 7.7.0, review the breaking changes, then mitigate the impact to your application. // The following section is re-used in the Installation and Upgrade Guide -// tag::notable-breaking-changes[] - -// end::notable-breaking-changes[] [float] ==== Breaking changes for users @@ -7444,9 +7896,6 @@ Breaking changes can prevent your application from optimal operation and perform * <> // The following section is re-used in the Installation and Upgrade Guide -//tag::notable-breaking-changes[] - -// end::notable-breaking-changes[] [float] [[user-facing-changes]] @@ -9842,7 +10291,6 @@ The following section is re-used in the Installation and Upgrade Guide [[breaking_70_notable]] === Notable breaking changes //// -// tag::notable-breaking-changes[] [float] [[breaking_75_search_instead_of-msearch]] @@ -9870,7 +10318,6 @@ Any installs that previously enabled the Code app will now log a warning when Kibana starts up. It's safe to remove all configurations starting with `xpack.code.`. Starting in 8.0, these warnings will become errors that prevent Kibana from starting up. -// end::notable-breaking-changes[] [float] [[enhancement-7.5.0]] @@ -10246,7 +10693,6 @@ The following section is re-used in the Installation and Upgrade Guide [[breaking_70_notable]] === Notable breaking changes //// -// tag::notable-breaking-changes[] [float] [[breaking_74_search_instead_of-msearch]] @@ -10262,9 +10708,6 @@ When the advanced setting `courier:batchSearches` is disabled, requests from *Discover*, *Visualize*, and *Dashboard* will now query {es} using the `_search` endpoint rather than the `_msearch` endpoint. - -// end::notable-breaking-changes[] - [float] [[enhancement-7.4.0]] === Enhancements @@ -10634,7 +11077,6 @@ The following section is re-used in the Installation and Upgrade Guide [[breaking_70_notable]] === Notable breaking changes //// -// tag::notable-breaking-changes[] [float] ==== Visibility of features after configuring a term join in Maps @@ -10655,8 +11097,6 @@ histograms might no longer work in 7.3. If you run into issues starting a advanced editor of the {transforms} wizard. The advanced editor will remove the unsupported attribute once the configuration gets applied. -// end::notable-breaking-changes[] - [float] [[breaking_73_dashboard_import_export]] ==== Dashboard import and export APIs @@ -10993,10 +11433,6 @@ and <>. //NOTE: The notable-breaking-changes tagged regions are re-used in the //Installation and Upgrade Guide -//tag::notable-breaking-changes[] - -// end::notable-breaking-changes[] - [float] [[breaking_72_index_pattern_changes]] @@ -11017,10 +11453,6 @@ on the root wildcard term. For example, a query on an index pattern such as //NOTE: The notable-breaking-changes tagged regions are re-used in the //Installation and Upgrade Guide -//tag::notable-breaking-changes[] - -// end::notable-breaking-changes[] - [float] [[enhancement-7.2.0]] === Enhancements @@ -11496,9 +11928,6 @@ The following section is re-used in the Installation and Upgrade Guide [[breaking_70_notable]] === Notable breaking changes //// -// tag::notable-breaking-changes[] - -// end::notable-breaking-changes[] [float] [[breaking_70_api_changes]] diff --git a/docs/api/machine-learning.asciidoc b/docs/api/machine-learning.asciidoc new file mode 100644 index 0000000000000..265896e6340df --- /dev/null +++ b/docs/api/machine-learning.asciidoc @@ -0,0 +1,11 @@ +[[machine-learning-api]] +== {ml-cap} APIs + +//Manage {kib} saved objects, including dashboards, visualizations, and more. + +The following {ml} API is available: + +* <> +//to retrieve a single {kib} saved object by ID + +include::machine-learning/sync.asciidoc[] diff --git a/docs/api/machine-learning/sync.asciidoc b/docs/api/machine-learning/sync.asciidoc new file mode 100644 index 0000000000000..5f19bc17ab2fb --- /dev/null +++ b/docs/api/machine-learning/sync.asciidoc @@ -0,0 +1,79 @@ +[[machine-learning-api-sync]] +=== Sync {ml} saved objects API +++++ +Sync {ml} saved objects +++++ + +Synchronizes {kib} saved objects for {ml} jobs. + +[[machine-learning-api-sync-request]] +==== Request + +`GET :/api/ml/saved_objects/sync` + +`GET :/s//api/ml/saved_objects/sync` + + +[[machine-learning-api-sync-path-params]] +==== Path parameters + +`space_id`:: +(Optional, string) An identifier for the space. If `space_id` is not provided in +the URL the default space is used. + +[[machine-learning-api-sync-query-params]] +==== Query parameters + +`simulate`:: +(Optional, boolean) When `true`, simulates the synchronization by only returning +the list actions that _would_ be performed. + +[[machine-learning-api-sync-response-body]] +==== Response body + +`datafeedsAdded`:: +(array) If a saved object for an {anomaly-job} is missing a {dfeed} identifier, +it is added. This list contains the {dfeed} identifiers and indicates whether +the synchronization was successful. + +`datafeedsRemoved`:: +(array) If saved objects exist for {dfeeds} that no longer exist, they are +deleted. This list contains the {dfeed} identifiers and indicates whether the +synchronization was successful. + +`savedObjectsCreated`:: +(array) If saved objects are missing for {ml} jobs, they are created. This +list contains the job identifiers and indicates whether the synchronization was +successful. + +`savedObjectsDeleted`:: +(array) If saved objects exist for jobs that no longer exist, they are deleted. +This list contains the job identifiers and indicates whether the synchronization +was successful. + +[[machine-learning-api-sync-codes]] +==== Response code + +`200`:: + Indicates a successful call. + +[[machine-learning-api-sync-example]] +==== Example + +Retrieve the list of {ml} saved objects that require synchronization: + +[source,sh] +-------------------------------------------------- +$ curl -X GET api/ml/saved_objects/sync?simulate=true +-------------------------------------------------- +// KIBANA + +If there are two jobs and a {dfeed} that need to be synchronized, for example, +the API returns the following: + +[source,sh] +-------------------------------------------------- +{{"savedObjectsCreated":{"myjob1":{"success":true},"myjob2":{"success":true}},"savedObjectsDeleted":{},"datafeedsAdded":{},"datafeedsRemoved":{"myfeed3":{"success":true}}} +-------------------------------------------------- + +To perform the synchronization, re-run the API and omit the `simulate` parameter. \ No newline at end of file diff --git a/docs/apm/dependencies.asciidoc b/docs/apm/dependencies.asciidoc new file mode 100644 index 0000000000000..b3afdc4880df5 --- /dev/null +++ b/docs/apm/dependencies.asciidoc @@ -0,0 +1,32 @@ +[role="xpack"] +[[dependencies]] +=== Dependencies + +APM agents collect details about external calls made from instrumented services. +Sometimes, these external calls resolve into a downstream service that's instrumented -- in these cases, +you can utilize <> to drill down into problematic downstream services. +Other times, though, it's not possible to instrument a downstream dependency -- +like with a database or third-party service. +**Dependencies** gives you a window into these uninstrumented, downstream dependencies. + +[role="screenshot"] +image::apm/images/dependencies.png[Dependencies view in the APM app in Kibana] + +Many application issues are caused by slow or unresponsive downstream dependencies. +And because a single, slow dependency can significantly impact the end-user experience, +it's important to be able to quickly identify these problems and determine the root cause. + +Select a dependency to see detailed latency, throughput, and failed transaction rate metrics. + +[role="screenshot"] +image::apm/images/dependencies-drilldown.png[Dependencies drilldown view in the APM app in Kibana] + +When viewing a dependency, consider your pattern of usage with that dependency. +If your usage pattern _hasn't_ increased or decreased, +but the experience has been negatively effected -- either with an increase in latency or errors, +there's likely a problem with the dependency that needs to be addressed. + +If your usage pattern _has_ changed, the dependency view can quickly show you whether +that pattern change exists in all upstream services, or just a subset of your services. +You might then start digging into traces coming from +impacted services to determine why that pattern change has occurred. diff --git a/docs/apm/errors.asciidoc b/docs/apm/errors.asciidoc index c468d7f0235b2..d8fc75bf50340 100644 --- a/docs/apm/errors.asciidoc +++ b/docs/apm/errors.asciidoc @@ -4,19 +4,21 @@ TIP: {apm-overview-ref-v}/errors.html[Errors] are groups of exceptions with a similar exception or log message. -The *Errors* overview provides a high-level view of the error message and culprit, -the number of occurrences, and the most recent occurrence. -Just like the transaction overview, you'll notice we group together like errors. -This makes it very easy to quickly see which errors are affecting your services, +The *Errors* overview provides a high-level view of the exceptions that APM agents catch, +or that users manually report with APM agent APIs. +Like errors are grouped together to make it easy to quickly see which errors are affecting your services, and to take actions to rectify them. +A service returning a 5xx code from a request handler, controller, etc., will not create +an exception that an APM agent can catch, and will therefore not show up in this view. + [role="screenshot"] -image::apm/images/apm-errors-overview.png[Example view of the errors overview in the APM app in Kibana] +image::apm/images/apm-errors-overview.png[APM Errors overview] Selecting an error group ID or error message brings you to the *Error group*. [role="screenshot"] -image::apm/images/apm-error-group.png[Example view of the error group page in the APM app in Kibana] +image::apm/images/apm-error-group.png[APM Error group] Here, you'll see the error message, culprit, and the number of occurrences over time. diff --git a/docs/apm/getting-started.asciidoc b/docs/apm/getting-started.asciidoc index 357f694453f4b..c0cb89b51fcc1 100644 --- a/docs/apm/getting-started.asciidoc +++ b/docs/apm/getting-started.asciidoc @@ -29,6 +29,7 @@ start with: * <> * <> +* <> * <> Notice something awry? Select a service or trace and dive deeper with: @@ -46,6 +47,8 @@ include::services.asciidoc[] include::traces.asciidoc[] +include::dependencies.asciidoc[] + include::service-maps.asciidoc[] include::service-overview.asciidoc[] diff --git a/docs/apm/images/all-instances.png b/docs/apm/images/all-instances.png index e77c8af2c46f6..70028b5a9b58b 100644 Binary files a/docs/apm/images/all-instances.png and b/docs/apm/images/all-instances.png differ diff --git a/docs/apm/images/apm-distributed-tracing.png b/docs/apm/images/apm-distributed-tracing.png index 0dbffa591d43a..4d1b8cde20e95 100644 Binary files a/docs/apm/images/apm-distributed-tracing.png and b/docs/apm/images/apm-distributed-tracing.png differ diff --git a/docs/apm/images/apm-geo-ui.png b/docs/apm/images/apm-geo-ui.png index 5bbe713c908a4..69c1390a27989 100644 Binary files a/docs/apm/images/apm-geo-ui.png and b/docs/apm/images/apm-geo-ui.png differ diff --git a/docs/apm/images/apm-logs-tab.png b/docs/apm/images/apm-logs-tab.png index 891d2b7a1dd69..c79be8b5eb0b7 100644 Binary files a/docs/apm/images/apm-logs-tab.png and b/docs/apm/images/apm-logs-tab.png differ diff --git a/docs/apm/images/apm-services-overview.png b/docs/apm/images/apm-services-overview.png index 7aeb5f1ac379f..271c0347aa53e 100644 Binary files a/docs/apm/images/apm-services-overview.png and b/docs/apm/images/apm-services-overview.png differ diff --git a/docs/apm/images/apm-span-detail.png b/docs/apm/images/apm-span-detail.png index c9f55575b2232..d0b6a4de3d3df 100644 Binary files a/docs/apm/images/apm-span-detail.png and b/docs/apm/images/apm-span-detail.png differ diff --git a/docs/apm/images/apm-transaction-duration-dist.png b/docs/apm/images/apm-transaction-duration-dist.png index 91ae6c3a59ad2..9c7ab5dd67dc0 100644 Binary files a/docs/apm/images/apm-transaction-duration-dist.png and b/docs/apm/images/apm-transaction-duration-dist.png differ diff --git a/docs/apm/images/apm-transaction-sample.png b/docs/apm/images/apm-transaction-sample.png index 54eea902f0311..a9490fc20d853 100644 Binary files a/docs/apm/images/apm-transaction-sample.png and b/docs/apm/images/apm-transaction-sample.png differ diff --git a/docs/apm/images/apm-transactions-overview.png b/docs/apm/images/apm-transactions-overview.png index 66cf739a861b7..34cd0219b895d 100644 Binary files a/docs/apm/images/apm-transactions-overview.png and b/docs/apm/images/apm-transactions-overview.png differ diff --git a/docs/apm/images/apm-transactions-table.png b/docs/apm/images/apm-transactions-table.png index b573adfb0c450..8a3415bc9a9f1 100644 Binary files a/docs/apm/images/apm-transactions-table.png and b/docs/apm/images/apm-transactions-table.png differ diff --git a/docs/apm/images/dependencies-drilldown.png b/docs/apm/images/dependencies-drilldown.png new file mode 100644 index 0000000000000..4c491c1ffa254 Binary files /dev/null and b/docs/apm/images/dependencies-drilldown.png differ diff --git a/docs/apm/images/dependencies.png b/docs/apm/images/dependencies.png new file mode 100644 index 0000000000000..260025d31654b Binary files /dev/null and b/docs/apm/images/dependencies.png differ diff --git a/docs/apm/images/error-rate.png b/docs/apm/images/error-rate.png index 036c7a08302bd..845fa2af07de1 100644 Binary files a/docs/apm/images/error-rate.png and b/docs/apm/images/error-rate.png differ diff --git a/docs/apm/images/spans-dependencies.png b/docs/apm/images/spans-dependencies.png index d6e26a5061a6e..558099dd450c1 100644 Binary files a/docs/apm/images/spans-dependencies.png and b/docs/apm/images/spans-dependencies.png differ diff --git a/docs/apm/index.asciidoc b/docs/apm/index.asciidoc index 1041755208efc..e6eb72a5fe805 100644 --- a/docs/apm/index.asciidoc +++ b/docs/apm/index.asciidoc @@ -11,8 +11,7 @@ endif::[] [partintro] -- -The APM app in {kib} is provided with the basic license. -It allows you to monitor your software services and applications in real-time; +The APM app in {kib} allows you to monitor your software services and applications in real-time; visualize detailed performance information on your services, identify and analyze errors, and monitor host-level and agent-specific metrics like JVM and Go runtime metrics. diff --git a/docs/apm/service-overview.asciidoc b/docs/apm/service-overview.asciidoc index f1096a4e43bbc..05537cef58c98 100644 --- a/docs/apm/service-overview.asciidoc +++ b/docs/apm/service-overview.asciidoc @@ -69,34 +69,43 @@ image::apm/images/traffic-transactions.png[Traffic and transactions] [discrete] [[service-error-rates]] -=== Error rate and errors +=== Failed transaction rate and errors -The *Error rate* chart displays the average error rates relating to the service, within a specific time range. -An HTTP response code greater than 400 does not necessarily indicate a failed transaction. -<>. +The failed transaction rate represents the percentage of failed transactions from the perspective of the selected service. +It's useful for visualizing unexpected increases, decreases, or irregular patterns in a service's transactions. ++ +[TIP] +==== +HTTP **transactions** from the HTTP server perspective do not consider a `4xx` status code (client error) as a failure +because the failure was caused by the caller, not the HTTP server. Thus, `event.outcome=success` and there will be no increase in failed transaction rate. + +HTTP **spans** from the client perspective however, are considered failures if the HTTP status code is ≥ 400. +These spans will set `event.outcome=failure` and increase the failed transaction rate. + +If there is no HTTP status, both transactions and spans are considered successful unless an error is reported. +==== The *Errors* table provides a high-level view of each error message when it first and last occurred, along with the total number of occurrences. This makes it very easy to quickly see which errors affect your services and take actions to rectify them. To do so, click *View errors*. [role="screenshot"] -image::apm/images/error-rate.png[Error rate and errors] +image::apm/images/error-rate.png[failed transaction rate and errors] [discrete] [[service-span-duration]] === Span types average duration and dependencies -The *Average duration by span type* chart visualizes each span type's average duration and helps you determine +The *Time spent by span type* chart visualizes each span type's average duration and helps you determine which spans could be slowing down transactions. The "app" label displayed under the chart indicates that something was happening within the application. This could signal that the agent does not have auto-instrumentation for whatever was happening during that time or that the time was spent in the application code and not in database or external requests. The *Dependencies* table displays a list of downstream services or external connections relevant -to the service at the selected time range. The table displays latency, traffic, error rate, and the impact of +to the service at the selected time range. The table displays latency, throughput, failed transaction rate, and the impact of each dependency. By default, dependencies are sorted by _Impact_ to show the most used and the slowest dependency. -If there is a particular dependency you are interested in, click *View service map* to view the related -<>. +If there is a particular dependency you are interested in, click *<>* to learn more about it. NOTE: Displaying dependencies for services instrumented with the Real User Monitoring (RUM) agent requires an agent version ≥ v5.6.3. @@ -106,11 +115,11 @@ image::apm/images/spans-dependencies.png[Span type duration and dependencies] [discrete] [[service-instances]] -=== All instances +=== Instances -The *All instances* table displays a list of all the available service instances within the selected time range. -Depending on how the service runs, the instance could be a host or a container. The table displays latency, traffic, -errors, CPU usage, and memory usage for each instance. By default, instances are sorted by _Throughput_. +The *Instances* table displays a list of all the available service instances within the selected time range. +Depending on how the service runs, the instance could be a host or a container. The table displays latency, throughput, +failed transaction, CPU usage, and memory usage for each instance. By default, instances are sorted by _Throughput_. [role="screenshot"] image::apm/images/all-instances.png[All instances] diff --git a/docs/apm/set-up.asciidoc b/docs/apm/set-up.asciidoc index b2e78bd08bc93..3cbe45ec913b7 100644 --- a/docs/apm/set-up.asciidoc +++ b/docs/apm/set-up.asciidoc @@ -8,7 +8,7 @@ APM is available via the navigation sidebar in {Kib}. If you have not already installed and configured Elastic APM, -the *Setup Instructions* in Kibana will get you started. +the *Add data* page will get you started. [role="screenshot"] image::apm/images/apm-setup.png[Installation instructions on the APM page in Kibana] @@ -17,10 +17,9 @@ image::apm/images/apm-setup.png[Installation instructions on the APM page in Kib [[apm-configure-index-pattern]] === Load the index pattern -Index patterns tell Kibana which Elasticsearch indices you want to explore. +Index patterns tell {kib} which {es} indices you want to explore. An APM index pattern is necessary for certain features in the APM app, like the query bar. -To set up the correct index pattern, -simply click *Load Kibana objects* at the bottom of the Setup Instructions. +To set up the correct index pattern, on the *Add data* page, click *Load Kibana objects*. [role="screenshot"] image::apm/images/apm-index-pattern.png[Setup index pattern for APM in Kibana] diff --git a/docs/apm/transactions.asciidoc b/docs/apm/transactions.asciidoc index 76006d375d075..c0850e4e9d507 100644 --- a/docs/apm/transactions.asciidoc +++ b/docs/apm/transactions.asciidoc @@ -8,7 +8,7 @@ APM agents automatically collect performance metrics on HTTP requests, database [role="screenshot"] image::apm/images/apm-transactions-overview.png[Example view of transactions table in the APM app in Kibana] -The *Latency*, *transactions per minute*, *Error rate*, and *Average duration by span type* +The *Latency*, *transactions per minute*, *Failed transaction rate*, and *Average duration by span type* charts display information on all transactions associated with the selected service: *Latency*:: @@ -23,17 +23,17 @@ Useful for determining if more responses than usual are being served with a part Like in the latency graph, you can zoom in on anomalies to further investigate them. [[transaction-error-rate]] -*Error rate*:: -The error rate represents the percentage of failed transactions from the perspective of the selected service. +*Failed transaction rate*:: +The failed transaction rate represents the percentage of failed transactions from the perspective of the selected service. It's useful for visualizing unexpected increases, decreases, or irregular patterns in a service's transactions. + [TIP] ==== HTTP **transactions** from the HTTP server perspective do not consider a `4xx` status code (client error) as a failure -because the failure was caused by the caller, not the HTTP server. Thus, there will be no increase in error rate. +because the failure was caused by the caller, not the HTTP server. Thus, `event.outcome=success` and there will be no increase in failed transaction rate. HTTP **spans** from the client perspective however, are considered failures if the HTTP status code is ≥ 400. -These spans will increase the error rate. +These spans will set `event.outcome=failure` and increase the failed transaction rate. If there is no HTTP status, both transactions and spans are considered successful unless an error is reported. ==== @@ -97,7 +97,7 @@ This page is visually similar to the transaction overview, but it shows data fro the selected transaction group. [role="screenshot"] -image::apm/images/apm-transaction-response-dist.png[Example view of response time distribution] +image::apm/images/apm-transactions-overview.png[Example view of response time distribution] [[transaction-duration-distribution]] ==== Latency distribution @@ -110,10 +110,10 @@ It's the requests on the right, the ones taking longer than average, that we pro [role="screenshot"] image::apm/images/apm-transaction-duration-dist.png[Example view of latency distribution graph] -Select a latency duration _bucket_ to display up to ten trace samples. +Click and drag to select a latency duration _bucket_ to display up to 500 trace samples. [[transaction-trace-sample]] -==== Trace sample +==== Trace samples Trace samples are based on the _bucket_ selection in the *Latency distribution* chart; update the samples by selecting a new _bucket_. @@ -167,4 +167,11 @@ and solve problems. [role="screenshot"] image::apm/images/apm-logs-tab.png[APM logs tab] -// To do: link to log correlation +[[transaction-latency-correlations]] +==== Correlations + +Correlations surface attributes of your data that are potentially correlated with high-latency or erroneous transactions. +To learn more, see <>. + +[role="screenshot"] +image::apm/images/correlations-hover.png[APM lattency correlations] diff --git a/docs/dev-tools/console/console.asciidoc b/docs/dev-tools/console/console.asciidoc index e1cd156e6a9e4..f29ddb1a600db 100644 --- a/docs/dev-tools/console/console.asciidoc +++ b/docs/dev-tools/console/console.asciidoc @@ -134,6 +134,7 @@ shortcuts, click *Help*. [[console-settings]] === Disable Console +deprecated:[7.16.0,"In 8.0 and later, this setting will no longer be supported."] If you don’t want to use *Console*, you can disable it by setting `console.enabled` to `false` in your `kibana.yml` configuration file. Changing this setting causes the server to regenerate assets on the next startup, diff --git a/docs/developer/plugin-list.asciidoc b/docs/developer/plugin-list.asciidoc index 3002e0b144011..fca60d27490dd 100644 --- a/docs/developer/plugin-list.asciidoc +++ b/docs/developer/plugin-list.asciidoc @@ -301,7 +301,7 @@ The plugin exposes the static DefaultEditorController class to consume. |Contains the timelion visualization and the timelion backend. -|{kib-repo}blob/{branch}/src/plugins/vis_type_timeseries[visTypeTimeseries] +|{kib-repo}blob/{branch}/src/plugins/vis_types/timeseries[visTypeTimeseries] |WARNING: Missing README. 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..2bc7f6cba594d 100644 --- a/docs/development/core/server/kibana-plugin-core-server.deprecationsservicesetup.md +++ b/docs/development/core/server/kibana-plugin-core-server.deprecationsservicesetup.md @@ -27,6 +27,7 @@ async function getDeprecations({ esClient, savedObjectsClient }: GetDeprecations const deprecations: DeprecationsDetails[] = []; const count = await getFooCount(savedObjectsClient); if (count > 0) { + // Example of a manual correctiveAction deprecations.push({ title: i18n.translate('xpack.foo.deprecations.title', { defaultMessage: `Foo's are deprecated` 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 8c42884eb0b31..2c3dd9f3f8434 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectstypemanagementdefinition.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectstypemanagementdefinition.md @@ -25,4 +25,5 @@ export interface SavedObjectsTypeManagementDefinition | [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. | diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectstypemanagementdefinition.visibleinmanagement.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectstypemanagementdefinition.visibleinmanagement.md new file mode 100644 index 0000000000000..33ddc8e8c8307 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectstypemanagementdefinition.visibleinmanagement.md @@ -0,0 +1,18 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsTypeManagementDefinition](./kibana-plugin-core-server.savedobjectstypemanagementdefinition.md) > [visibleInManagement](./kibana-plugin-core-server.savedobjectstypemanagementdefinition.visibleinmanagement.md) + +## SavedObjectsTypeManagementDefinition.visibleInManagement property + +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`. + +Signature: + +```typescript +visibleInManagement?: boolean; +``` + +## Remarks + +`importableAndExportable` must be `true` to specify this property. + diff --git a/docs/osquery/images/enter-query.png b/docs/osquery/images/enter-query.png new file mode 100644 index 0000000000000..154d2dcad4857 Binary files /dev/null and b/docs/osquery/images/enter-query.png differ diff --git a/docs/osquery/images/live-queries-history.png b/docs/osquery/images/live-queries-history.png new file mode 100644 index 0000000000000..6f49917a685d6 Binary files /dev/null and b/docs/osquery/images/live-queries-history.png differ diff --git a/docs/osquery/images/live-query-check-results.png b/docs/osquery/images/live-query-check-results.png new file mode 100644 index 0000000000000..df292309e0853 Binary files /dev/null and b/docs/osquery/images/live-query-check-results.png differ diff --git a/docs/osquery/images/live-query-history.png b/docs/osquery/images/live-query-history.png new file mode 100644 index 0000000000000..97d9ccb1bda88 Binary files /dev/null and b/docs/osquery/images/live-query-history.png differ diff --git a/docs/osquery/images/play-icon.png b/docs/osquery/images/play-icon.png new file mode 100644 index 0000000000000..724d17b5a381d Binary files /dev/null and b/docs/osquery/images/play-icon.png differ diff --git a/docs/osquery/images/schedule-query.png b/docs/osquery/images/schedule-query.png new file mode 100644 index 0000000000000..51d83f2540aca Binary files /dev/null and b/docs/osquery/images/schedule-query.png differ diff --git a/docs/osquery/images/scheduled-query-groupds.png b/docs/osquery/images/scheduled-query-groupds.png new file mode 100644 index 0000000000000..bb7fd8ae87563 Binary files /dev/null and b/docs/osquery/images/scheduled-query-groupds.png differ diff --git a/docs/osquery/images/table-icon.png b/docs/osquery/images/table-icon.png new file mode 100644 index 0000000000000..5c4e9e78d9f09 Binary files /dev/null and b/docs/osquery/images/table-icon.png differ diff --git a/docs/osquery/osquery.asciidoc b/docs/osquery/osquery.asciidoc new file mode 100644 index 0000000000000..1e4e6604a7c70 --- /dev/null +++ b/docs/osquery/osquery.asciidoc @@ -0,0 +1,419 @@ +[chapter] +[role="xpack"] +[[osquery]] += Osquery + +https://osquery.io[Osquery] is an open source tool that lets you query operating systems, like a database, providing you with visibility into your infrastructure and operating systems. +Using basic SQL commands, you can ask questions about devices, such as servers, +Docker containers, and computers running Linux, macOS, or Windows. +The https://osquery.io/schema[extensive schema] helps with a variety of use cases, +including vulnerability detection, compliance monitoring, incident investigations, and more. + +With Osquery, you can: + + * Run live queries for one or more agents + * Schedule queries to capture changes to OS state over time + * View a history of past queries and their results + * Save queries and build a library of queries for specific use cases + +Osquery results are stored in {es}, so that you can +search, analyze, and visualize Osquery data in {kib}. + +Osquery is powered by the *Osquery Manager* integration. +For information on how to set up *Osquery Manager*, refer to <>. + +[float] +== Required privileges + +To use *Osquery Manager*, you must be assigned to a role with the following privileges: + +* `Read` privileges for the `logs-osquery_manager.result*` index. +* {kib} privileges for **Osquery Manager**. The `All` privilege +enables you to run, schedule, and save queries. `Read` enables you to +view live and scheduled query results, but you cannot run live queries or edit. + +[float] +[[osquery-run-query]] +== Run live queries + +To inspect a host or test queries you want to schedule, run a query against one or more agents or policies, +and view the results. + +. Open the main menu, and then click *Osquery*. + +. In the *Live queries* view, click **New live query**. + +. Select one or more agents or groups to query. Start typing in the search field, +and you'll get suggestions for agents by name, ID, platform, and policy. + +. Enter a query or select a query from your saved queries. ++ +[role="screenshot"] +image::images/enter-query.png[Select saved query dropdown name showing query name and description] + +. Click **Submit**. + +. Review the results in a table, or navigate to *Discover* to dive deeper into the response, +or to the drag-and-drop *Lens* editor to create visualizations. +. To view more information about the request, such as failures, open the *Status* tab. +. To optionally save the query for future use, click *Save for later* and define the ID, +description, and other +<>. + +To view a history of the past queries you have run, open the *Live queries history*. + +* To replay a query, click image:images/play-icon.png[Right-pointing triangle]. + +* To view the query <> and <>, +click image:images/table-icon.png[Table icon]. ++ +[role="screenshot"] +image::images/live-query-check-results.png[Results of OSquery] + + +[float] +[[osquery-schedule-query]] +== Schedule queries + +Group and schedule queries to run on a specified interval, in seconds. +For example, you might create a group that checks +for IT compliance-type issues, and +another group that monitors for evidence of malware. + +. Open the **Scheduled query groups** tab. + +. Click a group name to view the details. ++ +Details include the last time each query ran, how many results were returned, and the number of agents the query ran against. +If there are errors, expand the row to view the details. ++ +[role="screenshot"] +image::images/scheduled-query-groupds.png[Shows last results last time it ran, how many results returned, number of agents it ran against, if it is actually running and if there are errors] + +. To make changes, click *Edit*. + +.. To add a query to the group, click *Add query*, and then enter the query ID and interval. +Optionally, set the minimum Osquery version and platform, +or <>. + +.. To upload queries from a .conf query pack, drag the pack to the drop zone under the query table. To explore the community packs that Osquery publishes, click *Example packs*. + +. Click *Save query*. The queries run when the policy receives the update. + +. View scheduled history results in < or the drag-and-drop <> editor. + + +[float] +[[osquery-map-fields]] +== Map Osquery fields to ECS fields + +When you schedule queries, you can optionally map query results to fields in +the {ecs-ref}/ecs-reference.html[Elastic Common Schema] (ECS), +which standardizes your Osquery data for use across detections, machine learning, +and any other areas that rely on ECS-compliant data. +The query results include the original `osquery.` +and the mapped ECS field. For example, if you update a query to map `osquery.name` to `user.name`, the query results include both fields. + +. Edit a scheduled query group, and then click the edit icon for the query that you want to map. + +. In **ECS mapping**, select the Osquery result fields you want to map to ECS fields. ++ +The fields available in the **Osquery.results** column are based on the SQL query entered, +and only include fields that the query returns. + +When mapping fields: + +. To add a new row for additional fields to map, click the plus icon. + +. To remove any mapped rows, click the trash icon. + +. To save changes to the query, click *Save*. + +. To save changes to the group, click *Save query*. + + +[float] +[[osquery-manage-query]] +== Edit saved queries + +Add or edit saved queries to the *Saved queries* tab. + +. Go to the saved queries, then click **Add saved query** or the edit icon. +. Provide the following fields: + +* The unique identifier. + +* A brief description. + +* The SQL query. + +* The defaults for the scheduled query group, which is included when you add the query to a scheduled query group. + +** The frequency to run the query. + +** The minimum https://github.com/osquery/osquery/releases)[version of Osquery] required to run the query. + +** The operating system required to run the query. For information about supported platforms per table, click *OSquery schema* in the *Edit query* flyout. + +. Click **Save query**. + +[float] +[[osquery-status]] +== Osquery status + +A query can have the following status: + +[cols="2*<"] +|=== +| Successful | The query successfully completed. +| Failed | The query encountered a problem, such as an issue with the query or the agent was disconnected, and might have failed. +| Not yet responded | The query has not been sent to the agent. +| Expired | The action request timed out. The agent may be offline. +|=== + +NOTE: If an agent is offline, the request status remains **pending** as {kib} retries the request. +By default, a query request times out after five minutes. The time out applies to the time it takes +to deliver the action request to an agent to run a query. If the action completes after the timeout period, +the results are still returned. + + +[float] +[[osquery-results]] +== Osquery results + +For the fields that can be returned in Osquery results, +refer to https://docs.elastic.co/en/integrations/osquery_manager#exported-fields[exported fields]. +Scheduled Osquery +results can also include ECS fields, if the query has a defined ECS mapping. + +Osquery responses include the following information: + +* Everything prefaced with `osquery.` is part of the query response. These fields are not mapped to ECS. + +* By default, the `host.*` and `agent.*` fields are mapped to ECS. + +* The `action_data.query` has the query that was sent. + +* All query results are https://osquery.readthedocs.io/en/stable/deployment/logging/#snapshot-logs[snapshot logs] +that represent a point in time with a set of results, with no differentials. +https://osquery.readthedocs.io/en/stable/deployment/logging/#differential-logs[Differential logs] are unsupported. + +* Osquery data is stored in the `logs-osquery_manager.result-default` datastream, and the result row data is under the `osquery` property in the document. + +The following example shows a successful Osquery result: + + +```ts +{ + "_index": ".ds-logs-osquery_manager.result-default-2021.04.12-2021.04.12-000001", + "_id": "R3ZwxngBKwN-X8eyQbxy", + "_version": 1, + "_score": null, + "fields": { + "osquery.seconds": [ + "7" + ], + "action_data.id": [ + "72d3ec71-7635-461e-a15d-f728819ae27f" + ], + "osquery.seconds.number": [ + 7 + ], + "osquery.hours.number": [ + 6 + ], + "host.hostname": [ + "MacBook-Pro.local" + ], + "type": [ + "MacBook-Pro.local" + ], + "host.mac": [ + "ad:de:48:00:12:22", + "a6:83:e7:cb:91:ee" + ], + "osquery.total_seconds.number": [ + 1060627 + ], + "host.os.build": [ + "20D91" + ], + "host.ip": [ + "192.168.31.171", + "fe80::b5b1:39ff:faa1:3b39" + ], + "agent.type": [ + "osquerybeat" + ], + "action_data.query": [ + "select * from uptime;" + ], + "osquery.minutes": [ + "37" + ], + "action_id": [ + "5099c02d-bd6d-4b88-af90-d80dcdc945df" + ], + "host.os.version": [ + "10.16" + ], + "host.os.kernel": [ + "20.3.0" + ], + "host.os.name": [ + "Mac OS X" + ], + "agent.name": [ + "MacBook-Pro.local" + ], + "host.name": [ + "MacBook-Pro.local" + ], + "osquery.total_seconds": [ + "1060627" + ], + "host.id": [ + "155D977D-8EA8-5BDE-94A2-D78A7B545198" + ], + "osquery.hours": [ + "6" + ], + "osquery.days": [ + "12" + ], + "host.os.type": [ + "macos" + ], + "osquery.days.number": [ + 12 + ], + "host.architecture": [ + "x86_64" + ], + "@timestamp": [ + "2021-04-12T14:15:45.060Z" + ], + "agent.id": [ + "196a0086-a612-48b1-930a-300565b3efaf" + ], + "host.os.platform": [ + "darwin" + ], + "ecs.version": [ + "1.8.0" + ], + "agent.ephemeral_id": [ + "5cb88e34-50fe-4c13-b81c-d2b7187505ea" + ], + "agent.version": [ + "7.13.0" + ], + "host.os.family": [ + "darwin" + ], + "osquery.minutes.number": [ + 37 + ] + } +} +``` + +The following is an example of an **error response** for an undefined action query: + +```ts +{ + "_index": ".ds-.fleet-actions-results-2021.04.10-000001", + "_id": "qm7mvHgBKwN-X8eyYB1x", + "_version": 1, + "_score": null, + "fields": { + "completed_at": [ + "2021-04-10T17:48:32.268Z" + ], + "error.keyword": [ + "action undefined" + ], + "@timestamp": [ + "2021-04-10T17:48:32.000Z" + ], + "action_data.query": [ + "select * from uptime;" + ], + "action_data.id": [ + "2c95bb2c-8ab6-4e8c-ac01-a1abb693ea00" + ], + "agent_id": [ + "c21b4c9c-6f36-49f0-8b60-08490fc619ce" + ], + "action_id": [ + "53454d3b-c8cd-4a50-b5b4-f85da17b4be2" + ], + "started_at": [ + "2021-04-10T17:48:32.267Z" + ], + "error": [ + "action undefined" + ] + } +} +``` +[float] +[[manage-osquery-integration]] +== Manage the integration + +[float] +== System requirements + +* {fleet-guide}/fleet-overview.html[Fleet] is enabled on your cluster, and +one or more {fleet-guide}/elastic-agent-installation-configuration.html[Elastic Agents] is enrolled. +* The https://docs.elastic.co/en/integrations/osquery_manager[*Osquery Manager*] integration +has been added and configured +for an agent policy through Fleet. +This integration supports x64 architecture on Windows, MacOS, and Linux platforms, +and ARM64 architecture on Linux. + +NOTE: The original {filebeat-ref}/filebeat-module-osquery.html[Filebeat Osquery module] +and the https://docs.elastic.co/en/integrations/osquery[Osquery Log Collection] +integration collect logs from self-managed Osquery deployments. +The *Osquery Manager* integration manages Osquery deployments +and supports running and scheduling queries from {kib}. + +[float] +== Customize Osquery sub-feature privileges + +Depending on your https://www.elastic.co/subscriptions[subscription level], +you can further customize the sub-feature privileges +for *Osquery Manager*. These include options to grant specific access for running live queries, +running saved queries, saving queries, and scheduling queries. For example, +you can create roles for users who can only run live or saved queries, but who cannot save or schedule queries. +This is useful for teams who need in-depth and detailed control. + +[float] +== Upgrade Osquery versions + +The https://github.com/osquery/osquery/releases[Osquery version] available on an Elastic Agent +is associated to the version of Osquery Beat on the Agent. +To get the latest version of Osquery Beat, +https://www.elastic.co/guide/en/fleet/master/upgrade-elastic-agent.html[upgrade your Elastic Agent]. + +[float] +== Debug issues +If you encounter issues with *Osquery Manager*, find the relevant logs for the {elastic-agent} +and Osquerybeat in the installed agent directory, then adjust the agent path for your setup. + +The relevant logs look similar to the following example paths: + +```ts +`/data/elastic-agent-054e22/logs/elastic-agent-json.log-*` +`/data/elastic-agent-054e22/logs/default/osquerybeat-json.log` +``` + +To get more details in the logs, change the agent logging level to debug: + +. Open the main menu, and then select **Fleet**. + +. Select the agent that you want to debug. + +. On the **Logs** tab, change the **Agent logging level** to **debug**, and then click **Apply changes**. ++ +`agent.logging.level` is updated in `fleet.yml`, and the logging level is changed to `debug`. diff --git a/docs/settings/alert-action-settings.asciidoc b/docs/settings/alert-action-settings.asciidoc index 2086a0490d052..405cfd2dc1af0 100644 --- a/docs/settings/alert-action-settings.asciidoc +++ b/docs/settings/alert-action-settings.asciidoc @@ -32,6 +32,7 @@ Be sure to back up the encryption key value somewhere safe, as your alerting rul ==== Action settings `xpack.actions.enabled`:: +deprecated:[7.16.0,"In 8.0 and later, this setting will no longer be supported."] Feature toggle that enables Actions in {kib}. If `false`, all features dependent on Actions are disabled, including the *Observability* and *Security* apps. Default: `true`. diff --git a/docs/settings/apm-settings.asciidoc b/docs/settings/apm-settings.asciidoc index ea874752eb540..916a5d92b1f4f 100644 --- a/docs/settings/apm-settings.asciidoc +++ b/docs/settings/apm-settings.asciidoc @@ -46,7 +46,8 @@ Changing these settings may disable features of the APM App. [cols="2*<"] |=== | `xpack.apm.enabled` - | Set to `false` to disable the APM app. Defaults to `true`. + | deprecated:[7.16.0,"In 8.0 and later, this setting will no longer be supported."] + Set to `false` to disable the APM app. Defaults to `true`. | `xpack.apm.maxServiceEnvironments` | Maximum number of unique service environments recognized by the UI. Defaults to `100`. diff --git a/docs/settings/dev-settings.asciidoc b/docs/settings/dev-settings.asciidoc index b7edf36851d91..bcf4420cdadca 100644 --- a/docs/settings/dev-settings.asciidoc +++ b/docs/settings/dev-settings.asciidoc @@ -13,6 +13,7 @@ They are enabled by default. ==== Grok Debugger settings `xpack.grokdebugger.enabled` {ess-icon}:: +deprecated:[7.16.0,"In 8.0 and later, this setting will no longer be supported."] Set to `true` to enable the <>. Defaults to `true`. @@ -21,6 +22,7 @@ Set to `true` to enable the <>. Defaults to `t ==== {searchprofiler} settings `xpack.searchprofiler.enabled`:: +deprecated:[7.16.0,"In 8.0 and later, this setting will no longer be supported."] Set to `true` to enable the <>. Defaults to `true`. [float] @@ -28,4 +30,5 @@ Set to `true` to enable the <>. Defaults to `tr ==== Painless Lab settings `xpack.painless_lab.enabled`:: +deprecated:[7.16.0,"In 8.0 and later, this setting will no longer be supported."] When set to `true`, enables the <>. Defaults to `true`. diff --git a/docs/settings/fleet-settings.asciidoc b/docs/settings/fleet-settings.asciidoc index 734c87aab204c..1aa1e10c24cf3 100644 --- a/docs/settings/fleet-settings.asciidoc +++ b/docs/settings/fleet-settings.asciidoc @@ -21,7 +21,8 @@ See the {fleet-guide}/index.html[{fleet}] docs for more information. [cols="2*<"] |=== | `xpack.fleet.enabled` {ess-icon} - | Set to `true` (default) to enable {fleet}. + | deprecated:[7.16.0,"In 8.0 and later, this setting will no longer be supported."] + Set to `true` (default) to enable {fleet}. | `xpack.fleet.agents.enabled` {ess-icon} | Set to `true` (default) to enable {fleet}. |=== @@ -86,6 +87,8 @@ Optional properties are: be changed by updating the {kib} config. `is_default`:: If `true`, this policy is the default agent policy. `is_default_fleet_server`:: If `true`, this policy is the default {fleet-server} agent policy. + `data_output_id`:: ID of the output to send data (Need to be identical to `monitoring_output_id`) + `monitoring_output_id`:: ID of the output to send monitoring data. (Need to be identical to `data_output_id`) `package_policies`:: List of integration policies to add to this policy. `name`::: (required) Name of the integration policy. `package`::: (required) Integration that this policy configures @@ -96,6 +99,20 @@ Optional properties are: integration. Follows the same schema as integration inputs, with the exception that any object in `vars` can be passed `frozen: true` in order to prevent that specific `var` from being edited by the user. + +| `xpack.fleet.outputs` + | List of ouputs that are configured when the {fleet} app starts. +Required properties are: + + `id`:: Unique ID for this output. The ID should be a string. + `name`:: Output name. + `type`:: Type of Output. Currently we only support "elasticsearch". + `hosts`:: Array that contains the list of host for that output. + `config`:: Extra config for that output. + +Optional properties are: + + `is_default`:: If `true`, this output is the default output. |=== Example configuration: diff --git a/docs/settings/general-infra-logs-ui-settings.asciidoc b/docs/settings/general-infra-logs-ui-settings.asciidoc index 2a9d4df1ff43c..282239dcf166c 100644 --- a/docs/settings/general-infra-logs-ui-settings.asciidoc +++ b/docs/settings/general-infra-logs-ui-settings.asciidoc @@ -1,7 +1,8 @@ [cols="2*<"] |=== | `xpack.infra.enabled` - | Set to `false` to disable the Logs and Metrics app plugin {kib}. Defaults to `true`. + | deprecated:[7.16.0,"In 8.0 and later, this setting will no longer be supported."] + Set to `false` to disable the Logs and Metrics app plugin {kib}. Defaults to `true`. | `xpack.infra.sources.default.logAlias` | Index pattern for matching indices that contain log data. Defaults to `filebeat-*,kibana_sample_data_logs*`. To match multiple wildcard patterns, use a comma to separate the names, with no space after the comma. For example, `logstash-app1-*,default-logs-*`. diff --git a/docs/settings/graph-settings.asciidoc b/docs/settings/graph-settings.asciidoc index 093edb0d08547..793a8aae73158 100644 --- a/docs/settings/graph-settings.asciidoc +++ b/docs/settings/graph-settings.asciidoc @@ -8,4 +8,5 @@ You do not need to configure any settings to use the {graph-features}. `xpack.graph.enabled` {ess-icon}:: +deprecated:[7.16.0,"In 8.0 and later, this setting will no longer be supported."] Set to `false` to disable the {graph-features}. diff --git a/docs/settings/ml-settings.asciidoc b/docs/settings/ml-settings.asciidoc index 92d0c0b491ce7..59fa236e08275 100644 --- a/docs/settings/ml-settings.asciidoc +++ b/docs/settings/ml-settings.asciidoc @@ -14,7 +14,8 @@ enabled by default. [cols="2*<"] |=== | `xpack.ml.enabled` {ess-icon} - | Set to `true` (default) to enable {kib} {ml-features}. + + | deprecated:[7.16.0,"In 8.0 and later, this setting will no longer be supported."] + Set to `true` (default) to enable {kib} {ml-features}. + + If set to `false` in `kibana.yml`, the {ml} icon is hidden in this {kib} instance. If `xpack.ml.enabled` is set to `true` in `elasticsearch.yml`, however, diff --git a/docs/settings/monitoring-settings.asciidoc b/docs/settings/monitoring-settings.asciidoc index 31148f0abf4e1..03c11007c64c4 100644 --- a/docs/settings/monitoring-settings.asciidoc +++ b/docs/settings/monitoring-settings.asciidoc @@ -32,7 +32,8 @@ For more information, see [cols="2*<"] |=== | `monitoring.enabled` - | Set to `true` (default) to enable the {monitor-features} in {kib}. Unlike the + | deprecated:[7.16.0,"In 8.0 and later, this setting will no longer be supported."] + Set to `true` (default) to enable the {monitor-features} in {kib}. Unlike the <> setting, when this setting is `false`, the monitoring back-end does not run and {kib} stats are not sent to the monitoring cluster. diff --git a/docs/settings/security-settings.asciidoc b/docs/settings/security-settings.asciidoc index 69f255f36105e..5e4b267c0f4d4 100644 --- a/docs/settings/security-settings.asciidoc +++ b/docs/settings/security-settings.asciidoc @@ -15,7 +15,8 @@ You do not need to configure any additional settings to use the [cols="2*<"] |=== | `xpack.security.enabled` - | By default, {kib} automatically detects whether to enable the + | deprecated:[7.16.0,"In 8.0 and later, this setting will no longer be supported."] + By default, {kib} automatically detects whether to enable the {security-features} based on the license and whether {es} {security-features} are enabled. + + diff --git a/docs/settings/spaces-settings.asciidoc b/docs/settings/spaces-settings.asciidoc index 3b643f76f0c09..8504464da1dfb 100644 --- a/docs/settings/spaces-settings.asciidoc +++ b/docs/settings/spaces-settings.asciidoc @@ -15,8 +15,9 @@ roles when Security is enabled. [cols="2*<"] |=== | `xpack.spaces.enabled` - | Set to `true` (default) to enable Spaces in {kib}. - This setting is deprecated. Starting in 8.0, it will not be possible to disable this plugin. + | deprecated:[7.16.0,"In 8.0 and later, this setting will no longer be supported."] + Set to `true` (default) to enable Spaces in {kib}. + This setting is deprecated. Starting in 8.0, it will not be possible to disable this plugin. | `xpack.spaces.maxSpaces` | The maximum amount of Spaces that can be used with this instance of {kib}. Some operations diff --git a/docs/settings/url-drilldown-settings.asciidoc b/docs/settings/url-drilldown-settings.asciidoc index 8be3a21bfbffc..ca414d4f650e9 100644 --- a/docs/settings/url-drilldown-settings.asciidoc +++ b/docs/settings/url-drilldown-settings.asciidoc @@ -9,7 +9,8 @@ Configure the URL drilldown settings in your `kibana.yml` configuration file. [cols="2*<"] |=== | [[url-drilldown-enabled]] `url_drilldown.enabled` - | When `true`, enables URL drilldowns on your {kib} instance. + | deprecated:[7.16.0,"In 8.0 and later, this setting will no longer be supported."] + When `true`, enables URL drilldowns on your {kib} instance. | [[external-URL-policy]] `externalUrl.policy` | Configures the external URL policies. URL drilldowns respect the global *External URL* service, which you can use to deny or allow external URLs. diff --git a/docs/setup/docker.asciidoc b/docs/setup/docker.asciidoc index cc14e79c54f15..01fc6e2e76f92 100644 --- a/docs/setup/docker.asciidoc +++ b/docs/setup/docker.asciidoc @@ -125,7 +125,7 @@ Some example translations are shown here: **Environment Variable**:: **Kibana Setting** `SERVER_NAME`:: `server.name` `SERVER_BASEPATH`:: `server.basePath` -`MONITORING_ENABLED`:: `monitoring.enabled` +`ELASTICSEARCH_HOSTS`:: `elasticsearch.hosts` In general, any setting listed in <> can be configured with this technique. diff --git a/docs/setup/settings.asciidoc b/docs/setup/settings.asciidoc index e8d0b4b593c3f..2f4fd4d052dad 100644 --- a/docs/setup/settings.asciidoc +++ b/docs/setup/settings.asciidoc @@ -21,7 +21,8 @@ configuration using `${MY_ENV_VAR}` syntax. |=== | `console.enabled:` - | Toggling this causes the server to regenerate assets on the next startup, + | deprecated:[7.16.0,"In 8.0 and later, this setting will no longer be supported."] +Toggling this causes the server to regenerate assets on the next startup, which may cause a delay before pages start being served. Set to `false` to disable Console. *Default: `true`* @@ -794,12 +795,13 @@ sources and images. When false, Vega can only get data from {es}. *Default: `fal | Enables you to view the underlying documents in a data series from a dashboard panel. *Default: `false`* | `xpack.license_management.enabled` - | Set this value to false to -disable the License Management UI. *Default: `true`* + | deprecated:[7.16.0,"In 8.0 and later, this setting will no longer be supported."] +Set this value to false to disable the License Management UI. +*Default: `true`* | `xpack.rollup.enabled:` - | Set this value to false to disable the -Rollup UI. *Default: true* + | deprecated:[7.16.0,"In 8.0 and later, this setting will no longer be supported."] +Set this value to false to disable the Rollup UI. *Default: true* | `i18n.locale` {ess-icon} | Set this value to change the {kib} interface language. diff --git a/docs/user/api.asciidoc b/docs/user/api.asciidoc index 00aa3c545df69..12e200bb0ba27 100644 --- a/docs/user/api.asciidoc +++ b/docs/user/api.asciidoc @@ -96,6 +96,7 @@ include::{kib-repo-dir}/api/alerting.asciidoc[] include::{kib-repo-dir}/api/actions-and-connectors.asciidoc[] include::{kib-repo-dir}/api/dashboard-api.asciidoc[] include::{kib-repo-dir}/api/logstash-configuration-management.asciidoc[] +include::{kib-repo-dir}/api/machine-learning.asciidoc[] include::{kib-repo-dir}/api/url-shortening.asciidoc[] include::{kib-repo-dir}/api/task-manager/health.asciidoc[] include::{kib-repo-dir}/api/upgrade-assistant.asciidoc[] diff --git a/docs/user/dashboard/lens.asciidoc b/docs/user/dashboard/lens.asciidoc index 0f130f15c8a77..c3e0a5523a78d 100644 --- a/docs/user/dashboard/lens.asciidoc +++ b/docs/user/dashboard/lens.asciidoc @@ -441,4 +441,20 @@ Pagination in a data table is unsupported. To use pagination in data tables, cre [%collapsible] ==== Specifying the color for a single data point, such as a single bar or line, is unsupported. -==== \ No newline at end of file +==== + +[discrete] +[[is-it-possible-to-inspect-the-elasticsearch-queries-in-Lens]] +.*How do I inspect {es} queries in visualizations?* +[%collapsible] +==== +You can inspect the requests sent by the visualization to {es} using the Inspector. It can be accessed within the editor or in the dashboard. +==== + +[discrete] +[[how-to-isolate-a-single-series-in-a-chart]] +.*How do I isolate a single series in a chart?* +[%collapsible] +==== +For area, line, and bar charts, press Shift, then click the series in the legend. All other series are automatically unselected. +==== diff --git a/docs/user/index.asciidoc b/docs/user/index.asciidoc index 0b5b9b634bca5..515b292d23b8d 100644 --- a/docs/user/index.asciidoc +++ b/docs/user/index.asciidoc @@ -34,12 +34,14 @@ include::{kib-repo-dir}/siem/index.asciidoc[] include::dev-tools.asciidoc[] +include::{kib-repo-dir}/fleet/fleet.asciidoc[] + +include::{kib-repo-dir}/osquery/osquery.asciidoc[] + include::monitoring/index.asciidoc[] include::management.asciidoc[] -include::{kib-repo-dir}/fleet/fleet.asciidoc[] - include::api.asciidoc[] include::plugins.asciidoc[] diff --git a/docs/user/plugins.asciidoc b/docs/user/plugins.asciidoc index 0ef5d1a237510..c604526d6c933 100644 --- a/docs/user/plugins.asciidoc +++ b/docs/user/plugins.asciidoc @@ -149,6 +149,8 @@ NOTE: Removing a plugin will result in an "optimize" run which will delay the ne [[disable-plugin]] == Disable plugins +deprecated:[7.16.0,"In 8.0 and later, this setting will only be supported for a subset of plugins that have opted in to the behavior."] + Use the following command to disable a plugin: [source,shell] @@ -158,7 +160,7 @@ Use the following command to disable a plugin: NOTE: Disabling or enabling a plugin will result in an "optimize" run which will delay the start of {kib}. -<1> You can find a plugin's plugin ID as the value of the `name` property in the plugin's `package.json` file. +<1> You can find a plugin's plugin ID as the value of the `name` property in the plugin's `kibana.json` file. [float] [[configure-plugin-manager]] diff --git a/examples/embeddable_examples/kibana.json b/examples/embeddable_examples/kibana.json index d725a5c94a9c8..103857804b5d4 100644 --- a/examples/embeddable_examples/kibana.json +++ b/examples/embeddable_examples/kibana.json @@ -9,7 +9,7 @@ "githubTeam": "kibana-app-services" }, "description": "Example app that shows how to register custom embeddables", - "requiredPlugins": ["embeddable", "uiActions", "savedObjects", "dashboard"], + "requiredPlugins": ["embeddable", "uiActions", "savedObjects", "dashboard", "kibanaUtils"], "optionalPlugins": [], "extraPublicDirs": ["public/todo", "public/hello_world", "public/todo/todo_ref_embeddable"], "requiredBundles": ["kibanaReact"] diff --git a/examples/embeddable_examples/public/migrations/migrations_embeddable_factory.ts b/examples/embeddable_examples/public/migrations/migrations_embeddable_factory.ts index c1ceaaca3e466..61e6bfa56ec47 100644 --- a/examples/embeddable_examples/public/migrations/migrations_embeddable_factory.ts +++ b/examples/embeddable_examples/public/migrations/migrations_embeddable_factory.ts @@ -7,6 +7,7 @@ */ import { i18n } from '@kbn/i18n'; +import { EmbeddableStateWithType } from '../../../../src/plugins/embeddable/common'; import { IContainer, EmbeddableInput, @@ -35,6 +36,16 @@ export class SimpleEmbeddableFactoryDefinition '7.3.0': migration730, }; + public extract(state: EmbeddableStateWithType) { + // this embeddable does not store references to other saved objects + return { state, references: [] }; + } + + public inject(state: EmbeddableStateWithType) { + // this embeddable does not store references to other saved objects + return state; + } + /** * In our simple example, we let everyone have permissions to edit this. Most * embeddables should check the UI Capabilities service to be sure of diff --git a/examples/embeddable_examples/server/merge_migration_function_maps.ts b/examples/embeddable_examples/server/merge_migration_function_maps.ts new file mode 100644 index 0000000000000..01a46949e6bbf --- /dev/null +++ b/examples/embeddable_examples/server/merge_migration_function_maps.ts @@ -0,0 +1,25 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { mergeWith } from 'lodash'; +import type { SerializableRecord } from '@kbn/utility-types'; +import { MigrateFunctionsObject, MigrateFunction } from '../../../src/plugins/kibana_utils/common'; + +export const mergeMigrationFunctionMaps = ( + obj1: MigrateFunctionsObject, + obj2: MigrateFunctionsObject +) => { + const customizer = (objValue: MigrateFunction, srcValue: MigrateFunction) => { + if (!srcValue || !objValue) { + return srcValue || objValue; + } + return (state: SerializableRecord) => objValue(srcValue(state)); + }; + + return mergeWith({ ...obj1 }, obj2, customizer); +}; diff --git a/examples/embeddable_examples/server/searchable_list_saved_object.ts b/examples/embeddable_examples/server/searchable_list_saved_object.ts index ac4656c7c2b77..a3b12a05323f0 100644 --- a/examples/embeddable_examples/server/searchable_list_saved_object.ts +++ b/examples/embeddable_examples/server/searchable_list_saved_object.ts @@ -9,9 +9,12 @@ import { mapValues } from 'lodash'; import { SavedObjectsType, SavedObjectUnsanitizedDoc } from 'kibana/server'; import { EmbeddableSetup } from '../../../src/plugins/embeddable/server'; +// NOTE: this should rather be imported from 'plugins/kibana_utils/server' but examples at the moment don't +// allow static imports from plugins so this code was duplicated +import { mergeMigrationFunctionMaps } from './merge_migration_function_maps'; export const searchableListSavedObject = (embeddable: EmbeddableSetup) => { - return { + const searchableListSO: SavedObjectsType = { name: 'searchableList', hidden: false, namespaceType: 'single', @@ -30,14 +33,22 @@ export const searchableListSavedObject = (embeddable: EmbeddableSetup) => { }, }, migrations: () => { - // we assume all the migration will be done by embeddables service and that saved object holds no extra state besides that of searchable list embeddable input\ - // if saved object would hold additional information we would need to merge the response from embeddables.getAllMigrations with our custom migrations. - return mapValues(embeddable.getAllMigrations(), (migrate) => { + // there are no migrations defined for the saved object at the moment, possibly they would be added in the future + const searchableListSavedObjectMigrations = {}; + + // we don't know if embeddables have any migrations defined so we need to fetch them and map the received functions so we pass + // them the correct input and that we correctly map the response + const embeddableMigrations = mapValues(embeddable.getAllMigrations(), (migrate) => { return (state: SavedObjectUnsanitizedDoc) => ({ ...state, attributes: migrate(state.attributes), }); }); + + // we merge our and embeddable migrations and return + return mergeMigrationFunctionMaps(searchableListSavedObjectMigrations, embeddableMigrations); }, - } as SavedObjectsType; + }; + + return searchableListSO; }; diff --git a/package.json b/package.json index 52b9a8a644c45..d1d02f8502b42 100644 --- a/package.json +++ b/package.json @@ -107,14 +107,14 @@ "@elastic/search-ui-app-search-connector": "^1.6.0", "@emotion/react": "^11.4.0", "@hapi/accept": "^5.0.2", - "@hapi/boom": "^9.1.1", + "@hapi/boom": "^9.1.4", "@hapi/cookie": "^11.0.2", "@hapi/good-squeeze": "6.0.0", "@hapi/h2o2": "^9.1.0", - "@hapi/hapi": "^20.0.3", - "@hapi/hoek": "^9.1.1", - "@hapi/inert": "^6.0.3", - "@hapi/podium": "^4.1.1", + "@hapi/hapi": "^20.2.0", + "@hapi/hoek": "^9.2.0", + "@hapi/inert": "^6.0.4", + "@hapi/podium": "^4.1.3", "@hapi/wreck": "^17.1.0", "@kbn/ace": "link:bazel-bin/packages/kbn-ace", "@kbn/alerts": "link:bazel-bin/packages/kbn-alerts", @@ -220,7 +220,7 @@ "deep-freeze-strict": "^1.1.1", "deepmerge": "^4.2.2", "del": "^5.1.0", - "elastic-apm-node": "^3.20.0", + "elastic-apm-node": "^3.21.1", "execa": "^4.0.2", "exit-hook": "^2.2.0", "expiry-js": "0.1.7", @@ -530,11 +530,10 @@ "@types/glob": "^7.1.2", "@types/gulp": "^4.0.6", "@types/gulp-zip": "^4.0.1", - "@types/hapi__cookie": "^10.1.1", - "@types/hapi__h2o2": "^8.3.2", - "@types/hapi__hapi": "^20.0.2", - "@types/hapi__inert": "^5.2.2", - "@types/hapi__podium": "^3.4.1", + "@types/hapi__cookie": "^10.1.3", + "@types/hapi__h2o2": "^8.3.3", + "@types/hapi__hapi": "^20.0.9", + "@types/hapi__inert": "^5.2.3", "@types/has-ansi": "^3.0.0", "@types/he": "^1.1.1", "@types/history": "^4.7.3", @@ -575,7 +574,7 @@ "@types/nock": "^10.0.3", "@types/node": "14.14.44", "@types/node-fetch": "^2.5.7", - "@types/node-forge": "^0.10.4", + "@types/node-forge": "^0.10.5", "@types/nodemailer": "^6.4.0", "@types/normalize-path": "^3.0.0", "@types/object-hash": "^1.3.0", @@ -666,7 +665,7 @@ "callsites": "^3.1.0", "chai": "3.5.0", "chance": "1.0.18", - "chromedriver": "^92.0.1", + "chromedriver": "^93.0.1", "clean-webpack-plugin": "^3.0.0", "cmd-shim": "^2.1.0", "compression-webpack-plugin": "^4.0.0", diff --git a/packages/kbn-apm-config-loader/BUILD.bazel b/packages/kbn-apm-config-loader/BUILD.bazel index 81189171412b6..8fb513a746ec3 100644 --- a/packages/kbn-apm-config-loader/BUILD.bazel +++ b/packages/kbn-apm-config-loader/BUILD.bazel @@ -36,6 +36,7 @@ RUNTIME_DEPS = [ TYPES_DEPS = [ "//packages/elastic-safer-lodash-set", "//packages/kbn-utils", + "@npm//elastic-apm-node", "@npm//@types/jest", "@npm//@types/js-yaml", "@npm//@types/lodash", diff --git a/packages/kbn-apm-config-loader/src/config.test.ts b/packages/kbn-apm-config-loader/src/config.test.ts index e3cfbf43f841e..4e4dbf81740b3 100644 --- a/packages/kbn-apm-config-loader/src/config.test.ts +++ b/packages/kbn-apm-config-loader/src/config.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 type { Labels } from 'elastic-apm-node'; import { packageMock, mockedRootDir, @@ -46,7 +46,8 @@ describe('ApmConfiguration', () => { it('sets the git revision from `git rev-parse` command in non distribution mode', () => { gitRevExecMock.mockReturnValue('some-git-rev'); const config = new ApmConfiguration(mockedRootDir, {}, false); - expect(config.getConfig('serviceName').globalLabels?.git_rev).toBe('some-git-rev'); + const labels = config.getConfig('serviceName').globalLabels as Labels; + expect(labels.git_rev).toBe('some-git-rev'); }); it('sets the git revision from `pkg.build.sha` in distribution mode', () => { @@ -58,13 +59,15 @@ describe('ApmConfiguration', () => { }, }; const config = new ApmConfiguration(mockedRootDir, {}, true); - expect(config.getConfig('serviceName').globalLabels?.git_rev).toBe('distribution-sha'); + const labels = config.getConfig('serviceName').globalLabels as Labels; + expect(labels.git_rev).toBe('distribution-sha'); }); it('reads the kibana uuid from the uuid file', () => { readUuidFileMock.mockReturnValue('instance-uuid'); const config = new ApmConfiguration(mockedRootDir, {}, false); - expect(config.getConfig('serviceName').globalLabels?.kibana_uuid).toBe('instance-uuid'); + const labels = config.getConfig('serviceName').globalLabels as Labels; + expect(labels.kibana_uuid).toBe('instance-uuid'); }); it('uses the uuid from the kibana config if present', () => { @@ -75,7 +78,8 @@ describe('ApmConfiguration', () => { }, }; const config = new ApmConfiguration(mockedRootDir, kibanaConfig, false); - expect(config.getConfig('serviceName').globalLabels?.kibana_uuid).toBe('uuid-from-config'); + const labels = config.getConfig('serviceName').globalLabels as Labels; + expect(labels.kibana_uuid).toBe('uuid-from-config'); }); it('overrides metricsInterval, breakdownMetrics, captureHeaders, and captureBody when `isDistributable` is true', () => { diff --git a/packages/kbn-apm-config-loader/src/config.ts b/packages/kbn-apm-config-loader/src/config.ts index 1436d396aae59..ad2fd63f0fec4 100644 --- a/packages/kbn-apm-config-loader/src/config.ts +++ b/packages/kbn-apm-config-loader/src/config.ts @@ -12,17 +12,17 @@ import { execSync } from 'child_process'; // deep import to avoid loading the whole package import { getDataPath } from '@kbn/utils'; import { readFileSync } from 'fs'; -import { ApmAgentConfig } from './types'; +import type { AgentConfigOptions } from 'elastic-apm-node'; // https://www.elastic.co/guide/en/apm/agent/nodejs/current/configuration.html -const DEFAULT_CONFIG: ApmAgentConfig = { +const DEFAULT_CONFIG: AgentConfigOptions = { active: false, environment: 'development', logUncaughtExceptions: true, globalLabels: {}, }; -const CENTRALIZED_SERVICE_BASE_CONFIG: ApmAgentConfig = { +const CENTRALIZED_SERVICE_BASE_CONFIG: AgentConfigOptions = { serverUrl: 'https://38b80fbd79fb4c91bae06b4642d4d093.apm.us-east-1.aws.cloud.es.io', // The secretToken below is intended to be hardcoded in this file even though @@ -39,7 +39,7 @@ const CENTRALIZED_SERVICE_BASE_CONFIG: ApmAgentConfig = { breakdownMetrics: true, }; -const CENTRALIZED_SERVICE_DIST_CONFIG: ApmAgentConfig = { +const CENTRALIZED_SERVICE_DIST_CONFIG: AgentConfigOptions = { metricsInterval: '120s', captureBody: 'off', captureHeaders: false, @@ -47,7 +47,7 @@ const CENTRALIZED_SERVICE_DIST_CONFIG: ApmAgentConfig = { }; export class ApmConfiguration { - private baseConfig?: ApmAgentConfig; + private baseConfig?: AgentConfigOptions; private kibanaVersion: string; private pkgBuild: Record; @@ -62,7 +62,7 @@ export class ApmConfiguration { this.pkgBuild = build; } - public getConfig(serviceName: string): ApmAgentConfig { + public getConfig(serviceName: string): AgentConfigOptions { return { ...this.getBaseConfig(), serviceName, @@ -107,8 +107,8 @@ export class ApmConfiguration { /** * Override some config values when specific environment variables are used */ - private getConfigFromEnv(): ApmAgentConfig { - const config: ApmAgentConfig = {}; + private getConfigFromEnv(): AgentConfigOptions { + const config: AgentConfigOptions = {}; if (process.env.ELASTIC_APM_ACTIVE === 'true') { config.active = true; @@ -142,7 +142,7 @@ export class ApmConfiguration { * Get the elastic.apm configuration from the --config file, supersedes the * default config. */ - private getConfigFromKibanaConfig(): ApmAgentConfig { + private getConfigFromKibanaConfig(): AgentConfigOptions { return this.rawKibanaConfig?.elastic?.apm ?? {}; } @@ -150,7 +150,7 @@ export class ApmConfiguration { * Get the configuration from the apm.dev.js file, supersedes config * from the --config file, disabled when running the distributable */ - private getDevConfig(): ApmAgentConfig { + private getDevConfig(): AgentConfigOptions { if (this.isDistributable) { return {}; } @@ -167,7 +167,7 @@ export class ApmConfiguration { * Determine the Kibana UUID, initialized the value of `globalLabels.kibana_uuid` * when the UUID can be determined. */ - private getUuidConfig(): ApmAgentConfig { + private getUuidConfig(): AgentConfigOptions { // try to access the `server.uuid` value from the config file first. // if not manually defined, we will then read the value from the `{DATA_FOLDER}/uuid` file. // note that as the file is created by the platform AFTER apm init, the file @@ -207,7 +207,7 @@ export class ApmConfiguration { * When running Kibana with ELASTIC_APM_ENVIRONMENT=ci we attempt to grab * some environment variables we populate in CI related to the build under test */ - private getCiConfig(): ApmAgentConfig { + private getCiConfig(): AgentConfigOptions { if (process.env.ELASTIC_APM_ENVIRONMENT !== 'ci') { return {}; } diff --git a/packages/kbn-apm-config-loader/src/config_loader.ts b/packages/kbn-apm-config-loader/src/config_loader.ts index 75f69481da76a..be0af8deceb87 100644 --- a/packages/kbn-apm-config-loader/src/config_loader.ts +++ b/packages/kbn-apm-config-loader/src/config_loader.ts @@ -5,10 +5,9 @@ * in compliance with, at your election, the Elastic License 2.0 or the Server * Side Public License, v 1. */ - +import type { AgentConfigOptions } from 'elastic-apm-node'; import { getConfigurationFilePaths, getConfigFromFiles, applyConfigOverrides } from './utils'; import { ApmConfiguration } from './config'; -import { ApmAgentConfig } from './types'; let apmConfig: ApmConfiguration | undefined; @@ -32,7 +31,7 @@ export const loadConfiguration = ( return apmConfig; }; -export const getConfiguration = (serviceName: string): ApmAgentConfig | undefined => { +export const getConfiguration = (serviceName: string): AgentConfigOptions | undefined => { // integration test runner starts a kibana server that import the module without initializing APM. // so we need to check initialization of the config. // note that we can't just load the configuration during this module's import diff --git a/packages/kbn-apm-config-loader/src/index.ts b/packages/kbn-apm-config-loader/src/index.ts index da42bfad1841a..b16f6dcfd418f 100644 --- a/packages/kbn-apm-config-loader/src/index.ts +++ b/packages/kbn-apm-config-loader/src/index.ts @@ -9,4 +9,3 @@ export { getConfiguration } from './config_loader'; export { initApm } from './init_apm'; export type { ApmConfiguration } from './config'; -export type { ApmAgentConfig } from './types'; diff --git a/packages/kbn-apm-config-loader/src/types.ts b/packages/kbn-apm-config-loader/src/types.ts deleted file mode 100644 index 2e80631638e4c..0000000000000 --- a/packages/kbn-apm-config-loader/src/types.ts +++ /dev/null @@ -1,29 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 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. - */ - -// There is an (incomplete) `AgentConfigOptions` type declared in node_modules/elastic-apm-node/index.d.ts -// but it's not exported, and using ts tricks to retrieve the type via Parameters[0] -// causes errors in the generated .d.ts file because of esModuleInterop and the fact that the apm module -// is just exporting an instance of the `ApmAgent` type. -export interface ApmAgentConfig { - active?: boolean; - environment?: string; - serviceName?: string; - serviceVersion?: string; - serverUrl?: string; - secretToken?: string; - logUncaughtExceptions?: boolean; - globalLabels?: Record; - centralConfig?: boolean; - metricsInterval?: string; - captureSpanStackTraces?: boolean; - transactionSampleRate?: number; - breakdownMetrics?: boolean; - captureHeaders?: boolean; - captureBody?: 'off' | 'all' | 'errors' | 'transactions'; -} diff --git a/packages/kbn-cli-dev-mode/src/base_path_proxy_server.ts b/packages/kbn-cli-dev-mode/src/base_path_proxy_server.ts index 6d12d5d05f07c..b198e6139d5d7 100644 --- a/packages/kbn-cli-dev-mode/src/base_path_proxy_server.ts +++ b/packages/kbn-cli-dev-mode/src/base_path_proxy_server.ts @@ -131,7 +131,7 @@ export class BasePathProxyServer { agent: this.httpsAgent, passThrough: true, xforward: true, - mapUri: async (request) => { + mapUri: async (request: Request) => { return { // Passing in this header to merge it is a workaround until this is fixed: // https://github.com/hapijs/h2o2/issues/124 diff --git a/packages/kbn-config/src/config_service.test.ts b/packages/kbn-config/src/config_service.test.ts index aa520e7189e54..754de1c0a99f5 100644 --- a/packages/kbn-config/src/config_service.test.ts +++ b/packages/kbn-config/src/config_service.test.ts @@ -364,6 +364,37 @@ test('read "enabled" even if its schema is not present', async () => { expect(isEnabled).toBe(true); }); +test('logs deprecation if schema is not present and "enabled" is used', async () => { + const initialConfig = { + foo: { + enabled: true, + }, + }; + + const rawConfigProvider = rawConfigServiceMock.create({ rawConfig: initialConfig }); + const configService = new ConfigService(rawConfigProvider, defaultEnv, logger); + + await configService.isEnabledAtPath('foo'); + expect(configService.getHandledDeprecatedConfigs()).toMatchInlineSnapshot(` + Array [ + Array [ + "foo", + Array [ + Object { + "correctiveActions": Object { + "manualSteps": Array [ + "Remove \\"foo.enabled\\" from the Kibana config file, CLI flag, or environment variable (in Docker only) before upgrading to 8.0.0.", + ], + }, + "message": "Configuring \\"foo.enabled\\" is deprecated and will be removed in 8.0.0.", + "title": "Setting \\"foo.enabled\\" is deprecated", + }, + ], + ], + ] + `); +}); + test('allows plugins to specify "enabled" flag via validation schema', async () => { const initialConfig = {}; diff --git a/packages/kbn-config/src/config_service.ts b/packages/kbn-config/src/config_service.ts index 514992891ad1b..5883ce8ab513c 100644 --- a/packages/kbn-config/src/config_service.ts +++ b/packages/kbn-config/src/config_service.ts @@ -177,6 +177,23 @@ export class ConfigService { // if plugin hasn't got a config schema, we try to read "enabled" directly const isEnabled = validatedConfig?.enabled ?? config.get(enabledPath); + // if we implicitly added an `enabled` config to a plugin without a schema, + // we log a deprecation warning, as this will not be supported in 8.0 + if (validatedConfig?.enabled === undefined && isEnabled !== undefined) { + const deprecationPath = pathToString(enabledPath); + const deprecatedConfigDetails: DeprecatedConfigDetails = { + title: `Setting "${deprecationPath}" is deprecated`, + message: `Configuring "${deprecationPath}" is deprecated and will be removed in 8.0.0.`, + correctiveActions: { + manualSteps: [ + `Remove "${deprecationPath}" from the Kibana config file, CLI flag, or environment variable (in Docker only) before upgrading to 8.0.0.`, + ], + }, + }; + this.deprecationLog.warn(deprecatedConfigDetails.message); + this.markDeprecatedConfigAsHandled(namespace, deprecatedConfigDetails); + } + // not declared. consider that plugin is enabled by default if (isEnabled === undefined) { return true; @@ -220,9 +237,7 @@ export class ConfigService { if (!context.silent) { deprecationMessages.push(context.message); } - const handledDeprecatedConfig = this.handledDeprecatedConfigs.get(domainId) || []; - handledDeprecatedConfig.push(context); - this.handledDeprecatedConfigs.set(domainId, handledDeprecatedConfig); + this.markDeprecatedConfigAsHandled(domainId, context); }; applyDeprecations(rawConfig, deprecations, createAddDeprecation); @@ -260,6 +275,12 @@ export class ConfigService { this.log.debug(`Marking config path as handled: ${path}`); this.handledPaths.add(path); } + + private markDeprecatedConfigAsHandled(domainId: string, config: DeprecatedConfigDetails) { + const handledDeprecatedConfig = this.handledDeprecatedConfigs.get(domainId) || []; + handledDeprecatedConfig.push(config); + this.handledDeprecatedConfigs.set(domainId, handledDeprecatedConfig); + } } const createPluginEnabledPath = (configPath: string | string[]) => { diff --git a/packages/kbn-config/src/deprecation/deprecation_factory.test.ts b/packages/kbn-config/src/deprecation/deprecation_factory.test.ts index 0a605cbc1c532..dfd6b8fac681f 100644 --- a/packages/kbn-config/src/deprecation/deprecation_factory.test.ts +++ b/packages/kbn-config/src/deprecation/deprecation_factory.test.ts @@ -10,7 +10,8 @@ import { DeprecatedConfigDetails } from './types'; import { configDeprecationFactory } from './deprecation_factory'; describe('DeprecationFactory', () => { - const { rename, unused, renameFromRoot, unusedFromRoot } = configDeprecationFactory; + const { deprecate, deprecateFromRoot, rename, renameFromRoot, unused, unusedFromRoot } = + configDeprecationFactory; const addDeprecation = jest.fn(); @@ -18,6 +19,139 @@ describe('DeprecationFactory', () => { addDeprecation.mockClear(); }); + describe('deprecate', () => { + it('logs a warning when property is present', () => { + const rawConfig = { + myplugin: { + deprecated: 'deprecated', + valid: 'valid', + }, + someOtherPlugin: { + property: 'value', + }, + }; + const commands = deprecate('deprecated', '8.0.0')(rawConfig, 'myplugin', addDeprecation); + expect(commands).toBeUndefined(); + expect(addDeprecation.mock.calls).toMatchInlineSnapshot(` + Array [ + Array [ + Object { + "correctiveActions": Object { + "manualSteps": Array [ + "Remove \\"myplugin.deprecated\\" from the Kibana config file, CLI flag, or environment variable (in Docker only) before upgrading to 8.0.0.", + ], + }, + "message": "Configuring \\"myplugin.deprecated\\" is deprecated and will be removed in 8.0.0.", + "title": "Setting \\"myplugin.deprecated\\" is deprecated", + }, + ], + ] + `); + }); + + it('handles deeply nested keys', () => { + const rawConfig = { + myplugin: { + section: { + deprecated: 'deprecated', + }, + valid: 'valid', + }, + someOtherPlugin: { + property: 'value', + }, + }; + const commands = deprecate('section.deprecated', '8.0.0')( + rawConfig, + 'myplugin', + addDeprecation + ); + expect(commands).toBeUndefined(); + expect(addDeprecation.mock.calls).toMatchInlineSnapshot(` + Array [ + Array [ + Object { + "correctiveActions": Object { + "manualSteps": Array [ + "Remove \\"myplugin.section.deprecated\\" from the Kibana config file, CLI flag, or environment variable (in Docker only) before upgrading to 8.0.0.", + ], + }, + "message": "Configuring \\"myplugin.section.deprecated\\" is deprecated and will be removed in 8.0.0.", + "title": "Setting \\"myplugin.section.deprecated\\" is deprecated", + }, + ], + ] + `); + }); + + it('does not log if unused property is not present', () => { + const rawConfig = { + myplugin: { + valid: 'valid', + }, + someOtherPlugin: { + property: 'value', + }, + }; + const commands = deprecate('deprecated', '8.0.0')(rawConfig, 'myplugin', addDeprecation); + expect(commands).toBeUndefined(); + expect(addDeprecation).toBeCalledTimes(0); + }); + }); + + describe('deprecateFromRoot', () => { + it('logs a warning when property is present', () => { + const rawConfig = { + myplugin: { + deprecated: 'deprecated', + valid: 'valid', + }, + someOtherPlugin: { + property: 'value', + }, + }; + const commands = deprecateFromRoot('myplugin.deprecated', '8.0.0')( + rawConfig, + 'does-not-matter', + addDeprecation + ); + expect(commands).toBeUndefined(); + expect(addDeprecation.mock.calls).toMatchInlineSnapshot(` + Array [ + Array [ + Object { + "correctiveActions": Object { + "manualSteps": Array [ + "Remove \\"myplugin.deprecated\\" from the Kibana config file, CLI flag, or environment variable (in Docker only) before upgrading to 8.0.0.", + ], + }, + "message": "Configuring \\"myplugin.deprecated\\" is deprecated and will be removed in 8.0.0.", + "title": "Setting \\"myplugin.deprecated\\" is deprecated", + }, + ], + ] + `); + }); + + it('does not log if unused property is not present', () => { + const rawConfig = { + myplugin: { + valid: 'valid', + }, + someOtherPlugin: { + property: 'value', + }, + }; + const commands = deprecateFromRoot('myplugin.deprecated', '8.0.0')( + rawConfig, + 'does-not-matter', + addDeprecation + ); + expect(commands).toBeUndefined(); + expect(addDeprecation).toBeCalledTimes(0); + }); + }); + describe('rename', () => { it('moves the property to rename and logs a warning if old property exist and new one does not', () => { const rawConfig = { @@ -132,7 +266,7 @@ describe('DeprecationFactory', () => { "Remove \\"myplugin.deprecated\\" from the config.", ], }, - "message": "Setting \\"$myplugin.deprecated\\" has been replaced by \\"$myplugin.renamed\\". However, both keys are present. Ignoring \\"$myplugin.deprecated\\"", + "message": "Setting \\"myplugin.deprecated\\" has been replaced by \\"myplugin.renamed\\". However, both keys are present. Ignoring \\"myplugin.deprecated\\"", "title": "Setting \\"myplugin.deprecated\\" is deprecated", }, ], @@ -269,7 +403,7 @@ describe('DeprecationFactory', () => { "Remove \\"myplugin.deprecated\\" from the config.", ], }, - "message": "Setting \\"$myplugin.deprecated\\" has been replaced by \\"$myplugin.renamed\\". However, both keys are present. Ignoring \\"$myplugin.deprecated\\"", + "message": "Setting \\"myplugin.deprecated\\" has been replaced by \\"myplugin.renamed\\". However, both keys are present. Ignoring \\"myplugin.deprecated\\"", "title": "Setting \\"myplugin.deprecated\\" is deprecated", }, ], diff --git a/packages/kbn-config/src/deprecation/deprecation_factory.ts b/packages/kbn-config/src/deprecation/deprecation_factory.ts index 119b9b11237dc..1d61733715bd9 100644 --- a/packages/kbn-config/src/deprecation/deprecation_factory.ts +++ b/packages/kbn-config/src/deprecation/deprecation_factory.ts @@ -24,6 +24,37 @@ const getDeprecationTitle = (deprecationPath: string) => { }); }; +const _deprecate = ( + config: Record, + rootPath: string, + addDeprecation: AddConfigDeprecation, + deprecatedKey: string, + removeBy: string, + details?: Partial +): void => { + const fullPath = getPath(rootPath, deprecatedKey); + if (get(config, fullPath) === undefined) { + return; + } + addDeprecation({ + title: getDeprecationTitle(fullPath), + message: i18n.translate('kbnConfig.deprecations.deprecatedSettingMessage', { + defaultMessage: 'Configuring "{fullPath}" is deprecated and will be removed in {removeBy}.', + values: { fullPath, removeBy }, + }), + correctiveActions: { + manualSteps: [ + i18n.translate('kbnConfig.deprecations.deprecatedSetting.manualStepOneMessage', { + defaultMessage: + 'Remove "{fullPath}" from the Kibana config file, CLI flag, or environment variable (in Docker only) before upgrading to {removeBy}.', + values: { fullPath, removeBy }, + }), + ], + }, + ...details, + }); +}; + const _rename = ( config: Record, rootPath: string, @@ -67,7 +98,7 @@ const _rename = ( title: getDeprecationTitle(fullOldPath), message: i18n.translate('kbnConfig.deprecations.conflictSettingMessage', { defaultMessage: - 'Setting "${fullOldPath}" has been replaced by "${fullNewPath}". However, both keys are present. Ignoring "${fullOldPath}"', + 'Setting "{fullOldPath}" has been replaced by "{fullNewPath}". However, both keys are present. Ignoring "{fullOldPath}"', values: { fullOldPath, fullNewPath }, }), correctiveActions: { @@ -125,6 +156,24 @@ const _unused = ( }; }; +const deprecate = + ( + unusedKey: string, + removeBy: string, + details?: Partial + ): ConfigDeprecation => + (config, rootPath, addDeprecation) => + _deprecate(config, rootPath, addDeprecation, unusedKey, removeBy, details); + +const deprecateFromRoot = + ( + unusedKey: string, + removeBy: string, + details?: Partial + ): ConfigDeprecation => + (config, rootPath, addDeprecation) => + _deprecate(config, '', addDeprecation, unusedKey, removeBy, details); + const rename = (oldKey: string, newKey: string, details?: Partial): ConfigDeprecation => (config, rootPath, addDeprecation) => @@ -154,6 +203,8 @@ const getPath = (rootPath: string, subPath: string) => * @internal */ export const configDeprecationFactory: ConfigDeprecationFactory = { + deprecate, + deprecateFromRoot, rename, renameFromRoot, unused, diff --git a/packages/kbn-config/src/deprecation/types.ts b/packages/kbn-config/src/deprecation/types.ts index 0e1f36121e50e..47a31b9e6725a 100644 --- a/packages/kbn-config/src/deprecation/types.ts +++ b/packages/kbn-config/src/deprecation/types.ts @@ -91,6 +91,7 @@ export interface ConfigDeprecationCommand { * @example * ```typescript * const provider: ConfigDeprecationProvider = ({ rename, unused }) => [ + * deprecate('deprecatedKey', '8.0.0'), * rename('oldKey', 'newKey'), * unused('deprecatedKey'), * (config, path) => ({ unset: [{ key: 'path.to.key' }] }) @@ -119,6 +120,43 @@ export type ConfigDeprecationProvider = (factory: ConfigDeprecationFactory) => C */ export interface ConfigDeprecationFactory { + /** + * Deprecate a configuration property from inside a plugin's configuration path. + * Will log a deprecation warning if the deprecatedKey was found. + * + * @example + * Log a deprecation warning indicating 'myplugin.deprecatedKey' should be removed by `8.0.0` + * ```typescript + * const provider: ConfigDeprecationProvider = ({ deprecate }) => [ + * deprecate('deprecatedKey', '8.0.0'), + * ] + * ``` + */ + deprecate( + deprecatedKey: string, + removeBy: string, + details?: Partial + ): ConfigDeprecation; + /** + * Deprecate a configuration property from the root configuration. + * Will log a deprecation warning if the deprecatedKey was found. + * + * This should be only used when deprecating properties from different configuration's path. + * To deprecate properties from inside a plugin's configuration, use 'deprecate' instead. + * + * @example + * Log a deprecation warning indicating 'myplugin.deprecatedKey' should be removed by `8.0.0` + * ```typescript + * const provider: ConfigDeprecationProvider = ({ deprecate }) => [ + * deprecateFromRoot('deprecatedKey', '8.0.0'), + * ] + * ``` + */ + deprecateFromRoot( + deprecatedKey: string, + removeBy: string, + details?: Partial + ): ConfigDeprecation; /** * Rename a configuration property from inside a plugin's configuration path. * Will log a deprecation warning if the oldKey was found and deprecation applied. diff --git a/packages/kbn-legacy-logging/BUILD.bazel b/packages/kbn-legacy-logging/BUILD.bazel index 1148cf1d38b65..c4927fe076e15 100644 --- a/packages/kbn-legacy-logging/BUILD.bazel +++ b/packages/kbn-legacy-logging/BUILD.bazel @@ -29,6 +29,7 @@ RUNTIME_DEPS = [ "//packages/kbn-utils", "@npm//@elastic/numeral", "@npm//@hapi/hapi", + "@npm//@hapi/podium", "@npm//chokidar", "@npm//lodash", "@npm//moment-timezone", @@ -41,12 +42,12 @@ TYPES_DEPS = [ "//packages/kbn-config-schema", "//packages/kbn-utils", "@npm//@elastic/numeral", + "@npm//@hapi/podium", "@npm//chokidar", "@npm//query-string", "@npm//rxjs", "@npm//tslib", "@npm//@types/hapi__hapi", - "@npm//@types/hapi__podium", "@npm//@types/jest", "@npm//@types/lodash", "@npm//@types/moment-timezone", diff --git a/packages/kbn-legacy-logging/src/legacy_logging_server.ts b/packages/kbn-legacy-logging/src/legacy_logging_server.ts index c02eb2803515a..f6c42dd1b161f 100644 --- a/packages/kbn-legacy-logging/src/legacy_logging_server.ts +++ b/packages/kbn-legacy-logging/src/legacy_logging_server.ts @@ -112,7 +112,6 @@ export class LegacyLoggingServer { tags: [getLegacyLogLevel(level), ...context.split('.'), ...tags], timestamp: timestamp.getTime(), }) - // @ts-expect-error @hapi/podium emit is actually an async function .catch((err) => { // eslint-disable-next-line no-console console.error('An unexpected error occurred while writing to the log:', err.stack); diff --git a/packages/kbn-legacy-logging/src/setup_logging.ts b/packages/kbn-legacy-logging/src/setup_logging.ts index 800ed2e523274..a045469e81251 100644 --- a/packages/kbn-legacy-logging/src/setup_logging.ts +++ b/packages/kbn-legacy-logging/src/setup_logging.ts @@ -23,7 +23,7 @@ export async function setupLogging( // thrown every time we start the server. // In order to keep using the legacy logger until we remove it I'm just adding // a new hard limit here. - process.stdout.setMaxListeners(40); + process.stdout.setMaxListeners(60); return await server.register({ plugin: good, diff --git a/packages/kbn-storybook/templates/index.ejs b/packages/kbn-storybook/templates/index.ejs index 1fca20b1ff8e8..3473f73ca131f 100644 --- a/packages/kbn-storybook/templates/index.ejs +++ b/packages/kbn-storybook/templates/index.ejs @@ -16,12 +16,11 @@ - - + - + diff --git a/packages/kbn-test/src/failed_tests_reporter/run_failed_tests_reporter_cli.ts b/packages/kbn-test/src/failed_tests_reporter/run_failed_tests_reporter_cli.ts index d4bf625ee2d3b..0129614fe658d 100644 --- a/packages/kbn-test/src/failed_tests_reporter/run_failed_tests_reporter_cli.ts +++ b/packages/kbn-test/src/failed_tests_reporter/run_failed_tests_reporter_cli.ts @@ -37,17 +37,24 @@ export function runFailedTestsReporterCli() { } if (updateGithub) { - let branch: string | undefined = ''; + let branch: string = ''; let isPr = false; if (process.env.BUILDKITE === 'true') { - branch = process.env.BUILDKITE_BRANCH; + branch = process.env.BUILDKITE_BRANCH || ''; isPr = process.env.BUILDKITE_PULL_REQUEST === 'true'; + updateGithub = process.env.REPORT_FAILED_TESTS_TO_GITHUB === 'true'; } else { // JOB_NAME is formatted as `elastic+kibana+7.x` in some places and `elastic+kibana+7.x/JOB=kibana-intake,node=immutable` in others const jobNameSplit = (process.env.JOB_NAME || '').split(/\+|\//); - branch = jobNameSplit.length >= 3 ? jobNameSplit[2] : process.env.GIT_BRANCH; + branch = jobNameSplit.length >= 3 ? jobNameSplit[2] : process.env.GIT_BRANCH || ''; isPr = !!process.env.ghprbPullId; + + const isMasterOrVersion = branch === 'master' || branch.match(/^\d+\.(x|\d+)$/); + if (!isMasterOrVersion || isPr) { + log.info('Failure issues only created on master/version branch jobs'); + updateGithub = false; + } } if (!branch) { @@ -55,12 +62,6 @@ export function runFailedTestsReporterCli() { 'Unable to determine originating branch from job name or other environment variables' ); } - - const isMasterOrVersion = branch === 'master' || branch.match(/^\d+\.(x|\d+)$/); - if (!isMasterOrVersion || isPr) { - log.info('Failure issues only created on master/version branch jobs'); - updateGithub = false; - } } const githubApi = new GithubApi({ 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 61ba8eb157ee3..9837d45ddd869 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 @@ -33,6 +33,28 @@ describe('createRouter', () => { }, }, children: [ + { + path: '/services/{serviceName}/errors', + element: <>, + params: t.type({ + path: t.type({ + serviceName: t.string, + }), + }), + children: [ + { + path: '/services/{serviceName}/errors/{groupId}', + element: <>, + params: t.type({ + path: t.type({ groupId: t.string }), + }), + }, + { + path: '/services/{serviceName}/errors', + element: <>, + }, + ], + }, { path: '/services', element: <>, @@ -43,7 +65,7 @@ describe('createRouter', () => { }), }, { - path: '/services', + path: '/services/{serviceName}', element: <>, children: [ { @@ -252,6 +274,28 @@ describe('createRouter', () => { }, }); }); + + it('matches deep routes', () => { + history.push('/services/opbeans-java/errors/foo?rangeFrom=now-15m&rangeTo=now'); + + const matchedRoutes = router.matchRoutes( + '/services/{serviceName}/errors/{groupId}', + history.location + ); + + expect(matchedRoutes.length).toEqual(4); + + expect(matchedRoutes[matchedRoutes.length - 1].match).toEqual({ + isExact: true, + params: { + path: { + groupId: 'foo', + }, + }, + path: '/services/:serviceName/errors/:groupId', + url: '/services/opbeans-java/errors/foo', + }); + }); }); describe('link', () => { 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 7f2ac818fc9b9..13f09e7546de5 100644 --- a/packages/kbn-typed-react-router-config/src/create_router.ts +++ b/packages/kbn-typed-react-router-config/src/create_router.ts @@ -26,7 +26,7 @@ const deepExactRt: typeof deepExactRtTyped = deepExactRtNonTyped; const mergeRt: typeof mergeRtTyped = mergeRtNonTyped; function toReactRouterPath(path: string) { - return path.replace(/(?:{([^\/]+)})/, ':$1'); + return path.replace(/(?:{([^\/]+)})/g, ':$1'); } export function createRouter(routes: TRoutes): Router { diff --git a/src/core/public/application/application_service.test.ts b/src/core/public/application/application_service.test.ts index de9e4d4496f3b..f348936d26795 100644 --- a/src/core/public/application/application_service.test.ts +++ b/src/core/public/application/application_service.test.ts @@ -804,6 +804,21 @@ describe('#start()', () => { `); }); + it("when openInNewTab is true it doesn't update currentApp$ after mounting", async () => { + service.setup(setupDeps); + + const { currentAppId$, navigateToApp } = await service.start(startDeps); + const stop$ = new Subject(); + const promise = currentAppId$.pipe(bufferCount(4), takeUntil(stop$)).toPromise(); + + await navigateToApp('delta', { openInNewTab: true }); + stop$.next(); + + const appIds = await promise; + + expect(appIds).toBeUndefined(); + }); + it('updates httpLoadingCount$ while mounting', async () => { // Use a memory history so that mounting the component will work const { createMemoryHistory } = jest.requireActual('history'); diff --git a/src/core/public/application/application_service.tsx b/src/core/public/application/application_service.tsx index 2e804bf2f5413..3ba0d78cf15fd 100644 --- a/src/core/public/application/application_service.tsx +++ b/src/core/public/application/application_service.tsx @@ -250,16 +250,15 @@ export class ApplicationService { if (path === undefined) { path = applications$.value.get(appId)?.defaultPath; } - if (!navigatingToSameApp) { - this.appInternalStates.delete(this.currentAppId$.value!); - } if (openInNewTab) { this.openInNewTab!(getAppUrl(availableMounters, appId, path)); } else { + if (!navigatingToSameApp) { + this.appInternalStates.delete(this.currentAppId$.value!); + } this.navigate!(getAppUrl(availableMounters, appId, path), state, replace); + this.currentAppId$.next(appId); } - - this.currentAppId$.next(appId); } }; diff --git a/src/core/public/doc_links/doc_links_service.ts b/src/core/public/doc_links/doc_links_service.ts index b31166925f8b4..771b53994ece9 100644 --- a/src/core/public/doc_links/doc_links_service.ts +++ b/src/core/public/doc_links/doc_links_service.ts @@ -157,8 +157,20 @@ export class DocLinksService { rollupJobs: `${KIBANA_DOCS}data-rollups.html`, elasticsearch: { docsBase: `${ELASTICSEARCH_DOCS}`, + apiCompatibilityHeader: `${ELASTICSEARCH_DOCS}api-conventions.html#api-compatibility`, asyncSearch: `${ELASTICSEARCH_DOCS}async-search-intro.html`, dataStreams: `${ELASTICSEARCH_DOCS}data-streams.html`, + deprecationLogging: `${ELASTICSEARCH_DOCS}logging.html#deprecation-logging`, + ilm: `${ELASTICSEARCH_DOCS}index-lifecycle-management.html`, + ilmForceMerge: `${ELASTICSEARCH_DOCS}ilm-forcemerge.html`, + ilmFreeze: `${ELASTICSEARCH_DOCS}ilm-freeze.html`, + ilmPhaseTransitions: `${ELASTICSEARCH_DOCS}ilm-index-lifecycle.html#ilm-phase-transitions`, + ilmReadOnly: `${ELASTICSEARCH_DOCS}ilm-readonly.html`, + ilmRollover: `${ELASTICSEARCH_DOCS}ilm-rollover.html`, + ilmSearchableSnapshot: `${ELASTICSEARCH_DOCS}ilm-searchable-snapshot.html`, + ilmSetPriority: `${ELASTICSEARCH_DOCS}ilm-set-priority.html`, + ilmShrink: `${ELASTICSEARCH_DOCS}ilm-shrink.html`, + ilmWaitForSnapshot: `${ELASTICSEARCH_DOCS}ilm-wait-for-snapshot.html`, indexModules: `${ELASTICSEARCH_DOCS}index-modules.html`, indexSettings: `${ELASTICSEARCH_DOCS}index-modules.html#index-modules-settings`, indexTemplates: `${ELASTICSEARCH_DOCS}index-templates.html`, @@ -200,16 +212,17 @@ export class DocLinksService { mappingStore: `${ELASTICSEARCH_DOCS}mapping-store.html`, mappingTermVector: `${ELASTICSEARCH_DOCS}term-vector.html`, mappingTypesRemoval: `${ELASTICSEARCH_DOCS}removal-of-types.html`, + migrateIndexAllocationFilters: `${ELASTICSEARCH_DOCS}migrate-index-allocation-filters.html`, nodeRoles: `${ELASTICSEARCH_DOCS}modules-node.html#node-roles`, - remoteClusters: `${ELASTICSEARCH_DOCS}modules-remote-clusters.html`, - remoteClustersProxy: `${ELASTICSEARCH_DOCS}modules-remote-clusters.html#proxy-mode`, - remoteClusersProxySettings: `${ELASTICSEARCH_DOCS}modules-remote-clusters.html#remote-cluster-proxy-settings`, + releaseHighlights: `${ELASTICSEARCH_DOCS}release-highlights.html`, + remoteClusters: `${ELASTICSEARCH_DOCS}remote-clusters.html`, + 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`, - deprecationLogging: `${ELASTICSEARCH_DOCS}logging.html#deprecation-logging`, - setupUpgrade: `${ELASTICSEARCH_DOCS}setup-upgrade.html`, - releaseHighlights: `${ELASTICSEARCH_DOCS}release-highlights.html`, }, siem: { guide: `${ELASTIC_WEBSITE_URL}guide/en/security/${DOC_LINK_VERSION}/index.html`, @@ -336,7 +349,7 @@ export class DocLinksService { elasticsearchSettings: `${ELASTICSEARCH_DOCS}security-settings.html`, elasticsearchEnableSecurity: `${ELASTICSEARCH_DOCS}configuring-stack-security.html`, indicesPrivileges: `${ELASTICSEARCH_DOCS}security-privileges.html#privileges-list-indices`, - kibanaTLS: `${KIBANA_DOCS}configuring-tls.html`, + kibanaTLS: `${ELASTICSEARCH_DOCS}security-basic-setup.html#encrypt-internode-communication`, kibanaPrivileges: `${KIBANA_DOCS}kibana-privileges.html`, mappingRoles: `${ELASTICSEARCH_DOCS}mapping-roles.html`, mappingRolesFieldRules: `${ELASTICSEARCH_DOCS}role-mapping-resources.html#mapping-roles-rule-field`, @@ -390,7 +403,7 @@ export class DocLinksService { }, snapshotRestore: { guide: `${KIBANA_DOCS}snapshot-repositories.html`, - changeIndexSettings: `${ELASTICSEARCH_DOCS}snapshots-restore-snapshot.html#change-index-settings-during-restore`, + changeIndexSettings: `${ELASTICSEARCH_DOCS}index-modules.html`, createSnapshot: `${ELASTICSEARCH_DOCS}snapshots-take-snapshot.html`, getSnapshot: `${ELASTICSEARCH_DOCS}get-snapshot-api.html`, registerSharedFileSystem: `${ELASTICSEARCH_DOCS}snapshots-register-repository.html#snapshots-filesystem-repository`, @@ -398,6 +411,7 @@ export class DocLinksService { registerUrl: `${ELASTICSEARCH_DOCS}snapshots-register-repository.html#snapshots-read-only-repository`, restoreSnapshot: `${ELASTICSEARCH_DOCS}snapshots-restore-snapshot.html`, restoreSnapshotApi: `${ELASTICSEARCH_DOCS}restore-snapshot-api.html#restore-snapshot-api-request-body`, + searchableSnapshotSharedCache: `${ELASTICSEARCH_DOCS}searchable-snapshots.html#searchable-snapshots-shared-cache`, }, ingest: { append: `${ELASTICSEARCH_DOCS}append-processor.html`, @@ -415,6 +429,7 @@ export class DocLinksService { fail: `${ELASTICSEARCH_DOCS}fail-processor.html`, foreach: `${ELASTICSEARCH_DOCS}foreach-processor.html`, geoIp: `${ELASTICSEARCH_DOCS}geoip-processor.html`, + geoMatch: `${ELASTICSEARCH_DOCS}geo-match-enrich-policy-type.html`, grok: `${ELASTICSEARCH_DOCS}grok-processor.html`, gsub: `${ELASTICSEARCH_DOCS}gsub-processor.html`, htmlString: `${ELASTICSEARCH_DOCS}htmlstrip-processor.html`, 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 index 240b41266abb6..27aae5968ba88 100644 --- a/src/core/server/saved_objects/migrations/core/migration_context.test.ts +++ b/src/core/server/saved_objects/migrations/core/migration_context.test.ts @@ -74,4 +74,18 @@ describe('disableUnknownTypeMappingFields', () => { }, }); }); + + 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 index d7f7aff45a470..96c47bcf38d0a 100644 --- a/src/core/server/saved_objects/migrations/core/migration_context.ts +++ b/src/core/server/saved_objects/migrations/core/migration_context.ts @@ -154,7 +154,7 @@ export function disableUnknownTypeMappingFields( ): IndexMapping { const targetTypes = Object.keys(activeMappings.properties); - const disabledTypesProperties = Object.keys(sourceMappings.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 diff --git a/src/core/server/saved_objects/migrationsv2/README.md b/src/core/server/saved_objects/migrationsv2/README.md index 5121e66052f40..a6b8e01a3dc6c 100644 --- a/src/core/server/saved_objects/migrationsv2/README.md +++ b/src/core/server/saved_objects/migrationsv2/README.md @@ -36,7 +36,7 @@ - [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_INDEX](#reindex_source_to_temp_index) + - [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) @@ -284,11 +284,11 @@ Read the next batch of outdated documents from the source index by using search ### New control state 1. If the batch contained > 0 documents - → `REINDEX_SOURCE_TO_TEMP_INDEX` + → `REINDEX_SOURCE_TO_TEMP_TRANSFORM` 2. If there are no more documents returned → `REINDEX_SOURCE_TO_TEMP_CLOSE_PIT` -## REINDEX_SOURCE_TO_TEMP_INDEX +## REINDEX_SOURCE_TO_TEMP_TRANSFORM ### Next action `transformRawDocs` @@ -357,7 +357,7 @@ 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_INDEX` step, we need to search for outdated documents +`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 diff --git a/src/core/server/saved_objects/migrationsv2/actions/integration_tests/actions.test.ts b/src/core/server/saved_objects/migrationsv2/actions/integration_tests/actions.test.ts index 936746ddc6930..32d12e13434aa 100644 --- a/src/core/server/saved_objects/migrationsv2/actions/integration_tests/actions.test.ts +++ b/src/core/server/saved_objects/migrationsv2/actions/integration_tests/actions.test.ts @@ -802,7 +802,9 @@ describe('migration actions', () => { } `); }); - it('resolves left wait_for_task_completion_timeout when the task does not finish within the timeout', async () => { + + // FLAKY https://github.com/elastic/kibana/issues/113012 + it.skip('resolves left wait_for_task_completion_timeout when the task does not finish within the timeout', async () => { const res = (await reindex({ client, sourceIndex: 'existing_index_with_docs', diff --git a/src/core/server/saved_objects/migrationsv2/integration_tests/migration_from_v1.test.ts b/src/core/server/saved_objects/migrationsv2/integration_tests/migration_from_older_v1.test.ts similarity index 64% rename from src/core/server/saved_objects/migrationsv2/integration_tests/migration_from_v1.test.ts rename to src/core/server/saved_objects/migrationsv2/integration_tests/migration_from_older_v1.test.ts index 0f336d7fba43a..8e01e11eaccfb 100644 --- a/src/core/server/saved_objects/migrationsv2/integration_tests/migration_from_v1.test.ts +++ b/src/core/server/saved_objects/migrationsv2/integration_tests/migration_from_older_v1.test.ts @@ -20,7 +20,7 @@ import { InternalCoreStart } from '../../../internal_types'; import { Root } from '../../../root'; const kibanaVersion = Env.createDefault(REPO_ROOT, getEnvOptions()).packageInfo.version; -const logFilePath = Path.join(__dirname, 'migration_from_v1.log'); +const logFilePath = Path.join(__dirname, 'migration_from_older_v1.log'); const asyncUnlink = Util.promisify(Fs.unlink); async function removeLogFile() { @@ -28,7 +28,35 @@ async function removeLogFile() { await asyncUnlink(logFilePath).catch(() => void 0); } -describe('migration v2', () => { +const assertMigratedDocuments = (arr: any[], target: any[]) => target.every((v) => arr.includes(v)); + +function sortByTypeAndId(a: { type: string; id: string }, b: { type: string; id: string }) { + return a.type.localeCompare(b.type) || a.id.localeCompare(b.id); +} + +async function fetchDocuments(esClient: ElasticsearchClient, index: string) { + const { body } = await esClient.search({ + index, + body: { + query: { + match_all: {}, + }, + _source: ['type', 'id'], + }, + }); + + return body.hits.hits + .map((h) => ({ + ...h._source, + id: h._id, + })) + .sort(sortByTypeAndId); +} + +describe('migrating from 7.3.0-xpack which used v1 migrations', () => { + const migratedIndex = `.kibana_${kibanaVersion}_001`; + const originalIndex = `.kibana_1`; // v1 migrations index + let esServer: kbnTestServer.TestElasticsearchUtils; let root: Root; let coreStart: InternalCoreStart; @@ -130,65 +158,50 @@ describe('migration v2', () => { await new Promise((resolve) => setTimeout(resolve, 10000)); }; - // FLAKY: https://github.com/elastic/kibana/issues/87968 - describe.skip('migrating from 7.3.0-xpack version', () => { - const migratedIndex = `.kibana_${kibanaVersion}_001`; - - beforeAll(async () => { - await removeLogFile(); - await startServers({ - oss: false, - dataArchive: Path.join(__dirname, 'archives', '7.3.0_xpack_sample_saved_objects.zip'), - }); + beforeAll(async () => { + await removeLogFile(); + await startServers({ + oss: false, + dataArchive: Path.join(__dirname, 'archives', '7.3.0_xpack_sample_saved_objects.zip'), }); + }); - afterAll(async () => { - await stopServers(); - }); + afterAll(async () => { + await stopServers(); + }); - it('creates the new index and the correct aliases', async () => { - const { body } = await esClient.indices.get( - { - index: migratedIndex, - }, - { ignore: [404] } - ); + it('creates the new index and the correct aliases', async () => { + const { body } = await esClient.indices.get( + { + index: migratedIndex, + }, + { ignore: [404] } + ); - const response = body[migratedIndex]; + const response = body[migratedIndex]; - expect(response).toBeDefined(); - expect(Object.keys(response.aliases!).sort()).toEqual([ - '.kibana', - `.kibana_${kibanaVersion}`, - ]); - }); + expect(response).toBeDefined(); + expect(Object.keys(response.aliases!).sort()).toEqual(['.kibana', `.kibana_${kibanaVersion}`]); + }); - it('copies all the document of the previous index to the new one', async () => { - const migratedIndexResponse = await esClient.count({ - index: migratedIndex, - }); - const oldIndexResponse = await esClient.count({ - index: '.kibana_1', - }); + it('copies all the document of the previous index to the new one', async () => { + const originalDocs = await fetchDocuments(esClient, originalIndex); + const migratedDocs = await fetchDocuments(esClient, migratedIndex); + expect(assertMigratedDocuments(migratedDocs, originalDocs)); + }); - // Use a >= comparison since once Kibana has started it might create new - // documents like telemetry tasks - expect(migratedIndexResponse.body.count).toBeGreaterThanOrEqual(oldIndexResponse.body.count); + it('migrates the documents to the highest version', async () => { + const expectedVersions = getExpectedVersionPerType(); + const res = await esClient.search({ + index: migratedIndex, + body: { + sort: ['_doc'], + }, + size: 10000, }); - - it('migrates the documents to the highest version', async () => { - const expectedVersions = getExpectedVersionPerType(); - const res = await esClient.search({ - index: migratedIndex, - body: { - sort: ['_doc'], - }, - size: 10000, - }); - const allDocuments = res.body.hits.hits as SavedObjectsRawDoc[]; - allDocuments.forEach((doc) => { - assertMigrationVersion(doc, expectedVersions); - }); + const allDocuments = res.body.hits.hits as SavedObjectsRawDoc[]; + allDocuments.forEach((doc) => { + assertMigrationVersion(doc, expectedVersions); }); }); }); diff --git a/src/core/server/saved_objects/migrationsv2/migrations_state_action_machine.ts b/src/core/server/saved_objects/migrationsv2/migrations_state_action_machine.ts index d4ad724911277..3a5e592a8b9bf 100644 --- a/src/core/server/saved_objects/migrationsv2/migrations_state_action_machine.ts +++ b/src/core/server/saved_objects/migrationsv2/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 { ReindexSourceToTempIndex, ReindexSourceToTempIndexBulk, State } from './types'; +import { ReindexSourceToTempTransform, ReindexSourceToTempIndexBulk, State } from './types'; import { SavedObjectsRawDoc } from '../serialization'; interface StateTransitionLogMeta extends LogMeta { @@ -115,7 +115,9 @@ export async function migrationStateActionMachine({ const redactedNewState = { ...newState, ...{ - outdatedDocuments: ((newState as ReindexSourceToTempIndex).outdatedDocuments ?? []).map( + outdatedDocuments: ( + (newState as ReindexSourceToTempTransform).outdatedDocuments ?? [] + ).map( (doc) => ({ _id: doc._id, diff --git a/src/core/server/saved_objects/migrationsv2/model/model.test.ts b/src/core/server/saved_objects/migrationsv2/model/model.test.ts index 033a18b488841..3e48a7147bffd 100644 --- a/src/core/server/saved_objects/migrationsv2/model/model.test.ts +++ b/src/core/server/saved_objects/migrationsv2/model/model.test.ts @@ -20,7 +20,7 @@ import type { ReindexSourceToTempOpenPit, ReindexSourceToTempRead, ReindexSourceToTempClosePit, - ReindexSourceToTempIndex, + ReindexSourceToTempTransform, RefreshTarget, UpdateTargetMappingsState, UpdateTargetMappingsWaitForTaskState, @@ -962,7 +962,7 @@ describe('migrations v2 model', () => { progress: createInitialProgress(), }; - it('REINDEX_SOURCE_TO_TEMP_READ -> REINDEX_SOURCE_TO_TEMP_INDEX if the index has outdated documents to reindex', () => { + it('REINDEX_SOURCE_TO_TEMP_READ -> REINDEX_SOURCE_TO_TEMP_TRANSFORM if the index has outdated documents to reindex', () => { const outdatedDocuments = [{ _id: '1', _source: { type: 'vis' } }]; const lastHitSortValue = [123456]; const res: ResponseType<'REINDEX_SOURCE_TO_TEMP_READ'> = Either.right({ @@ -970,8 +970,8 @@ describe('migrations v2 model', () => { lastHitSortValue, totalHits: 1, }); - const newState = model(state, res) as ReindexSourceToTempIndex; - expect(newState.controlState).toBe('REINDEX_SOURCE_TO_TEMP_INDEX'); + const newState = model(state, res) as ReindexSourceToTempTransform; + expect(newState.controlState).toBe('REINDEX_SOURCE_TO_TEMP_TRANSFORM'); expect(newState.outdatedDocuments).toBe(outdatedDocuments); expect(newState.lastHitSortValue).toBe(lastHitSortValue); expect(newState.progress.processed).toBe(undefined); @@ -1032,16 +1032,16 @@ describe('migrations v2 model', () => { it('REINDEX_SOURCE_TO_TEMP_CLOSE_PIT -> SET_TEMP_WRITE_BLOCK if action succeeded', () => { const res: ResponseType<'REINDEX_SOURCE_TO_TEMP_CLOSE_PIT'> = Either.right({}); - const newState = model(state, res) as ReindexSourceToTempIndex; + const newState = model(state, res) as ReindexSourceToTempTransform; expect(newState.controlState).toBe('SET_TEMP_WRITE_BLOCK'); expect(newState.sourceIndex).toEqual(state.sourceIndex); }); }); - describe('REINDEX_SOURCE_TO_TEMP_INDEX', () => { - const state: ReindexSourceToTempIndex = { + describe('REINDEX_SOURCE_TO_TEMP_TRANSFORM', () => { + const state: ReindexSourceToTempTransform = { ...baseState, - controlState: 'REINDEX_SOURCE_TO_TEMP_INDEX', + controlState: 'REINDEX_SOURCE_TO_TEMP_TRANSFORM', outdatedDocuments: [], versionIndexReadyActions: Option.none, sourceIndex: Option.some('.kibana') as Option.Some, @@ -1059,8 +1059,8 @@ describe('migrations v2 model', () => { }, ] as SavedObjectsRawDoc[]; - it('REINDEX_SOURCE_TO_TEMP_INDEX -> REINDEX_SOURCE_TO_TEMP_INDEX_BULK if action succeeded', () => { - const res: ResponseType<'REINDEX_SOURCE_TO_TEMP_INDEX'> = Either.right({ + it('REINDEX_SOURCE_TO_TEMP_TRANSFORM -> REINDEX_SOURCE_TO_TEMP_INDEX_BULK if action succeeded', () => { + const res: ResponseType<'REINDEX_SOURCE_TO_TEMP_TRANSFORM'> = Either.right({ processedDocs, }); const newState = model(state, res) as ReindexSourceToTempIndexBulk; @@ -1071,7 +1071,7 @@ describe('migrations v2 model', () => { }); it('increments the progress.processed counter', () => { - const res: ResponseType<'REINDEX_SOURCE_TO_TEMP_INDEX'> = Either.right({ + const res: ResponseType<'REINDEX_SOURCE_TO_TEMP_TRANSFORM'> = Either.right({ processedDocs, }); @@ -1089,8 +1089,8 @@ describe('migrations v2 model', () => { expect(newState.progress.processed).toBe(2); }); - it('REINDEX_SOURCE_TO_TEMP_INDEX -> REINDEX_SOURCE_TO_TEMP_READ if action succeeded but we have carried through previous failures', () => { - const res: ResponseType<'REINDEX_SOURCE_TO_TEMP_INDEX'> = Either.right({ + it('REINDEX_SOURCE_TO_TEMP_TRANSFORM -> REINDEX_SOURCE_TO_TEMP_READ if action succeeded but we have carried through previous failures', () => { + const res: ResponseType<'REINDEX_SOURCE_TO_TEMP_TRANSFORM'> = Either.right({ processedDocs, }); const testState = { @@ -1098,15 +1098,15 @@ describe('migrations v2 model', () => { corruptDocumentIds: ['a:b'], transformErrors: [], }; - const newState = model(testState, res) as ReindexSourceToTempIndex; + const newState = model(testState, res) as ReindexSourceToTempTransform; expect(newState.controlState).toEqual('REINDEX_SOURCE_TO_TEMP_READ'); expect(newState.corruptDocumentIds.length).toEqual(1); expect(newState.transformErrors.length).toEqual(0); expect(newState.progress.processed).toBe(0); }); - it('REINDEX_SOURCE_TO_TEMP_INDEX -> REINDEX_SOURCE_TO_TEMP_READ when response is left documents_transform_failed', () => { - const res: ResponseType<'REINDEX_SOURCE_TO_TEMP_INDEX'> = Either.left({ + it('REINDEX_SOURCE_TO_TEMP_TRANSFORM -> REINDEX_SOURCE_TO_TEMP_READ when response is left documents_transform_failed', () => { + const res: ResponseType<'REINDEX_SOURCE_TO_TEMP_TRANSFORM'> = Either.left({ type: 'documents_transform_failed', corruptDocumentIds: ['a:b'], transformErrors: [], diff --git a/src/core/server/saved_objects/migrationsv2/model/model.ts b/src/core/server/saved_objects/migrationsv2/model/model.ts index 8aa3d7b83b295..5d8862e48df1a 100644 --- a/src/core/server/saved_objects/migrationsv2/model/model.ts +++ b/src/core/server/saved_objects/migrationsv2/model/model.ts @@ -446,7 +446,7 @@ export const model = (currentState: State, resW: ResponseType): if (res.right.outdatedDocuments.length > 0) { return { ...stateP, - controlState: 'REINDEX_SOURCE_TO_TEMP_INDEX', + controlState: 'REINDEX_SOURCE_TO_TEMP_TRANSFORM', outdatedDocuments: res.right.outdatedDocuments, lastHitSortValue: res.right.lastHitSortValue, progress, @@ -489,11 +489,11 @@ export const model = (currentState: State, resW: ResponseType): } else { throwBadResponse(stateP, res); } - } else if (stateP.controlState === 'REINDEX_SOURCE_TO_TEMP_INDEX') { + } else if (stateP.controlState === 'REINDEX_SOURCE_TO_TEMP_TRANSFORM') { // We follow a similar control flow as for // outdated document search -> outdated document transform -> transform documents bulk index // collecting issues along the way rather than failing - // REINDEX_SOURCE_TO_TEMP_INDEX handles the document transforms + // REINDEX_SOURCE_TO_TEMP_TRANSFORM handles the document transforms const res = resW as ExcludeRetryableEsError>; // Increment the processed documents, no matter what the results are. diff --git a/src/core/server/saved_objects/migrationsv2/next.ts b/src/core/server/saved_objects/migrationsv2/next.ts index 3f3714552725b..433c0998f7567 100644 --- a/src/core/server/saved_objects/migrationsv2/next.ts +++ b/src/core/server/saved_objects/migrationsv2/next.ts @@ -12,7 +12,7 @@ import type { ReindexSourceToTempOpenPit, ReindexSourceToTempRead, ReindexSourceToTempClosePit, - ReindexSourceToTempIndex, + ReindexSourceToTempTransform, MarkVersionIndexReady, InitState, LegacyCreateReindexTargetState, @@ -105,7 +105,7 @@ export const nextActionMap = (client: ElasticsearchClient, transformRawDocs: Tra }), REINDEX_SOURCE_TO_TEMP_CLOSE_PIT: (state: ReindexSourceToTempClosePit) => Actions.closePit({ client, pitId: state.sourceIndexPitId }), - REINDEX_SOURCE_TO_TEMP_INDEX: (state: ReindexSourceToTempIndex) => + REINDEX_SOURCE_TO_TEMP_TRANSFORM: (state: ReindexSourceToTempTransform) => Actions.transformDocs({ transformRawDocs, outdatedDocuments: state.outdatedDocuments }), REINDEX_SOURCE_TO_TEMP_INDEX_BULK: (state: ReindexSourceToTempIndexBulk) => Actions.bulkOverwriteTransformedDocuments({ diff --git a/src/core/server/saved_objects/migrationsv2/types.ts b/src/core/server/saved_objects/migrationsv2/types.ts index 49ce12c53aa1a..4f6419930c6cc 100644 --- a/src/core/server/saved_objects/migrationsv2/types.ts +++ b/src/core/server/saved_objects/migrationsv2/types.ts @@ -233,8 +233,8 @@ export interface ReindexSourceToTempClosePit extends PostInitState { readonly sourceIndexPitId: string; } -export interface ReindexSourceToTempIndex extends PostInitState { - readonly controlState: 'REINDEX_SOURCE_TO_TEMP_INDEX'; +export interface ReindexSourceToTempTransform extends PostInitState { + readonly controlState: 'REINDEX_SOURCE_TO_TEMP_TRANSFORM'; readonly outdatedDocuments: SavedObjectsRawDoc[]; readonly sourceIndexPitId: string; readonly lastHitSortValue: number[] | undefined; @@ -434,7 +434,7 @@ export type State = Readonly< | ReindexSourceToTempOpenPit | ReindexSourceToTempRead | ReindexSourceToTempClosePit - | ReindexSourceToTempIndex + | ReindexSourceToTempTransform | ReindexSourceToTempIndexBulk | SetTempWriteBlock | CloneTempToSource diff --git a/src/core/server/saved_objects/saved_objects_type_registry.test.ts b/src/core/server/saved_objects/saved_objects_type_registry.test.ts index 872b61706c526..c86a1a54f14a8 100644 --- a/src/core/server/saved_objects/saved_objects_type_registry.test.ts +++ b/src/core/server/saved_objects/saved_objects_type_registry.test.ts @@ -47,6 +47,59 @@ describe('SavedObjectTypeRegistry', () => { }).toThrowErrorMatchingInlineSnapshot(`"Type 'typeA' is already registered"`); }); + it('throws when `management.visibleInManagement` is specified but `management.importableAndExportable` is undefined or false', () => { + expect(() => { + registry.registerType( + createType({ + name: 'typeA', + management: { + visibleInManagement: true, + }, + }) + ); + }).toThrowErrorMatchingInlineSnapshot( + `"Type typeA: 'management.importableAndExportable' must be 'true' when specifying 'management.visibleInManagement'"` + ); + + expect(() => { + registry.registerType( + createType({ + name: 'typeA', + management: { + visibleInManagement: false, + }, + }) + ); + }).toThrowErrorMatchingInlineSnapshot( + `"Type typeA: 'management.importableAndExportable' must be 'true' when specifying 'management.visibleInManagement'"` + ); + + expect(() => { + registry.registerType( + createType({ + name: 'typeA', + management: { + importableAndExportable: false, + visibleInManagement: false, + }, + }) + ); + }).toThrowErrorMatchingInlineSnapshot( + `"Type typeA: 'management.importableAndExportable' must be 'true' when specifying 'management.visibleInManagement'"` + ); + expect(() => { + registry.registerType( + createType({ + name: 'typeA', + management: { + importableAndExportable: true, + visibleInManagement: false, + }, + }) + ); + }).not.toThrow(); + }); + it('throws when `management.onExport` is specified but `management.importableAndExportable` is undefined or false', () => { expect(() => { registry.registerType( diff --git a/src/core/server/saved_objects/saved_objects_type_registry.ts b/src/core/server/saved_objects/saved_objects_type_registry.ts index ba5960c59239d..444af58eee801 100644 --- a/src/core/server/saved_objects/saved_objects_type_registry.ts +++ b/src/core/server/saved_objects/saved_objects_type_registry.ts @@ -134,5 +134,10 @@ const validateType = ({ name, management }: SavedObjectsType) => { `Type ${name}: 'management.importableAndExportable' must be 'true' when specifying 'management.onExport'` ); } + if (management.visibleInManagement !== undefined && !management.importableAndExportable) { + throw new Error( + `Type ${name}: 'management.importableAndExportable' must be 'true' when specifying 'management.visibleInManagement'` + ); + } } }; diff --git a/src/core/server/saved_objects/types.ts b/src/core/server/saved_objects/types.ts index 83f46c521a836..b2a6be38bb526 100644 --- a/src/core/server/saved_objects/types.ts +++ b/src/core/server/saved_objects/types.ts @@ -356,6 +356,15 @@ export interface SavedObjectsTypeManagementDefinition { * Is the type importable or exportable. Defaults to `false`. */ importableAndExportable?: 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`. + * + * @remarks `importableAndExportable` must be `true` to specify this property. + */ + visibleInManagement?: boolean; /** * The default search field to use for this type. Defaults to `id`. */ diff --git a/src/core/server/server.api.md b/src/core/server/server.api.md index bd959e2cb5a7d..a558a121625bb 100644 --- a/src/core/server/server.api.md +++ b/src/core/server/server.api.md @@ -2728,6 +2728,7 @@ export interface SavedObjectsTypeManagementDefinition { isExportable?: SavedObjectsExportablePredicate; onExport?: SavedObjectsExportTransform; onImport?: SavedObjectsImportHook; + visibleInManagement?: boolean; } // @public diff --git a/src/dev/build/tasks/os_packages/docker_generator/run.ts b/src/dev/build/tasks/os_packages/docker_generator/run.ts index c5a4ff64d2188..21f223a09f60d 100644 --- a/src/dev/build/tasks/os_packages/docker_generator/run.ts +++ b/src/dev/build/tasks/os_packages/docker_generator/run.ts @@ -47,7 +47,7 @@ export async function runDockerGenerator( // General docker var config const license = 'Elastic License'; - const imageTag = 'docker.elastic.co/kibana/kibana'; + const imageTag = `docker.elastic.co/kibana${flags.cloud ? '-ci' : ''}/kibana`; const version = config.getBuildVersion(); const artifactArchitecture = flags.architecture === 'aarch64' ? 'aarch64' : 'x86_64'; const artifactPrefix = `kibana-${version}-linux`; diff --git a/src/dev/storybook/aliases.ts b/src/dev/storybook/aliases.ts index 9395c5fdf8834..a61a2618d6428 100644 --- a/src/dev/storybook/aliases.ts +++ b/src/dev/storybook/aliases.ts @@ -24,6 +24,7 @@ export const storybookAliases = { expression_reveal_image: 'src/plugins/expression_reveal_image/.storybook', expression_shape: 'src/plugins/expression_shape/.storybook', expression_tagcloud: 'src/plugins/chart_expressions/expression_tagcloud/.storybook', + fleet: 'x-pack/plugins/fleet/storybook', infra: 'x-pack/plugins/infra/.storybook', security_solution: 'x-pack/plugins/security_solution/.storybook', ui_actions_enhanced: 'x-pack/plugins/ui_actions_enhanced/.storybook', diff --git a/src/plugins/apm_oss/server/index.ts b/src/plugins/apm_oss/server/index.ts index bf6baf1876074..f2f6777672e33 100644 --- a/src/plugins/apm_oss/server/index.ts +++ b/src/plugins/apm_oss/server/index.ts @@ -10,7 +10,8 @@ import { schema, TypeOf } from '@kbn/config-schema'; import { ConfigDeprecationProvider, PluginInitializerContext } from '../../../core/server'; import { APMOSSPlugin } from './plugin'; -const deprecations: ConfigDeprecationProvider = ({ unused }) => [ +const deprecations: ConfigDeprecationProvider = ({ deprecate, unused }) => [ + deprecate('enabled', '8.0.0'), unused('fleetMode'), unused('indexPattern'), ]; diff --git a/src/plugins/console/server/index.ts b/src/plugins/console/server/index.ts index 736a7e1ae3c97..cd05652c62838 100644 --- a/src/plugins/console/server/index.ts +++ b/src/plugins/console/server/index.ts @@ -16,6 +16,6 @@ export { ConsoleSetup, ConsoleStart } from './types'; export const plugin = (ctx: PluginInitializerContext) => new ConsoleServerPlugin(ctx); export const config: PluginConfigDescriptor = { - deprecations: ({ unused }) => [unused('ssl')], + deprecations: ({ deprecate, unused, rename }) => [deprecate('enabled', '8.0.0'), unused('ssl')], schema: configSchema, }; diff --git a/src/plugins/dashboard/public/application/hooks/use_dashboard_app_state.test.tsx b/src/plugins/dashboard/public/application/hooks/use_dashboard_app_state.test.tsx index 1ac9d680915c6..c3b4075690261 100644 --- a/src/plugins/dashboard/public/application/hooks/use_dashboard_app_state.test.tsx +++ b/src/plugins/dashboard/public/application/hooks/use_dashboard_app_state.test.tsx @@ -59,9 +59,7 @@ const createDashboardAppStateServices = () => { const defaults = makeDefaultServices(); const indexPatterns = {} as IndexPatternsContract; const defaultIndexPattern = { id: 'foo', fields: [{ name: 'bar' }] } as IIndexPattern; - indexPatterns.ensureDefaultIndexPattern = jest - .fn() - .mockImplementation(() => Promise.resolve(true)); + indexPatterns.ensureDefaultDataView = jest.fn().mockImplementation(() => Promise.resolve(true)); indexPatterns.getDefault = jest .fn() .mockImplementation(() => Promise.resolve(defaultIndexPattern)); diff --git a/src/plugins/dashboard/public/application/hooks/use_dashboard_app_state.ts b/src/plugins/dashboard/public/application/hooks/use_dashboard_app_state.ts index 6f46f7155aac5..9f4e9d051323c 100644 --- a/src/plugins/dashboard/public/application/hooks/use_dashboard_app_state.ts +++ b/src/plugins/dashboard/public/application/hooks/use_dashboard_app_state.ts @@ -237,27 +237,25 @@ export const useDashboardAppState = ({ .pipe(debounceTime(DashboardConstants.CHANGE_CHECK_DEBOUNCE)) .subscribe((states) => { const [lastSaved, current] = states; - const unsavedChanges = - current.viewMode === ViewMode.EDIT ? diffDashboardState(lastSaved, current) : {}; - - let savedTimeChanged = false; + const unsavedChanges = diffDashboardState(lastSaved, current); + + const savedTimeChanged = + lastSaved.timeRestore && + !areTimeRangesEqual( + { + from: savedDashboard?.timeFrom, + to: savedDashboard?.timeTo, + }, + timefilter.getTime() + ); /** - * changes to the time filter should only be considered 'unsaved changes' when + * changes to the dashboard should only be considered 'unsaved changes' when * editing the dashboard */ - if (current.viewMode === ViewMode.EDIT) { - savedTimeChanged = - lastSaved.timeRestore && - !areTimeRangesEqual( - { - from: savedDashboard?.timeFrom, - to: savedDashboard?.timeTo, - }, - timefilter.getTime() - ); - } - const hasUnsavedChanges = Object.keys(unsavedChanges).length > 0 || savedTimeChanged; + const hasUnsavedChanges = + current.viewMode === ViewMode.EDIT && + (Object.keys(unsavedChanges).length > 0 || savedTimeChanged); setDashboardAppState((s) => ({ ...s, hasUnsavedChanges })); unsavedChanges.viewMode = current.viewMode; // always push view mode into session store. diff --git a/src/plugins/dashboard/public/application/lib/dashboard_session_storage.ts b/src/plugins/dashboard/public/application/lib/dashboard_session_storage.ts index 7d0e60c0609a8..a696c8bc15b83 100644 --- a/src/plugins/dashboard/public/application/lib/dashboard_session_storage.ts +++ b/src/plugins/dashboard/public/application/lib/dashboard_session_storage.ts @@ -11,6 +11,7 @@ import { Storage } from '../../services/kibana_utils'; import { NotificationsStart } from '../../services/core'; import { panelStorageErrorStrings } from '../../dashboard_strings'; import { DashboardState } from '../../types'; +import { ViewMode } from '../../services/embeddable'; export const DASHBOARD_PANELS_UNSAVED_ID = 'unsavedDashboard'; const DASHBOARD_PANELS_SESSION_KEY = 'dashboardStateManagerPanels'; @@ -69,6 +70,7 @@ export class DashboardSessionStorage { const dashboardsWithUnsavedChanges: string[] = []; Object.keys(dashboardStatesInSpace).map((dashboardId) => { if ( + dashboardStatesInSpace[dashboardId].viewMode === ViewMode.EDIT && Object.keys(dashboardStatesInSpace[dashboardId]).some( (stateKey) => stateKey !== 'viewMode' ) diff --git a/src/plugins/dashboard/public/application/lib/load_saved_dashboard_state.ts b/src/plugins/dashboard/public/application/lib/load_saved_dashboard_state.ts index 9069173c15e8f..fcff740c48e15 100644 --- a/src/plugins/dashboard/public/application/lib/load_saved_dashboard_state.ts +++ b/src/plugins/dashboard/public/application/lib/load_saved_dashboard_state.ts @@ -51,7 +51,7 @@ export const loadSavedDashboardState = async ({ notifications.toasts.addWarning(getDashboard60Warning()); return; } - await indexPatterns.ensureDefaultIndexPattern(); + await indexPatterns.ensureDefaultDataView(); let savedDashboard: DashboardSavedObject | undefined; try { savedDashboard = (await savedDashboards.get(savedDashboardId)) as DashboardSavedObject; diff --git a/src/plugins/dashboard/public/application/listing/dashboard_listing.tsx b/src/plugins/dashboard/public/application/listing/dashboard_listing.tsx index 39d6e5fcd0882..df8a673fbb919 100644 --- a/src/plugins/dashboard/public/application/listing/dashboard_listing.tsx +++ b/src/plugins/dashboard/public/application/listing/dashboard_listing.tsx @@ -7,7 +7,7 @@ */ import { FormattedMessage } from '@kbn/i18n/react'; -import { EuiLink, EuiButton, EuiEmptyPrompt } from '@elastic/eui'; +import { EuiLink, EuiButton, EuiEmptyPrompt, EuiBasicTableColumn } from '@elastic/eui'; import React, { useCallback, useEffect, useMemo, useState } from 'react'; import { attemptLoadDashboardByTitle } from '../lib'; import { DashboardAppServices, DashboardRedirect } from '../../types'; @@ -231,7 +231,7 @@ const getTableColumns = ( sortable: true, }, ...(savedObjectsTagging ? [savedObjectsTagging.ui.getTableColumnDefinition()] : []), - ]; + ] as unknown as Array>>; }; const getNoItemsMessage = ( diff --git a/src/plugins/dashboard/public/application/listing/dashboard_no_match.tsx b/src/plugins/dashboard/public/application/listing/dashboard_no_match.tsx index 03f7b0e162229..91361836f59d4 100644 --- a/src/plugins/dashboard/public/application/listing/dashboard_no_match.tsx +++ b/src/plugins/dashboard/public/application/listing/dashboard_no_match.tsx @@ -23,9 +23,8 @@ export const DashboardNoMatch = ({ history }: { history: RouteComponentProps['hi useEffect(() => { services.restorePreviousUrl(); - const { navigated } = services.urlForwarding.navigateToLegacyKibanaUrl( - history.location.pathname + history.location.pathname + history.location.search ); if (!navigated) { diff --git a/src/plugins/data/common/data_views/index_pattern.stub.ts b/src/plugins/data/common/data_views/data_view.stub.ts similarity index 59% rename from src/plugins/data/common/data_views/index_pattern.stub.ts rename to src/plugins/data/common/data_views/data_view.stub.ts index 16624087f83b3..a3279434c7a0b 100644 --- a/src/plugins/data/common/data_views/index_pattern.stub.ts +++ b/src/plugins/data/common/data_views/data_view.stub.ts @@ -7,12 +7,15 @@ */ import { stubFieldSpecMap, stubLogstashFieldSpecMap } from './field.stub'; -import { createStubIndexPattern } from './data_views/index_pattern.stub'; -export { createStubIndexPattern } from './data_views/index_pattern.stub'; +import { createStubDataView } from './data_views/data_view.stub'; +export { + createStubDataView, + createStubDataView as createStubIndexPattern, +} from './data_views/data_view.stub'; import { SavedObject } from '../../../../core/types'; -import { IndexPatternAttributes } from '../types'; +import { DataViewAttributes } from '../types'; -export const stubIndexPattern = createStubIndexPattern({ +export const stubDataView = createStubDataView({ spec: { id: 'logstash-*', fields: stubFieldSpecMap, @@ -21,7 +24,9 @@ export const stubIndexPattern = createStubIndexPattern({ }, }); -export const stubIndexPatternWithoutTimeField = createStubIndexPattern({ +export const stubIndexPattern = stubDataView; + +export const stubDataViewWithoutTimeField = createStubDataView({ spec: { id: 'logstash-*', fields: stubFieldSpecMap, @@ -29,7 +34,9 @@ export const stubIndexPatternWithoutTimeField = createStubIndexPattern({ }, }); -export const stubLogstashIndexPattern = createStubIndexPattern({ +export const stubIndexPatternWithoutTimeField = stubDataViewWithoutTimeField; + +export const stubLogstashDataView = createStubDataView({ spec: { id: 'logstash-*', title: 'logstash-*', @@ -38,9 +45,11 @@ export const stubLogstashIndexPattern = createStubIndexPattern({ }, }); -export function stubbedSavedObjectIndexPattern( +export const stubLogstashIndexPattern = stubLogstashDataView; + +export function stubbedSavedObjectDataView( id: string | null = null -): SavedObject { +): SavedObject { return { id: id ?? '', type: 'index-pattern', @@ -53,3 +62,5 @@ export function stubbedSavedObjectIndexPattern( references: [], }; } + +export const stubbedSavedObjectIndexPattern = stubbedSavedObjectDataView; diff --git a/src/plugins/data/common/data_views/data_views/__snapshots__/index_pattern.test.ts.snap b/src/plugins/data/common/data_views/data_views/__snapshots__/data_view.test.ts.snap similarity index 100% rename from src/plugins/data/common/data_views/data_views/__snapshots__/index_pattern.test.ts.snap rename to src/plugins/data/common/data_views/data_views/__snapshots__/data_view.test.ts.snap diff --git a/src/plugins/data/common/data_views/data_views/__snapshots__/index_patterns.test.ts.snap b/src/plugins/data/common/data_views/data_views/__snapshots__/data_views.test.ts.snap similarity index 100% rename from src/plugins/data/common/data_views/data_views/__snapshots__/index_patterns.test.ts.snap rename to src/plugins/data/common/data_views/data_views/__snapshots__/data_views.test.ts.snap diff --git a/src/plugins/data/common/data_views/data_views/_pattern_cache.ts b/src/plugins/data/common/data_views/data_views/_pattern_cache.ts index f304d0e93d79c..19db5b21e5934 100644 --- a/src/plugins/data/common/data_views/data_views/_pattern_cache.ts +++ b/src/plugins/data/common/data_views/data_views/_pattern_cache.ts @@ -16,12 +16,12 @@ export interface DataViewCache { } export function createDataViewCache(): DataViewCache { - const vals: Record = {}; + const vals: Record> = {}; const cache: DataViewCache = { get: (id: string) => { return vals[id]; }, - set: (id: string, prom: any) => { + set: (id: string, prom: Promise) => { vals[id] = prom; return prom; }, diff --git a/src/plugins/data/common/data_views/data_views/index_pattern.stub.ts b/src/plugins/data/common/data_views/data_views/data_view.stub.ts similarity index 85% rename from src/plugins/data/common/data_views/data_views/index_pattern.stub.ts rename to src/plugins/data/common/data_views/data_views/data_view.stub.ts index 3b6660c6d93dc..5ff2d077812a8 100644 --- a/src/plugins/data/common/data_views/data_views/index_pattern.stub.ts +++ b/src/plugins/data/common/data_views/data_views/data_view.stub.ts @@ -6,18 +6,18 @@ * Side Public License, v 1. */ -import { IndexPattern } from './data_view'; +import { DataView } from './data_view'; import { DataViewSpec } from '../types'; import { FieldFormatsStartCommon } from '../../../../field_formats/common'; import { fieldFormatsMock } from '../../../../field_formats/common/mocks'; /** - * Create a custom stub index pattern. Use it in your unit tests where an {@link IndexPattern} expected. + * Create a custom stub index pattern. Use it in your unit tests where an {@link DataView} expected. * @param spec - Serialized index pattern object * @param opts - Specify index pattern options * @param deps - Optionally provide dependencies, you can provide a custom field formats implementation, by default a dummy mock is used * - * @returns - an {@link IndexPattern} instance + * @returns - an {@link DataView} instance * * * @example @@ -32,7 +32,7 @@ import { fieldFormatsMock } from '../../../../field_formats/common/mocks'; * * ``` */ -export const createStubIndexPattern = ({ +export const createStubDataView = ({ spec, opts, deps, @@ -45,12 +45,10 @@ export const createStubIndexPattern = ({ deps?: { fieldFormats?: FieldFormatsStartCommon; }; -}): IndexPattern => { - const indexPattern = new IndexPattern({ +}): DataView => + new DataView({ spec, metaFields: opts?.metaFields ?? ['_id', '_type', '_source'], shortDotsEnable: opts?.shortDotsEnable, fieldFormats: deps?.fieldFormats ?? fieldFormatsMock, }); - return indexPattern; -}; diff --git a/src/plugins/data/common/data_views/data_views/index_pattern.test.ts b/src/plugins/data/common/data_views/data_views/data_view.test.ts similarity index 99% rename from src/plugins/data/common/data_views/data_views/index_pattern.test.ts rename to src/plugins/data/common/data_views/data_views/data_view.test.ts index 5fd1d0d051acb..6aea86a7adae7 100644 --- a/src/plugins/data/common/data_views/data_views/index_pattern.test.ts +++ b/src/plugins/data/common/data_views/data_views/data_view.test.ts @@ -18,7 +18,7 @@ import { fieldFormatsMock } from '../../../../field_formats/common/mocks'; import { FieldFormat } from '../../../../field_formats/common'; import { RuntimeField } from '../types'; import { stubLogstashFields } from '../field.stub'; -import { stubbedSavedObjectIndexPattern } from '../index_pattern.stub'; +import { stubbedSavedObjectIndexPattern } from '../data_view.stub'; class MockFieldFormatter {} diff --git a/src/plugins/data/common/data_views/data_views/data_view.ts b/src/plugins/data/common/data_views/data_views/data_view.ts index e2b22724cbe10..c4d6f173ef423 100644 --- a/src/plugins/data/common/data_views/data_views/data_view.ts +++ b/src/plugins/data/common/data_views/data_views/data_view.ts @@ -10,6 +10,7 @@ import _, { each, reject } from 'lodash'; import { castEsToKbnFieldTypeName } from '@kbn/field-types'; +import type { estypes } from '@elastic/elasticsearch'; import { FieldAttrs, FieldAttrSet, DataViewAttributes } from '../..'; import type { RuntimeField } from '../types'; import { DuplicateField } from '../../../../kibana_utils/common'; @@ -158,7 +159,7 @@ export class DataView implements IIndexPattern { }; getComputedFields() { - const scriptFields: any = {}; + const scriptFields: Record = {}; if (!this.fields) { return { storedFields: ['*'], @@ -170,23 +171,21 @@ export class DataView implements IIndexPattern { // Date value returned in "_source" could be in any number of formats // Use a docvalue for each date field to ensure standardized formats when working with date fields - // indexPattern.flattenHit will override "_source" values when the same field is also defined in "fields" - const docvalueFields = reject(this.fields.getByType('date'), 'scripted').map( - (dateField: any) => { - return { - field: dateField.name, - format: - dateField.esTypes && dateField.esTypes.indexOf('date_nanos') !== -1 - ? 'strict_date_time' - : 'date_time', - }; - } - ); + // dataView.flattenHit will override "_source" values when the same field is also defined in "fields" + const docvalueFields = reject(this.fields.getByType('date'), 'scripted').map((dateField) => { + return { + field: dateField.name, + format: + dateField.esTypes && dateField.esTypes.indexOf('date_nanos') !== -1 + ? 'strict_date_time' + : 'date_time', + }; + }); each(this.getScriptedFields(), function (field) { scriptFields[field.name] = { script: { - source: field.script, + source: field.script as string, lang: field.lang, }, }; @@ -227,7 +226,7 @@ export class DataView implements IIndexPattern { */ getSourceFiltering() { return { - excludes: (this.sourceFilters && this.sourceFilters.map((filter: any) => filter.value)) || [], + excludes: (this.sourceFilters && this.sourceFilters.map((filter) => filter.value)) || [], }; } @@ -322,8 +321,8 @@ export class DataView implements IIndexPattern { } isTimeNanosBased(): boolean { - const timeField: any = this.getTimeField(); - return timeField && timeField.esTypes && timeField.esTypes.indexOf('date_nanos') !== -1; + const timeField = this.getTimeField(); + return !!(timeField && timeField.esTypes && timeField.esTypes.indexOf('date_nanos') !== -1); } getTimeField() { diff --git a/src/plugins/data/common/data_views/data_views/index_patterns.test.ts b/src/plugins/data/common/data_views/data_views/data_views.test.ts similarity index 99% rename from src/plugins/data/common/data_views/data_views/index_patterns.test.ts rename to src/plugins/data/common/data_views/data_views/data_views.test.ts index cdc7ca3b9dce7..c5c4802e1bf0c 100644 --- a/src/plugins/data/common/data_views/data_views/index_patterns.test.ts +++ b/src/plugins/data/common/data_views/data_views/data_views.test.ts @@ -11,7 +11,7 @@ import { DataViewsService, DataView } from '.'; import { fieldFormatsMock } from '../../../../field_formats/common/mocks'; import { UiSettingsCommon, SavedObjectsClientCommon, SavedObject } from '../types'; -import { stubbedSavedObjectIndexPattern } from '../index_pattern.stub'; +import { stubbedSavedObjectIndexPattern } from '../data_view.stub'; const createFieldsFetcher = jest.fn().mockImplementation(() => ({ getFieldsForWildcard: jest.fn().mockImplementation(() => { diff --git a/src/plugins/data/common/data_views/data_views/data_views.ts b/src/plugins/data/common/data_views/data_views/data_views.ts index dd9c322500f05..ca1e587c4fce8 100644 --- a/src/plugins/data/common/data_views/data_views/data_views.ts +++ b/src/plugins/data/common/data_views/data_views/data_views.ts @@ -80,8 +80,8 @@ export class DataViewsService { private onNotification: OnNotification; private onError: OnError; private onUnsupportedTimePattern: OnUnsupportedTimePattern; - private indexPatternCache: ReturnType; - ensureDefaultIndexPattern: EnsureDefaultDataView; + private dataViewCache: ReturnType; + ensureDefaultDataView: EnsureDefaultDataView; constructor({ uiSettings, @@ -100,12 +100,9 @@ export class DataViewsService { this.onNotification = onNotification; this.onError = onError; this.onUnsupportedTimePattern = onUnsupportedTimePattern; - this.ensureDefaultIndexPattern = createEnsureDefaultDataView( - uiSettings, - onRedirectNoIndexPattern - ); + this.ensureDefaultDataView = createEnsureDefaultDataView(uiSettings, onRedirectNoIndexPattern); - this.indexPatternCache = createDataViewCache(); + this.dataViewCache = createDataViewCache(); } /** @@ -194,9 +191,9 @@ export class DataViewsService { clearCache = (id?: string) => { this.savedObjectsCache = null; if (id) { - this.indexPatternCache.clear(id); + this.dataViewCache.clear(id); } else { - this.indexPatternCache.clearAll(); + this.dataViewCache.clearAll(); } }; @@ -293,7 +290,7 @@ export class DataViewsService { indexPattern.fields.replaceAll(fieldsWithSavedAttrs); } catch (err) { if (err instanceof DataViewMissingIndices) { - this.onNotification({ title: (err as any).message, color: 'danger', iconType: 'alert' }); + this.onNotification({ title: err.message, color: 'danger', iconType: 'alert' }); } this.onError(err, { @@ -338,7 +335,7 @@ export class DataViewsService { return this.fieldArrayToMap(updatedFieldList, fieldAttrs); } catch (err) { if (err instanceof DataViewMissingIndices) { - this.onNotification({ title: (err as any).message, color: 'danger', iconType: 'alert' }); + this.onNotification({ title: err.message, color: 'danger', iconType: 'alert' }); return {}; } @@ -479,7 +476,7 @@ export class DataViewsService { } catch (err) { if (err instanceof DataViewMissingIndices) { this.onNotification({ - title: (err as any).message, + title: err.message, color: 'danger', iconType: 'alert', }); @@ -537,12 +534,11 @@ export class DataViewsService { get = async (id: string): Promise => { const indexPatternPromise = - this.indexPatternCache.get(id) || - this.indexPatternCache.set(id, this.getSavedObjectAndInit(id)); + this.dataViewCache.get(id) || this.dataViewCache.set(id, this.getSavedObjectAndInit(id)); // don't cache failed requests indexPatternPromise.catch(() => { - this.indexPatternCache.clear(id); + this.dataViewCache.clear(id); }); return indexPatternPromise; @@ -612,7 +608,7 @@ export class DataViewsService { )) as SavedObject; const createdIndexPattern = await this.initFromSavedObject(response); - this.indexPatternCache.set(createdIndexPattern.id!, Promise.resolve(createdIndexPattern)); + this.dataViewCache.set(createdIndexPattern.id!, Promise.resolve(createdIndexPattern)); if (this.savedObjectsCache) { this.savedObjectsCache.push(response as SavedObject); } @@ -700,7 +696,7 @@ export class DataViewsService { indexPattern.version = samePattern.version; // Clear cache - this.indexPatternCache.clear(indexPattern.id!); + this.dataViewCache.clear(indexPattern.id!); // Try the save again return this.updateSavedObject(indexPattern, saveAttempts, ignoreErrors); @@ -714,7 +710,7 @@ export class DataViewsService { * @param indexPatternId: Id of kibana Index Pattern to delete */ async delete(indexPatternId: string) { - this.indexPatternCache.clear(indexPatternId); + this.dataViewCache.clear(indexPatternId); return this.savedObjectsClient.delete(DATA_VIEW_SAVED_OBJECT_TYPE, indexPatternId); } } diff --git a/src/plugins/data/common/data_views/data_views/flatten_hit.test.ts b/src/plugins/data/common/data_views/data_views/flatten_hit.test.ts index f8e1309a38ffe..73232a65b6b72 100644 --- a/src/plugins/data/common/data_views/data_views/flatten_hit.test.ts +++ b/src/plugins/data/common/data_views/data_views/flatten_hit.test.ts @@ -6,11 +6,11 @@ * Side Public License, v 1. */ -import { IndexPattern } from './data_view'; +import { DataView } from './data_view'; import { fieldFormatsMock } from '../../../../field_formats/common/mocks'; import { flattenHitWrapper } from './flatten_hit'; -import { stubbedSavedObjectIndexPattern } from '../index_pattern.stub'; +import { stubbedSavedObjectIndexPattern } from '../data_view.stub'; class MockFieldFormatter {} @@ -24,7 +24,7 @@ function create(id: string) { attributes: { timeFieldName, fields, title }, } = stubbedSavedObjectIndexPattern(id); - return new IndexPattern({ + return new DataView({ spec: { id, type, @@ -41,7 +41,7 @@ function create(id: string) { } describe('flattenHit', () => { - let indexPattern: IndexPattern; + let indexPattern: DataView; // create an indexPattern instance for each test beforeEach(() => { diff --git a/src/plugins/data/common/data_views/data_views/flatten_hit.ts b/src/plugins/data/common/data_views/data_views/flatten_hit.ts index 58a5dff66acc8..ddf484affa298 100644 --- a/src/plugins/data/common/data_views/data_views/flatten_hit.ts +++ b/src/plugins/data/common/data_views/data_views/flatten_hit.ts @@ -103,11 +103,11 @@ function decorateFlattenedWrapper(hit: Record, metaFields: Record, deep = false) { const decorateFlattened = decorateFlattenedWrapper(hit, metaFields); const cached = cache.get(hit); - const flattened = cached || flattenHit(indexPattern, hit, deep); + const flattened = cached || flattenHit(dataView, hit, deep); if (!cached) { cache.set(hit, { ...flattened }); } diff --git a/src/plugins/data/common/data_views/data_views/format_hit.ts b/src/plugins/data/common/data_views/data_views/format_hit.ts index b226013752628..39f7fef564eb0 100644 --- a/src/plugins/data/common/data_views/data_views/format_hit.ts +++ b/src/plugins/data/common/data_views/data_views/format_hit.ts @@ -15,24 +15,24 @@ const partialFormattedCache = new WeakMap(); // Takes a hit, merges it with any stored/scripted fields, and with the metaFields // returns a formatted version -export function formatHitProvider(indexPattern: DataView, defaultFormat: any) { +export function formatHitProvider(dataView: DataView, defaultFormat: any) { function convert( hit: Record, val: any, fieldName: string, type: FieldFormatsContentType = 'html' ) { - const field = indexPattern.fields.getByName(fieldName); - const format = field ? indexPattern.getFormatterForField(field) : defaultFormat; + const field = dataView.fields.getByName(fieldName); + const format = field ? dataView.getFormatterForField(field) : defaultFormat; - return format.convert(val, type, { field, hit, indexPattern }); + return format.convert(val, type, { field, hit, indexPattern: dataView }); } function formatHit(hit: Record, type: string = 'html') { if (type === 'text') { // formatHit of type text is for react components to get rid of // since it's currently just used at the discover's doc view table, caching is not necessary - const flattened = indexPattern.flattenHit(hit); + const flattened = dataView.flattenHit(hit); const result: Record = {}; for (const [key, value] of Object.entries(flattened)) { result[key] = convert(hit, value, key, type); @@ -53,7 +53,7 @@ export function formatHitProvider(indexPattern: DataView, defaultFormat: any) { const cache: Record = {}; formattedCache.set(hit, cache); - _.forOwn(indexPattern.flattenHit(hit), function (val: any, fieldName?: string) { + _.forOwn(dataView.flattenHit(hit), function (val: any, fieldName?: string) { // sync the formatted and partial cache if (!fieldName) { return; @@ -77,7 +77,7 @@ export function formatHitProvider(indexPattern: DataView, defaultFormat: any) { partialFormattedCache.set(hit, partials); } - const val = fieldName === '_source' ? hit._source : indexPattern.flattenHit(hit)[fieldName]; + const val = fieldName === '_source' ? hit._source : dataView.flattenHit(hit)[fieldName]; return convert(hit, val, fieldName); }; diff --git a/src/plugins/data/common/data_views/errors/data_view_saved_object_conflict.ts b/src/plugins/data/common/data_views/errors/data_view_saved_object_conflict.ts new file mode 100644 index 0000000000000..3fcb281655727 --- /dev/null +++ b/src/plugins/data/common/data_views/errors/data_view_saved_object_conflict.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 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 class DataViewSavedObjectConflictError extends Error { + constructor(savedObjectId: string) { + super(`Conflict loading DataView saved object, id: ${savedObjectId}`); + this.name = 'DataViewSavedObjectConflictError'; + } +} diff --git a/src/plugins/data/common/data_views/errors/duplicate_index_pattern.ts b/src/plugins/data/common/data_views/errors/duplicate_index_pattern.ts index d35b09e39aa76..942c104eee4e5 100644 --- a/src/plugins/data/common/data_views/errors/duplicate_index_pattern.ts +++ b/src/plugins/data/common/data_views/errors/duplicate_index_pattern.ts @@ -9,6 +9,6 @@ export class DuplicateDataViewError extends Error { constructor(message: string) { super(message); - this.name = 'DuplicateIndexPatternError'; + this.name = 'DuplicateDataViewError'; } } diff --git a/src/plugins/data/common/data_views/errors/index.ts b/src/plugins/data/common/data_views/errors/index.ts index 63bd1ac5f5848..20ff90d3fd6cf 100644 --- a/src/plugins/data/common/data_views/errors/index.ts +++ b/src/plugins/data/common/data_views/errors/index.ts @@ -7,3 +7,4 @@ */ export * from './duplicate_index_pattern'; +export * from './data_view_saved_object_conflict'; diff --git a/src/plugins/data/common/data_views/field.stub.ts b/src/plugins/data/common/data_views/field.stub.ts index 03bb0dee33db3..7ff51007bcefa 100644 --- a/src/plugins/data/common/data_views/field.stub.ts +++ b/src/plugins/data/common/data_views/field.stub.ts @@ -6,10 +6,10 @@ * Side Public License, v 1. */ -import { FieldSpec, IndexPatternField } from '.'; +import { FieldSpec, DataViewField } from '.'; -export const createIndexPatternFieldStub = ({ spec }: { spec: FieldSpec }): IndexPatternField => { - return new IndexPatternField(spec); +export const createIndexPatternFieldStub = ({ spec }: { spec: FieldSpec }): DataViewField => { + return new DataViewField(spec); }; export const stubFieldSpecMap: Record = { @@ -71,7 +71,7 @@ export const stubFieldSpecMap: Record = { }, }; -export const stubFields: IndexPatternField[] = Object.values(stubFieldSpecMap).map((spec) => +export const stubFields: DataViewField[] = Object.values(stubFieldSpecMap).map((spec) => createIndexPatternFieldStub({ spec }) ); @@ -404,6 +404,6 @@ export const stubLogstashFieldSpecMap: Record = { }, }; -export const stubLogstashFields: IndexPatternField[] = Object.values(stubLogstashFieldSpecMap).map( +export const stubLogstashFields: DataViewField[] = Object.values(stubLogstashFieldSpecMap).map( (spec) => createIndexPatternFieldStub({ spec }) ); diff --git a/src/plugins/data/common/data_views/fields/__snapshots__/index_pattern_field.test.ts.snap b/src/plugins/data/common/data_views/fields/__snapshots__/data_view_field.test.ts.snap similarity index 100% rename from src/plugins/data/common/data_views/fields/__snapshots__/index_pattern_field.test.ts.snap rename to src/plugins/data/common/data_views/fields/__snapshots__/data_view_field.test.ts.snap diff --git a/src/plugins/data/common/data_views/fields/index_pattern_field.test.ts b/src/plugins/data/common/data_views/fields/data_view_field.test.ts similarity index 98% rename from src/plugins/data/common/data_views/fields/index_pattern_field.test.ts rename to src/plugins/data/common/data_views/fields/data_view_field.test.ts index 906cb0ad1badd..9107036c15c1a 100644 --- a/src/plugins/data/common/data_views/fields/index_pattern_field.test.ts +++ b/src/plugins/data/common/data_views/fields/data_view_field.test.ts @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import { IndexPatternField } from './index_pattern_field'; +import { IndexPatternField } from './data_view_field'; import { IndexPattern } from '..'; import { KBN_FIELD_TYPES } from '../../../common'; import { FieldSpec, RuntimeField } from '../types'; diff --git a/src/plugins/data/common/data_views/fields/index_pattern_field.ts b/src/plugins/data/common/data_views/fields/data_view_field.ts similarity index 100% rename from src/plugins/data/common/data_views/fields/index_pattern_field.ts rename to src/plugins/data/common/data_views/fields/data_view_field.ts diff --git a/src/plugins/data/common/data_views/fields/field_list.ts b/src/plugins/data/common/data_views/fields/field_list.ts index 8dd407e16e4c0..e2c850c0c4dd0 100644 --- a/src/plugins/data/common/data_views/fields/field_list.ts +++ b/src/plugins/data/common/data_views/fields/field_list.ts @@ -8,7 +8,7 @@ import { findIndex } from 'lodash'; import { IFieldType } from './types'; -import { DataViewField } from './index_pattern_field'; +import { DataViewField } from './data_view_field'; import { FieldSpec, DataViewFieldMap } from '../types'; import { DataView } from '../data_views'; diff --git a/src/plugins/data/common/data_views/fields/index.ts b/src/plugins/data/common/data_views/fields/index.ts index 53c8ed213cda7..0ff7397c4f7b5 100644 --- a/src/plugins/data/common/data_views/fields/index.ts +++ b/src/plugins/data/common/data_views/fields/index.ts @@ -9,4 +9,4 @@ export * from './types'; export { isFilterable, isNestedField } from './utils'; export * from './field_list'; -export * from './index_pattern_field'; +export * from './data_view_field'; diff --git a/src/plugins/data/common/data_views/lib/get_title.ts b/src/plugins/data/common/data_views/lib/get_title.ts index efebbc302f22c..94185eae46893 100644 --- a/src/plugins/data/common/data_views/lib/get_title.ts +++ b/src/plugins/data/common/data_views/lib/get_title.ts @@ -6,17 +6,18 @@ * Side Public License, v 1. */ -import { SavedObjectsClientContract, SimpleSavedObject } from '../../../../../core/public'; +import { SavedObjectsClientContract } from '../../../../../core/public'; import { DATA_VIEW_SAVED_OBJECT_TYPE } from '../../constants'; +import { DataViewAttributes } from '../types'; export async function getTitle( client: SavedObjectsClientContract, indexPatternId: string -): Promise> { - const savedObject = (await client.get( +): Promise { + const savedObject = await client.get( DATA_VIEW_SAVED_OBJECT_TYPE, indexPatternId - )) as SimpleSavedObject; + ); if (savedObject.error) { throw new Error(`Unable to get index-pattern title: ${savedObject.error.message}`); diff --git a/src/plugins/data/common/data_views/lib/index.ts b/src/plugins/data/common/data_views/lib/index.ts index ae59c7d417818..0554232e64cae 100644 --- a/src/plugins/data/common/data_views/lib/index.ts +++ b/src/plugins/data/common/data_views/lib/index.ts @@ -8,7 +8,6 @@ export { DataViewMissingIndices } from './errors'; export { getTitle } from './get_title'; -export { isDefault } from './is_default'; export * from './types'; -export { validateDataView } from './validate_index_pattern'; +export { validateDataView } from './validate_data_view'; diff --git a/src/plugins/data/common/data_views/lib/validate_index_pattern.test.ts b/src/plugins/data/common/data_views/lib/validate_data_view.test.ts similarity index 94% rename from src/plugins/data/common/data_views/lib/validate_index_pattern.test.ts rename to src/plugins/data/common/data_views/lib/validate_data_view.test.ts index ed90da122484e..edf20440931e3 100644 --- a/src/plugins/data/common/data_views/lib/validate_index_pattern.test.ts +++ b/src/plugins/data/common/data_views/lib/validate_data_view.test.ts @@ -8,7 +8,7 @@ import { CONTAINS_SPACES_KEY, ILLEGAL_CHARACTERS_KEY, ILLEGAL_CHARACTERS_VISIBLE } from './types'; -import { validateDataView } from './validate_index_pattern'; +import { validateDataView } from './validate_data_view'; describe('Index Pattern Utils', () => { describe('Validation', () => { diff --git a/src/plugins/data/common/data_views/lib/validate_index_pattern.ts b/src/plugins/data/common/data_views/lib/validate_data_view.ts similarity index 92% rename from src/plugins/data/common/data_views/lib/validate_index_pattern.ts rename to src/plugins/data/common/data_views/lib/validate_data_view.ts index 454d0bc1a0c6e..f86ba28e7cde4 100644 --- a/src/plugins/data/common/data_views/lib/validate_index_pattern.ts +++ b/src/plugins/data/common/data_views/lib/validate_data_view.ts @@ -24,7 +24,7 @@ function findIllegalCharacters(indexPattern: string): string[] { } export function validateDataView(indexPattern: string) { - const errors: Record = {}; + const errors: { [ILLEGAL_CHARACTERS_KEY]?: string[]; [CONTAINS_SPACES_KEY]?: boolean } = {}; const illegalCharacters = findIllegalCharacters(indexPattern); diff --git a/src/plugins/data/common/data_views/mocks.ts b/src/plugins/data/common/data_views/mocks.ts index 6e82118f7b8b8..9585b6e60f923 100644 --- a/src/plugins/data/common/data_views/mocks.ts +++ b/src/plugins/data/common/data_views/mocks.ts @@ -7,4 +7,4 @@ */ export * from './fields/fields.mocks'; -export * from './data_views/index_pattern.stub'; +export * from './data_views/data_view.stub'; diff --git a/src/plugins/data/common/data_views/types.ts b/src/plugins/data/common/data_views/types.ts index d7f65e43dd2df..f0be1a88302a2 100644 --- a/src/plugins/data/common/data_views/types.ts +++ b/src/plugins/data/common/data_views/types.ts @@ -100,8 +100,8 @@ export type OnUnsupportedTimePattern = ({ }) => void; export interface UiSettingsCommon { - get: (key: string) => Promise; - getAll: () => Promise>; + get: (key: string) => Promise; + getAll: () => Promise>; set: (key: string, value: any) => Promise; remove: (key: string) => Promise; } diff --git a/src/plugins/data/common/search/aggs/buckets/_terms_other_bucket_helper.test.ts b/src/plugins/data/common/search/aggs/buckets/_terms_other_bucket_helper.test.ts index 215d3ce13f55e..7ec176d7ab11a 100644 --- a/src/plugins/data/common/search/aggs/buckets/_terms_other_bucket_helper.test.ts +++ b/src/plugins/data/common/search/aggs/buckets/_terms_other_bucket_helper.test.ts @@ -377,6 +377,86 @@ describe('Terms Agg Other bucket helper', () => { } }); + test('correctly builds query for nested terms agg with one disabled', () => { + const oneDisabledNestedTerms = { + aggs: [ + { + id: '2', + type: BUCKET_TYPES.TERMS, + enabled: false, + params: { + field: { + name: 'machine.os.raw', + indexPattern, + filterable: true, + }, + size: 2, + otherBucket: false, + missingBucket: true, + }, + }, + { + id: '1', + type: BUCKET_TYPES.TERMS, + params: { + field: { + name: 'geo.src', + indexPattern, + filterable: true, + }, + size: 2, + otherBucket: true, + missingBucket: false, + }, + }, + ], + }; + const aggConfigs = getAggConfigs(oneDisabledNestedTerms.aggs); + const agg = buildOtherBucketAgg( + aggConfigs, + aggConfigs.aggs[1] as IBucketAggConfig, + singleTermResponse + ); + const expectedResponse = { + 'other-filter': { + aggs: undefined, + filters: { + filters: { + '': { + bool: { + filter: [ + { + exists: { + field: 'geo.src', + }, + }, + ], + must: [], + must_not: [ + { + match_phrase: { + 'geo.src': 'ios', + }, + }, + { + match_phrase: { + 'geo.src': 'win xp', + }, + }, + ], + should: [], + }, + }, + }, + }, + }, + }; + expect(agg).toBeDefined(); + if (agg) { + expect(agg()).toEqual(expectedResponse); + } + }); + test('does not build query if sum_other_doc_count is 0 (exhaustive terms)', () => { const aggConfigs = getAggConfigs(nestedTerm.aggs); expect( diff --git a/src/plugins/data/common/search/aggs/buckets/_terms_other_bucket_helper.ts b/src/plugins/data/common/search/aggs/buckets/_terms_other_bucket_helper.ts index 39fba23a42210..436cc5614ac80 100644 --- a/src/plugins/data/common/search/aggs/buckets/_terms_other_bucket_helper.ts +++ b/src/plugins/data/common/search/aggs/buckets/_terms_other_bucket_helper.ts @@ -129,7 +129,9 @@ export const buildOtherBucketAgg = ( aggWithOtherBucket: IBucketAggConfig, response: any ) => { - const bucketAggs = aggConfigs.aggs.filter((agg) => agg.type.type === AggGroupNames.Buckets); + const bucketAggs = aggConfigs.aggs.filter( + (agg) => agg.type.type === AggGroupNames.Buckets && agg.enabled + ); const index = bucketAggs.findIndex((agg) => agg.id === aggWithOtherBucket.id); const aggs = aggConfigs.toDsl(); const indexPattern = aggWithOtherBucket.aggConfigs.indexPattern; diff --git a/src/plugins/data/common/search/aggs/metrics/top_hit.test.ts b/src/plugins/data/common/search/aggs/metrics/top_hit.test.ts index 04d382f1aa6d1..37ce9c4edb8d1 100644 --- a/src/plugins/data/common/search/aggs/metrics/top_hit.test.ts +++ b/src/plugins/data/common/search/aggs/metrics/top_hit.test.ts @@ -133,28 +133,28 @@ describe('Top hit metric', () => { }); it('should request the _source field', () => { - init({ field: '_source' }); - expect(aggDsl.top_hits._source).toBeTruthy(); - expect(aggDsl.top_hits.docvalue_fields).toBeUndefined(); + init({ fieldName: '_source' }); + expect(aggDsl.top_hits._source).toBe(true); + expect(aggDsl.top_hits.fields).toBeUndefined(); }); - it('requests both source and docvalues_fields for non-text aggregatable fields', () => { + it('requests fields for non-text aggregatable fields', () => { init({ fieldName: 'bytes', readFromDocValues: true }); - expect(aggDsl.top_hits._source).toBe('bytes'); - expect(aggDsl.top_hits.docvalue_fields).toEqual([{ field: 'bytes' }]); + expect(aggDsl.top_hits._source).toBe(false); + expect(aggDsl.top_hits.fields).toEqual([{ field: 'bytes' }]); }); - it('requests both source and docvalues_fields for date aggregatable fields', () => { + it('requests fields for date aggregatable fields', () => { init({ fieldName: '@timestamp', readFromDocValues: true, fieldType: KBN_FIELD_TYPES.DATE }); - expect(aggDsl.top_hits._source).toBe('@timestamp'); - expect(aggDsl.top_hits.docvalue_fields).toEqual([{ field: '@timestamp', format: 'date_time' }]); + expect(aggDsl.top_hits._source).toBe(false); + expect(aggDsl.top_hits.fields).toEqual([{ field: '@timestamp', format: 'date_time' }]); }); - it('requests just source for aggregatable text fields', () => { + it('requests fields for aggregatable text fields', () => { init({ fieldName: 'machine.os' }); - expect(aggDsl.top_hits._source).toBe('machine.os'); - expect(aggDsl.top_hits.docvalue_fields).toBeUndefined(); + expect(aggDsl.top_hits._source).toBe(false); + expect(aggDsl.top_hits.fields).toEqual([{ field: 'machine.os' }]); }); describe('try to get the value from the top hit', () => { diff --git a/src/plugins/data/common/search/aggs/metrics/top_hit.ts b/src/plugins/data/common/search/aggs/metrics/top_hit.ts index 094b5cda9a46d..a4bd99d6b210d 100644 --- a/src/plugins/data/common/search/aggs/metrics/top_hit.ts +++ b/src/plugins/data/common/search/aggs/metrics/top_hit.ts @@ -78,8 +78,8 @@ export const getTopHitMetricAgg = () => { }, }; } else { - if (field.readFromDocValues) { - output.params.docvalue_fields = [ + if (field.name !== '_source') { + output.params.fields = [ { field: field.name, // always format date fields as date_time to avoid @@ -89,7 +89,7 @@ export const getTopHitMetricAgg = () => { }, ]; } - output.params._source = field.name === '_source' ? true : field.name; + output.params._source = field.name === '_source'; } }, }, diff --git a/src/plugins/data/common/stubs.ts b/src/plugins/data/common/stubs.ts index 36bd3357e7098..5cddcf397f442 100644 --- a/src/plugins/data/common/stubs.ts +++ b/src/plugins/data/common/stubs.ts @@ -7,5 +7,5 @@ */ export * from './data_views/field.stub'; -export * from './data_views/index_pattern.stub'; +export * from './data_views/data_view.stub'; export * from './es_query/stubs'; diff --git a/src/plugins/data/public/data_views/data_views/index_pattern.stub.ts b/src/plugins/data/public/data_views/data_views/data_view.stub.ts similarity index 87% rename from src/plugins/data/public/data_views/data_views/index_pattern.stub.ts rename to src/plugins/data/public/data_views/data_views/data_view.stub.ts index 49d31def92384..b3d8448064c65 100644 --- a/src/plugins/data/public/data_views/data_views/index_pattern.stub.ts +++ b/src/plugins/data/public/data_views/data_views/data_view.stub.ts @@ -10,15 +10,15 @@ import { CoreSetup } from 'kibana/public'; import { FieldFormatsStartCommon } from '../../../../field_formats/common'; import { getFieldFormatsRegistry } from '../../../../field_formats/public/mocks'; import * as commonStubs from '../../../common/stubs'; -import { IndexPattern, IndexPatternSpec } from '../../../common'; +import { DataView, DataViewSpec } from '../../../common'; import { coreMock } from '../../../../../core/public/mocks'; /** - * Create a custom stub index pattern. Use it in your unit tests where an {@link IndexPattern} expected. + * Create a custom stub index pattern. Use it in your unit tests where an {@link DataView} expected. * @param spec - Serialized index pattern object * @param opts - Specify index pattern options * @param deps - Optionally provide dependencies, you can provide a custom field formats implementation, by default client side registry with real formatters implementation is used * - * @returns - an {@link IndexPattern} instance + * @returns - an {@link DataView} instance * * @remark - This is a client side version, a browser-agnostic version is available in {@link commonStubs | common}. * The main difference is that client side version by default uses client side field formats service, where common version uses a dummy field formats mock. @@ -35,12 +35,12 @@ import { coreMock } from '../../../../../core/public/mocks'; * * ``` */ -export const createStubIndexPattern = ({ +export const createStubDataView = ({ spec, opts, deps, }: { - spec: IndexPatternSpec; + spec: DataViewSpec; opts?: { shortDotsEnable?: boolean; metaFields?: string[]; @@ -49,8 +49,8 @@ export const createStubIndexPattern = ({ fieldFormats?: FieldFormatsStartCommon; core?: CoreSetup; }; -}): IndexPattern => { - return commonStubs.createStubIndexPattern({ +}): DataView => { + return commonStubs.createStubDataView({ spec, opts, deps: { diff --git a/src/plugins/data/public/data_views/data_views/index_patterns_api_client.test.mock.ts b/src/plugins/data/public/data_views/data_views/data_views_api_client.test.mock.ts similarity index 100% rename from src/plugins/data/public/data_views/data_views/index_patterns_api_client.test.mock.ts rename to src/plugins/data/public/data_views/data_views/data_views_api_client.test.mock.ts diff --git a/src/plugins/data/public/data_views/data_views/index_patterns_api_client.test.ts b/src/plugins/data/public/data_views/data_views/data_views_api_client.test.ts similarity index 83% rename from src/plugins/data/public/data_views/data_views/index_patterns_api_client.test.ts rename to src/plugins/data/public/data_views/data_views/data_views_api_client.test.ts index a6742852533a0..09ee001c218b5 100644 --- a/src/plugins/data/public/data_views/data_views/index_patterns_api_client.test.ts +++ b/src/plugins/data/public/data_views/data_views/data_views_api_client.test.ts @@ -6,16 +6,16 @@ * Side Public License, v 1. */ -import { http } from './index_patterns_api_client.test.mock'; -import { IndexPatternsApiClient } from './index_patterns_api_client'; +import { http } from './data_views_api_client.test.mock'; +import { DataViewsApiClient } from './data_views_api_client'; describe('IndexPatternsApiClient', () => { let fetchSpy: jest.SpyInstance; - let indexPatternsApiClient: IndexPatternsApiClient; + let indexPatternsApiClient: DataViewsApiClient; beforeEach(() => { fetchSpy = jest.spyOn(http, 'fetch').mockImplementation(() => Promise.resolve({})); - indexPatternsApiClient = new IndexPatternsApiClient(http); + indexPatternsApiClient = new DataViewsApiClient(http); }); test('uses the right URI to fetch fields for time patterns', async function () { diff --git a/src/plugins/data/public/data_views/data_views/index_patterns_api_client.ts b/src/plugins/data/public/data_views/data_views/data_views_api_client.ts similarity index 95% rename from src/plugins/data/public/data_views/data_views/index_patterns_api_client.ts rename to src/plugins/data/public/data_views/data_views/data_views_api_client.ts index 295cd99e7e017..d11ec7cfa003d 100644 --- a/src/plugins/data/public/data_views/data_views/index_patterns_api_client.ts +++ b/src/plugins/data/public/data_views/data_views/data_views_api_client.ts @@ -10,13 +10,13 @@ import { HttpSetup } from 'src/core/public'; import { DataViewMissingIndices } from '../../../common/data_views/lib'; import { GetFieldsOptions, - IIndexPatternsApiClient, + IDataViewsApiClient, GetFieldsOptionsTimePattern, } from '../../../common/data_views/types'; const API_BASE_URL: string = `/api/index_patterns/`; -export class IndexPatternsApiClient implements IIndexPatternsApiClient { +export class DataViewsApiClient implements IDataViewsApiClient { private http: HttpSetup; constructor(http: HttpSetup) { diff --git a/src/plugins/data/public/data_views/data_views/index.ts b/src/plugins/data/public/data_views/data_views/index.ts index 3c15538a7a2e9..49b7367aef6e7 100644 --- a/src/plugins/data/public/data_views/data_views/index.ts +++ b/src/plugins/data/public/data_views/data_views/index.ts @@ -9,4 +9,4 @@ export * from '../../../common/data_views/data_views'; export * from './redirect_no_index_pattern'; export * from './on_unsupported_time_pattern'; -export * from './index_patterns_api_client'; +export * from './data_views_api_client'; diff --git a/src/plugins/data/public/data_views/index.ts b/src/plugins/data/public/data_views/index.ts index 587c866c23291..a46e9133154aa 100644 --- a/src/plugins/data/public/data_views/index.ts +++ b/src/plugins/data/public/data_views/index.ts @@ -12,7 +12,6 @@ export { ILLEGAL_CHARACTERS_VISIBLE, ILLEGAL_CHARACTERS, validateDataView, - isDefault, } from '../../common/data_views/lib'; export { flattenHitWrapper, @@ -27,7 +26,7 @@ export { IndexPatternsService, IndexPatternsContract, IndexPattern, - IndexPatternsApiClient, + DataViewsApiClient, DataViewsService, DataViewsContract, DataView, diff --git a/src/plugins/data/public/data_views/saved_objects_client_wrapper.test.ts b/src/plugins/data/public/data_views/saved_objects_client_wrapper.test.ts new file mode 100644 index 0000000000000..221a18ac7fab7 --- /dev/null +++ b/src/plugins/data/public/data_views/saved_objects_client_wrapper.test.ts @@ -0,0 +1,55 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 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 { SavedObjectsClientPublicToCommon } from './saved_objects_client_wrapper'; +import { savedObjectsServiceMock } from 'src/core/public/mocks'; + +import { DataViewSavedObjectConflictError } from '../../common/data_views'; + +describe('SavedObjectsClientPublicToCommon', () => { + const soClient = savedObjectsServiceMock.createStartContract().client; + + test('get saved object - exactMatch', async () => { + const mockedSavedObject = { + version: 'abc', + }; + soClient.resolve = jest + .fn() + .mockResolvedValue({ outcome: 'exactMatch', saved_object: mockedSavedObject }); + const service = new SavedObjectsClientPublicToCommon(soClient); + const result = await service.get('index-pattern', '1'); + expect(result).toStrictEqual(mockedSavedObject); + }); + + test('get saved object - aliasMatch', async () => { + const mockedSavedObject = { + version: 'def', + }; + soClient.resolve = jest + .fn() + .mockResolvedValue({ outcome: 'aliasMatch', saved_object: mockedSavedObject }); + const service = new SavedObjectsClientPublicToCommon(soClient); + const result = await service.get('index-pattern', '1'); + expect(result).toStrictEqual(mockedSavedObject); + }); + + test('get saved object - conflict', async () => { + const mockedSavedObject = { + version: 'ghi', + }; + + soClient.resolve = jest + .fn() + .mockResolvedValue({ outcome: 'conflict', saved_object: mockedSavedObject }); + const service = new SavedObjectsClientPublicToCommon(soClient); + + await expect(service.get('index-pattern', '1')).rejects.toThrow( + DataViewSavedObjectConflictError + ); + }); +}); diff --git a/src/plugins/data/public/data_views/saved_objects_client_wrapper.ts b/src/plugins/data/public/data_views/saved_objects_client_wrapper.ts index c8e633c6ec878..0d497d1203e2f 100644 --- a/src/plugins/data/public/data_views/saved_objects_client_wrapper.ts +++ b/src/plugins/data/public/data_views/saved_objects_client_wrapper.ts @@ -12,9 +12,10 @@ import { SavedObjectsClientCommon, SavedObjectsClientCommonFindArgs, SavedObject, + DataViewSavedObjectConflictError, } from '../../common/data_views'; -type SOClient = Pick; +type SOClient = Pick; const simpleSavedObjectToSavedObject = (simpleSavedObject: SimpleSavedObject): SavedObject => ({ @@ -33,8 +34,11 @@ export class SavedObjectsClientPublicToCommon implements SavedObjectsClientCommo } async get(type: string, id: string) { - const response = await this.savedObjectClient.get(type, id); - return simpleSavedObjectToSavedObject(response); + const response = await this.savedObjectClient.resolve(type, id); + if (response.outcome === 'conflict') { + throw new DataViewSavedObjectConflictError(id); + } + return simpleSavedObjectToSavedObject(response.saved_object); } async update( type: string, diff --git a/src/plugins/data/public/data_views/ui_settings_wrapper.ts b/src/plugins/data/public/data_views/ui_settings_wrapper.ts index e0998ed72b2e6..f8ae317391fa3 100644 --- a/src/plugins/data/public/data_views/ui_settings_wrapper.ts +++ b/src/plugins/data/public/data_views/ui_settings_wrapper.ts @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import { IUiSettingsClient } from 'src/core/public'; +import { IUiSettingsClient, PublicUiSettingsParams, UserProvidedValues } from 'src/core/public'; import { UiSettingsCommon } from '../../common'; export class UiSettingsPublicToCommon implements UiSettingsCommon { @@ -14,11 +14,11 @@ export class UiSettingsPublicToCommon implements UiSettingsCommon { constructor(uiSettings: IUiSettingsClient) { this.uiSettings = uiSettings; } - get(key: string) { + get(key: string): Promise { return Promise.resolve(this.uiSettings.get(key)); } - getAll() { + getAll(): Promise>> { return Promise.resolve(this.uiSettings.getAll()); } diff --git a/src/plugins/data/public/index.ts b/src/plugins/data/public/index.ts index 6480a0a340340..e1f5b98baca9c 100644 --- a/src/plugins/data/public/index.ts +++ b/src/plugins/data/public/index.ts @@ -45,7 +45,6 @@ import { CONTAINS_SPACES_KEY, ILLEGAL_CHARACTERS_VISIBLE, ILLEGAL_CHARACTERS, - isDefault, validateDataView, flattenHitWrapper, } from './data_views'; @@ -58,7 +57,6 @@ export const indexPatterns = { CONTAINS_SPACES_KEY, ILLEGAL_CHARACTERS_VISIBLE, ILLEGAL_CHARACTERS, - isDefault, isFilterable, isNestedField, validate: validateDataView, diff --git a/src/plugins/data/public/plugin.ts b/src/plugins/data/public/plugin.ts index c8e63e5d43f06..c68a6b8a3ff80 100644 --- a/src/plugins/data/public/plugin.ts +++ b/src/plugins/data/public/plugin.ts @@ -22,10 +22,10 @@ import { SearchService } from './search/search_service'; import { QueryService } from './query'; import { createIndexPatternSelect } from './ui/index_pattern_select'; import { - IndexPatternsService, + DataViewsService, onRedirectNoIndexPattern, onUnsupportedTimePattern, - IndexPatternsApiClient, + DataViewsApiClient, UiSettingsPublicToCommon, } from './data_views'; import { @@ -146,10 +146,10 @@ export class DataPublicPlugin setOverlays(overlays); setUiSettings(uiSettings); - const indexPatterns = new IndexPatternsService({ + const indexPatterns = new DataViewsService({ uiSettings: new UiSettingsPublicToCommon(uiSettings), savedObjectsClient: new SavedObjectsClientPublicToCommon(savedObjects.client), - apiClient: new IndexPatternsApiClient(http), + apiClient: new DataViewsApiClient(http), fieldFormats, onNotification: (toastInputFields) => { notifications.toasts.add(toastInputFields); diff --git a/src/plugins/data/public/search/errors/es_error.test.tsx b/src/plugins/data/public/search/errors/es_error.test.tsx index fd1100ba34afc..4d1bc8b03b8f2 100644 --- a/src/plugins/data/public/search/errors/es_error.test.tsx +++ b/src/plugins/data/public/search/errors/es_error.test.tsx @@ -25,4 +25,31 @@ describe('EsError', () => { expect(typeof esError.attributes).toEqual('object'); expect(esError.attributes).toEqual(error.attributes); }); + + it('contains some explanation of the error in the message', () => { + // error taken from Vega's issue + const error = { + message: + 'x_content_parse_exception: [x_content_parse_exception] Reason: [1:78] [date_histogram] failed to parse field [calendar_interval]', + statusCode: 400, + attributes: { + root_cause: [ + { + type: 'x_content_parse_exception', + reason: '[1:78] [date_histogram] failed to parse field [calendar_interval]', + }, + ], + type: 'x_content_parse_exception', + reason: '[1:78] [date_histogram] failed to parse field [calendar_interval]', + caused_by: { + type: 'illegal_argument_exception', + reason: 'The supplied interval [2q] could not be parsed as a calendar interval.', + }, + }, + } as any; + const esError = new EsError(error); + expect(esError.message).toEqual( + 'EsError: The supplied interval [2q] could not be parsed as a calendar interval.' + ); + }); }); diff --git a/src/plugins/data/public/search/errors/es_error.tsx b/src/plugins/data/public/search/errors/es_error.tsx index 3303d48bf2adb..71c11af48830f 100644 --- a/src/plugins/data/public/search/errors/es_error.tsx +++ b/src/plugins/data/public/search/errors/es_error.tsx @@ -9,6 +9,7 @@ import React from 'react'; import { EuiCodeBlock, EuiSpacer } from '@elastic/eui'; import { ApplicationStart } from 'kibana/public'; +import { i18n } from '@kbn/i18n'; import { KbnError } from '../../../../kibana_utils/common'; import { IEsError } from './types'; import { getRootCause } from './utils'; @@ -17,7 +18,12 @@ export class EsError extends KbnError { readonly attributes: IEsError['attributes']; constructor(protected readonly err: IEsError) { - super('EsError'); + super( + `EsError: ${ + getRootCause(err)?.reason || + i18n.translate('data.esError.unknownRootCause', { defaultMessage: 'unknown' }) + }` + ); this.attributes = err.attributes; } diff --git a/src/plugins/data/public/search/errors/utils.ts b/src/plugins/data/public/search/errors/utils.ts index aba4e965d64c8..cb3e83dc8001c 100644 --- a/src/plugins/data/public/search/errors/utils.ts +++ b/src/plugins/data/public/search/errors/utils.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 { FailedShard } from './types'; +import type { ErrorCause } from '@elastic/elasticsearch/api/types'; +import type { FailedShard, Reason } from './types'; import { KibanaServerError } from '../../../../kibana_utils/common'; export function getFailedShards(err: KibanaServerError): FailedShard | undefined { @@ -15,6 +15,16 @@ export function getFailedShards(err: KibanaServerError): FailedShard | unde return failedShards ? failedShards[0] : undefined; } +function getNestedCause(err: KibanaServerError | ErrorCause): Reason { + const attr = ((err as KibanaServerError).attributes || err) as ErrorCause; + const { type, reason, caused_by: causedBy } = attr; + if (causedBy) { + return getNestedCause(causedBy); + } + return { type, reason }; +} + export function getRootCause(err: KibanaServerError) { - return getFailedShards(err)?.reason; + // Give shard failures priority, then try to get the error navigating nested objects + return getFailedShards(err)?.reason || getNestedCause(err); } diff --git a/src/plugins/data/public/search/search_interceptor/search_interceptor.test.ts b/src/plugins/data/public/search/search_interceptor/search_interceptor.test.ts index 155638250a2a4..7186938816d5f 100644 --- a/src/plugins/data/public/search/search_interceptor/search_interceptor.test.ts +++ b/src/plugins/data/public/search/search_interceptor/search_interceptor.test.ts @@ -501,12 +501,12 @@ describe('SearchInterceptor', () => { opts: { isRestore?: boolean; isStored?: boolean; - sessionId: string; + sessionId?: string; } | null ) => { const sessionServiceMock = sessionService as jest.Mocked; sessionServiceMock.getSearchOptions.mockImplementation(() => - opts + opts && opts.sessionId ? { sessionId: opts.sessionId, isRestore: opts.isRestore ?? false, @@ -515,6 +515,7 @@ describe('SearchInterceptor', () => { : null ); sessionServiceMock.isRestore.mockReturnValue(!!opts?.isRestore); + sessionServiceMock.getSessionId.mockImplementation(() => opts?.sessionId); fetchMock.mockResolvedValue({ result: 200 }); }; @@ -606,6 +607,41 @@ describe('SearchInterceptor', () => { expect(SearchSessionIncompleteWarning).toBeCalledTimes(0); }); + test('should not show warning if a search outside of session is running', async () => { + setup({ + isRestore: false, + isStored: false, + }); + + const responses = [ + { + time: 10, + value: { + isPartial: false, + isRunning: false, + isRestored: false, + id: 1, + rawResponse: { + took: 1, + }, + }, + }, + ]; + mockFetchImplementation(responses); + + const response = searchInterceptor.search( + {}, + { + sessionId: undefined, + } + ); + response.subscribe({ next, error, complete }); + + await timeTravel(10); + + expect(SearchSessionIncompleteWarning).toBeCalledTimes(0); + }); + test('should show warning once if a search is not available during restore', async () => { setup({ isRestore: true, diff --git a/src/plugins/data/public/search/search_interceptor/search_interceptor.ts b/src/plugins/data/public/search/search_interceptor/search_interceptor.ts index ff3c173fd18cf..180e826b5bc4e 100644 --- a/src/plugins/data/public/search/search_interceptor/search_interceptor.ts +++ b/src/plugins/data/public/search/search_interceptor/search_interceptor.ts @@ -352,8 +352,14 @@ export class SearchInterceptor { ); }), tap((response) => { - if (this.deps.session.isRestore() && response.isRestored === false) { - this.showRestoreWarning(this.deps.session.getSessionId()); + const isSearchInScopeOfSession = + sessionId && sessionId === this.deps.session.getSessionId(); + if ( + isSearchInScopeOfSession && + this.deps.session.isRestore() && + response.isRestored === false + ) { + this.showRestoreWarning(sessionId); } }), finalize(() => { diff --git a/src/plugins/data/public/stubs.ts b/src/plugins/data/public/stubs.ts index 8e790a2991b05..3d160a56bd8cf 100644 --- a/src/plugins/data/public/stubs.ts +++ b/src/plugins/data/public/stubs.ts @@ -7,4 +7,4 @@ */ export * from '../common/stubs'; -export { createStubIndexPattern } from './data_views/data_views/index_pattern.stub'; +export { createStubDataView } from './data_views/data_views/data_view.stub'; diff --git a/src/plugins/data/server/data_views/routes.ts b/src/plugins/data/server/data_views/routes.ts index 32fa50940bca7..9488285fc7e2c 100644 --- a/src/plugins/data/server/data_views/routes.ts +++ b/src/plugins/data/server/data_views/routes.ts @@ -7,7 +7,7 @@ */ import { schema } from '@kbn/config-schema'; -import { HttpServiceSetup, RequestHandlerContext, StartServicesAccessor } from 'kibana/server'; +import { HttpServiceSetup, StartServicesAccessor } from 'kibana/server'; import { IndexPatternsFetcher } from './fetcher'; import { registerCreateIndexPatternRoute } from './routes/create_index_pattern'; import { registerGetIndexPatternRoute } from './routes/get_index_pattern'; @@ -154,7 +154,7 @@ export function registerRoutes( }), }, }, - async (context: RequestHandlerContext, request: any, response: any) => { + async (context, request, response) => { const { asCurrentUser } = context.core.elasticsearch.client; const indexPatterns = new IndexPatternsFetcher(asCurrentUser); const { pattern, interval, look_back: lookBack, meta_fields: metaFields } = request.query; diff --git a/src/plugins/data/server/data_views/saved_objects_client_wrapper.test.ts b/src/plugins/data/server/data_views/saved_objects_client_wrapper.test.ts new file mode 100644 index 0000000000000..bbe857894b3f0 --- /dev/null +++ b/src/plugins/data/server/data_views/saved_objects_client_wrapper.test.ts @@ -0,0 +1,55 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 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 { SavedObjectsClientServerToCommon } from './saved_objects_client_wrapper'; +import { SavedObjectsClientContract } from 'src/core/server'; + +import { DataViewSavedObjectConflictError } from '../../common/data_views'; + +describe('SavedObjectsClientPublicToCommon', () => { + const soClient = { resolve: jest.fn() } as unknown as SavedObjectsClientContract; + + test('get saved object - exactMatch', async () => { + const mockedSavedObject = { + version: 'abc', + }; + soClient.resolve = jest + .fn() + .mockResolvedValue({ outcome: 'exactMatch', saved_object: mockedSavedObject }); + const service = new SavedObjectsClientServerToCommon(soClient); + const result = await service.get('index-pattern', '1'); + expect(result).toStrictEqual(mockedSavedObject); + }); + + test('get saved object - aliasMatch', async () => { + const mockedSavedObject = { + version: 'def', + }; + soClient.resolve = jest + .fn() + .mockResolvedValue({ outcome: 'aliasMatch', saved_object: mockedSavedObject }); + const service = new SavedObjectsClientServerToCommon(soClient); + const result = await service.get('index-pattern', '1'); + expect(result).toStrictEqual(mockedSavedObject); + }); + + test('get saved object - conflict', async () => { + const mockedSavedObject = { + version: 'ghi', + }; + + soClient.resolve = jest + .fn() + .mockResolvedValue({ outcome: 'conflict', saved_object: mockedSavedObject }); + const service = new SavedObjectsClientServerToCommon(soClient); + + await expect(service.get('index-pattern', '1')).rejects.toThrow( + DataViewSavedObjectConflictError + ); + }); +}); diff --git a/src/plugins/data/server/data_views/saved_objects_client_wrapper.ts b/src/plugins/data/server/data_views/saved_objects_client_wrapper.ts index 034f59aa52568..22024cfad9057 100644 --- a/src/plugins/data/server/data_views/saved_objects_client_wrapper.ts +++ b/src/plugins/data/server/data_views/saved_objects_client_wrapper.ts @@ -10,6 +10,7 @@ import { SavedObjectsClientContract, SavedObject } from 'src/core/server'; import { SavedObjectsClientCommon, SavedObjectsClientCommonFindArgs, + DataViewSavedObjectConflictError, } from '../../common/data_views'; export class SavedObjectsClientServerToCommon implements SavedObjectsClientCommon { @@ -23,7 +24,11 @@ export class SavedObjectsClientServerToCommon implements SavedObjectsClientCommo } async get(type: string, id: string) { - return await this.savedObjectClient.get(type, id); + const response = await this.savedObjectClient.resolve(type, id); + if (response.outcome === 'conflict') { + throw new DataViewSavedObjectConflictError(id); + } + return response.saved_object; } async update( type: string, diff --git a/src/plugins/data/server/data_views/ui_settings_wrapper.ts b/src/plugins/data/server/data_views/ui_settings_wrapper.ts index 3b00aab7d6bdd..dce552205db2e 100644 --- a/src/plugins/data/server/data_views/ui_settings_wrapper.ts +++ b/src/plugins/data/server/data_views/ui_settings_wrapper.ts @@ -14,11 +14,11 @@ export class UiSettingsServerToCommon implements UiSettingsCommon { constructor(uiSettings: IUiSettingsClient) { this.uiSettings = uiSettings; } - get(key: string) { + get(key: string): Promise { return this.uiSettings.get(key); } - getAll() { + getAll(): Promise> { return this.uiSettings.getAll(); } diff --git a/src/plugins/data/server/index.ts b/src/plugins/data/server/index.ts index 9d2e94bcf15c0..a17c66c694b2d 100644 --- a/src/plugins/data/server/index.ts +++ b/src/plugins/data/server/index.ts @@ -54,6 +54,7 @@ export { IndexPattern, IndexPatternsService, IndexPatternsService as IndexPatternsCommonService, + DataView, } from '../common'; /** diff --git a/src/plugins/data/server/search/strategies/eql_search/eql_search_strategy.test.ts b/src/plugins/data/server/search/strategies/eql_search/eql_search_strategy.test.ts index 58a5e875f7c93..d32080928d630 100644 --- a/src/plugins/data/server/search/strategies/eql_search/eql_search_strategy.test.ts +++ b/src/plugins/data/server/search/strategies/eql_search/eql_search_strategy.test.ts @@ -132,7 +132,6 @@ describe('EQL search strategy', () => { expect(request).toEqual( expect.objectContaining({ ignore_unavailable: true, - ignore_throttled: true, }) ); }); diff --git a/src/plugins/data/server/search/strategies/ese_search/request_utils.test.ts b/src/plugins/data/server/search/strategies/ese_search/request_utils.test.ts index 272e41e8bf82d..91b323de7c07b 100644 --- a/src/plugins/data/server/search/strategies/ese_search/request_utils.test.ts +++ b/src/plugins/data/server/search/strategies/ese_search/request_utils.test.ts @@ -31,12 +31,12 @@ const getMockSearchSessionsConfig = ({ describe('request utils', () => { describe('getIgnoreThrottled', () => { - test('returns `ignore_throttled` as `true` when `includeFrozen` is `false`', async () => { + test('does not return `ignore_throttled` when `includeFrozen` is `false`', async () => { const mockUiSettingsClient = getMockUiSettingsClient({ [UI_SETTINGS.SEARCH_INCLUDE_FROZEN]: false, }); const result = await getIgnoreThrottled(mockUiSettingsClient); - expect(result.ignore_throttled).toBe(true); + expect(result).not.toHaveProperty('ignore_throttled'); }); test('returns `ignore_throttled` as `false` when `includeFrozen` is `true`', async () => { diff --git a/src/plugins/data/server/search/strategies/ese_search/request_utils.ts b/src/plugins/data/server/search/strategies/ese_search/request_utils.ts index 8bf4473355ccf..e224215571ca9 100644 --- a/src/plugins/data/server/search/strategies/ese_search/request_utils.ts +++ b/src/plugins/data/server/search/strategies/ese_search/request_utils.ts @@ -23,7 +23,7 @@ export async function getIgnoreThrottled( uiSettingsClient: IUiSettingsClient ): Promise> { const includeFrozen = await uiSettingsClient.get(UI_SETTINGS.SEARCH_INCLUDE_FROZEN); - return { ignore_throttled: !includeFrozen }; + return includeFrozen ? { ignore_throttled: false } : {}; } /** diff --git a/src/plugins/discover/public/application/apps/main/components/layout/discover_layout.tsx b/src/plugins/discover/public/application/apps/main/components/layout/discover_layout.tsx index 7e3d7ff10b3a6..c2d09f31e3e0a 100644 --- a/src/plugins/discover/public/application/apps/main/components/layout/discover_layout.tsx +++ b/src/plugins/discover/public/application/apps/main/components/layout/discover_layout.tsx @@ -23,11 +23,7 @@ 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, - indexPatterns as indexPatternsUtils, -} from '../../../../../../../data/public'; +import { esFilters, IndexPatternField } from '../../../../../../../data/public'; import { DiscoverSidebarResponsive } from '../sidebar'; import { DiscoverLayoutProps } from './types'; import { SEARCH_FIELDS_FROM_SOURCE } from '../../../../../../common'; @@ -79,7 +75,7 @@ export function DiscoverLayout({ }, [dataState.fetchStatus]); const timeField = useMemo(() => { - return indexPatternsUtils.isDefault(indexPattern) ? indexPattern.timeFieldName : undefined; + return indexPattern.type !== 'rollup' ? indexPattern.timeFieldName : undefined; }, [indexPattern]); const [isSidebarClosed, setIsSidebarClosed] = useState(false); diff --git a/src/plugins/discover/public/application/apps/main/components/sidebar/__snapshots__/discover_index_pattern_management.test.tsx.snap b/src/plugins/discover/public/application/apps/main/components/sidebar/__snapshots__/discover_index_pattern_management.test.tsx.snap index 3ad902ed22fe8..ebb06e0b2ecd3 100644 --- a/src/plugins/discover/public/application/apps/main/components/sidebar/__snapshots__/discover_index_pattern_management.test.tsx.snap +++ b/src/plugins/discover/public/application/apps/main/components/sidebar/__snapshots__/discover_index_pattern_management.test.tsx.snap @@ -106,7 +106,7 @@ exports[`Discover IndexPattern Management renders correctly 1`] = ` } } selectedIndexPattern={ - IndexPattern { + DataView { "allowNoIndex": false, "deleteFieldFormat": [Function], "fieldAttrs": Object {}, diff --git a/src/plugins/discover/public/application/apps/main/components/top_nav/get_top_nav_links.ts b/src/plugins/discover/public/application/apps/main/components/top_nav/get_top_nav_links.ts index 58a7242974ba2..ba4cd8c3cd524 100644 --- a/src/plugins/discover/public/application/apps/main/components/top_nav/get_top_nav_links.ts +++ b/src/plugins/discover/public/application/apps/main/components/top_nav/get_top_nav_links.ts @@ -112,7 +112,7 @@ export const getTopNavLinks = ({ const sharingData = await getSharingData( searchSource, state.appStateContainer.getState(), - services.uiSettings + services ); services.share.toggleShareContextMenu({ diff --git a/src/plugins/discover/public/application/apps/main/discover_main_route.tsx b/src/plugins/discover/public/application/apps/main/discover_main_route.tsx index 53f95f38c96bd..5141908e44ade 100644 --- a/src/plugins/discover/public/application/apps/main/discover_main_route.tsx +++ b/src/plugins/discover/public/application/apps/main/discover_main_route.tsx @@ -59,7 +59,7 @@ export function DiscoverMainRoute({ services, history }: DiscoverMainProps) { const savedSearchId = id; async function loadDefaultOrCurrentIndexPattern(usedSavedSearch: SavedSearch) { - await data.indexPatterns.ensureDefaultIndexPattern(); + await data.indexPatterns.ensureDefaultDataView(); const { appStateContainer } = getState({ history, uiSettings: config }); const { index } = appStateContainer.getState(); const ip = await loadIndexPattern(index || '', data.indexPatterns, config); diff --git a/src/plugins/discover/public/application/apps/main/utils/get_sharing_data.test.ts b/src/plugins/discover/public/application/apps/main/utils/get_sharing_data.test.ts index 25d0ca5d66eb4..e7205c3f9bc69 100644 --- a/src/plugins/discover/public/application/apps/main/utils/get_sharing_data.test.ts +++ b/src/plugins/discover/public/application/apps/main/utils/get_sharing_data.test.ts @@ -7,32 +7,37 @@ */ import { Capabilities, IUiSettingsClient } from 'kibana/public'; -import { IndexPattern } from 'src/plugins/data/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 { getSharingData, showPublicUrlSwitch } from './get_sharing_data'; describe('getSharingData', () => { - let mockConfig: IUiSettingsClient; + let services: DiscoverServices; beforeEach(() => { - mockConfig = { - get: (key: string) => { - if (key === SORT_DEFAULT_ORDER_SETTING) { - return 'desc'; - } - if (key === DOC_HIDE_TIME_COLUMN_SETTING) { + services = { + data: dataPluginMock.createStartContract(), + uiSettings: { + get: (key: string) => { + if (key === SORT_DEFAULT_ORDER_SETTING) { + return 'desc'; + } + if (key === DOC_HIDE_TIME_COLUMN_SETTING) { + return false; + } return false; - } - return false; + }, }, - } as unknown as IUiSettingsClient; + } as DiscoverServices; }); test('returns valid data for sharing', async () => { const searchSourceMock = createSearchSourceMock({ index: indexPatternMock }); - const result = await getSharingData(searchSourceMock, { columns: [] }, mockConfig); + const result = await getSharingData(searchSourceMock, { columns: [] }, services); expect(result).toMatchInlineSnapshot(` Object { "columns": Array [], @@ -53,7 +58,7 @@ describe('getSharingData', () => { const result = await getSharingData( searchSourceMock, { columns: ['column_a', 'column_b'] }, - mockConfig + services ); expect(result).toMatchInlineSnapshot(` Object { @@ -90,7 +95,7 @@ describe('getSharingData', () => { 'cool-field-6', ], }, - mockConfig + services ); expect(result).toMatchInlineSnapshot(` Object { @@ -116,7 +121,7 @@ describe('getSharingData', () => { }); test('fields conditionally do not have prepended timeField', async () => { - mockConfig = { + services.uiSettings = { get: (key: string) => { if (key === DOC_HIDE_TIME_COLUMN_SETTING) { return true; @@ -141,7 +146,7 @@ describe('getSharingData', () => { 'cool-field-6', ], }, - mockConfig + services ); expect(result).toMatchInlineSnapshot(` Object { diff --git a/src/plugins/discover/public/application/apps/main/utils/get_sharing_data.ts b/src/plugins/discover/public/application/apps/main/utils/get_sharing_data.ts index 65001f49f4d68..420ff0fa11eeb 100644 --- a/src/plugins/discover/public/application/apps/main/utils/get_sharing_data.ts +++ b/src/plugins/discover/public/application/apps/main/utils/get_sharing_data.ts @@ -6,8 +6,10 @@ * Side Public License, v 1. */ -import type { Capabilities, IUiSettingsClient } from 'kibana/public'; -import { ISearchSource } from '../../../../../../data/common'; +import type { Capabilities } from 'kibana/public'; +import type { IUiSettingsClient } from 'src/core/public'; +import type { DataPublicPluginStart } from 'src/plugins/data/public'; +import type { ISearchSource } from 'src/plugins/data/common'; import { DOC_HIDE_TIME_COLUMN_SETTING, SORT_DEFAULT_ORDER_SETTING } from '../../../../../common'; import type { SavedSearch, SortOrder } from '../../../../saved_searches/types'; import { getSortForSearchSource } from '../components/doc_table'; @@ -19,8 +21,9 @@ import { AppState } from '../services/discover_state'; export async function getSharingData( currentSearchSource: ISearchSource, state: AppState | SavedSearch, - config: IUiSettingsClient + services: { uiSettings: IUiSettingsClient; data: DataPublicPluginStart } ) { + const { uiSettings: config, data } = services; const searchSource = currentSearchSource.createCopy(); const index = searchSource.getField('index')!; @@ -28,6 +31,8 @@ export async function getSharingData( 'sort', getSortForSearchSource(state.sort as SortOrder[], index, config.get(SORT_DEFAULT_ORDER_SETTING)) ); + // When sharing externally we preserve relative time values + searchSource.setField('filter', data.query.timefilter.timefilter.createRelativeFilter(index)); searchSource.removeField('highlight'); searchSource.removeField('highlightAll'); searchSource.removeField('aggs'); 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 index 74e63c399743f..4dfcbc7b79712 100644 --- 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 @@ -10,7 +10,6 @@ import { SORT_DEFAULT_ORDER_SETTING } from '../../../../../common'; import { IndexPattern, ISearchSource } from '../../../../../../data/common'; import { SortOrder } from '../../../../saved_searches/types'; import { DiscoverServices } from '../../../../build_services'; -import { indexPatterns as indexPatternsUtils } from '../../../../../../data/public'; import { getSortForSearchSource } from '../components/doc_table'; /** @@ -52,12 +51,9 @@ export function updateSearchSource( // document-like response. .setPreferredSearchStrategyId('default'); - // this is not the default index pattern, it determines that it's not of type rollup - if (indexPatternsUtils.isDefault(indexPattern)) { - searchSource.setField( - 'filter', - data.query.timefilter.timefilter.createRelativeFilter(indexPattern) - ); + 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) { diff --git a/src/plugins/es_ui_shared/__packages_do_not_import__/authorization/components/authorization_provider.tsx b/src/plugins/es_ui_shared/__packages_do_not_import__/authorization/components/authorization_provider.tsx index be49a7697afca..7d8d00156e541 100644 --- a/src/plugins/es_ui_shared/__packages_do_not_import__/authorization/components/authorization_provider.tsx +++ b/src/plugins/es_ui_shared/__packages_do_not_import__/authorization/components/authorization_provider.tsx @@ -13,7 +13,7 @@ import { useRequest } from '../../../public/request'; import { Privileges, Error as CustomError } from '../types'; -interface Authorization { +export interface Authorization { isLoading: boolean; apiError: CustomError | null; privileges: Privileges; diff --git a/src/plugins/es_ui_shared/__packages_do_not_import__/authorization/components/index.ts b/src/plugins/es_ui_shared/__packages_do_not_import__/authorization/components/index.ts index f8eb7e3c7c0c8..75d79a204f141 100644 --- a/src/plugins/es_ui_shared/__packages_do_not_import__/authorization/components/index.ts +++ b/src/plugins/es_ui_shared/__packages_do_not_import__/authorization/components/index.ts @@ -10,6 +10,7 @@ export { AuthorizationProvider, AuthorizationContext, useAuthorizationContext, + Authorization, } from './authorization_provider'; export { WithPrivileges } from './with_privileges'; diff --git a/src/plugins/es_ui_shared/__packages_do_not_import__/authorization/components/with_privileges.test.ts b/src/plugins/es_ui_shared/__packages_do_not_import__/authorization/components/with_privileges.test.ts new file mode 100644 index 0000000000000..243bfdb995f5d --- /dev/null +++ b/src/plugins/es_ui_shared/__packages_do_not_import__/authorization/components/with_privileges.test.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 { convertPrivilegesToArray } from './with_privileges'; + +describe('convertPrivilegesToArray', () => { + test('extracts section and privilege', () => { + expect(convertPrivilegesToArray('index.index_name')).toEqual([['index', 'index_name']]); + expect(convertPrivilegesToArray(['index.index_name', 'cluster.management'])).toEqual([ + ['index', 'index_name'], + ['cluster', 'management'], + ]); + expect(convertPrivilegesToArray('index.index_name.with-many.dots')).toEqual([ + ['index', 'index_name.with-many.dots'], + ]); + }); + + test('throws when it cannot extract section and privilege', () => { + expect(() => { + convertPrivilegesToArray('bad_privilege_string'); + }).toThrow('Required privilege must have the format "section.privilege"'); + }); +}); diff --git a/src/plugins/es_ui_shared/__packages_do_not_import__/authorization/components/with_privileges.tsx b/src/plugins/es_ui_shared/__packages_do_not_import__/authorization/components/with_privileges.tsx index c0e675877c562..6485bd7f45e55 100644 --- a/src/plugins/es_ui_shared/__packages_do_not_import__/authorization/components/with_privileges.tsx +++ b/src/plugins/es_ui_shared/__packages_do_not_import__/authorization/components/with_privileges.tsx @@ -10,13 +10,14 @@ import { MissingPrivileges } from '../types'; import { useAuthorizationContext } from './authorization_provider'; +type Privileges = string | string[]; interface Props { /** * Each required privilege must have the format "section.privilege". * To indicate that *all* privileges from a section are required, we can use the asterix * e.g. "index.*" */ - privileges: string | string[]; + privileges: Privileges; children: (childrenProps: { isLoading: boolean; hasPrivileges: boolean; @@ -26,24 +27,30 @@ interface Props { type Privilege = [string, string]; -const toArray = (value: string | string[]): string[] => +const toArray = (value: Privileges): string[] => Array.isArray(value) ? (value as string[]) : ([value] as string[]); -export const WithPrivileges = ({ privileges: requiredPrivileges, children }: Props) => { - const { isLoading, privileges } = useAuthorizationContext(); - - const privilegesToArray: Privilege[] = toArray(requiredPrivileges).map((p) => { - const [section, privilege] = p.split('.'); - if (!privilege) { - // Oh! we forgot to use the dot "." notation. +export const convertPrivilegesToArray = (privileges: Privileges): Privilege[] => { + return toArray(privileges).map((p) => { + // Since an privilege can contain a dot in its name: + // * `section` needs to be extracted from the beginning of the string until the first dot + // * `privilege` should be everything after the dot + const indexOfFirstPeriod = p.indexOf('.'); + if (indexOfFirstPeriod === -1) { throw new Error('Required privilege must have the format "section.privilege"'); } - return [section, privilege]; + + return [p.slice(0, indexOfFirstPeriod), p.slice(indexOfFirstPeriod + 1)]; }); +}; + +export const WithPrivileges = ({ privileges: requiredPrivileges, children }: Props) => { + const { isLoading, privileges } = useAuthorizationContext(); + const privilegesArray = convertPrivilegesToArray(requiredPrivileges); const hasPrivileges = isLoading ? false - : privilegesToArray.every((privilege) => { + : privilegesArray.every((privilege) => { const [section, requiredPrivilege] = privilege; if (!privileges.missingPrivileges[section]) { // if the section does not exist in our missingPriviledges, everything is OK @@ -61,7 +68,7 @@ export const WithPrivileges = ({ privileges: requiredPrivileges, children }: Pro return !privileges.missingPrivileges[section]!.includes(requiredPrivilege); }); - const privilegesMissing = privilegesToArray.reduce((acc, [section, privilege]) => { + const privilegesMissing = privilegesArray.reduce((acc, [section, privilege]) => { if (privilege === '*') { acc[section] = privileges.missingPrivileges[section] || []; } else if ( diff --git a/src/plugins/es_ui_shared/__packages_do_not_import__/authorization/index.ts b/src/plugins/es_ui_shared/__packages_do_not_import__/authorization/index.ts index e63d98512a2cd..9ccbc5a5cd3df 100644 --- a/src/plugins/es_ui_shared/__packages_do_not_import__/authorization/index.ts +++ b/src/plugins/es_ui_shared/__packages_do_not_import__/authorization/index.ts @@ -14,6 +14,7 @@ export { SectionError, PageError, useAuthorizationContext, + Authorization, } from './components'; export { Privileges, MissingPrivileges, Error } from './types'; diff --git a/src/plugins/es_ui_shared/common/index.ts b/src/plugins/es_ui_shared/common/index.ts index b8cfe0ae48585..1c2955b8e5e28 100644 --- a/src/plugins/es_ui_shared/common/index.ts +++ b/src/plugins/es_ui_shared/common/index.ts @@ -6,4 +6,4 @@ * Side Public License, v 1. */ -export { Privileges, MissingPrivileges } from '../__packages_do_not_import__/authorization'; +export { Privileges, MissingPrivileges } from '../__packages_do_not_import__/authorization/types'; diff --git a/src/plugins/es_ui_shared/public/authorization/index.ts b/src/plugins/es_ui_shared/public/authorization/index.ts index f68ad3da2a4b5..b8fb2f45794ee 100644 --- a/src/plugins/es_ui_shared/public/authorization/index.ts +++ b/src/plugins/es_ui_shared/public/authorization/index.ts @@ -17,4 +17,5 @@ export { PageError, useAuthorizationContext, WithPrivileges, + Authorization, } from '../../__packages_do_not_import__/authorization'; diff --git a/src/plugins/es_ui_shared/public/index.ts b/src/plugins/es_ui_shared/public/index.ts index 9db00bc4be8df..2dc50536ca631 100644 --- a/src/plugins/es_ui_shared/public/index.ts +++ b/src/plugins/es_ui_shared/public/index.ts @@ -45,6 +45,7 @@ export { PageError, Error, useAuthorizationContext, + Authorization, } from './authorization'; export { Forms, ace, GlobalFlyout, XJson }; diff --git a/src/plugins/es_ui_shared/static/forms/helpers/field_validators/index_pattern_field.ts b/src/plugins/es_ui_shared/static/forms/helpers/field_validators/index_pattern_field.ts index 5aadefa6005fa..1b6667fce41ab 100644 --- a/src/plugins/es_ui_shared/static/forms/helpers/field_validators/index_pattern_field.ts +++ b/src/plugins/es_ui_shared/static/forms/helpers/field_validators/index_pattern_field.ts @@ -37,7 +37,7 @@ export const indexPatternField = // Validate illegal characters const errors = indexPatterns.validate(value); - if (errors[indexPatterns.ILLEGAL_CHARACTERS_KEY]) { + if (errors.ILLEGAL_CHARACTERS) { return { code: 'ERR_FIELD_FORMAT', formatType: 'INDEX_PATTERN', @@ -45,8 +45,8 @@ export const indexPatternField = defaultMessage: 'The index pattern contains the invalid {characterListLength, plural, one {character} other {characters}} { characterList }.', values: { - characterList: errors[indexPatterns.ILLEGAL_CHARACTERS_KEY].join(' '), - characterListLength: errors[indexPatterns.ILLEGAL_CHARACTERS_KEY].length, + characterList: errors.ILLEGAL_CHARACTERS.join(' '), + characterListLength: errors.ILLEGAL_CHARACTERS.length, }, }), }; diff --git a/src/plugins/expressions/common/execution/execution.ts b/src/plugins/expressions/common/execution/execution.ts index 0c4185c82dc3c..0bb12951202a5 100644 --- a/src/plugins/expressions/common/execution/execution.ts +++ b/src/plugins/expressions/common/execution/execution.ts @@ -180,7 +180,7 @@ export class Execution< const ast = execution.ast || parseExpression(this.expression); this.state = createExecutionContainer({ - ...executor.state.get(), + ...executor.state, state: 'not-started', ast, }); diff --git a/src/plugins/expressions/common/executor/executor.ts b/src/plugins/expressions/common/executor/executor.ts index 55d3a7b897864..ce411ea94eafe 100644 --- a/src/plugins/expressions/common/executor/executor.ts +++ b/src/plugins/expressions/common/executor/executor.ts @@ -49,7 +49,7 @@ export class TypesRegistry implements IRegistry { } public get(id: string): ExpressionType | null { - return this.executor.state.selectors.getType(id); + return this.executor.getType(id) ?? null; } public toJS(): Record { @@ -71,7 +71,7 @@ export class FunctionsRegistry implements IRegistry { } public get(id: string): ExpressionFunction | null { - return this.executor.state.selectors.getFunction(id); + return this.executor.getFunction(id) ?? null; } public toJS(): Record { @@ -95,22 +95,44 @@ export class Executor = Record; + public readonly container: ExecutorContainer; /** * @deprecated */ - public readonly functions: FunctionsRegistry; + public readonly functions = new FunctionsRegistry(this); /** * @deprecated */ - public readonly types: TypesRegistry; + public readonly types = new TypesRegistry(this); + + protected parent?: Executor; constructor(state?: ExecutorState) { - this.state = createExecutorContainer(state); - this.functions = new FunctionsRegistry(this); - this.types = new TypesRegistry(this); + this.container = createExecutorContainer(state); + } + + public get state(): ExecutorState { + const parent = this.parent?.state; + const state = this.container.get(); + + return { + ...(parent ?? {}), + ...state, + types: { + ...(parent?.types ?? {}), + ...state.types, + }, + functions: { + ...(parent?.functions ?? {}), + ...state.functions, + }, + context: { + ...(parent?.context ?? {}), + ...state.context, + }, + }; } public registerFunction( @@ -119,15 +141,18 @@ export class Executor = Record { - return { ...this.state.get().functions }; + return { + ...(this.parent?.getFunctions() ?? {}), + ...this.container.get().functions, + }; } public registerType( @@ -136,23 +161,30 @@ export class Executor = Record { - return { ...this.state.get().types }; + return { + ...(this.parent?.getTypes() ?? {}), + ...this.container.get().types, + }; } public extendContext(extraContext: Record) { - this.state.transitions.extendContext(extraContext); + this.container.transitions.extendContext(extraContext); } public get context(): Record { - return this.state.selectors.getContext(); + return { + ...(this.parent?.context ?? {}), + ...this.container.selectors.getContext(), + }; } /** @@ -199,18 +231,15 @@ export class Executor = Record { - return asts.map((arg) => { - if (arg && typeof arg === 'object') { - return this.walkAst(arg, action); - } - return arg; - }); - }); + link.arguments = mapValues(fnArgs, (asts) => + asts.map((arg) => + arg != null && typeof arg === 'object' ? this.walkAst(arg, action) : arg + ) + ); action(fn, link); } @@ -275,39 +304,19 @@ export class Executor = Record { - if (!fn.migrations[version]) return link; - const updatedAst = fn.migrations[version](link) as ExpressionAstFunction; - link.arguments = updatedAst.arguments; - link.type = updatedAst.type; + if (!fn.migrations[version]) { + return; + } + + ({ arguments: link.arguments, type: link.type } = fn.migrations[version]( + link + ) as ExpressionAstFunction); }); } public fork(): Executor { - const initialState = this.state.get(); - const fork = new Executor(initialState); - - /** - * Synchronize registry state - make any new types, functions and context - * also available in the forked instance of `Executor`. - */ - this.state.state$.subscribe(({ types, functions, context }) => { - const state = fork.state.get(); - fork.state.set({ - ...state, - types: { - ...types, - ...state.types, - }, - functions: { - ...functions, - ...state.functions, - }, - context: { - ...context, - ...state.context, - }, - }); - }); + const fork = new Executor(); + fork.parent = this; return fork; } diff --git a/src/plugins/expressions/common/service/expressions_services.test.ts b/src/plugins/expressions/common/service/expressions_services.test.ts index db73d300e1273..620917dc64d4d 100644 --- a/src/plugins/expressions/common/service/expressions_services.test.ts +++ b/src/plugins/expressions/common/service/expressions_services.test.ts @@ -17,11 +17,16 @@ describe('ExpressionsService', () => { const expressions = new ExpressionsService(); expect(expressions.setup()).toMatchObject({ + getFunction: expect.any(Function), getFunctions: expect.any(Function), + getRenderer: expect.any(Function), + getRenderers: expect.any(Function), + getType: expect.any(Function), + getTypes: expect.any(Function), registerFunction: expect.any(Function), registerType: expect.any(Function), registerRenderer: expect.any(Function), - run: expect.any(Function), + fork: expect.any(Function), }); }); @@ -30,7 +35,16 @@ describe('ExpressionsService', () => { expressions.setup(); expect(expressions.start()).toMatchObject({ + getFunction: expect.any(Function), getFunctions: expect.any(Function), + getRenderer: expect.any(Function), + getRenderers: expect.any(Function), + getType: expect.any(Function), + getTypes: expect.any(Function), + registerFunction: expect.any(Function), + registerType: expect.any(Function), + registerRenderer: expect.any(Function), + execute: expect.any(Function), run: expect.any(Function), }); }); @@ -54,21 +68,21 @@ describe('ExpressionsService', () => { const service = new ExpressionsService(); const fork = service.fork(); - expect(fork.executor.state.get().types).toEqual(service.executor.state.get().types); + expect(fork.getTypes()).toEqual(service.getTypes()); }); test('fork keeps all functions of the origin service', () => { const service = new ExpressionsService(); const fork = service.fork(); - expect(fork.executor.state.get().functions).toEqual(service.executor.state.get().functions); + expect(fork.getFunctions()).toEqual(service.getFunctions()); }); test('fork keeps context of the origin service', () => { const service = new ExpressionsService(); const fork = service.fork(); - expect(fork.executor.state.get().context).toEqual(service.executor.state.get().context); + expect(fork.executor.state.context).toEqual(service.executor.state.context); }); test('newly registered functions in origin are also available in fork', () => { @@ -82,7 +96,7 @@ describe('ExpressionsService', () => { fn: () => {}, }); - expect(fork.executor.state.get().functions).toEqual(service.executor.state.get().functions); + expect(fork.getFunctions()).toEqual(service.getFunctions()); }); test('newly registered functions in fork are NOT available in origin', () => { @@ -96,14 +110,15 @@ describe('ExpressionsService', () => { fn: () => {}, }); - expect(Object.values(fork.executor.state.get().functions)).toHaveLength( - Object.values(service.executor.state.get().functions).length + 1 + expect(Object.values(fork.getFunctions())).toHaveLength( + Object.values(service.getFunctions()).length + 1 ); }); test('fork can execute an expression with newly registered function', async () => { const service = new ExpressionsService(); const fork = service.fork(); + fork.start(); service.registerFunction({ name: '__test__', @@ -118,5 +133,28 @@ describe('ExpressionsService', () => { expect(result).toBe('123'); }); + + test('throw on fork if the service is already started', async () => { + const service = new ExpressionsService(); + service.start(); + + expect(() => service.fork()).toThrow(); + }); + }); + + describe('.execute()', () => { + test('throw if the service is not started', () => { + const expressions = new ExpressionsService(); + + expect(() => expressions.execute('foo', null)).toThrow(); + }); + }); + + describe('.run()', () => { + test('throw if the service is not started', () => { + const expressions = new ExpressionsService(); + + expect(() => expressions.run('foo', null)).toThrow(); + }); }); }); diff --git a/src/plugins/expressions/common/service/expressions_services.ts b/src/plugins/expressions/common/service/expressions_services.ts index 2be4f5207bb82..f21eaa34d7868 100644 --- a/src/plugins/expressions/common/service/expressions_services.ts +++ b/src/plugins/expressions/common/service/expressions_services.ts @@ -41,22 +41,86 @@ import { * The public contract that `ExpressionsService` provides to other plugins * in Kibana Platform in *setup* life-cycle. */ -export type ExpressionsServiceSetup = Pick< - ExpressionsService, - | 'getFunction' - | 'getFunctions' - | 'getRenderer' - | 'getRenderers' - | 'getType' - | 'getTypes' - | 'registerFunction' - | 'registerRenderer' - | 'registerType' - | 'run' - | 'fork' - | 'extract' - | 'inject' ->; +export interface ExpressionsServiceSetup { + /** + * Get a registered `ExpressionFunction` by its name, which was registered + * using the `registerFunction` method. The returned `ExpressionFunction` + * instance is an internal representation of the function in Expressions + * service - do not mutate that object. + * @deprecated Use start contract instead. + */ + getFunction(name: string): ReturnType; + + /** + * Returns POJO map of all registered expression functions, where keys are + * names of the functions and values are `ExpressionFunction` instances. + * @deprecated Use start contract instead. + */ + getFunctions(): ReturnType; + + /** + * Returns POJO map of all registered expression types, where keys are + * names of the types and values are `ExpressionType` instances. + * @deprecated Use start contract instead. + */ + getTypes(): ReturnType; + + /** + * Create a new instance of `ExpressionsService`. The new instance inherits + * all state of the original `ExpressionsService`, including all expression + * types, expression functions and context. Also, all new types and functions + * registered in the original services AFTER the forking event will be + * available in the forked instance. However, all new types and functions + * registered in the forked instances will NOT be available to the original + * service. + * @param name A fork name that can be used to get fork instance later. + */ + fork(name?: string): ExpressionsService; + + /** + * Register an expression function, which will be possible to execute as + * part of the expression pipeline. + * + * Below we register a function which simply sleeps for given number of + * milliseconds to delay the execution and outputs its input as-is. + * + * ```ts + * expressions.registerFunction({ + * name: 'sleep', + * args: { + * time: { + * aliases: ['_'], + * help: 'Time in milliseconds for how long to sleep', + * types: ['number'], + * }, + * }, + * help: '', + * fn: async (input, args, context) => { + * await new Promise(r => setTimeout(r, args.time)); + * return input; + * }, + * } + * ``` + * + * The actual function is defined in the `fn` key. The function can be *async*. + * It receives three arguments: (1) `input` is the output of the previous function + * or the initial input of the expression if the function is first in chain; + * (2) `args` are function arguments as defined in expression string, that can + * be edited by user (e.g in case of Canvas); (3) `context` is a shared object + * passed to all functions that can be used for side-effects. + */ + registerFunction( + functionDefinition: AnyExpressionFunctionDefinition | (() => AnyExpressionFunctionDefinition) + ): void; + + registerType( + typeDefinition: AnyExpressionTypeDefinition | (() => AnyExpressionTypeDefinition) + ): void; + + registerRenderer( + definition: AnyExpressionRenderDefinition | (() => AnyExpressionRenderDefinition) + ): void; +} export interface ExpressionExecutionParams { searchContext?: SerializableRecord; @@ -97,7 +161,13 @@ export interface ExpressionsServiceStart { * instance is an internal representation of the function in Expressions * service - do not mutate that object. */ - getFunction: (name: string) => ReturnType; + getFunction(name: string): ReturnType; + + /** + * Returns POJO map of all registered expression functions, where keys are + * names of the functions and values are `ExpressionFunction` instances. + */ + getFunctions(): ReturnType; /** * Get a registered `ExpressionRenderer` by its name, which was registered @@ -105,7 +175,13 @@ export interface ExpressionsServiceStart { * instance is an internal representation of the renderer in Expressions * service - do not mutate that object. */ - getRenderer: (name: string) => ReturnType; + getRenderer(name: string): ReturnType; + + /** + * Returns POJO map of all registered expression renderers, where keys are + * names of the renderers and values are `ExpressionRenderer` instances. + */ + getRenderers(): ReturnType; /** * Get a registered `ExpressionType` by its name, which was registered @@ -113,7 +189,13 @@ export interface ExpressionsServiceStart { * instance is an internal representation of the type in Expressions * service - do not mutate that object. */ - getType: (name: string) => ReturnType; + getType(name: string): ReturnType; + + /** + * Returns POJO map of all registered expression types, where keys are + * names of the types and values are `ExpressionType` instances. + */ + getTypes(): ReturnType; /** * Executes expression string or a parsed expression AST and immediately @@ -139,34 +221,23 @@ export interface ExpressionsServiceStart { * expressions.run('...', null, { elasticsearchClient }); * ``` */ - run: ( + run( ast: string | ExpressionAstExpression, input: Input, params?: ExpressionExecutionParams - ) => Observable>; + ): Observable>; /** * Starts expression execution and immediately returns `ExecutionContract` * instance that tracks the progress of the execution and can be used to * interact with the execution. */ - execute: ( + execute( ast: string | ExpressionAstExpression, // This any is for legacy reasons. input: Input, params?: ExpressionExecutionParams - ) => ExecutionContract; - - /** - * Create a new instance of `ExpressionsService`. The new instance inherits - * all state of the original `ExpressionsService`, including all expression - * types, expression functions and context. Also, all new types and functions - * registered in the original services AFTER the forking event will be - * available in the forked instance. However, all new types and functions - * registered in the forked instances will NOT be available to the original - * service. - */ - fork: () => ExpressionsService; + ): ExecutionContract; } export interface ExpressionServiceParams { @@ -193,7 +264,19 @@ export interface ExpressionServiceParams { * * so that JSDoc appears in developers IDE when they use those `plugins.expressions.registerFunction(`. */ -export class ExpressionsService implements PersistableStateService { +export class ExpressionsService + implements + PersistableStateService, + ExpressionsServiceSetup, + ExpressionsServiceStart +{ + /** + * @note Workaround since the expressions service is frozen. + */ + private static started = new WeakSet(); + private children = new Map(); + private parent?: ExpressionsService; + public readonly executor: Executor; public readonly renderers: ExpressionRendererRegistry; @@ -205,94 +288,85 @@ export class ExpressionsService implements PersistableStateService { - * await new Promise(r => setTimeout(r, args.time)); - * return input; - * }, - * } - * ``` - * - * The actual function is defined in the `fn` key. The function can be *async*. - * It receives three arguments: (1) `input` is the output of the previous function - * or the initial input of the expression if the function is first in chain; - * (2) `args` are function arguments as defined in expression string, that can - * be edited by user (e.g in case of Canvas); (3) `context` is a shared object - * passed to all functions that can be used for side-effects. - */ - public readonly registerFunction = ( - functionDefinition: AnyExpressionFunctionDefinition | (() => AnyExpressionFunctionDefinition) - ): void => this.executor.registerFunction(functionDefinition); - - public readonly registerType = ( - typeDefinition: AnyExpressionTypeDefinition | (() => AnyExpressionTypeDefinition) - ): void => this.executor.registerType(typeDefinition); + private isStarted(): boolean { + return !!(ExpressionsService.started.has(this) || this.parent?.isStarted()); + } - public readonly registerRenderer = ( - definition: AnyExpressionRenderDefinition | (() => AnyExpressionRenderDefinition) - ): void => this.renderers.register(definition); + private assertSetup() { + if (this.isStarted()) { + throw new Error('The expression service is already started and can no longer be configured.'); + } + } - public readonly run: ExpressionsServiceStart['run'] = (ast, input, params) => - this.executor.run(ast, input, params); + private assertStart() { + if (!this.isStarted()) { + throw new Error('The expressions service has not started yet.'); + } + } public readonly getFunction: ExpressionsServiceStart['getFunction'] = (name) => this.executor.getFunction(name); - /** - * Returns POJO map of all registered expression functions, where keys are - * names of the functions and values are `ExpressionFunction` instances. - */ - public readonly getFunctions = (): ReturnType => + public readonly getFunctions: ExpressionsServiceStart['getFunctions'] = () => this.executor.getFunctions(); - public readonly getRenderer: ExpressionsServiceStart['getRenderer'] = (name) => - this.renderers.get(name); + public readonly getRenderer: ExpressionsServiceStart['getRenderer'] = (name) => { + this.assertStart(); - /** - * Returns POJO map of all registered expression renderers, where keys are - * names of the renderers and values are `ExpressionRenderer` instances. - */ - public readonly getRenderers = (): ReturnType => - this.renderers.toJS(); + return this.renderers.get(name); + }; - public readonly getType: ExpressionsServiceStart['getType'] = (name) => - this.executor.getType(name); + public readonly getRenderers: ExpressionsServiceStart['getRenderers'] = () => { + this.assertStart(); - /** - * Returns POJO map of all registered expression types, where keys are - * names of the types and values are `ExpressionType` instances. - */ - public readonly getTypes = (): ReturnType => this.executor.getTypes(); + return this.renderers.toJS(); + }; + + public readonly getType: ExpressionsServiceStart['getType'] = (name) => { + this.assertStart(); + + return this.executor.getType(name); + }; + + public readonly getTypes: ExpressionsServiceStart['getTypes'] = () => this.executor.getTypes(); + + public readonly registerFunction: ExpressionsServiceSetup['registerFunction'] = ( + functionDefinition + ) => this.executor.registerFunction(functionDefinition); + + public readonly registerType: ExpressionsServiceSetup['registerType'] = (typeDefinition) => + this.executor.registerType(typeDefinition); + + public readonly registerRenderer: ExpressionsServiceSetup['registerRenderer'] = (definition) => + this.renderers.register(definition); + + public readonly fork: ExpressionsServiceSetup['fork'] = (name) => { + this.assertSetup(); + + const executor = this.executor.fork(); + const renderers = this.renderers; + const fork = new (this.constructor as typeof ExpressionsService)({ executor, renderers }); + fork.parent = this; + + if (name) { + this.children.set(name, fork); + } + + return fork; + }; public readonly execute: ExpressionsServiceStart['execute'] = ((ast, input, params) => { + this.assertStart(); const execution = this.executor.createExecution(ast, params); execution.start(input); + return execution.contract; }) as ExpressionsServiceStart['execute']; - public readonly fork = () => { - const executor = this.executor.fork(); - const renderers = this.renderers; - const fork = new (this.constructor as typeof ExpressionsService)({ executor, renderers }); + public readonly run: ExpressionsServiceStart['run'] = (ast, input, params) => { + this.assertStart(); - return fork; + return this.executor.run(ast, input, params); }; /** @@ -371,8 +445,12 @@ export class ExpressionsService implements PersistableStateService { service.registerFunction(func); } + service.start(); + const moduleMock = { __execution: undefined, __getLastExecution: () => moduleMock.__execution, diff --git a/src/plugins/expressions/public/mocks.tsx b/src/plugins/expressions/public/mocks.tsx index 3a5450fc02837..f2f6a6807f339 100644 --- a/src/plugins/expressions/public/mocks.tsx +++ b/src/plugins/expressions/public/mocks.tsx @@ -16,19 +16,13 @@ export type Start = jest.Mocked; const createSetupContract = (): Setup => { const setupContract: Setup = { - extract: jest.fn(), fork: jest.fn(), getFunction: jest.fn(), getFunctions: jest.fn(), - getRenderer: jest.fn(), - getRenderers: jest.fn(), - getType: jest.fn(), getTypes: jest.fn(), - inject: jest.fn(), registerFunction: jest.fn(), registerRenderer: jest.fn(), registerType: jest.fn(), - run: jest.fn(), }; return setupContract; }; @@ -38,10 +32,12 @@ const createStartContract = (): Start => { execute: jest.fn(), ExpressionLoader: jest.fn(), ExpressionRenderHandler: jest.fn(), - fork: jest.fn(), getFunction: jest.fn(), + getFunctions: jest.fn(), getRenderer: jest.fn(), + getRenderers: jest.fn(), getType: jest.fn(), + getTypes: jest.fn(), loader: jest.fn(), ReactExpressionRenderer: jest.fn((props) => <>), render: jest.fn(), diff --git a/src/plugins/expressions/public/plugin.test.ts b/src/plugins/expressions/public/plugin.test.ts index 1963eb1f1b3f7..61ff0d8b54033 100644 --- a/src/plugins/expressions/public/plugin.test.ts +++ b/src/plugins/expressions/public/plugin.test.ts @@ -32,16 +32,6 @@ describe('ExpressionsPublicPlugin', () => { expect(setup.getFunctions().add.name).toBe('add'); }); }); - - describe('.run()', () => { - test('can execute simple expression', async () => { - const { setup } = await expressionsPluginMock.createPlugin(); - const { result } = await setup - .run('var_set name="foo" value="bar" | var name="foo"', null) - .toPromise(); - expect(result).toBe('bar'); - }); - }); }); describe('start contract', () => { diff --git a/src/plugins/expressions/server/mocks.ts b/src/plugins/expressions/server/mocks.ts index f4379145f6a6c..bf36ab3c5daa9 100644 --- a/src/plugins/expressions/server/mocks.ts +++ b/src/plugins/expressions/server/mocks.ts @@ -13,37 +13,24 @@ import { coreMock } from '../../../core/server/mocks'; export type Setup = jest.Mocked; export type Start = jest.Mocked; -const createSetupContract = (): Setup => { - const setupContract: Setup = { - extract: jest.fn(), - fork: jest.fn(), - getFunction: jest.fn(), - getFunctions: jest.fn(), - getRenderer: jest.fn(), - getRenderers: jest.fn(), - getType: jest.fn(), - getTypes: jest.fn(), - inject: jest.fn(), - registerFunction: jest.fn(), - registerRenderer: jest.fn(), - registerType: jest.fn(), - run: jest.fn(), - }; - return setupContract; -}; - -const createStartContract = (): Start => { - const startContract: Start = { +const createSetupContract = (): Setup => ({ + fork: jest.fn(), + getFunction: jest.fn(), + getFunctions: jest.fn(), + getTypes: jest.fn(), + registerFunction: jest.fn(), + registerRenderer: jest.fn(), + registerType: jest.fn(), +}); + +const createStartContract = (): Start => + ({ execute: jest.fn(), - fork: jest.fn(), getFunction: jest.fn(), getRenderer: jest.fn(), getType: jest.fn(), run: jest.fn(), - }; - - return startContract; -}; + } as unknown as Start); const createPlugin = async () => { const pluginInitializerContext = coreMock.createPluginInitializerContext(); diff --git a/src/plugins/expressions/server/plugin.test.ts b/src/plugins/expressions/server/plugin.test.ts index c41cda36e7623..52ecf1ff9979e 100644 --- a/src/plugins/expressions/server/plugin.test.ts +++ b/src/plugins/expressions/server/plugin.test.ts @@ -24,15 +24,5 @@ describe('ExpressionsServerPlugin', () => { expect(setup.getFunctions().add.name).toBe('add'); }); }); - - describe('.run()', () => { - test('can execute simple expression', async () => { - const { setup } = await expressionsPluginMock.createPlugin(); - const { result } = await setup - .run('var_set name="foo" value="bar" | var name="foo"', null) - .toPromise(); - expect(result).toBe('bar'); - }); - }); }); }); diff --git a/src/plugins/kibana_react/README.md b/src/plugins/kibana_react/README.md index adbdb628ea9dd..5071c6c93f152 100644 --- a/src/plugins/kibana_react/README.md +++ b/src/plugins/kibana_react/README.md @@ -75,20 +75,6 @@ const Demo = ({ kibana }) => { export default withKibana(Demo); ``` -Using `` render prop. - -```tsx -import { UseKibana } from 'kibana-react'; - -const Demo = () => { - return ( - - {(kibana) =>
{kibana.services.uiSettings.get('theme:darkMode') ? 'dark' : 'light'}
} -
- ); -}; -``` - ## `uiSettings` service Wrappers around Core's `uiSettings` service. diff --git a/src/plugins/kibana_react/public/code_editor/code_editor.test.helpers.tsx b/src/plugins/kibana_react/public/code_editor/code_editor.test.helpers.tsx index cee1ecad2196a..e0b868f3a8b08 100644 --- a/src/plugins/kibana_react/public/code_editor/code_editor.test.helpers.tsx +++ b/src/plugins/kibana_react/public/code_editor/code_editor.test.helpers.tsx @@ -9,9 +9,9 @@ import React, { useEffect, KeyboardEventHandler } from 'react'; import { monaco } from '@kbn/monaco'; function createEditorInstance() { - const keyDownListeners: any[] = []; - const didShowListeners: any[] = []; - const didHideListeners: any[] = []; + const keyDownListeners: Array<(e?: unknown) => void> = []; + const didShowListeners: Array<(e?: unknown) => void> = []; + const didHideListeners: Array<(e?: unknown) => void> = []; let areSuggestionsVisible = false; const editorInstance = { @@ -69,7 +69,10 @@ type MockedEditor = ReturnType; export const mockedEditorInstance: MockedEditor = createEditorInstance(); // mock -const mockMonacoEditor = ({ editorWillMount, editorDidMount }: any) => { +const mockMonacoEditor = ({ + editorWillMount, + editorDidMount, +}: Record void>) => { editorWillMount(monaco); useEffect(() => { diff --git a/src/plugins/kibana_react/public/code_editor/code_editor.tsx b/src/plugins/kibana_react/public/code_editor/code_editor.tsx index 29b905c511d4c..07bda19bf6d14 100644 --- a/src/plugins/kibana_react/public/code_editor/code_editor.tsx +++ b/src/plugins/kibana_react/public/code_editor/code_editor.tsx @@ -135,7 +135,9 @@ export const CodeEditor: React.FC = ({ const MonacoEditor: typeof ReactMonacoEditor = useMemo(() => { const isMockedComponent = typeof ReactMonacoEditor === 'function' && ReactMonacoEditor.name === 'JestMockEditor'; - return isMockedComponent ? (ReactMonacoEditor as any)() : ReactMonacoEditor; + return isMockedComponent + ? (ReactMonacoEditor as unknown as () => typeof ReactMonacoEditor)() + : ReactMonacoEditor; }, []); const isReadOnly = options?.readOnly ?? false; diff --git a/src/plugins/kibana_react/public/context/context.tsx b/src/plugins/kibana_react/public/context/context.tsx index 974bd4e894791..c9dc3df90864d 100644 --- a/src/plugins/kibana_react/public/context/context.tsx +++ b/src/plugins/kibana_react/public/context/context.tsx @@ -26,7 +26,7 @@ export const useKibana = (): KibanaReactContextValue< > => useContext(context as unknown as React.Context>); -export const withKibana = }>( +export const withKibana = }>( type: React.ComponentType ): React.FC> => { const EnhancedType: React.FC> = (props: Omit) => { @@ -36,10 +36,6 @@ export const withKibana = return EnhancedType; }; -export const UseKibana: React.FC<{ - children: (kibana: KibanaReactContextValue) => React.ReactNode; -}> = ({ children }) => <>{children(useKibana())}; - export const createKibanaReactContext = ( services: Services ): KibanaReactContext => { @@ -58,7 +54,7 @@ export const createKibanaReactContext = ( () => createKibanaReactContext({ ...services, ...oldValue.services, ...newServices }), [services, oldValue, newServices] ); - return createElement(context.Provider as React.ComponentType, { + return createElement(context.Provider, { value: newValue, children, }); diff --git a/src/plugins/kibana_react/public/context/index.ts b/src/plugins/kibana_react/public/context/index.ts index 12f8bfccbb592..b34951b298836 100644 --- a/src/plugins/kibana_react/public/context/index.ts +++ b/src/plugins/kibana_react/public/context/index.ts @@ -12,6 +12,5 @@ export { KibanaContextProvider, useKibana, withKibana, - UseKibana, } from './context'; export { KibanaReactContext, KibanaReactContextValue, KibanaServices } from './types'; diff --git a/src/plugins/kibana_react/public/page_template/no_data_page/no_data_card/elastic_agent_card.tsx b/src/plugins/kibana_react/public/page_template/no_data_page/no_data_card/elastic_agent_card.tsx index ad3fb0e2e6396..f071bd9fab25a 100644 --- a/src/plugins/kibana_react/public/page_template/no_data_page/no_data_card/elastic_agent_card.tsx +++ b/src/plugins/kibana_react/public/page_template/no_data_page/no_data_card/elastic_agent_card.tsx @@ -26,6 +26,7 @@ export const ElasticAgentCard: FunctionComponent = ({ title, href, button, + layout, ...cardRest }) => { const { @@ -58,7 +59,8 @@ export const ElasticAgentCard: FunctionComponent = ({ image={addBasePath(`${basePathUrl}elastic_agent_card.svg`)} betaBadgeLabel={recommended ? NO_DATA_RECOMMENDED : undefined} footer={footer} - {...(cardRest as any)} + layout={layout as 'vertical' | undefined} + {...cardRest} /> ); }; diff --git a/src/plugins/kibana_react/public/page_template/no_data_page/no_data_card/elastic_beats_card.tsx b/src/plugins/kibana_react/public/page_template/no_data_page/no_data_card/elastic_beats_card.tsx index d0a8ceddb8dc5..0372d12096489 100644 --- a/src/plugins/kibana_react/public/page_template/no_data_page/no_data_card/elastic_beats_card.tsx +++ b/src/plugins/kibana_react/public/page_template/no_data_page/no_data_card/elastic_beats_card.tsx @@ -23,6 +23,7 @@ export const ElasticBeatsCard: FunctionComponent = ({ button, href, solution, // unused for now + layout, ...cardRest }) => { const { @@ -58,7 +59,8 @@ export const ElasticBeatsCard: FunctionComponent = ({ )} betaBadgeLabel={recommended ? NO_DATA_RECOMMENDED : undefined} footer={footer} - {...(cardRest as any)} + layout={layout as 'vertical' | undefined} + {...cardRest} /> ); }; diff --git a/src/plugins/kibana_react/public/page_template/no_data_page/no_data_card/no_data_card.tsx b/src/plugins/kibana_react/public/page_template/no_data_page/no_data_card/no_data_card.tsx index 682d0267e1a7e..9cc38cc5f6038 100644 --- a/src/plugins/kibana_react/public/page_template/no_data_page/no_data_card/no_data_card.tsx +++ b/src/plugins/kibana_react/public/page_template/no_data_page/no_data_card/no_data_card.tsx @@ -6,6 +6,7 @@ * Side Public License, v 1. */ +import { i18n } from '@kbn/i18n'; import React, { FunctionComponent } from 'react'; import { EuiButton, EuiCard, EuiCardProps } from '@elastic/eui'; import { NoDataPageActions, NO_DATA_RECOMMENDED } from '../no_data_page'; @@ -17,6 +18,7 @@ export const NoDataCard: FunctionComponent = ({ recommended, title, button, + layout, ...cardRest }) => { const footer = @@ -25,10 +27,16 @@ export const NoDataCard: FunctionComponent = ({ return ( ); }; diff --git a/src/plugins/kibana_react/public/react_router_navigate/react_router_navigate.tsx b/src/plugins/kibana_react/public/react_router_navigate/react_router_navigate.tsx index db122d034371d..5abd77c9dc9b6 100644 --- a/src/plugins/kibana_react/public/react_router_navigate/react_router_navigate.tsx +++ b/src/plugins/kibana_react/public/react_router_navigate/react_router_navigate.tsx @@ -7,6 +7,7 @@ */ import { ScopedHistory } from 'kibana/public'; +import { MouseEvent } from 'react'; import { History, parsePath } from 'history'; interface LocationObject { @@ -15,10 +16,10 @@ interface LocationObject { hash?: string; } -const isModifiedEvent = (event: any) => +const isModifiedEvent = (event: MouseEvent) => !!(event.metaKey || event.altKey || event.ctrlKey || event.shiftKey); -const isLeftClickEvent = (event: any) => event.button === 0; +const isLeftClickEvent = (event: MouseEvent) => event.button === 0; export const toLocationObject = (to: string | LocationObject) => typeof to === 'string' ? parsePath(to) : to; @@ -34,7 +35,7 @@ export const reactRouterNavigate = ( export const reactRouterOnClickHandler = (history: ScopedHistory | History, to: string | LocationObject, onClickCallback?: Function) => - (event: any) => { + (event: MouseEvent) => { if (onClickCallback) { onClickCallback(event); } @@ -43,7 +44,9 @@ export const reactRouterOnClickHandler = return; } - if (event.target.getAttribute('target')) { + if ( + (event.target as unknown as { getAttribute: (a: string) => unknown })?.getAttribute('target') + ) { return; } diff --git a/src/plugins/kibana_react/public/split_panel/containers/panel_container.tsx b/src/plugins/kibana_react/public/split_panel/containers/panel_container.tsx index 86f73f9331881..69beb565ad857 100644 --- a/src/plugins/kibana_react/public/split_panel/containers/panel_container.tsx +++ b/src/plugins/kibana_react/public/split_panel/containers/panel_container.tsx @@ -17,7 +17,7 @@ export interface Props { children: ReactNode; className?: string; resizerClassName?: string; - onPanelWidthChange?: (arrayOfPanelWidths: number[]) => any; + onPanelWidthChange?: (arrayOfPanelWidths: number[]) => void; } interface State { diff --git a/src/plugins/kibana_react/public/split_panel/context.tsx b/src/plugins/kibana_react/public/split_panel/context.tsx index bad39f8bd5ef9..e236b5037e7f3 100644 --- a/src/plugins/kibana_react/public/split_panel/context.tsx +++ b/src/plugins/kibana_react/public/split_panel/context.tsx @@ -12,7 +12,7 @@ import { PanelRegistry } from './registry'; const PanelContext = createContext({ registry: new PanelRegistry() }); interface ContextProps { - children: any; + children: JSX.Element; registry: PanelRegistry; } diff --git a/src/plugins/kibana_react/public/table_list_view/table_list_view.tsx b/src/plugins/kibana_react/public/table_list_view/table_list_view.tsx index 30d09f4bf8657..18a88790b425a 100644 --- a/src/plugins/kibana_react/public/table_list_view/table_list_view.tsx +++ b/src/plugins/kibana_react/public/table_list_view/table_list_view.tsx @@ -26,17 +26,13 @@ import React from 'react'; import { KibanaPageTemplate } from '../page_template'; import { toMountPoint } from '../util'; -interface Item { - id?: string; -} - -export interface TableListViewProps { +export interface TableListViewProps { createItem?(): void; - deleteItems?(items: object[]): Promise; - editItem?(item: object): void; + deleteItems?(items: V[]): Promise; + editItem?(item: V): void; entityName: string; entityNamePlural: string; - findItems(query: string): Promise<{ total: number; hits: object[] }>; + findItems(query: string): Promise<{ total: number; hits: V[] }>; listingLimit: number; initialFilter: string; initialPageSize: number; @@ -44,7 +40,7 @@ export interface TableListViewProps { * Should be an EuiEmptyPrompt (but TS doesn't support this typing) */ emptyPrompt?: JSX.Element; - tableColumns: Array>; + tableColumns: Array>; tableListTitle: string; toastNotifications: ToastsStart; /** @@ -63,8 +59,8 @@ export interface TableListViewProps { searchFilters?: SearchFilterConfig[]; } -export interface TableListViewState { - items: object[]; +export interface TableListViewState { + items: V[]; hasInitialFetchReturned: boolean; isFetchingItems: boolean; isDeletingItems: boolean; @@ -81,11 +77,14 @@ export interface TableListViewState { // and not supporting server-side paging. // This component does not try to tackle these problems (yet) and is just feature matching the legacy component // TODO support server side sorting/paging once title and description are sortable on the server. -class TableListView extends React.Component { +class TableListView extends React.Component< + TableListViewProps, + TableListViewState +> { private pagination = {}; private _isMounted = false; - constructor(props: TableListViewProps) { + constructor(props: TableListViewProps) { super(props); this.pagination = { @@ -134,7 +133,7 @@ class TableListView extends React.Component(response.hits, 'title') : response.hits, totalItems: response.total, showLimitError: response.total > this.props.listingLimit, }); @@ -404,17 +403,17 @@ class TableListView extends React.Component { + onSelectionChange: (obj: V[]) => { this.setState({ selectedIds: obj - .map((item) => item.id) - .filter((id: undefined | string): id is string => Boolean(id)), + .map((item) => (item as Record)?.id) + .filter((id): id is string => Boolean(id)), }); }, } : undefined; - const actions: EuiTableActionsColumnType['actions'] = [ + const actions: EuiTableActionsColumnType['actions'] = [ { name: i18n.translate('kibana-react.tableListView.listing.table.editActionName', { defaultMessage: 'Edit', @@ -427,7 +426,7 @@ class TableListView extends React.Component !error, + enabled: (v) => !(v as unknown as { error: string })?.error, onClick: this.props.editItem, }, ]; diff --git a/src/plugins/kibana_react/public/util/mount_point_portal.tsx b/src/plugins/kibana_react/public/util/mount_point_portal.tsx index 07d2edfa717b9..d0753c68d1f2d 100644 --- a/src/plugins/kibana_react/public/util/mount_point_portal.tsx +++ b/src/plugins/kibana_react/public/util/mount_point_portal.tsx @@ -60,12 +60,12 @@ export const MountPointPortal: React.FC = ({ children, se } }; -class MountPointPortalErrorBoundary extends Component<{}, { error?: any }> { +class MountPointPortalErrorBoundary extends Component<{}, { error?: unknown }> { state = { error: undefined, }; - static getDerivedStateFromError(error: any) { + static getDerivedStateFromError(error: unknown) { return { error }; } diff --git a/src/plugins/kibana_react/public/util/test_helpers/react_mount_serializer.ts b/src/plugins/kibana_react/public/util/test_helpers/react_mount_serializer.ts index 6438ff0b8d98f..4d1575bf16184 100644 --- a/src/plugins/kibana_react/public/util/test_helpers/react_mount_serializer.ts +++ b/src/plugins/kibana_react/public/util/test_helpers/react_mount_serializer.ts @@ -6,11 +6,14 @@ * Side Public License, v 1. */ -export function test(value: any) { +export function test(value?: Record) { return value && value.__reactMount__; } -export function print(value: any, serialize: any) { +export function print( + value: Record, + serialize: (args: Record) => { replace: (s1: string, s2: string) => unknown } +) { // there is no proper way to correctly indent multiline values // so the trick here is to use the Object representation and rewriting the root object name return serialize({ diff --git a/src/plugins/kibana_utils/public/index.ts b/src/plugins/kibana_utils/public/index.ts index b0e0b8b2298ab..0ac4c61f4a711 100644 --- a/src/plugins/kibana_utils/public/index.ts +++ b/src/plugins/kibana_utils/public/index.ts @@ -6,6 +6,9 @@ * Side Public License, v 1. */ +import { PluginInitializerContext } from 'src/core/public'; +import { KibanaUtilsPublicPlugin } from './plugin'; + // TODO: https://github.com/elastic/kibana/issues/109893 /* eslint-disable @kbn/eslint/no_export_all */ @@ -78,10 +81,8 @@ export { export { applyDiff } from './state_management/utils/diff_object'; export { createStartServicesGetter, StartServicesGetter } from './core/create_start_service_getter'; -/** dummy plugin, we just want kibanaUtils to have its own bundle */ -export function plugin() { - return new (class KibanaUtilsPlugin { - setup() {} - start() {} - })(); +export { KibanaUtilsSetup } from './plugin'; + +export function plugin(initializerContext: PluginInitializerContext) { + return new KibanaUtilsPublicPlugin(initializerContext); } diff --git a/src/plugins/kibana_utils/public/mocks.ts b/src/plugins/kibana_utils/public/mocks.ts new file mode 100644 index 0000000000000..a537c2fc74e90 --- /dev/null +++ b/src/plugins/kibana_utils/public/mocks.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 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 { KibanaUtilsSetup, KibanaUtilsStart } from './plugin'; + +export type Setup = jest.Mocked; +export type Start = jest.Mocked; + +const createSetupContract = (): Setup => { + return { + setVersion: jest.fn(), + }; +}; + +const createStartContract = (): Start => { + return undefined; +}; + +export const kibanaUtilsPluginMock = { + createSetupContract, + createStartContract, +}; diff --git a/src/plugins/kibana_utils/public/plugin.ts b/src/plugins/kibana_utils/public/plugin.ts new file mode 100644 index 0000000000000..b255aa34ccfdb --- /dev/null +++ b/src/plugins/kibana_utils/public/plugin.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 { PluginInitializerContext, CoreSetup, CoreStart, Plugin } from 'src/core/public'; +import { History } from 'history'; +import { setVersion } from './set_version'; + +export interface KibanaUtilsSetup { + setVersion: (history: Pick) => void; +} + +export type KibanaUtilsStart = undefined; + +export class KibanaUtilsPublicPlugin implements Plugin { + private readonly version: string; + + constructor(initializerContext: PluginInitializerContext) { + this.version = initializerContext.env.packageInfo.version; + } + + public setup(core: CoreSetup): KibanaUtilsSetup { + return { + setVersion: this.setVersion, + }; + } + + public start(core: CoreStart): KibanaUtilsStart { + return undefined; + } + + public stop() {} + + private setVersion = (history: Pick) => { + setVersion(history, this.version); + }; +} diff --git a/src/plugins/kibana_utils/public/set_version.test.ts b/src/plugins/kibana_utils/public/set_version.test.ts new file mode 100644 index 0000000000000..eb70d889d0f03 --- /dev/null +++ b/src/plugins/kibana_utils/public/set_version.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 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 { History } from 'history'; +import { setVersion } from './set_version'; + +describe('setVersion', () => { + test('sets version, if one is not set', () => { + const history: Pick = { + location: { + hash: '', + search: '', + pathname: '/', + state: {}, + }, + replace: jest.fn(), + }; + setVersion(history, '1.2.3'); + + expect(history.replace).toHaveBeenCalledTimes(1); + expect(history.replace).toHaveBeenCalledWith('/?_v=1.2.3'); + }); + + test('overwrites, if version already set to a different value', () => { + const history: Pick = { + location: { + hash: '/view/dashboards', + search: 'a=b&_v=7.16.6', + pathname: '/foo/bar', + state: {}, + }, + replace: jest.fn(), + }; + setVersion(history, '8.0.0'); + + expect(history.replace).toHaveBeenCalledTimes(1); + expect(history.replace).toHaveBeenCalledWith('/foo/bar?a=b&_v=8.0.0#/view/dashboards'); + }); + + test('does nothing, if version already set to correct value', () => { + const history: Pick = { + location: { + hash: '/view/dashboards', + search: 'a=b&_v=8.0.0', + pathname: '/foo/bar', + state: {}, + }, + replace: jest.fn(), + }; + setVersion(history, '8.0.0'); + + expect(history.replace).toHaveBeenCalledTimes(0); + }); +}); diff --git a/src/plugins/kibana_utils/public/set_version.ts b/src/plugins/kibana_utils/public/set_version.ts new file mode 100644 index 0000000000000..b3acb39ed5134 --- /dev/null +++ b/src/plugins/kibana_utils/public/set_version.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 { History } from 'history'; + +export const setVersion = (history: Pick, version: string) => { + const search = new URLSearchParams(history.location.search); + if (search.get('_v') === version) return; + search.set('_v', version); + const path = + history.location.pathname + + '?' + + search.toString() + + (history.location.hash ? '#' + history.location.hash : ''); + history.replace(path); +}; diff --git a/src/plugins/kibana_utils/server/index.ts b/src/plugins/kibana_utils/server/index.ts index 483c5aa92b45e..42847042be151 100644 --- a/src/plugins/kibana_utils/server/index.ts +++ b/src/plugins/kibana_utils/server/index.ts @@ -15,6 +15,7 @@ export { Get, Set, url, + mergeMigrationFunctionMaps, } from '../common'; export { KbnServerError, reportServerError, getKbnServerError } from './report_server_error'; diff --git a/src/plugins/presentation_util/README.mdx b/src/plugins/presentation_util/README.mdx index 35b80e3634534..575e8002e6eb8 100755 --- a/src/plugins/presentation_util/README.mdx +++ b/src/plugins/presentation_util/README.mdx @@ -162,7 +162,7 @@ Once your services and providers are defined, and you have at least one set of f import { pluginServices } from './services'; import { registry } from './services/kibana'; - public async start( + public start( coreStart: CoreStart, startPlugins: StartDeps ): Promise { 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 2f45ee211c8c9..bd8d69d6b693e 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 @@ -20,7 +20,7 @@ import { coreMock } from '../../../../core/public/mocks'; import { dataPluginMock, createSearchSourceMock } from '../../../../plugins/data/public/mocks'; import { createStubIndexPattern } from '../../../../plugins/data/common/stubs'; import { SavedObjectAttributes, SimpleSavedObject } from 'kibana/public'; -import { IndexPattern } from '../../../data/common'; +import { DataView } from '../../../data/common'; import { savedObjectsDecoratorRegistryMock } from './decorators/registry.mock'; describe('Saved Object', () => { @@ -725,7 +725,7 @@ describe('Saved Object', () => { type: 'dashboard', afterESResp: afterESRespCallback, searchSource: true, - indexPattern: { id: indexPatternId } as IndexPattern, + indexPattern: { id: indexPatternId } as DataView, }; stubESResponse( @@ -752,7 +752,7 @@ describe('Saved Object', () => { return savedObject.init!().then(() => { expect(afterESRespCallback).toHaveBeenCalled(); const index = savedObject.searchSource!.getField('index'); - expect(index instanceof IndexPattern).toBe(true); + expect(index instanceof DataView).toBe(true); expect(index!.id).toEqual(indexPatternId); }); }); @@ -765,7 +765,7 @@ describe('Saved Object', () => { type: 'dashboard', afterESResp: afterESRespCallback, searchSource: false, - indexPattern: { id: indexPatternId } as IndexPattern, + indexPattern: { id: indexPatternId } as DataView, }; stubESResponse(getMockedDocResponse(indexPatternId)); diff --git a/src/plugins/saved_objects_management/server/routes/get_allowed_types.ts b/src/plugins/saved_objects_management/server/routes/get_allowed_types.ts index 13adf2764b555..ce371ea590230 100644 --- a/src/plugins/saved_objects_management/server/routes/get_allowed_types.ts +++ b/src/plugins/saved_objects_management/server/routes/get_allowed_types.ts @@ -17,6 +17,7 @@ export const registerGetAllowedTypesRoute = (router: IRouter) => { async (context, req, res) => { const allowedTypes = context.core.savedObjects.typeRegistry .getImportableAndExportableTypes() + .filter((type) => type.management!.visibleInManagement ?? true) .map((type) => type.name); return res.ok({ diff --git a/src/plugins/vis_default_editor/public/components/controls/date_ranges.test.tsx b/src/plugins/vis_default_editor/public/components/controls/date_ranges.test.tsx index f10c695662546..02cc0aadfff61 100644 --- a/src/plugins/vis_default_editor/public/components/controls/date_ranges.test.tsx +++ b/src/plugins/vis_default_editor/public/components/controls/date_ranges.test.tsx @@ -55,6 +55,9 @@ describe('DateRangesParamEditor component', () => { }); it('should validate range values with date math', function () { + const mockedConsoleWarn = jest.spyOn(console, 'warn'); // mocked console.warn to avoid console messages when running tests + mockedConsoleWarn.mockImplementation(() => {}); + const component = mountWithIntl(); // should allow empty values @@ -86,5 +89,7 @@ describe('DateRangesParamEditor component', () => { component.setProps({ value: [{ from: '5/5/2005+3d' }] }); expect(setValidity).toHaveBeenNthCalledWith(10, false); + + mockedConsoleWarn.mockRestore(); }); }); diff --git a/src/plugins/vis_default_editor/public/components/controls/percentiles.test.tsx b/src/plugins/vis_default_editor/public/components/controls/percentiles.test.tsx index 849253840f18c..c009196c20d8c 100644 --- a/src/plugins/vis_default_editor/public/components/controls/percentiles.test.tsx +++ b/src/plugins/vis_default_editor/public/components/controls/percentiles.test.tsx @@ -9,10 +9,22 @@ import React from 'react'; import { AggParamEditorProps } from '../agg_param_props'; import { IAggConfig } from 'src/plugins/data/public'; -import { mount } from 'enzyme'; +import { mountWithIntl as mount } from '@kbn/test/jest'; import { PercentilesEditor } from './percentiles'; import { EditorVisState } from '../sidebar/state/reducers'; +// mocking random id generator function +jest.mock('@elastic/eui', () => { + const original = jest.requireActual('@elastic/eui'); + + return { + ...original, + htmlIdGenerator: (fn: unknown) => { + let counter = 0; + return () => counter++; + }, + }; +}); describe('PercentilesEditor component', () => { let setValue: jest.Mock; let setValidity: jest.Mock; diff --git a/src/plugins/vis_type_timeseries/tsconfig.json b/src/plugins/vis_type_timeseries/tsconfig.json deleted file mode 100644 index 68097d8cff786..0000000000000 --- a/src/plugins/vis_type_timeseries/tsconfig.json +++ /dev/null @@ -1,26 +0,0 @@ -{ - "extends": "../../../tsconfig.base.json", - "compilerOptions": { - "outDir": "./target/types", - "emitDeclarationOnly": true, - "declaration": true, - "declarationMap": true - }, - "include": [ - "common/**/*", - "public/**/*", - "server/**/*", - "*.ts" - ], - "references": [ - { "path": "../../core/tsconfig.json" }, - { "path": "../charts/tsconfig.json" }, - { "path": "../data/tsconfig.json" }, - { "path": "../expressions/tsconfig.json" }, - { "path": "../visualizations/tsconfig.json" }, - { "path": "../visualize/tsconfig.json" }, - { "path": "../kibana_utils/tsconfig.json" }, - { "path": "../kibana_react/tsconfig.json" }, - { "path": "../usage_collection/tsconfig.json" }, - ] -} diff --git a/src/plugins/vis_types/pie/config.ts b/src/plugins/vis_types/pie/config.ts new file mode 100644 index 0000000000000..b831d26854c30 --- /dev/null +++ b/src/plugins/vis_types/pie/config.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 { schema, TypeOf } from '@kbn/config-schema'; + +export const configSchema = schema.object({ + enabled: schema.boolean({ defaultValue: true }), +}); + +export type ConfigSchema = TypeOf; diff --git a/src/plugins/vis_types/pie/server/index.ts b/src/plugins/vis_types/pie/server/index.ts index 201071fbb5fca..1e92bedb3d11c 100644 --- a/src/plugins/vis_types/pie/server/index.ts +++ b/src/plugins/vis_types/pie/server/index.ts @@ -5,6 +5,13 @@ * in compliance with, at your election, the Elastic License 2.0 or the Server * Side Public License, v 1. */ + +import { PluginConfigDescriptor } from 'src/core/server'; +import { configSchema, ConfigSchema } from '../config'; import { VisTypePieServerPlugin } from './plugin'; +export const config: PluginConfigDescriptor = { + schema: configSchema, +}; + export const plugin = () => new VisTypePieServerPlugin(); diff --git a/src/plugins/vis_types/pie/tsconfig.json b/src/plugins/vis_types/pie/tsconfig.json index 9a0a3418d72db..99e25a4eba632 100644 --- a/src/plugins/vis_types/pie/tsconfig.json +++ b/src/plugins/vis_types/pie/tsconfig.json @@ -9,7 +9,8 @@ "include": [ "common/**/*", "public/**/*", - "server/**/*" + "server/**/*", + "*.ts" ], "references": [ { "path": "../../../core/tsconfig.json" }, diff --git a/src/plugins/vis_types/table/config.ts b/src/plugins/vis_types/table/config.ts index b831d26854c30..c47a14395c3b4 100644 --- a/src/plugins/vis_types/table/config.ts +++ b/src/plugins/vis_types/table/config.ts @@ -10,6 +10,8 @@ import { schema, TypeOf } from '@kbn/config-schema'; export const configSchema = schema.object({ enabled: schema.boolean({ defaultValue: true }), + /** @deprecated **/ + legacyVisEnabled: schema.boolean({ defaultValue: false }), }); export type ConfigSchema = TypeOf; diff --git a/src/plugins/vis_types/table/server/index.ts b/src/plugins/vis_types/table/server/index.ts index b98fdd9c445db..eed1134f3ff48 100644 --- a/src/plugins/vis_types/table/server/index.ts +++ b/src/plugins/vis_types/table/server/index.ts @@ -14,8 +14,10 @@ import { registerVisTypeTableUsageCollector } from './usage_collector'; export const config: PluginConfigDescriptor = { schema: configSchema, - deprecations: ({ renameFromRoot }) => [ + deprecations: ({ renameFromRoot, unused }) => [ renameFromRoot('table_vis.enabled', 'vis_type_table.enabled'), + // Unused property which should be removed after releasing Kibana v8.0: + unused('legacyVisEnabled'), ], }; diff --git a/src/plugins/vis_types/timelion/config.ts b/src/plugins/vis_types/timelion/config.ts index cfd3d13c277e9..629b11cc95b00 100644 --- a/src/plugins/vis_types/timelion/config.ts +++ b/src/plugins/vis_types/timelion/config.ts @@ -11,6 +11,11 @@ import { schema, TypeOf } from '@kbn/config-schema'; export const configSchema = schema.object({ graphiteUrls: schema.maybe(schema.arrayOf(schema.string())), enabled: schema.boolean({ defaultValue: true }), + // should be removed in v8.0 + /** @deprecated **/ + ui: schema.object({ + enabled: schema.boolean({ defaultValue: true }), + }), }); export type ConfigSchema = TypeOf; diff --git a/src/plugins/vis_types/timelion/public/helpers/timelion_request_handler.ts b/src/plugins/vis_types/timelion/public/helpers/timelion_request_handler.ts index e44d74cfd72ab..e9a076b4dc832 100644 --- a/src/plugins/vis_types/timelion/public/helpers/timelion_request_handler.ts +++ b/src/plugins/vis_types/timelion/public/helpers/timelion_request_handler.ts @@ -120,7 +120,7 @@ export function getTimelionRequestHandler({ const err = new Error( `${i18n.translate('timelion.requestHandlerErrorTitle', { defaultMessage: 'Timelion request error', - })}: ${e.body.title} ${e.body.message}` + })}:${e.body.title ? ' ' + e.body.title : ''} ${e.body.message}` ); err.stack = e.stack; throw err; 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 b7bdbcdcb57a6..3710d015f3f69 100644 --- a/src/plugins/vis_types/timelion/server/handlers/chain_runner.js +++ b/src/plugins/vis_types/timelion/server/handlers/chain_runner.js @@ -25,7 +25,15 @@ export default function chainRunner(tlConfig) { let sheet; function throwWithCell(cell, exception) { - throw new Error(' in cell #' + (cell + 1) + ': ' + exception.message); + throw new Error( + i18n.translate('timelion.serverSideErrors.errorInCell', { + defaultMessage: ' in cell #{number}: {message}', + values: { + number: cell + 1, + message: exception.message, + }, + }) + ); } // Invokes a modifier function, resolving arguments into series as needed diff --git a/src/plugins/vis_types/timelion/server/index.ts b/src/plugins/vis_types/timelion/server/index.ts index 396ef8b61c7bc..ef7baf981de1a 100644 --- a/src/plugins/vis_types/timelion/server/index.ts +++ b/src/plugins/vis_types/timelion/server/index.ts @@ -12,6 +12,14 @@ import { TimelionPlugin } from './plugin'; export const config: PluginConfigDescriptor = { schema: configSchema, + deprecations: ({ renameFromRoot, unused }) => [ + renameFromRoot('timelion_vis.enabled', 'vis_type_timelion.enabled'), + renameFromRoot('timelion.enabled', 'vis_type_timelion.enabled'), + renameFromRoot('timelion.graphiteUrls', 'vis_type_timelion.graphiteUrls'), + // Unused properties which should be removed after releasing Kibana v8.0: + renameFromRoot('timelion.ui.enabled', 'vis_type_timelion.ui.enabled', { silent: true }), + unused('ui.enabled'), + ], }; export const plugin = (initializerContext: PluginInitializerContext) => diff --git a/src/plugins/vis_types/timelion/server/routes/run.ts b/src/plugins/vis_types/timelion/server/routes/run.ts index b3ab3c61c15d8..b8c0ce4ea6599 100644 --- a/src/plugins/vis_types/timelion/server/routes/run.ts +++ b/src/plugins/vis_types/timelion/server/routes/run.ts @@ -94,15 +94,18 @@ export function runRoute( allowedGraphiteUrls: configManager.getGraphiteUrls(), esShardTimeout: configManager.getEsShardTimeout(), }); - const chainRunner = chainRunnerFn(tlConfig); - const sheet = await Bluebird.all(chainRunner.processRequest(request.body)); - - return response.ok({ - body: { - sheet, - stats: chainRunner.getStats(), - }, - }); + try { + const chainRunner = chainRunnerFn(tlConfig); + const sheet = await Bluebird.all(chainRunner.processRequest(request.body)); + return response.ok({ + body: { + sheet, + stats: chainRunner.getStats(), + }, + }); + } catch (e) { + return response.badRequest({ body: { message: e.message } }); + } }) ); } diff --git a/src/plugins/vis_type_timeseries/common/__mocks__/index_patterns_utils.ts b/src/plugins/vis_types/timeseries/common/__mocks__/index_patterns_utils.ts similarity index 100% rename from src/plugins/vis_type_timeseries/common/__mocks__/index_patterns_utils.ts rename to src/plugins/vis_types/timeseries/common/__mocks__/index_patterns_utils.ts diff --git a/src/plugins/vis_type_timeseries/common/agg_utils.test.ts b/src/plugins/vis_types/timeseries/common/agg_utils.test.ts similarity index 99% rename from src/plugins/vis_type_timeseries/common/agg_utils.test.ts rename to src/plugins/vis_types/timeseries/common/agg_utils.test.ts index 63d81e2c43d40..21fa870c2ca04 100644 --- a/src/plugins/vis_type_timeseries/common/agg_utils.test.ts +++ b/src/plugins/vis_types/timeseries/common/agg_utils.test.ts @@ -13,7 +13,7 @@ import { getAggsByPredicate, getAggsByType, } from './agg_utils'; -import { METRIC_TYPES } from '../../data/common'; +import { METRIC_TYPES } from '../../../data/common'; import { TSVB_METRIC_TYPES } from './enums'; import type { Metric } from './types'; diff --git a/src/plugins/vis_type_timeseries/common/agg_utils.ts b/src/plugins/vis_types/timeseries/common/agg_utils.ts similarity index 99% rename from src/plugins/vis_type_timeseries/common/agg_utils.ts rename to src/plugins/vis_types/timeseries/common/agg_utils.ts index 2f0488bdc4dbe..71f13542b10f4 100644 --- a/src/plugins/vis_type_timeseries/common/agg_utils.ts +++ b/src/plugins/vis_types/timeseries/common/agg_utils.ts @@ -9,7 +9,7 @@ import { i18n } from '@kbn/i18n'; import { filter } from 'lodash'; import { Assign } from 'utility-types'; -import { METRIC_TYPES } from '../../data/common'; +import { METRIC_TYPES } from '../../../data/common'; import { TSVB_METRIC_TYPES } from './enums'; import type { Metric, MetricType } from './types'; diff --git a/src/plugins/vis_type_timeseries/common/basic_aggs.ts b/src/plugins/vis_types/timeseries/common/basic_aggs.ts similarity index 100% rename from src/plugins/vis_type_timeseries/common/basic_aggs.ts rename to src/plugins/vis_types/timeseries/common/basic_aggs.ts diff --git a/src/plugins/vis_type_timeseries/common/calculate_label.test.ts b/src/plugins/vis_types/timeseries/common/calculate_label.test.ts similarity index 100% rename from src/plugins/vis_type_timeseries/common/calculate_label.test.ts rename to src/plugins/vis_types/timeseries/common/calculate_label.test.ts diff --git a/src/plugins/vis_type_timeseries/common/calculate_label.ts b/src/plugins/vis_types/timeseries/common/calculate_label.ts similarity index 98% rename from src/plugins/vis_type_timeseries/common/calculate_label.ts rename to src/plugins/vis_types/timeseries/common/calculate_label.ts index d054698536b5b..e5cf053d310aa 100644 --- a/src/plugins/vis_type_timeseries/common/calculate_label.ts +++ b/src/plugins/vis_types/timeseries/common/calculate_label.ts @@ -10,7 +10,7 @@ import { includes, startsWith } from 'lodash'; import { i18n } from '@kbn/i18n'; import { getMetricLabel } from './agg_utils'; import { extractFieldLabel } from './fields_utils'; -import { METRIC_TYPES } from '../../data/common'; +import { METRIC_TYPES } from '../../../data/common'; import { TSVB_METRIC_TYPES } from './enums'; import type { Metric, SanitizedFieldType } from './types'; diff --git a/src/plugins/vis_type_timeseries/common/constants.ts b/src/plugins/vis_types/timeseries/common/constants.ts similarity index 100% rename from src/plugins/vis_type_timeseries/common/constants.ts rename to src/plugins/vis_types/timeseries/common/constants.ts diff --git a/src/plugins/vis_type_timeseries/common/empty_label.ts b/src/plugins/vis_types/timeseries/common/empty_label.ts similarity index 100% rename from src/plugins/vis_type_timeseries/common/empty_label.ts rename to src/plugins/vis_types/timeseries/common/empty_label.ts diff --git a/src/plugins/vis_type_timeseries/common/enums/index.ts b/src/plugins/vis_types/timeseries/common/enums/index.ts similarity index 100% rename from src/plugins/vis_type_timeseries/common/enums/index.ts rename to src/plugins/vis_types/timeseries/common/enums/index.ts diff --git a/src/plugins/vis_type_timeseries/common/enums/metric_types.ts b/src/plugins/vis_types/timeseries/common/enums/metric_types.ts similarity index 100% rename from src/plugins/vis_type_timeseries/common/enums/metric_types.ts rename to src/plugins/vis_types/timeseries/common/enums/metric_types.ts diff --git a/src/plugins/vis_type_timeseries/common/enums/model_types.ts b/src/plugins/vis_types/timeseries/common/enums/model_types.ts similarity index 100% rename from src/plugins/vis_type_timeseries/common/enums/model_types.ts rename to src/plugins/vis_types/timeseries/common/enums/model_types.ts diff --git a/src/plugins/vis_type_timeseries/common/enums/panel_types.ts b/src/plugins/vis_types/timeseries/common/enums/panel_types.ts similarity index 100% rename from src/plugins/vis_type_timeseries/common/enums/panel_types.ts rename to src/plugins/vis_types/timeseries/common/enums/panel_types.ts diff --git a/src/plugins/vis_type_timeseries/common/enums/timerange_data_modes.ts b/src/plugins/vis_types/timeseries/common/enums/timerange_data_modes.ts similarity index 100% rename from src/plugins/vis_type_timeseries/common/enums/timerange_data_modes.ts rename to src/plugins/vis_types/timeseries/common/enums/timerange_data_modes.ts diff --git a/src/plugins/vis_type_timeseries/common/errors.ts b/src/plugins/vis_types/timeseries/common/errors.ts similarity index 100% rename from src/plugins/vis_type_timeseries/common/errors.ts rename to src/plugins/vis_types/timeseries/common/errors.ts diff --git a/src/plugins/vis_type_timeseries/common/fields_utils.test.ts b/src/plugins/vis_types/timeseries/common/fields_utils.test.ts similarity index 96% rename from src/plugins/vis_type_timeseries/common/fields_utils.test.ts rename to src/plugins/vis_types/timeseries/common/fields_utils.test.ts index f056c38b0c0c3..228dfbfd2db9d 100644 --- a/src/plugins/vis_type_timeseries/common/fields_utils.test.ts +++ b/src/plugins/vis_types/timeseries/common/fields_utils.test.ts @@ -7,7 +7,7 @@ */ import { toSanitizedFieldType } from './fields_utils'; -import type { FieldSpec } from '../../data/common'; +import type { FieldSpec } from '../../../data/common'; describe('fields_utils', () => { describe('toSanitizedFieldType', () => { diff --git a/src/plugins/vis_type_timeseries/common/fields_utils.ts b/src/plugins/vis_types/timeseries/common/fields_utils.ts similarity index 93% rename from src/plugins/vis_type_timeseries/common/fields_utils.ts rename to src/plugins/vis_types/timeseries/common/fields_utils.ts index 1af0340dfa525..d6987b9cdae9c 100644 --- a/src/plugins/vis_type_timeseries/common/fields_utils.ts +++ b/src/plugins/vis_types/timeseries/common/fields_utils.ts @@ -6,8 +6,8 @@ * Side Public License, v 1. */ -import { FieldSpec } from '../../data/common'; -import { isNestedField } from '../../data/common'; +import { FieldSpec } from '../../../data/common'; +import { isNestedField } from '../../../data/common'; import { FetchedIndexPattern, SanitizedFieldType } from './types'; import { FieldNotFoundError } from './errors'; diff --git a/src/plugins/vis_type_timeseries/common/index_patterns_utils.test.ts b/src/plugins/vis_types/timeseries/common/index_patterns_utils.test.ts similarity index 98% rename from src/plugins/vis_type_timeseries/common/index_patterns_utils.test.ts rename to src/plugins/vis_types/timeseries/common/index_patterns_utils.test.ts index cac607f7c0f90..e9f3be64079ac 100644 --- a/src/plugins/vis_type_timeseries/common/index_patterns_utils.test.ts +++ b/src/plugins/vis_types/timeseries/common/index_patterns_utils.test.ts @@ -12,7 +12,7 @@ import { fetchIndexPattern, } from './index_patterns_utils'; import { Panel } from './types'; -import { IndexPattern, IndexPatternsService } from '../../data/common'; +import { IndexPattern, IndexPatternsService } from '../../../data/common'; describe('isStringTypeIndexPattern', () => { test('should returns true on string-based index', () => { diff --git a/src/plugins/vis_type_timeseries/common/index_patterns_utils.ts b/src/plugins/vis_types/timeseries/common/index_patterns_utils.ts similarity index 97% rename from src/plugins/vis_type_timeseries/common/index_patterns_utils.ts rename to src/plugins/vis_types/timeseries/common/index_patterns_utils.ts index 1a8c277efbf7c..0a65e9e16d130 100644 --- a/src/plugins/vis_type_timeseries/common/index_patterns_utils.ts +++ b/src/plugins/vis_types/timeseries/common/index_patterns_utils.ts @@ -8,7 +8,7 @@ import { uniq } from 'lodash'; import type { Panel, IndexPatternValue, FetchedIndexPattern } from '../common/types'; -import { IndexPatternsService } from '../../data/common'; +import { IndexPatternsService } from '../../../data/common'; export const isStringTypeIndexPattern = ( indexPatternValue: IndexPatternValue diff --git a/src/plugins/vis_type_timeseries/common/interval_regexp.test.ts b/src/plugins/vis_types/timeseries/common/interval_regexp.test.ts similarity index 100% rename from src/plugins/vis_type_timeseries/common/interval_regexp.test.ts rename to src/plugins/vis_types/timeseries/common/interval_regexp.test.ts diff --git a/src/plugins/vis_type_timeseries/common/interval_regexp.ts b/src/plugins/vis_types/timeseries/common/interval_regexp.ts similarity index 100% rename from src/plugins/vis_type_timeseries/common/interval_regexp.ts rename to src/plugins/vis_types/timeseries/common/interval_regexp.ts diff --git a/src/plugins/vis_type_timeseries/common/last_value_utils.test.ts b/src/plugins/vis_types/timeseries/common/last_value_utils.test.ts similarity index 100% rename from src/plugins/vis_type_timeseries/common/last_value_utils.test.ts rename to src/plugins/vis_types/timeseries/common/last_value_utils.test.ts diff --git a/src/plugins/vis_type_timeseries/common/last_value_utils.ts b/src/plugins/vis_types/timeseries/common/last_value_utils.ts similarity index 100% rename from src/plugins/vis_type_timeseries/common/last_value_utils.ts rename to src/plugins/vis_types/timeseries/common/last_value_utils.ts diff --git a/src/plugins/vis_type_timeseries/common/operators_utils.test.ts b/src/plugins/vis_types/timeseries/common/operators_utils.test.ts similarity index 100% rename from src/plugins/vis_type_timeseries/common/operators_utils.test.ts rename to src/plugins/vis_types/timeseries/common/operators_utils.test.ts diff --git a/src/plugins/vis_type_timeseries/common/operators_utils.ts b/src/plugins/vis_types/timeseries/common/operators_utils.ts similarity index 100% rename from src/plugins/vis_type_timeseries/common/operators_utils.ts rename to src/plugins/vis_types/timeseries/common/operators_utils.ts diff --git a/src/plugins/vis_type_timeseries/common/to_percentile_number.ts b/src/plugins/vis_types/timeseries/common/to_percentile_number.ts similarity index 100% rename from src/plugins/vis_type_timeseries/common/to_percentile_number.ts rename to src/plugins/vis_types/timeseries/common/to_percentile_number.ts diff --git a/src/plugins/vis_type_timeseries/common/types/color_rules.ts b/src/plugins/vis_types/timeseries/common/types/color_rules.ts similarity index 100% rename from src/plugins/vis_type_timeseries/common/types/color_rules.ts rename to src/plugins/vis_types/timeseries/common/types/color_rules.ts diff --git a/src/plugins/vis_type_timeseries/common/types/index.ts b/src/plugins/vis_types/timeseries/common/types/index.ts similarity index 95% rename from src/plugins/vis_type_timeseries/common/types/index.ts rename to src/plugins/vis_types/timeseries/common/types/index.ts index fb8e217fe704c..123b6723d8ccd 100644 --- a/src/plugins/vis_type_timeseries/common/types/index.ts +++ b/src/plugins/vis_types/timeseries/common/types/index.ts @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import { Filter, IndexPattern, Query } from '../../../data/common'; +import { Filter, IndexPattern, Query } from '../../../../data/common'; import { Panel } from './panel_model'; export { Metric, Series, Panel, MetricType } from './panel_model'; diff --git a/src/plugins/vis_type_timeseries/common/types/panel_model.ts b/src/plugins/vis_types/timeseries/common/types/panel_model.ts similarity index 98% rename from src/plugins/vis_type_timeseries/common/types/panel_model.ts rename to src/plugins/vis_types/timeseries/common/types/panel_model.ts index 6fd2e727ade32..f71602fdf0443 100644 --- a/src/plugins/vis_type_timeseries/common/types/panel_model.ts +++ b/src/plugins/vis_types/timeseries/common/types/panel_model.ts @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import { METRIC_TYPES, Query } from '../../../data/common'; +import { METRIC_TYPES, Query } from '../../../../data/common'; import { PANEL_TYPES, TOOLTIP_MODES, TSVB_METRIC_TYPES } from '../enums'; import { IndexPatternValue, Annotation } from './index'; import { ColorRules, BackgroundColorRules, BarColorRules, GaugeColorRules } from './color_rules'; diff --git a/src/plugins/vis_type_timeseries/common/types/vis_data.ts b/src/plugins/vis_types/timeseries/common/types/vis_data.ts similarity index 100% rename from src/plugins/vis_type_timeseries/common/types/vis_data.ts rename to src/plugins/vis_types/timeseries/common/types/vis_data.ts diff --git a/src/plugins/vis_type_timeseries/common/ui_restrictions.ts b/src/plugins/vis_types/timeseries/common/ui_restrictions.ts similarity index 100% rename from src/plugins/vis_type_timeseries/common/ui_restrictions.ts rename to src/plugins/vis_types/timeseries/common/ui_restrictions.ts diff --git a/src/plugins/vis_type_timeseries/common/validate_interval.ts b/src/plugins/vis_types/timeseries/common/validate_interval.ts similarity index 93% rename from src/plugins/vis_type_timeseries/common/validate_interval.ts rename to src/plugins/vis_types/timeseries/common/validate_interval.ts index 7c7a4e7badfc0..78a2410f905c9 100644 --- a/src/plugins/vis_type_timeseries/common/validate_interval.ts +++ b/src/plugins/vis_types/timeseries/common/validate_interval.ts @@ -7,7 +7,7 @@ */ import { GTE_INTERVAL_RE } from './interval_regexp'; -import { parseInterval, TimeRangeBounds } from '../../data/common'; +import { parseInterval, TimeRangeBounds } from '../../../data/common'; import { ValidateIntervalError } from './errors'; export function validateInterval(bounds: TimeRangeBounds, interval: string, maxBuckets: number) { diff --git a/src/plugins/vis_type_timeseries/common/vis_data_utils.ts b/src/plugins/vis_types/timeseries/common/vis_data_utils.ts similarity index 100% rename from src/plugins/vis_type_timeseries/common/vis_data_utils.ts rename to src/plugins/vis_types/timeseries/common/vis_data_utils.ts diff --git a/src/plugins/vis_type_timeseries/jest.config.js b/src/plugins/vis_types/timeseries/jest.config.js similarity index 72% rename from src/plugins/vis_type_timeseries/jest.config.js rename to src/plugins/vis_types/timeseries/jest.config.js index 3d4333675f7d7..d6ddcaa3344b0 100644 --- a/src/plugins/vis_type_timeseries/jest.config.js +++ b/src/plugins/vis_types/timeseries/jest.config.js @@ -8,11 +8,11 @@ module.exports = { preset: '@kbn/test', - rootDir: '../../..', - roots: ['/src/plugins/vis_type_timeseries'], - coverageDirectory: '/target/kibana-coverage/jest/src/plugins/vis_type_timeseries', + rootDir: '../../../..', + roots: ['/src/plugins/vis_types/timeseries'], + coverageDirectory: '/target/kibana-coverage/jest/src/plugins/vis_types/timeseries', coverageReporters: ['text', 'html'], collectCoverageFrom: [ - '/src/plugins/vis_type_timeseries/{common,public,server}/**/*.{js,ts,tsx}', + '/src/plugins/vis_types/timeseries/{common,public,server}/**/*.{js,ts,tsx}', ], }; diff --git a/src/plugins/vis_type_timeseries/kibana.json b/src/plugins/vis_types/timeseries/kibana.json similarity index 100% rename from src/plugins/vis_type_timeseries/kibana.json rename to src/plugins/vis_types/timeseries/kibana.json diff --git a/src/plugins/vis_type_timeseries/public/application/_mixins.scss b/src/plugins/vis_types/timeseries/public/application/_mixins.scss similarity index 100% rename from src/plugins/vis_type_timeseries/public/application/_mixins.scss rename to src/plugins/vis_types/timeseries/public/application/_mixins.scss diff --git a/src/plugins/vis_type_timeseries/public/application/_tvb_editor.scss b/src/plugins/vis_types/timeseries/public/application/_tvb_editor.scss similarity index 100% rename from src/plugins/vis_type_timeseries/public/application/_tvb_editor.scss rename to src/plugins/vis_types/timeseries/public/application/_tvb_editor.scss diff --git a/src/plugins/vis_type_timeseries/public/application/_variables.scss b/src/plugins/vis_types/timeseries/public/application/_variables.scss similarity index 100% rename from src/plugins/vis_type_timeseries/public/application/_variables.scss rename to src/plugins/vis_types/timeseries/public/application/_variables.scss diff --git a/src/plugins/vis_type_timeseries/public/application/components/_annotations_editor.scss b/src/plugins/vis_types/timeseries/public/application/components/_annotations_editor.scss similarity index 100% rename from src/plugins/vis_type_timeseries/public/application/components/_annotations_editor.scss rename to src/plugins/vis_types/timeseries/public/application/components/_annotations_editor.scss diff --git a/src/plugins/vis_type_timeseries/public/application/components/_color_picker.scss b/src/plugins/vis_types/timeseries/public/application/components/_color_picker.scss similarity index 100% rename from src/plugins/vis_type_timeseries/public/application/components/_color_picker.scss rename to src/plugins/vis_types/timeseries/public/application/components/_color_picker.scss diff --git a/src/plugins/vis_type_timeseries/public/application/components/_color_rules.scss b/src/plugins/vis_types/timeseries/public/application/components/_color_rules.scss similarity index 100% rename from src/plugins/vis_type_timeseries/public/application/components/_color_rules.scss rename to src/plugins/vis_types/timeseries/public/application/components/_color_rules.scss diff --git a/src/plugins/vis_type_timeseries/public/application/components/_error.scss b/src/plugins/vis_types/timeseries/public/application/components/_error.scss similarity index 100% rename from src/plugins/vis_type_timeseries/public/application/components/_error.scss rename to src/plugins/vis_types/timeseries/public/application/components/_error.scss diff --git a/src/plugins/vis_type_timeseries/public/application/components/_index.scss b/src/plugins/vis_types/timeseries/public/application/components/_index.scss similarity index 100% rename from src/plugins/vis_type_timeseries/public/application/components/_index.scss rename to src/plugins/vis_types/timeseries/public/application/components/_index.scss diff --git a/src/plugins/vis_type_timeseries/public/application/components/_markdown_editor.scss b/src/plugins/vis_types/timeseries/public/application/components/_markdown_editor.scss similarity index 100% rename from src/plugins/vis_type_timeseries/public/application/components/_markdown_editor.scss rename to src/plugins/vis_types/timeseries/public/application/components/_markdown_editor.scss diff --git a/src/plugins/vis_type_timeseries/public/application/components/_no_data.scss b/src/plugins/vis_types/timeseries/public/application/components/_no_data.scss similarity index 100% rename from src/plugins/vis_type_timeseries/public/application/components/_no_data.scss rename to src/plugins/vis_types/timeseries/public/application/components/_no_data.scss diff --git a/src/plugins/vis_type_timeseries/public/application/components/_series_editor.scss b/src/plugins/vis_types/timeseries/public/application/components/_series_editor.scss similarity index 100% rename from src/plugins/vis_type_timeseries/public/application/components/_series_editor.scss rename to src/plugins/vis_types/timeseries/public/application/components/_series_editor.scss diff --git a/src/plugins/vis_type_timeseries/public/application/components/_vis_editor.scss b/src/plugins/vis_types/timeseries/public/application/components/_vis_editor.scss similarity index 100% rename from src/plugins/vis_type_timeseries/public/application/components/_vis_editor.scss rename to src/plugins/vis_types/timeseries/public/application/components/_vis_editor.scss diff --git a/src/plugins/vis_type_timeseries/public/application/components/_vis_editor_visualization.scss b/src/plugins/vis_types/timeseries/public/application/components/_vis_editor_visualization.scss similarity index 100% rename from src/plugins/vis_type_timeseries/public/application/components/_vis_editor_visualization.scss rename to src/plugins/vis_types/timeseries/public/application/components/_vis_editor_visualization.scss diff --git a/src/plugins/vis_type_timeseries/public/application/components/_vis_picker.scss b/src/plugins/vis_types/timeseries/public/application/components/_vis_picker.scss similarity index 100% rename from src/plugins/vis_type_timeseries/public/application/components/_vis_picker.scss rename to src/plugins/vis_types/timeseries/public/application/components/_vis_picker.scss diff --git a/src/plugins/vis_type_timeseries/public/application/components/_vis_with_splits.scss b/src/plugins/vis_types/timeseries/public/application/components/_vis_with_splits.scss similarity index 100% rename from src/plugins/vis_type_timeseries/public/application/components/_vis_with_splits.scss rename to src/plugins/vis_types/timeseries/public/application/components/_vis_with_splits.scss diff --git a/src/plugins/vis_type_timeseries/public/application/components/add_delete_buttons.test.tsx b/src/plugins/vis_types/timeseries/public/application/components/add_delete_buttons.test.tsx similarity index 100% rename from src/plugins/vis_type_timeseries/public/application/components/add_delete_buttons.test.tsx rename to src/plugins/vis_types/timeseries/public/application/components/add_delete_buttons.test.tsx diff --git a/src/plugins/vis_type_timeseries/public/application/components/add_delete_buttons.tsx b/src/plugins/vis_types/timeseries/public/application/components/add_delete_buttons.tsx similarity index 100% rename from src/plugins/vis_type_timeseries/public/application/components/add_delete_buttons.tsx rename to src/plugins/vis_types/timeseries/public/application/components/add_delete_buttons.tsx diff --git a/src/plugins/vis_type_timeseries/public/application/components/aggs/_agg_row.scss b/src/plugins/vis_types/timeseries/public/application/components/aggs/_agg_row.scss similarity index 100% rename from src/plugins/vis_type_timeseries/public/application/components/aggs/_agg_row.scss rename to src/plugins/vis_types/timeseries/public/application/components/aggs/_agg_row.scss diff --git a/src/plugins/vis_type_timeseries/public/application/components/aggs/_index.scss b/src/plugins/vis_types/timeseries/public/application/components/aggs/_index.scss similarity index 100% rename from src/plugins/vis_type_timeseries/public/application/components/aggs/_index.scss rename to src/plugins/vis_types/timeseries/public/application/components/aggs/_index.scss diff --git a/src/plugins/vis_type_timeseries/public/application/components/aggs/agg.tsx b/src/plugins/vis_types/timeseries/public/application/components/aggs/agg.tsx similarity index 100% rename from src/plugins/vis_type_timeseries/public/application/components/aggs/agg.tsx rename to src/plugins/vis_types/timeseries/public/application/components/aggs/agg.tsx diff --git a/src/plugins/vis_type_timeseries/public/application/components/aggs/agg_row.tsx b/src/plugins/vis_types/timeseries/public/application/components/aggs/agg_row.tsx similarity index 100% rename from src/plugins/vis_type_timeseries/public/application/components/aggs/agg_row.tsx rename to src/plugins/vis_types/timeseries/public/application/components/aggs/agg_row.tsx diff --git a/src/plugins/vis_type_timeseries/public/application/components/aggs/agg_select.test.tsx b/src/plugins/vis_types/timeseries/public/application/components/aggs/agg_select.test.tsx similarity index 100% rename from src/plugins/vis_type_timeseries/public/application/components/aggs/agg_select.test.tsx rename to src/plugins/vis_types/timeseries/public/application/components/aggs/agg_select.test.tsx diff --git a/src/plugins/vis_type_timeseries/public/application/components/aggs/agg_select.tsx b/src/plugins/vis_types/timeseries/public/application/components/aggs/agg_select.tsx similarity index 100% rename from src/plugins/vis_type_timeseries/public/application/components/aggs/agg_select.tsx rename to src/plugins/vis_types/timeseries/public/application/components/aggs/agg_select.tsx diff --git a/src/plugins/vis_type_timeseries/public/application/components/aggs/aggs.tsx b/src/plugins/vis_types/timeseries/public/application/components/aggs/aggs.tsx similarity index 100% rename from src/plugins/vis_type_timeseries/public/application/components/aggs/aggs.tsx rename to src/plugins/vis_types/timeseries/public/application/components/aggs/aggs.tsx diff --git a/src/plugins/vis_type_timeseries/public/application/components/aggs/calculation.js b/src/plugins/vis_types/timeseries/public/application/components/aggs/calculation.js similarity index 100% rename from src/plugins/vis_type_timeseries/public/application/components/aggs/calculation.js rename to src/plugins/vis_types/timeseries/public/application/components/aggs/calculation.js diff --git a/src/plugins/vis_type_timeseries/public/application/components/aggs/cumulative_sum.js b/src/plugins/vis_types/timeseries/public/application/components/aggs/cumulative_sum.js similarity index 100% rename from src/plugins/vis_type_timeseries/public/application/components/aggs/cumulative_sum.js rename to src/plugins/vis_types/timeseries/public/application/components/aggs/cumulative_sum.js diff --git a/src/plugins/vis_type_timeseries/public/application/components/aggs/derivative.js b/src/plugins/vis_types/timeseries/public/application/components/aggs/derivative.js similarity index 100% rename from src/plugins/vis_type_timeseries/public/application/components/aggs/derivative.js rename to src/plugins/vis_types/timeseries/public/application/components/aggs/derivative.js diff --git a/src/plugins/vis_type_timeseries/public/application/components/aggs/field_select.tsx b/src/plugins/vis_types/timeseries/public/application/components/aggs/field_select.tsx similarity index 100% rename from src/plugins/vis_type_timeseries/public/application/components/aggs/field_select.tsx rename to src/plugins/vis_types/timeseries/public/application/components/aggs/field_select.tsx diff --git a/src/plugins/vis_type_timeseries/public/application/components/aggs/filter_ratio.js b/src/plugins/vis_types/timeseries/public/application/components/aggs/filter_ratio.js similarity index 98% rename from src/plugins/vis_type_timeseries/public/application/components/aggs/filter_ratio.js rename to src/plugins/vis_types/timeseries/public/application/components/aggs/filter_ratio.js index 6a3f57e502796..59a372ccc1107 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/aggs/filter_ratio.js +++ b/src/plugins/vis_types/timeseries/public/application/components/aggs/filter_ratio.js @@ -24,7 +24,7 @@ import { EuiFormRow, } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; -import { KBN_FIELD_TYPES } from '../../../../../../plugins/data/public'; +import { KBN_FIELD_TYPES } from '../../../../../../../plugins/data/public'; import { getSupportedFieldsByMetricType } from '../lib/get_supported_fields_by_metric_type'; import { getDataStart } from '../../../services'; import { QueryBarWrapper } from '../query_bar_wrapper'; diff --git a/src/plugins/vis_type_timeseries/public/application/components/aggs/filter_ratio.test.js b/src/plugins/vis_types/timeseries/public/application/components/aggs/filter_ratio.test.js similarity index 97% rename from src/plugins/vis_type_timeseries/public/application/components/aggs/filter_ratio.test.js rename to src/plugins/vis_types/timeseries/public/application/components/aggs/filter_ratio.test.js index 5648a8d3e7133..bd9ceeeb74028 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/aggs/filter_ratio.test.js +++ b/src/plugins/vis_types/timeseries/public/application/components/aggs/filter_ratio.test.js @@ -11,7 +11,7 @@ import { mountWithIntl } from '@kbn/test/jest'; import { FilterRatioAgg } from './filter_ratio'; import { FIELDS, METRIC, SERIES, PANEL } from '../../../test_utils'; import { EuiComboBox } from '@elastic/eui'; -import { dataPluginMock } from '../../../../../data/public/mocks'; +import { dataPluginMock } from '../../../../../../data/public/mocks'; import { setDataStart } from '../../../services'; jest.mock('../query_bar_wrapper', () => ({ diff --git a/src/plugins/vis_type_timeseries/public/application/components/aggs/histogram_support.test.js b/src/plugins/vis_types/timeseries/public/application/components/aggs/histogram_support.test.js similarity index 97% rename from src/plugins/vis_type_timeseries/public/application/components/aggs/histogram_support.test.js rename to src/plugins/vis_types/timeseries/public/application/components/aggs/histogram_support.test.js index c4a49a393acd6..c131ba2ae804c 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/aggs/histogram_support.test.js +++ b/src/plugins/vis_types/timeseries/public/application/components/aggs/histogram_support.test.js @@ -12,7 +12,7 @@ import { Agg } from './agg'; import { FieldSelect } from './field_select'; import { FIELDS, METRIC, SERIES, PANEL } from '../../../test_utils'; import { setDataStart } from '../../../services'; -import { dataPluginMock } from '../../../../../data/public/mocks'; +import { dataPluginMock } from '../../../../../../data/public/mocks'; jest.mock('../query_bar_wrapper', () => ({ QueryBarWrapper: jest.fn(() => null), diff --git a/src/plugins/vis_type_timeseries/public/application/components/aggs/invalid_agg.tsx b/src/plugins/vis_types/timeseries/public/application/components/aggs/invalid_agg.tsx similarity index 100% rename from src/plugins/vis_type_timeseries/public/application/components/aggs/invalid_agg.tsx rename to src/plugins/vis_types/timeseries/public/application/components/aggs/invalid_agg.tsx diff --git a/src/plugins/vis_type_timeseries/public/application/components/aggs/math.js b/src/plugins/vis_types/timeseries/public/application/components/aggs/math.js similarity index 100% rename from src/plugins/vis_type_timeseries/public/application/components/aggs/math.js rename to src/plugins/vis_types/timeseries/public/application/components/aggs/math.js diff --git a/src/plugins/vis_type_timeseries/public/application/components/aggs/metric_select.js b/src/plugins/vis_types/timeseries/public/application/components/aggs/metric_select.js similarity index 100% rename from src/plugins/vis_type_timeseries/public/application/components/aggs/metric_select.js rename to src/plugins/vis_types/timeseries/public/application/components/aggs/metric_select.js diff --git a/src/plugins/vis_type_timeseries/public/application/components/aggs/moving_average.js b/src/plugins/vis_types/timeseries/public/application/components/aggs/moving_average.js similarity index 100% rename from src/plugins/vis_type_timeseries/public/application/components/aggs/moving_average.js rename to src/plugins/vis_types/timeseries/public/application/components/aggs/moving_average.js diff --git a/src/plugins/vis_type_timeseries/public/application/components/aggs/percentile.js b/src/plugins/vis_types/timeseries/public/application/components/aggs/percentile.js similarity index 98% rename from src/plugins/vis_type_timeseries/public/application/components/aggs/percentile.js rename to src/plugins/vis_types/timeseries/public/application/components/aggs/percentile.js index 94adb37de156b..3e4159cdf42dc 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/aggs/percentile.js +++ b/src/plugins/vis_types/timeseries/public/application/components/aggs/percentile.js @@ -23,7 +23,7 @@ import { EuiFormRow, } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; -import { KBN_FIELD_TYPES } from '../../../../../../plugins/data/public'; +import { KBN_FIELD_TYPES } from '../../../../../../../plugins/data/public'; import { Percentiles, newPercentile } from './percentile_ui'; import { PercentileHdr } from './percentile_hdr'; diff --git a/src/plugins/vis_type_timeseries/public/application/components/aggs/percentile_hdr.tsx b/src/plugins/vis_types/timeseries/public/application/components/aggs/percentile_hdr.tsx similarity index 100% rename from src/plugins/vis_type_timeseries/public/application/components/aggs/percentile_hdr.tsx rename to src/plugins/vis_types/timeseries/public/application/components/aggs/percentile_hdr.tsx diff --git a/src/plugins/vis_type_timeseries/public/application/components/aggs/percentile_rank/index.js b/src/plugins/vis_types/timeseries/public/application/components/aggs/percentile_rank/index.js similarity index 100% rename from src/plugins/vis_type_timeseries/public/application/components/aggs/percentile_rank/index.js rename to src/plugins/vis_types/timeseries/public/application/components/aggs/percentile_rank/index.js diff --git a/src/plugins/vis_type_timeseries/public/application/components/aggs/percentile_rank/multi_value_row.test.tsx b/src/plugins/vis_types/timeseries/public/application/components/aggs/percentile_rank/multi_value_row.test.tsx similarity index 100% rename from src/plugins/vis_type_timeseries/public/application/components/aggs/percentile_rank/multi_value_row.test.tsx rename to src/plugins/vis_types/timeseries/public/application/components/aggs/percentile_rank/multi_value_row.test.tsx diff --git a/src/plugins/vis_type_timeseries/public/application/components/aggs/percentile_rank/multi_value_row.tsx b/src/plugins/vis_types/timeseries/public/application/components/aggs/percentile_rank/multi_value_row.tsx similarity index 100% rename from src/plugins/vis_type_timeseries/public/application/components/aggs/percentile_rank/multi_value_row.tsx rename to src/plugins/vis_types/timeseries/public/application/components/aggs/percentile_rank/multi_value_row.tsx diff --git a/src/plugins/vis_type_timeseries/public/application/components/aggs/percentile_rank/percentile_rank.tsx b/src/plugins/vis_types/timeseries/public/application/components/aggs/percentile_rank/percentile_rank.tsx similarity index 98% rename from src/plugins/vis_type_timeseries/public/application/components/aggs/percentile_rank/percentile_rank.tsx rename to src/plugins/vis_types/timeseries/public/application/components/aggs/percentile_rank/percentile_rank.tsx index 7f9634ff15844..8b71dec15f17c 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/aggs/percentile_rank/percentile_rank.tsx +++ b/src/plugins/vis_types/timeseries/public/application/components/aggs/percentile_rank/percentile_rank.tsx @@ -26,7 +26,7 @@ import { createNumberHandler } from '../../lib/create_number_handler'; import { AggRow } from '../agg_row'; import { PercentileRankValues } from './percentile_rank_values'; -import { KBN_FIELD_TYPES } from '../../../../../../data/public'; +import { KBN_FIELD_TYPES } from '../../../../../../../data/public'; import type { Metric, Panel, SanitizedFieldType, Series } from '../../../../../common/types'; import { TSVB_DEFAULT_COLOR } from '../../../../../common/constants'; diff --git a/src/plugins/vis_type_timeseries/public/application/components/aggs/percentile_rank/percentile_rank_values.tsx b/src/plugins/vis_types/timeseries/public/application/components/aggs/percentile_rank/percentile_rank_values.tsx similarity index 100% rename from src/plugins/vis_type_timeseries/public/application/components/aggs/percentile_rank/percentile_rank_values.tsx rename to src/plugins/vis_types/timeseries/public/application/components/aggs/percentile_rank/percentile_rank_values.tsx diff --git a/src/plugins/vis_type_timeseries/public/application/components/aggs/percentile_ui.js b/src/plugins/vis_types/timeseries/public/application/components/aggs/percentile_ui.js similarity index 100% rename from src/plugins/vis_type_timeseries/public/application/components/aggs/percentile_ui.js rename to src/plugins/vis_types/timeseries/public/application/components/aggs/percentile_ui.js diff --git a/src/plugins/vis_type_timeseries/public/application/components/aggs/percentile_ui.test.tsx b/src/plugins/vis_types/timeseries/public/application/components/aggs/percentile_ui.test.tsx similarity index 100% rename from src/plugins/vis_type_timeseries/public/application/components/aggs/percentile_ui.test.tsx rename to src/plugins/vis_types/timeseries/public/application/components/aggs/percentile_ui.test.tsx diff --git a/src/plugins/vis_type_timeseries/public/application/components/aggs/positive_only.js b/src/plugins/vis_types/timeseries/public/application/components/aggs/positive_only.js similarity index 100% rename from src/plugins/vis_type_timeseries/public/application/components/aggs/positive_only.js rename to src/plugins/vis_types/timeseries/public/application/components/aggs/positive_only.js diff --git a/src/plugins/vis_type_timeseries/public/application/components/aggs/positive_rate.js b/src/plugins/vis_types/timeseries/public/application/components/aggs/positive_rate.js similarity index 98% rename from src/plugins/vis_type_timeseries/public/application/components/aggs/positive_rate.js rename to src/plugins/vis_types/timeseries/public/application/components/aggs/positive_rate.js index 09d9f2f1a62f2..ea3f8542e8595 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/aggs/positive_rate.js +++ b/src/plugins/vis_types/timeseries/public/application/components/aggs/positive_rate.js @@ -26,7 +26,7 @@ import { } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; import { i18n } from '@kbn/i18n'; -import { KBN_FIELD_TYPES } from '../../../../../../plugins/data/public'; +import { KBN_FIELD_TYPES } from '../../../../../../../plugins/data/public'; const UNIT_OPTIONS = [ { diff --git a/src/plugins/vis_type_timeseries/public/application/components/aggs/serial_diff.js b/src/plugins/vis_types/timeseries/public/application/components/aggs/serial_diff.js similarity index 100% rename from src/plugins/vis_type_timeseries/public/application/components/aggs/serial_diff.js rename to src/plugins/vis_types/timeseries/public/application/components/aggs/serial_diff.js diff --git a/src/plugins/vis_type_timeseries/public/application/components/aggs/series_agg.js b/src/plugins/vis_types/timeseries/public/application/components/aggs/series_agg.js similarity index 100% rename from src/plugins/vis_type_timeseries/public/application/components/aggs/series_agg.js rename to src/plugins/vis_types/timeseries/public/application/components/aggs/series_agg.js diff --git a/src/plugins/vis_type_timeseries/public/application/components/aggs/static.js b/src/plugins/vis_types/timeseries/public/application/components/aggs/static.js similarity index 100% rename from src/plugins/vis_type_timeseries/public/application/components/aggs/static.js rename to src/plugins/vis_types/timeseries/public/application/components/aggs/static.js diff --git a/src/plugins/vis_type_timeseries/public/application/components/aggs/std_agg.js b/src/plugins/vis_types/timeseries/public/application/components/aggs/std_agg.js similarity index 100% rename from src/plugins/vis_type_timeseries/public/application/components/aggs/std_agg.js rename to src/plugins/vis_types/timeseries/public/application/components/aggs/std_agg.js diff --git a/src/plugins/vis_type_timeseries/public/application/components/aggs/std_deviation.js b/src/plugins/vis_types/timeseries/public/application/components/aggs/std_deviation.js similarity index 98% rename from src/plugins/vis_type_timeseries/public/application/components/aggs/std_deviation.js rename to src/plugins/vis_types/timeseries/public/application/components/aggs/std_deviation.js index d4caa8a94652f..728cda8720268 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/aggs/std_deviation.js +++ b/src/plugins/vis_types/timeseries/public/application/components/aggs/std_deviation.js @@ -25,7 +25,7 @@ import { EuiSpacer, } from '@elastic/eui'; import { injectI18n, FormattedMessage } from '@kbn/i18n/react'; -import { KBN_FIELD_TYPES } from '../../../../../../plugins/data/public'; +import { KBN_FIELD_TYPES } from '../../../../../../../plugins/data/public'; const RESTRICT_FIELDS = KBN_FIELD_TYPES.NUMBER; diff --git a/src/plugins/vis_type_timeseries/public/application/components/aggs/std_sibling.js b/src/plugins/vis_types/timeseries/public/application/components/aggs/std_sibling.js similarity index 100% rename from src/plugins/vis_type_timeseries/public/application/components/aggs/std_sibling.js rename to src/plugins/vis_types/timeseries/public/application/components/aggs/std_sibling.js diff --git a/src/plugins/vis_type_timeseries/public/application/components/aggs/top_hit.js b/src/plugins/vis_types/timeseries/public/application/components/aggs/top_hit.js similarity index 99% rename from src/plugins/vis_type_timeseries/public/application/components/aggs/top_hit.js rename to src/plugins/vis_types/timeseries/public/application/components/aggs/top_hit.js index ade64fc3db8c5..caf601dfab83e 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/aggs/top_hit.js +++ b/src/plugins/vis_types/timeseries/public/application/components/aggs/top_hit.js @@ -24,7 +24,7 @@ import { EuiFormRow, } from '@elastic/eui'; import { injectI18n, FormattedMessage } from '@kbn/i18n/react'; -import { KBN_FIELD_TYPES } from '../../../../../../plugins/data/public'; +import { KBN_FIELD_TYPES } from '../../../../../../../plugins/data/public'; import { PANEL_TYPES } from '../../../../common/enums'; import { getIndexPatternKey } from '../../../../common/index_patterns_utils'; diff --git a/src/plugins/vis_type_timeseries/public/application/components/aggs/vars.js b/src/plugins/vis_types/timeseries/public/application/components/aggs/vars.js similarity index 100% rename from src/plugins/vis_type_timeseries/public/application/components/aggs/vars.js rename to src/plugins/vis_types/timeseries/public/application/components/aggs/vars.js diff --git a/src/plugins/vis_type_timeseries/public/application/components/annotation_row.tsx b/src/plugins/vis_types/timeseries/public/application/components/annotation_row.tsx similarity index 99% rename from src/plugins/vis_type_timeseries/public/application/components/annotation_row.tsx rename to src/plugins/vis_types/timeseries/public/application/components/annotation_row.tsx index 379c74d0d4bba..734bdfecac673 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/annotation_row.tsx +++ b/src/plugins/vis_types/timeseries/public/application/components/annotation_row.tsx @@ -21,7 +21,7 @@ import { import { FormattedMessage } from '@kbn/i18n/react'; import { getDataStart } from '../../services'; -import { KBN_FIELD_TYPES, Query } from '../../../../../plugins/data/public'; +import { KBN_FIELD_TYPES, Query } from '../../../../../../plugins/data/public'; import { AddDeleteButtons } from './add_delete_buttons'; import { ColorPicker } from './color_picker'; diff --git a/src/plugins/vis_type_timeseries/public/application/components/annotations_editor.tsx b/src/plugins/vis_types/timeseries/public/application/components/annotations_editor.tsx similarity index 100% rename from src/plugins/vis_type_timeseries/public/application/components/annotations_editor.tsx rename to src/plugins/vis_types/timeseries/public/application/components/annotations_editor.tsx diff --git a/src/plugins/vis_type_timeseries/public/application/components/color_picker.test.tsx b/src/plugins/vis_types/timeseries/public/application/components/color_picker.test.tsx similarity index 100% rename from src/plugins/vis_type_timeseries/public/application/components/color_picker.test.tsx rename to src/plugins/vis_types/timeseries/public/application/components/color_picker.test.tsx diff --git a/src/plugins/vis_type_timeseries/public/application/components/color_picker.tsx b/src/plugins/vis_types/timeseries/public/application/components/color_picker.tsx similarity index 100% rename from src/plugins/vis_type_timeseries/public/application/components/color_picker.tsx rename to src/plugins/vis_types/timeseries/public/application/components/color_picker.tsx diff --git a/src/plugins/vis_type_timeseries/public/application/components/color_rules.test.tsx b/src/plugins/vis_types/timeseries/public/application/components/color_rules.test.tsx similarity index 100% rename from src/plugins/vis_type_timeseries/public/application/components/color_rules.test.tsx rename to src/plugins/vis_types/timeseries/public/application/components/color_rules.test.tsx diff --git a/src/plugins/vis_type_timeseries/public/application/components/color_rules.tsx b/src/plugins/vis_types/timeseries/public/application/components/color_rules.tsx similarity index 100% rename from src/plugins/vis_type_timeseries/public/application/components/color_rules.tsx rename to src/plugins/vis_types/timeseries/public/application/components/color_rules.tsx diff --git a/src/plugins/vis_type_timeseries/public/application/components/data_format_picker.tsx b/src/plugins/vis_types/timeseries/public/application/components/data_format_picker.tsx similarity index 100% rename from src/plugins/vis_type_timeseries/public/application/components/data_format_picker.tsx rename to src/plugins/vis_types/timeseries/public/application/components/data_format_picker.tsx diff --git a/src/plugins/vis_type_timeseries/public/application/components/error.js b/src/plugins/vis_types/timeseries/public/application/components/error.js similarity index 100% rename from src/plugins/vis_type_timeseries/public/application/components/error.js rename to src/plugins/vis_types/timeseries/public/application/components/error.js diff --git a/src/plugins/vis_type_timeseries/public/application/components/icon_select/__snapshots__/icon_select.test.js.snap b/src/plugins/vis_types/timeseries/public/application/components/icon_select/__snapshots__/icon_select.test.js.snap similarity index 100% rename from src/plugins/vis_type_timeseries/public/application/components/icon_select/__snapshots__/icon_select.test.js.snap rename to src/plugins/vis_types/timeseries/public/application/components/icon_select/__snapshots__/icon_select.test.js.snap diff --git a/src/plugins/vis_type_timeseries/public/application/components/icon_select/icon_select.js b/src/plugins/vis_types/timeseries/public/application/components/icon_select/icon_select.js similarity index 100% rename from src/plugins/vis_type_timeseries/public/application/components/icon_select/icon_select.js rename to src/plugins/vis_types/timeseries/public/application/components/icon_select/icon_select.js diff --git a/src/plugins/vis_type_timeseries/public/application/components/icon_select/icon_select.test.js b/src/plugins/vis_types/timeseries/public/application/components/icon_select/icon_select.test.js similarity index 100% rename from src/plugins/vis_type_timeseries/public/application/components/icon_select/icon_select.test.js rename to src/plugins/vis_types/timeseries/public/application/components/icon_select/icon_select.test.js diff --git a/src/plugins/vis_type_timeseries/public/application/components/index_pattern.js b/src/plugins/vis_types/timeseries/public/application/components/index_pattern.js similarity index 99% rename from src/plugins/vis_type_timeseries/public/application/components/index_pattern.js rename to src/plugins/vis_types/timeseries/public/application/components/index_pattern.js index 1810424625022..e5d09c745b522 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/index_pattern.js +++ b/src/plugins/vis_types/timeseries/public/application/components/index_pattern.js @@ -26,7 +26,7 @@ import { createTextHandler } from './lib/create_text_handler'; import { IndexPatternSelect } from './lib/index_pattern_select'; import { YesNo } from './yes_no'; import { LastValueModePopover } from './last_value_mode_popover'; -import { KBN_FIELD_TYPES } from '../../../../data/public'; +import { KBN_FIELD_TYPES } from '../../../../../data/public'; import { FormValidationContext } from '../contexts/form_validation_context'; import { isGteInterval, validateReInterval, isAutoInterval } from './lib/get_interval'; import { i18n } from '@kbn/i18n'; @@ -36,7 +36,7 @@ import { AUTO_INTERVAL } from '../../../common/constants'; import { isTimerangeModeEnabled } from '../lib/check_ui_restrictions'; import { VisDataContext } from '../contexts/vis_data_context'; import { getDataStart, getUISettings } from '../../services'; -import { UI_SETTINGS } from '../../../../data/common'; +import { UI_SETTINGS } from '../../../../../data/common'; import { fetchIndexPattern } from '../../../common/index_patterns_utils'; const RESTRICT_FIELDS = [KBN_FIELD_TYPES.DATE]; diff --git a/src/plugins/vis_type_timeseries/public/application/components/last_value_mode_indicator.tsx b/src/plugins/vis_types/timeseries/public/application/components/last_value_mode_indicator.tsx similarity index 100% rename from src/plugins/vis_type_timeseries/public/application/components/last_value_mode_indicator.tsx rename to src/plugins/vis_types/timeseries/public/application/components/last_value_mode_indicator.tsx diff --git a/src/plugins/vis_type_timeseries/public/application/components/last_value_mode_popover.scss b/src/plugins/vis_types/timeseries/public/application/components/last_value_mode_popover.scss similarity index 100% rename from src/plugins/vis_type_timeseries/public/application/components/last_value_mode_popover.scss rename to src/plugins/vis_types/timeseries/public/application/components/last_value_mode_popover.scss diff --git a/src/plugins/vis_type_timeseries/public/application/components/last_value_mode_popover.tsx b/src/plugins/vis_types/timeseries/public/application/components/last_value_mode_popover.tsx similarity index 100% rename from src/plugins/vis_type_timeseries/public/application/components/last_value_mode_popover.tsx rename to src/plugins/vis_types/timeseries/public/application/components/last_value_mode_popover.tsx diff --git a/src/plugins/vis_type_timeseries/public/application/components/lib/agg_to_component.js b/src/plugins/vis_types/timeseries/public/application/components/lib/agg_to_component.js similarity index 100% rename from src/plugins/vis_type_timeseries/public/application/components/lib/agg_to_component.js rename to src/plugins/vis_types/timeseries/public/application/components/lib/agg_to_component.js diff --git a/src/plugins/vis_type_timeseries/public/application/components/lib/calculate_siblings.js b/src/plugins/vis_types/timeseries/public/application/components/lib/calculate_siblings.js similarity index 100% rename from src/plugins/vis_type_timeseries/public/application/components/lib/calculate_siblings.js rename to src/plugins/vis_types/timeseries/public/application/components/lib/calculate_siblings.js diff --git a/src/plugins/vis_type_timeseries/public/application/components/lib/calculate_siblings.test.js b/src/plugins/vis_types/timeseries/public/application/components/lib/calculate_siblings.test.js similarity index 100% rename from src/plugins/vis_type_timeseries/public/application/components/lib/calculate_siblings.test.js rename to src/plugins/vis_types/timeseries/public/application/components/lib/calculate_siblings.test.js diff --git a/src/plugins/vis_type_timeseries/public/application/components/lib/check_if_numeric_metric.test.ts b/src/plugins/vis_types/timeseries/public/application/components/lib/check_if_numeric_metric.test.ts similarity index 97% rename from src/plugins/vis_type_timeseries/public/application/components/lib/check_if_numeric_metric.test.ts rename to src/plugins/vis_types/timeseries/public/application/components/lib/check_if_numeric_metric.test.ts index 17827275f86d8..eb6ea561fec84 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/lib/check_if_numeric_metric.test.ts +++ b/src/plugins/vis_types/timeseries/public/application/components/lib/check_if_numeric_metric.test.ts @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import { METRIC_TYPES } from '../../../../../data/common'; +import { METRIC_TYPES } from '../../../../../../data/common'; import { TSVB_METRIC_TYPES } from '../../../../common/enums'; import { checkIfNumericMetric } from './check_if_numeric_metric'; diff --git a/src/plugins/vis_type_timeseries/public/application/components/lib/check_if_numeric_metric.ts b/src/plugins/vis_types/timeseries/public/application/components/lib/check_if_numeric_metric.ts similarity index 94% rename from src/plugins/vis_type_timeseries/public/application/components/lib/check_if_numeric_metric.ts rename to src/plugins/vis_types/timeseries/public/application/components/lib/check_if_numeric_metric.ts index a70abaeac9f82..139c13d7ddbda 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/lib/check_if_numeric_metric.ts +++ b/src/plugins/vis_types/timeseries/public/application/components/lib/check_if_numeric_metric.ts @@ -8,7 +8,7 @@ import { getIndexPatternKey } from '../../../../common/index_patterns_utils'; import { TSVB_METRIC_TYPES } from '../../../../common/enums'; -import { KBN_FIELD_TYPES } from '../../../../../data/public'; +import { KBN_FIELD_TYPES } from '../../../../../../data/public'; import type { Metric, IndexPatternValue } from '../../../../common/types'; import type { VisFields } from '../../lib/fetch_fields'; diff --git a/src/plugins/vis_type_timeseries/public/application/components/lib/check_if_series_have_same_formatters.test.ts b/src/plugins/vis_types/timeseries/public/application/components/lib/check_if_series_have_same_formatters.test.ts similarity index 100% rename from src/plugins/vis_type_timeseries/public/application/components/lib/check_if_series_have_same_formatters.test.ts rename to src/plugins/vis_types/timeseries/public/application/components/lib/check_if_series_have_same_formatters.test.ts diff --git a/src/plugins/vis_type_timeseries/public/application/components/lib/check_if_series_have_same_formatters.ts b/src/plugins/vis_types/timeseries/public/application/components/lib/check_if_series_have_same_formatters.ts similarity index 94% rename from src/plugins/vis_type_timeseries/public/application/components/lib/check_if_series_have_same_formatters.ts rename to src/plugins/vis_types/timeseries/public/application/components/lib/check_if_series_have_same_formatters.ts index afa1216406ab0..44715d1262d05 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/lib/check_if_series_have_same_formatters.ts +++ b/src/plugins/vis_types/timeseries/public/application/components/lib/check_if_series_have_same_formatters.ts @@ -9,7 +9,7 @@ import { last, isEqual } from 'lodash'; import { DATA_FORMATTERS } from '../../../../common/enums'; import type { Series } from '../../../../common/types'; -import type { FieldFormatMap } from '../../../../../data/common'; +import type { FieldFormatMap } from '../../../../../../data/common'; export const checkIfSeriesHaveSameFormatters = ( seriesModel: Series[], diff --git a/src/plugins/vis_type_timeseries/public/application/components/lib/collection_actions.test.ts b/src/plugins/vis_types/timeseries/public/application/components/lib/collection_actions.test.ts similarity index 100% rename from src/plugins/vis_type_timeseries/public/application/components/lib/collection_actions.test.ts rename to src/plugins/vis_types/timeseries/public/application/components/lib/collection_actions.test.ts diff --git a/src/plugins/vis_type_timeseries/public/application/components/lib/collection_actions.ts b/src/plugins/vis_types/timeseries/public/application/components/lib/collection_actions.ts similarity index 100% rename from src/plugins/vis_type_timeseries/public/application/components/lib/collection_actions.ts rename to src/plugins/vis_types/timeseries/public/application/components/lib/collection_actions.ts diff --git a/src/plugins/vis_type_timeseries/public/application/components/lib/convert_series_to_datatable.test.ts b/src/plugins/vis_types/timeseries/public/application/components/lib/convert_series_to_datatable.test.ts similarity index 100% rename from src/plugins/vis_type_timeseries/public/application/components/lib/convert_series_to_datatable.test.ts rename to src/plugins/vis_types/timeseries/public/application/components/lib/convert_series_to_datatable.test.ts diff --git a/src/plugins/vis_type_timeseries/public/application/components/lib/convert_series_to_datatable.ts b/src/plugins/vis_types/timeseries/public/application/components/lib/convert_series_to_datatable.ts similarity index 100% rename from src/plugins/vis_type_timeseries/public/application/components/lib/convert_series_to_datatable.ts rename to src/plugins/vis_types/timeseries/public/application/components/lib/convert_series_to_datatable.ts diff --git a/src/plugins/vis_type_timeseries/public/application/components/lib/convert_series_to_vars.js b/src/plugins/vis_types/timeseries/public/application/components/lib/convert_series_to_vars.js similarity index 100% rename from src/plugins/vis_type_timeseries/public/application/components/lib/convert_series_to_vars.js rename to src/plugins/vis_types/timeseries/public/application/components/lib/convert_series_to_vars.js diff --git a/src/plugins/vis_type_timeseries/public/application/components/lib/convert_series_to_vars.test.js b/src/plugins/vis_types/timeseries/public/application/components/lib/convert_series_to_vars.test.js similarity index 100% rename from src/plugins/vis_type_timeseries/public/application/components/lib/convert_series_to_vars.test.js rename to src/plugins/vis_types/timeseries/public/application/components/lib/convert_series_to_vars.test.js diff --git a/src/plugins/vis_type_timeseries/public/application/components/lib/create_change_handler.js b/src/plugins/vis_types/timeseries/public/application/components/lib/create_change_handler.js similarity index 100% rename from src/plugins/vis_type_timeseries/public/application/components/lib/create_change_handler.js rename to src/plugins/vis_types/timeseries/public/application/components/lib/create_change_handler.js diff --git a/src/plugins/vis_type_timeseries/public/application/components/lib/create_field_formatter.test.ts b/src/plugins/vis_types/timeseries/public/application/components/lib/create_field_formatter.test.ts similarity index 98% rename from src/plugins/vis_type_timeseries/public/application/components/lib/create_field_formatter.test.ts rename to src/plugins/vis_types/timeseries/public/application/components/lib/create_field_formatter.test.ts index c56c6820fff48..5a6b6a18f67e0 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/lib/create_field_formatter.test.ts +++ b/src/plugins/vis_types/timeseries/public/application/components/lib/create_field_formatter.test.ts @@ -7,7 +7,7 @@ */ import { createFieldFormatter } from './create_field_formatter'; -import { getFieldFormatsRegistry } from '../../../../../data/public/test_utils'; +import { getFieldFormatsRegistry } from '../../../../../../data/public/test_utils'; import { setFieldFormats } from '../../../services'; import { FORMATS_UI_SETTINGS } from 'src/plugins/field_formats/common'; import type { CoreSetup } from 'kibana/public'; diff --git a/src/plugins/vis_type_timeseries/public/application/components/lib/create_field_formatter.ts b/src/plugins/vis_types/timeseries/public/application/components/lib/create_field_formatter.ts similarity index 86% rename from src/plugins/vis_type_timeseries/public/application/components/lib/create_field_formatter.ts rename to src/plugins/vis_types/timeseries/public/application/components/lib/create_field_formatter.ts index 5cba549220f2c..a7606895e84aa 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/lib/create_field_formatter.ts +++ b/src/plugins/vis_types/timeseries/public/application/components/lib/create_field_formatter.ts @@ -9,9 +9,9 @@ import { isNumber } from 'lodash'; import { getFieldFormats } from '../../../services'; import { isEmptyValue, DISPLAY_EMPTY_VALUE } from '../../../../common/last_value_utils'; -import { FIELD_FORMAT_IDS } from '../../../../../field_formats/common'; -import type { FieldFormatMap } from '../../../../../data/common'; -import type { FieldFormatsContentType } from '../../../../../field_formats/common'; +import { FIELD_FORMAT_IDS } from '../../../../../../field_formats/common'; +import type { FieldFormatMap } from '../../../../../../data/common'; +import type { FieldFormatsContentType } from '../../../../../../field_formats/common'; const DEFAULT_FIELD_FORMAT = { id: 'number' }; diff --git a/src/plugins/vis_type_timeseries/public/application/components/lib/create_interval_based_formatter.ts b/src/plugins/vis_types/timeseries/public/application/components/lib/create_interval_based_formatter.ts similarity index 100% rename from src/plugins/vis_type_timeseries/public/application/components/lib/create_interval_based_formatter.ts rename to src/plugins/vis_types/timeseries/public/application/components/lib/create_interval_based_formatter.ts diff --git a/src/plugins/vis_type_timeseries/public/application/components/lib/create_number_handler.test.ts b/src/plugins/vis_types/timeseries/public/application/components/lib/create_number_handler.test.ts similarity index 100% rename from src/plugins/vis_type_timeseries/public/application/components/lib/create_number_handler.test.ts rename to src/plugins/vis_types/timeseries/public/application/components/lib/create_number_handler.test.ts diff --git a/src/plugins/vis_type_timeseries/public/application/components/lib/create_number_handler.ts b/src/plugins/vis_types/timeseries/public/application/components/lib/create_number_handler.ts similarity index 100% rename from src/plugins/vis_type_timeseries/public/application/components/lib/create_number_handler.ts rename to src/plugins/vis_types/timeseries/public/application/components/lib/create_number_handler.ts diff --git a/src/plugins/vis_type_timeseries/public/application/components/lib/create_select_handler.test.ts b/src/plugins/vis_types/timeseries/public/application/components/lib/create_select_handler.test.ts similarity index 100% rename from src/plugins/vis_type_timeseries/public/application/components/lib/create_select_handler.test.ts rename to src/plugins/vis_types/timeseries/public/application/components/lib/create_select_handler.test.ts diff --git a/src/plugins/vis_type_timeseries/public/application/components/lib/create_select_handler.ts b/src/plugins/vis_types/timeseries/public/application/components/lib/create_select_handler.ts similarity index 100% rename from src/plugins/vis_type_timeseries/public/application/components/lib/create_select_handler.ts rename to src/plugins/vis_types/timeseries/public/application/components/lib/create_select_handler.ts diff --git a/src/plugins/vis_type_timeseries/public/application/components/lib/create_text_handler.test.ts b/src/plugins/vis_types/timeseries/public/application/components/lib/create_text_handler.test.ts similarity index 100% rename from src/plugins/vis_type_timeseries/public/application/components/lib/create_text_handler.test.ts rename to src/plugins/vis_types/timeseries/public/application/components/lib/create_text_handler.test.ts diff --git a/src/plugins/vis_type_timeseries/public/application/components/lib/create_text_handler.ts b/src/plugins/vis_types/timeseries/public/application/components/lib/create_text_handler.ts similarity index 100% rename from src/plugins/vis_type_timeseries/public/application/components/lib/create_text_handler.ts rename to src/plugins/vis_types/timeseries/public/application/components/lib/create_text_handler.ts diff --git a/src/plugins/vis_type_timeseries/public/application/components/lib/durations.test.ts b/src/plugins/vis_types/timeseries/public/application/components/lib/durations.test.ts similarity index 100% rename from src/plugins/vis_type_timeseries/public/application/components/lib/durations.test.ts rename to src/plugins/vis_types/timeseries/public/application/components/lib/durations.test.ts diff --git a/src/plugins/vis_type_timeseries/public/application/components/lib/durations.ts b/src/plugins/vis_types/timeseries/public/application/components/lib/durations.ts similarity index 100% rename from src/plugins/vis_type_timeseries/public/application/components/lib/durations.ts rename to src/plugins/vis_types/timeseries/public/application/components/lib/durations.ts diff --git a/src/plugins/vis_type_timeseries/public/application/components/lib/get_axis_label_string.js b/src/plugins/vis_types/timeseries/public/application/components/lib/get_axis_label_string.js similarity index 100% rename from src/plugins/vis_type_timeseries/public/application/components/lib/get_axis_label_string.js rename to src/plugins/vis_types/timeseries/public/application/components/lib/get_axis_label_string.js diff --git a/src/plugins/vis_type_timeseries/public/application/components/lib/get_axis_label_string.test.js b/src/plugins/vis_types/timeseries/public/application/components/lib/get_axis_label_string.test.js similarity index 100% rename from src/plugins/vis_type_timeseries/public/application/components/lib/get_axis_label_string.test.js rename to src/plugins/vis_types/timeseries/public/application/components/lib/get_axis_label_string.test.js diff --git a/src/plugins/vis_type_timeseries/public/application/components/lib/get_click_filter_data.test.ts b/src/plugins/vis_types/timeseries/public/application/components/lib/get_click_filter_data.test.ts similarity index 100% rename from src/plugins/vis_type_timeseries/public/application/components/lib/get_click_filter_data.test.ts rename to src/plugins/vis_types/timeseries/public/application/components/lib/get_click_filter_data.test.ts diff --git a/src/plugins/vis_type_timeseries/public/application/components/lib/get_click_filter_data.ts b/src/plugins/vis_types/timeseries/public/application/components/lib/get_click_filter_data.ts similarity index 100% rename from src/plugins/vis_type_timeseries/public/application/components/lib/get_click_filter_data.ts rename to src/plugins/vis_types/timeseries/public/application/components/lib/get_click_filter_data.ts diff --git a/src/plugins/vis_type_timeseries/public/application/components/lib/get_default_query_language.ts b/src/plugins/vis_types/timeseries/public/application/components/lib/get_default_query_language.ts similarity index 89% rename from src/plugins/vis_type_timeseries/public/application/components/lib/get_default_query_language.ts rename to src/plugins/vis_types/timeseries/public/application/components/lib/get_default_query_language.ts index e6f65e71043c7..3cb7ee0388f94 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/lib/get_default_query_language.ts +++ b/src/plugins/vis_types/timeseries/public/application/components/lib/get_default_query_language.ts @@ -7,7 +7,7 @@ */ import { getUISettings } from '../../../services'; -import { UI_SETTINGS } from '../../../../../data/public'; +import { UI_SETTINGS } from '../../../../../../data/public'; export function getDefaultQueryLanguage() { return getUISettings().get(UI_SETTINGS.SEARCH_QUERY_LANGUAGE); diff --git a/src/plugins/vis_type_timeseries/public/application/components/lib/get_display_name.js b/src/plugins/vis_types/timeseries/public/application/components/lib/get_display_name.js similarity index 100% rename from src/plugins/vis_type_timeseries/public/application/components/lib/get_display_name.js rename to src/plugins/vis_types/timeseries/public/application/components/lib/get_display_name.js diff --git a/src/plugins/vis_type_timeseries/public/application/components/lib/get_formatter_type.test.ts b/src/plugins/vis_types/timeseries/public/application/components/lib/get_formatter_type.test.ts similarity index 100% rename from src/plugins/vis_type_timeseries/public/application/components/lib/get_formatter_type.test.ts rename to src/plugins/vis_types/timeseries/public/application/components/lib/get_formatter_type.test.ts diff --git a/src/plugins/vis_type_timeseries/public/application/components/lib/get_formatter_type.ts b/src/plugins/vis_types/timeseries/public/application/components/lib/get_formatter_type.ts similarity index 100% rename from src/plugins/vis_type_timeseries/public/application/components/lib/get_formatter_type.ts rename to src/plugins/vis_types/timeseries/public/application/components/lib/get_formatter_type.ts diff --git a/src/plugins/vis_type_timeseries/public/application/components/lib/get_interval.ts b/src/plugins/vis_types/timeseries/public/application/components/lib/get_interval.ts similarity index 98% rename from src/plugins/vis_type_timeseries/public/application/components/lib/get_interval.ts rename to src/plugins/vis_types/timeseries/public/application/components/lib/get_interval.ts index 4b232af299a19..ea86ef6dc7ae9 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/lib/get_interval.ts +++ b/src/plugins/vis_types/timeseries/public/application/components/lib/get_interval.ts @@ -9,7 +9,7 @@ import moment from 'moment'; import { i18n } from '@kbn/i18n'; import { get } from 'lodash'; -import { search } from '../../../../../../plugins/data/public'; +import { search } from '../../../../../../../plugins/data/public'; import { GTE_INTERVAL_RE } from '../../../../common/interval_regexp'; import { AUTO_INTERVAL } from '../../../../common/constants'; import { isVisTableData } from '../../../../common/vis_data_utils'; diff --git a/src/plugins/vis_type_timeseries/public/application/components/lib/get_metrics_field.test.ts b/src/plugins/vis_types/timeseries/public/application/components/lib/get_metrics_field.test.ts similarity index 100% rename from src/plugins/vis_type_timeseries/public/application/components/lib/get_metrics_field.test.ts rename to src/plugins/vis_types/timeseries/public/application/components/lib/get_metrics_field.test.ts diff --git a/src/plugins/vis_type_timeseries/public/application/components/lib/get_metrics_field.ts b/src/plugins/vis_types/timeseries/public/application/components/lib/get_metrics_field.ts similarity index 100% rename from src/plugins/vis_type_timeseries/public/application/components/lib/get_metrics_field.ts rename to src/plugins/vis_types/timeseries/public/application/components/lib/get_metrics_field.ts diff --git a/src/plugins/vis_type_timeseries/public/application/components/lib/get_supported_fields_by_metric_type.js b/src/plugins/vis_types/timeseries/public/application/components/lib/get_supported_fields_by_metric_type.js similarity index 87% rename from src/plugins/vis_type_timeseries/public/application/components/lib/get_supported_fields_by_metric_type.js rename to src/plugins/vis_types/timeseries/public/application/components/lib/get_supported_fields_by_metric_type.js index 7aa40d48994b8..2909e7804b1bf 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/lib/get_supported_fields_by_metric_type.js +++ b/src/plugins/vis_types/timeseries/public/application/components/lib/get_supported_fields_by_metric_type.js @@ -6,8 +6,8 @@ * Side Public License, v 1. */ -import { KBN_FIELD_TYPES } from '../../../../../../plugins/data/public'; -import { METRIC_TYPES } from '../../../../../data/common'; +import { KBN_FIELD_TYPES } from '../../../../../../../plugins/data/public'; +import { METRIC_TYPES } from '../../../../../../data/common'; import { TSVB_METRIC_TYPES } from '../../../../common/enums'; export function getSupportedFieldsByMetricType(type) { diff --git a/src/plugins/vis_type_timeseries/public/application/components/lib/get_supported_fields_by_metric_type.test.js b/src/plugins/vis_types/timeseries/public/application/components/lib/get_supported_fields_by_metric_type.test.js similarity index 95% rename from src/plugins/vis_type_timeseries/public/application/components/lib/get_supported_fields_by_metric_type.test.js rename to src/plugins/vis_types/timeseries/public/application/components/lib/get_supported_fields_by_metric_type.test.js index c009146abb7bd..878b4f7655f3e 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/lib/get_supported_fields_by_metric_type.test.js +++ b/src/plugins/vis_types/timeseries/public/application/components/lib/get_supported_fields_by_metric_type.test.js @@ -7,7 +7,7 @@ */ import { getSupportedFieldsByMetricType } from './get_supported_fields_by_metric_type'; -import { KBN_FIELD_TYPES } from '../../../../../../plugins/data/public'; +import { KBN_FIELD_TYPES } from '../../../../../../../plugins/data/public'; describe('getSupportedFieldsByMetricType', () => { const shouldHaveHistogramAndNumbers = (type) => diff --git a/src/plugins/vis_type_timeseries/public/application/components/lib/index_pattern_select/combo_box_select.tsx b/src/plugins/vis_types/timeseries/public/application/components/lib/index_pattern_select/combo_box_select.tsx similarity index 97% rename from src/plugins/vis_type_timeseries/public/application/components/lib/index_pattern_select/combo_box_select.tsx rename to src/plugins/vis_types/timeseries/public/application/components/lib/index_pattern_select/combo_box_select.tsx index 7111a63244c7f..ad60aee290640 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/lib/index_pattern_select/combo_box_select.tsx +++ b/src/plugins/vis_types/timeseries/public/application/components/lib/index_pattern_select/combo_box_select.tsx @@ -15,7 +15,7 @@ import { SwitchModePopover } from './switch_mode_popover'; import type { SelectIndexComponentProps } from './types'; import type { IndexPatternValue } from '../../../../../common/types'; -import type { IndexPatternsService } from '../../../../../../data/public'; +import type { IndexPatternsService } from '../../../../../../../data/public'; /** @internal **/ type IdsWithTitle = UnwrapPromise>; diff --git a/src/plugins/vis_type_timeseries/public/application/components/lib/index_pattern_select/field_text_select.tsx b/src/plugins/vis_types/timeseries/public/application/components/lib/index_pattern_select/field_text_select.tsx similarity index 100% rename from src/plugins/vis_type_timeseries/public/application/components/lib/index_pattern_select/field_text_select.tsx rename to src/plugins/vis_types/timeseries/public/application/components/lib/index_pattern_select/field_text_select.tsx diff --git a/src/plugins/vis_type_timeseries/public/application/components/lib/index_pattern_select/index.ts b/src/plugins/vis_types/timeseries/public/application/components/lib/index_pattern_select/index.ts similarity index 100% rename from src/plugins/vis_type_timeseries/public/application/components/lib/index_pattern_select/index.ts rename to src/plugins/vis_types/timeseries/public/application/components/lib/index_pattern_select/index.ts diff --git a/src/plugins/vis_type_timeseries/public/application/components/lib/index_pattern_select/index_pattern_select.tsx b/src/plugins/vis_types/timeseries/public/application/components/lib/index_pattern_select/index_pattern_select.tsx similarity index 98% rename from src/plugins/vis_type_timeseries/public/application/components/lib/index_pattern_select/index_pattern_select.tsx rename to src/plugins/vis_types/timeseries/public/application/components/lib/index_pattern_select/index_pattern_select.tsx index 927b3c608c16c..1029ac67cc43c 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/lib/index_pattern_select/index_pattern_select.tsx +++ b/src/plugins/vis_types/timeseries/public/application/components/lib/index_pattern_select/index_pattern_select.tsx @@ -19,7 +19,7 @@ import { ComboBoxSelect } from './combo_box_select'; import type { IndexPatternValue, FetchedIndexPattern } from '../../../../../common/types'; import { USE_KIBANA_INDEXES_KEY } from '../../../../../common/constants'; -import { IndexPattern } from '../../../../../../data/common'; +import { IndexPattern } from '../../../../../../../data/common'; export interface IndexPatternSelectProps { indexPatternName: string; diff --git a/src/plugins/vis_type_timeseries/public/application/components/lib/index_pattern_select/switch_mode_popover.tsx b/src/plugins/vis_types/timeseries/public/application/components/lib/index_pattern_select/switch_mode_popover.tsx similarity index 100% rename from src/plugins/vis_type_timeseries/public/application/components/lib/index_pattern_select/switch_mode_popover.tsx rename to src/plugins/vis_types/timeseries/public/application/components/lib/index_pattern_select/switch_mode_popover.tsx diff --git a/src/plugins/vis_type_timeseries/public/application/components/lib/index_pattern_select/types.ts b/src/plugins/vis_types/timeseries/public/application/components/lib/index_pattern_select/types.ts similarity index 93% rename from src/plugins/vis_type_timeseries/public/application/components/lib/index_pattern_select/types.ts rename to src/plugins/vis_types/timeseries/public/application/components/lib/index_pattern_select/types.ts index 18288f75d4c90..244e95e8db9dd 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/lib/index_pattern_select/types.ts +++ b/src/plugins/vis_types/timeseries/public/application/components/lib/index_pattern_select/types.ts @@ -7,7 +7,7 @@ */ import type { Assign } from '@kbn/utility-types'; import type { FetchedIndexPattern, IndexPatternValue } from '../../../../../common/types'; -import type { IndexPattern } from '../../../../../../data/common'; +import type { IndexPattern } from '../../../../../../../data/common'; /** @internal **/ export interface SelectIndexComponentProps { diff --git a/src/plugins/vis_type_timeseries/public/application/components/lib/label_date_formatter.test.ts b/src/plugins/vis_types/timeseries/public/application/components/lib/label_date_formatter.test.ts similarity index 100% rename from src/plugins/vis_type_timeseries/public/application/components/lib/label_date_formatter.test.ts rename to src/plugins/vis_types/timeseries/public/application/components/lib/label_date_formatter.test.ts diff --git a/src/plugins/vis_type_timeseries/public/application/components/lib/label_date_formatter.ts b/src/plugins/vis_types/timeseries/public/application/components/lib/label_date_formatter.ts similarity index 100% rename from src/plugins/vis_type_timeseries/public/application/components/lib/label_date_formatter.ts rename to src/plugins/vis_types/timeseries/public/application/components/lib/label_date_formatter.ts diff --git a/src/plugins/vis_type_timeseries/public/application/components/lib/new_metric_agg_fn.ts b/src/plugins/vis_types/timeseries/public/application/components/lib/new_metric_agg_fn.ts similarity index 89% rename from src/plugins/vis_type_timeseries/public/application/components/lib/new_metric_agg_fn.ts rename to src/plugins/vis_types/timeseries/public/application/components/lib/new_metric_agg_fn.ts index 28dd4c81510fc..9c7cc1225c5ac 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/lib/new_metric_agg_fn.ts +++ b/src/plugins/vis_types/timeseries/public/application/components/lib/new_metric_agg_fn.ts @@ -7,7 +7,7 @@ */ import uuid from 'uuid'; -import { METRIC_TYPES } from '../../../../../data/common'; +import { METRIC_TYPES } from '../../../../../../data/common'; import type { Metric } from '../../../../common/types'; export const newMetricAggFn = (): Metric => { diff --git a/src/plugins/vis_type_timeseries/public/application/components/lib/new_series_fn.js b/src/plugins/vis_types/timeseries/public/application/components/lib/new_series_fn.js similarity index 100% rename from src/plugins/vis_type_timeseries/public/application/components/lib/new_series_fn.js rename to src/plugins/vis_types/timeseries/public/application/components/lib/new_series_fn.js diff --git a/src/plugins/vis_type_timeseries/public/application/components/lib/re_id_series.js b/src/plugins/vis_types/timeseries/public/application/components/lib/re_id_series.js similarity index 100% rename from src/plugins/vis_type_timeseries/public/application/components/lib/re_id_series.js rename to src/plugins/vis_types/timeseries/public/application/components/lib/re_id_series.js diff --git a/src/plugins/vis_type_timeseries/public/application/components/lib/re_id_series.test.js b/src/plugins/vis_types/timeseries/public/application/components/lib/re_id_series.test.js similarity index 100% rename from src/plugins/vis_type_timeseries/public/application/components/lib/re_id_series.test.js rename to src/plugins/vis_types/timeseries/public/application/components/lib/re_id_series.test.js diff --git a/src/plugins/vis_type_timeseries/public/application/components/lib/reorder.ts b/src/plugins/vis_types/timeseries/public/application/components/lib/reorder.ts similarity index 100% rename from src/plugins/vis_type_timeseries/public/application/components/lib/reorder.ts rename to src/plugins/vis_types/timeseries/public/application/components/lib/reorder.ts diff --git a/src/plugins/vis_type_timeseries/public/application/components/lib/replace_vars.test.ts b/src/plugins/vis_types/timeseries/public/application/components/lib/replace_vars.test.ts similarity index 100% rename from src/plugins/vis_type_timeseries/public/application/components/lib/replace_vars.test.ts rename to src/plugins/vis_types/timeseries/public/application/components/lib/replace_vars.test.ts diff --git a/src/plugins/vis_type_timeseries/public/application/components/lib/replace_vars.ts b/src/plugins/vis_types/timeseries/public/application/components/lib/replace_vars.ts similarity index 100% rename from src/plugins/vis_type_timeseries/public/application/components/lib/replace_vars.ts rename to src/plugins/vis_types/timeseries/public/application/components/lib/replace_vars.ts diff --git a/src/plugins/vis_type_timeseries/public/application/components/lib/series_change_handler.js b/src/plugins/vis_types/timeseries/public/application/components/lib/series_change_handler.js similarity index 100% rename from src/plugins/vis_type_timeseries/public/application/components/lib/series_change_handler.js rename to src/plugins/vis_types/timeseries/public/application/components/lib/series_change_handler.js diff --git a/src/plugins/vis_type_timeseries/public/application/components/lib/stacked.js b/src/plugins/vis_types/timeseries/public/application/components/lib/stacked.js similarity index 100% rename from src/plugins/vis_type_timeseries/public/application/components/lib/stacked.js rename to src/plugins/vis_types/timeseries/public/application/components/lib/stacked.js diff --git a/src/plugins/vis_type_timeseries/public/application/components/lib/tick_formatter.js b/src/plugins/vis_types/timeseries/public/application/components/lib/tick_formatter.js similarity index 100% rename from src/plugins/vis_type_timeseries/public/application/components/lib/tick_formatter.js rename to src/plugins/vis_types/timeseries/public/application/components/lib/tick_formatter.js diff --git a/src/plugins/vis_type_timeseries/public/application/components/lib/tick_formatter.test.js b/src/plugins/vis_types/timeseries/public/application/components/lib/tick_formatter.test.js similarity index 93% rename from src/plugins/vis_type_timeseries/public/application/components/lib/tick_formatter.test.js rename to src/plugins/vis_types/timeseries/public/application/components/lib/tick_formatter.test.js index 9b9beae67e44f..8053c066114a3 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/lib/tick_formatter.test.js +++ b/src/plugins/vis_types/timeseries/public/application/components/lib/tick_formatter.test.js @@ -7,10 +7,10 @@ */ import { createTickFormatter } from './tick_formatter'; -import { getFieldFormatsRegistry } from '../../../../../data/public/test_utils'; +import { getFieldFormatsRegistry } from '../../../../../../data/public/test_utils'; import { setFieldFormats } from '../../../services'; -import { UI_SETTINGS } from '../../../../../data/public'; -import { FORMATS_UI_SETTINGS } from '../../../../../field_formats/common'; +import { UI_SETTINGS } from '../../../../../../data/public'; +import { FORMATS_UI_SETTINGS } from '../../../../../../field_formats/common'; const mockUiSettings = { get: (item) => { diff --git a/src/plugins/vis_type_timeseries/public/application/components/lib/types.ts b/src/plugins/vis_types/timeseries/public/application/components/lib/types.ts similarity index 100% rename from src/plugins/vis_type_timeseries/public/application/components/lib/types.ts rename to src/plugins/vis_types/timeseries/public/application/components/lib/types.ts diff --git a/src/plugins/vis_type_timeseries/public/application/components/markdown_editor.js b/src/plugins/vis_types/timeseries/public/application/components/markdown_editor.js similarity index 98% rename from src/plugins/vis_type_timeseries/public/application/components/markdown_editor.js rename to src/plugins/vis_types/timeseries/public/application/components/markdown_editor.js index 046b1c5799836..adee297fe0119 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/markdown_editor.js +++ b/src/plugins/vis_types/timeseries/public/application/components/markdown_editor.js @@ -15,7 +15,7 @@ import React, { Component } from 'react'; import { createTickFormatter } from './lib/tick_formatter'; import { convertSeriesToVars } from './lib/convert_series_to_vars'; import _ from 'lodash'; -import { CodeEditor, MarkdownLang } from '../../../../kibana_react/public'; +import { CodeEditor, MarkdownLang } from '../../../../../kibana_react/public'; import { EuiText, EuiCodeBlock, EuiSpacer, EuiTitle } from '@elastic/eui'; diff --git a/src/plugins/vis_type_timeseries/public/application/components/palette_picker.test.tsx b/src/plugins/vis_types/timeseries/public/application/components/palette_picker.test.tsx similarity index 97% rename from src/plugins/vis_type_timeseries/public/application/components/palette_picker.test.tsx rename to src/plugins/vis_types/timeseries/public/application/components/palette_picker.test.tsx index ae7fa5c59c73d..81b33943f8b04 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/palette_picker.test.tsx +++ b/src/plugins/vis_types/timeseries/public/application/components/palette_picker.test.tsx @@ -9,7 +9,7 @@ import React from 'react'; import { mountWithIntl } from '@kbn/test/jest'; import { ReactWrapper } from 'enzyme'; import { PalettePicker, PalettePickerProps } from './palette_picker'; -import { chartPluginMock } from '../../../../charts/public/mocks'; +import { chartPluginMock } from '../../../../../charts/public/mocks'; import { EuiColorPalettePicker } from '@elastic/eui'; import { PALETTES } from '../../../common/enums'; diff --git a/src/plugins/vis_type_timeseries/public/application/components/palette_picker.tsx b/src/plugins/vis_types/timeseries/public/application/components/palette_picker.tsx similarity index 100% rename from src/plugins/vis_type_timeseries/public/application/components/palette_picker.tsx rename to src/plugins/vis_types/timeseries/public/application/components/palette_picker.tsx diff --git a/src/plugins/vis_type_timeseries/public/application/components/panel_config/_index.scss b/src/plugins/vis_types/timeseries/public/application/components/panel_config/_index.scss similarity index 100% rename from src/plugins/vis_type_timeseries/public/application/components/panel_config/_index.scss rename to src/plugins/vis_types/timeseries/public/application/components/panel_config/_index.scss diff --git a/src/plugins/vis_type_timeseries/public/application/components/panel_config/_panel_config.scss b/src/plugins/vis_types/timeseries/public/application/components/panel_config/_panel_config.scss similarity index 100% rename from src/plugins/vis_type_timeseries/public/application/components/panel_config/_panel_config.scss rename to src/plugins/vis_types/timeseries/public/application/components/panel_config/_panel_config.scss diff --git a/src/plugins/vis_type_timeseries/public/application/components/panel_config/gauge.test.tsx b/src/plugins/vis_types/timeseries/public/application/components/panel_config/gauge.test.tsx similarity index 100% rename from src/plugins/vis_type_timeseries/public/application/components/panel_config/gauge.test.tsx rename to src/plugins/vis_types/timeseries/public/application/components/panel_config/gauge.test.tsx diff --git a/src/plugins/vis_type_timeseries/public/application/components/panel_config/gauge.tsx b/src/plugins/vis_types/timeseries/public/application/components/panel_config/gauge.tsx similarity index 100% rename from src/plugins/vis_type_timeseries/public/application/components/panel_config/gauge.tsx rename to src/plugins/vis_types/timeseries/public/application/components/panel_config/gauge.tsx diff --git a/src/plugins/vis_type_timeseries/public/application/components/panel_config/index.ts b/src/plugins/vis_types/timeseries/public/application/components/panel_config/index.ts similarity index 100% rename from src/plugins/vis_type_timeseries/public/application/components/panel_config/index.ts rename to src/plugins/vis_types/timeseries/public/application/components/panel_config/index.ts diff --git a/src/plugins/vis_type_timeseries/public/application/components/panel_config/markdown.tsx b/src/plugins/vis_types/timeseries/public/application/components/panel_config/markdown.tsx similarity index 99% rename from src/plugins/vis_type_timeseries/public/application/components/panel_config/markdown.tsx rename to src/plugins/vis_types/timeseries/public/application/components/panel_config/markdown.tsx index 7f82f95d250ea..b099209af4348 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/panel_config/markdown.tsx +++ b/src/plugins/vis_types/timeseries/public/application/components/panel_config/markdown.tsx @@ -42,7 +42,7 @@ import { getDefaultQueryLanguage } from '../lib/get_default_query_language'; import { VisDataContext } from '../../contexts/vis_data_context'; import { PanelConfigProps, PANEL_CONFIG_TABS } from './types'; import { TimeseriesVisParams } from '../../../types'; -import { CodeEditor, CssLang } from '../../../../../kibana_react/public'; +import { CodeEditor, CssLang } from '../../../../../../kibana_react/public'; const lessC = less(window, { env: 'production' }); diff --git a/src/plugins/vis_type_timeseries/public/application/components/panel_config/metric.tsx b/src/plugins/vis_types/timeseries/public/application/components/panel_config/metric.tsx similarity index 100% rename from src/plugins/vis_type_timeseries/public/application/components/panel_config/metric.tsx rename to src/plugins/vis_types/timeseries/public/application/components/panel_config/metric.tsx diff --git a/src/plugins/vis_type_timeseries/public/application/components/panel_config/panel_config.tsx b/src/plugins/vis_types/timeseries/public/application/components/panel_config/panel_config.tsx similarity index 100% rename from src/plugins/vis_type_timeseries/public/application/components/panel_config/panel_config.tsx rename to src/plugins/vis_types/timeseries/public/application/components/panel_config/panel_config.tsx diff --git a/src/plugins/vis_type_timeseries/public/application/components/panel_config/table.tsx b/src/plugins/vis_types/timeseries/public/application/components/panel_config/table.tsx similarity index 99% rename from src/plugins/vis_type_timeseries/public/application/components/panel_config/table.tsx rename to src/plugins/vis_types/timeseries/public/application/components/panel_config/table.tsx index 38cbd57b0b517..ecbd9767f34a6 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/panel_config/table.tsx +++ b/src/plugins/vis_types/timeseries/public/application/components/panel_config/table.tsx @@ -42,7 +42,7 @@ import { BUCKET_TYPES } from '../../../../common/enums'; import { PanelConfigProps, PANEL_CONFIG_TABS } from './types'; import { TimeseriesVisParams } from '../../../types'; import { getIndexPatternKey } from '../../../../common/index_patterns_utils'; -import { KBN_FIELD_TYPES } from '../../../../../data/public'; +import { KBN_FIELD_TYPES } from '../../../../../../data/public'; export class TablePanelConfig extends Component< PanelConfigProps, diff --git a/src/plugins/vis_type_timeseries/public/application/components/panel_config/timeseries.test.tsx b/src/plugins/vis_types/timeseries/public/application/components/panel_config/timeseries.test.tsx similarity index 100% rename from src/plugins/vis_type_timeseries/public/application/components/panel_config/timeseries.test.tsx rename to src/plugins/vis_types/timeseries/public/application/components/panel_config/timeseries.test.tsx diff --git a/src/plugins/vis_type_timeseries/public/application/components/panel_config/timeseries.tsx b/src/plugins/vis_types/timeseries/public/application/components/panel_config/timeseries.tsx similarity index 100% rename from src/plugins/vis_type_timeseries/public/application/components/panel_config/timeseries.tsx rename to src/plugins/vis_types/timeseries/public/application/components/panel_config/timeseries.tsx diff --git a/src/plugins/vis_type_timeseries/public/application/components/panel_config/top_n.tsx b/src/plugins/vis_types/timeseries/public/application/components/panel_config/top_n.tsx similarity index 100% rename from src/plugins/vis_type_timeseries/public/application/components/panel_config/top_n.tsx rename to src/plugins/vis_types/timeseries/public/application/components/panel_config/top_n.tsx diff --git a/src/plugins/vis_type_timeseries/public/application/components/panel_config/types.ts b/src/plugins/vis_types/timeseries/public/application/components/panel_config/types.ts similarity index 100% rename from src/plugins/vis_type_timeseries/public/application/components/panel_config/types.ts rename to src/plugins/vis_types/timeseries/public/application/components/panel_config/types.ts diff --git a/src/plugins/vis_type_timeseries/public/application/components/query_bar_wrapper.tsx b/src/plugins/vis_types/timeseries/public/application/components/query_bar_wrapper.tsx similarity index 98% rename from src/plugins/vis_type_timeseries/public/application/components/query_bar_wrapper.tsx rename to src/plugins/vis_types/timeseries/public/application/components/query_bar_wrapper.tsx index d3b249f54fe34..e0c66ea8d70a7 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/query_bar_wrapper.tsx +++ b/src/plugins/vis_types/timeseries/public/application/components/query_bar_wrapper.tsx @@ -11,7 +11,7 @@ import React, { useContext, useEffect, useState } from 'react'; import { CoreStartContext } from '../contexts/query_input_bar_context'; import type { IndexPatternValue } from '../../../common/types'; -import { QueryStringInput, QueryStringInputProps } from '../../../../../plugins/data/public'; +import { QueryStringInput, QueryStringInputProps } from '../../../../../../plugins/data/public'; import { getDataStart } from '../../services'; import { fetchIndexPattern, isStringTypeIndexPattern } from '../../../common/index_patterns_utils'; diff --git a/src/plugins/vis_type_timeseries/public/application/components/series.js b/src/plugins/vis_types/timeseries/public/application/components/series.js similarity index 100% rename from src/plugins/vis_type_timeseries/public/application/components/series.js rename to src/plugins/vis_types/timeseries/public/application/components/series.js diff --git a/src/plugins/vis_type_timeseries/public/application/components/series_config.js b/src/plugins/vis_types/timeseries/public/application/components/series_config.js similarity index 100% rename from src/plugins/vis_type_timeseries/public/application/components/series_config.js rename to src/plugins/vis_types/timeseries/public/application/components/series_config.js diff --git a/src/plugins/vis_type_timeseries/public/application/components/series_config_query_bar_with_ignore_global_filter.js b/src/plugins/vis_types/timeseries/public/application/components/series_config_query_bar_with_ignore_global_filter.js similarity index 100% rename from src/plugins/vis_type_timeseries/public/application/components/series_config_query_bar_with_ignore_global_filter.js rename to src/plugins/vis_types/timeseries/public/application/components/series_config_query_bar_with_ignore_global_filter.js diff --git a/src/plugins/vis_type_timeseries/public/application/components/series_drag_handler.tsx b/src/plugins/vis_types/timeseries/public/application/components/series_drag_handler.tsx similarity index 100% rename from src/plugins/vis_type_timeseries/public/application/components/series_drag_handler.tsx rename to src/plugins/vis_types/timeseries/public/application/components/series_drag_handler.tsx diff --git a/src/plugins/vis_type_timeseries/public/application/components/series_editor.js b/src/plugins/vis_types/timeseries/public/application/components/series_editor.js similarity index 100% rename from src/plugins/vis_type_timeseries/public/application/components/series_editor.js rename to src/plugins/vis_types/timeseries/public/application/components/series_editor.js diff --git a/src/plugins/vis_type_timeseries/public/application/components/split.js b/src/plugins/vis_types/timeseries/public/application/components/split.js similarity index 100% rename from src/plugins/vis_type_timeseries/public/application/components/split.js rename to src/plugins/vis_types/timeseries/public/application/components/split.js diff --git a/src/plugins/vis_type_timeseries/public/application/components/splits/__snapshots__/terms.test.js.snap b/src/plugins/vis_types/timeseries/public/application/components/splits/__snapshots__/terms.test.js.snap similarity index 100% rename from src/plugins/vis_type_timeseries/public/application/components/splits/__snapshots__/terms.test.js.snap rename to src/plugins/vis_types/timeseries/public/application/components/splits/__snapshots__/terms.test.js.snap diff --git a/src/plugins/vis_type_timeseries/public/application/components/splits/everything.js b/src/plugins/vis_types/timeseries/public/application/components/splits/everything.js similarity index 100% rename from src/plugins/vis_type_timeseries/public/application/components/splits/everything.js rename to src/plugins/vis_types/timeseries/public/application/components/splits/everything.js diff --git a/src/plugins/vis_type_timeseries/public/application/components/splits/filter.js b/src/plugins/vis_types/timeseries/public/application/components/splits/filter.js similarity index 100% rename from src/plugins/vis_type_timeseries/public/application/components/splits/filter.js rename to src/plugins/vis_types/timeseries/public/application/components/splits/filter.js diff --git a/src/plugins/vis_type_timeseries/public/application/components/splits/filter_items.js b/src/plugins/vis_types/timeseries/public/application/components/splits/filter_items.js similarity index 100% rename from src/plugins/vis_type_timeseries/public/application/components/splits/filter_items.js rename to src/plugins/vis_types/timeseries/public/application/components/splits/filter_items.js diff --git a/src/plugins/vis_type_timeseries/public/application/components/splits/filters.js b/src/plugins/vis_types/timeseries/public/application/components/splits/filters.js similarity index 100% rename from src/plugins/vis_type_timeseries/public/application/components/splits/filters.js rename to src/plugins/vis_types/timeseries/public/application/components/splits/filters.js diff --git a/src/plugins/vis_type_timeseries/public/application/components/splits/group_by_select.js b/src/plugins/vis_types/timeseries/public/application/components/splits/group_by_select.js similarity index 100% rename from src/plugins/vis_type_timeseries/public/application/components/splits/group_by_select.js rename to src/plugins/vis_types/timeseries/public/application/components/splits/group_by_select.js diff --git a/src/plugins/vis_type_timeseries/public/application/components/splits/terms.js b/src/plugins/vis_types/timeseries/public/application/components/splits/terms.js similarity index 99% rename from src/plugins/vis_type_timeseries/public/application/components/splits/terms.js rename to src/plugins/vis_types/timeseries/public/application/components/splits/terms.js index a668e5b727b48..b32af037533f7 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/splits/terms.js +++ b/src/plugins/vis_types/timeseries/public/application/components/splits/terms.js @@ -25,7 +25,7 @@ import { EuiFieldText, } from '@elastic/eui'; import { injectI18n, FormattedMessage } from '@kbn/i18n/react'; -import { KBN_FIELD_TYPES } from '../../../../../data/public'; +import { KBN_FIELD_TYPES } from '../../../../../../data/public'; import { STACKED_OPTIONS } from '../../visualizations/constants'; import { getIndexPatternKey } from '../../../../common/index_patterns_utils'; diff --git a/src/plugins/vis_type_timeseries/public/application/components/splits/terms.test.js b/src/plugins/vis_types/timeseries/public/application/components/splits/terms.test.js similarity index 100% rename from src/plugins/vis_type_timeseries/public/application/components/splits/terms.test.js rename to src/plugins/vis_types/timeseries/public/application/components/splits/terms.test.js diff --git a/src/plugins/vis_type_timeseries/public/application/components/splits/unsupported_split.js b/src/plugins/vis_types/timeseries/public/application/components/splits/unsupported_split.js similarity index 100% rename from src/plugins/vis_type_timeseries/public/application/components/splits/unsupported_split.js rename to src/plugins/vis_types/timeseries/public/application/components/splits/unsupported_split.js diff --git a/src/plugins/vis_type_timeseries/public/application/components/svg/bomb_icon.js b/src/plugins/vis_types/timeseries/public/application/components/svg/bomb_icon.js similarity index 100% rename from src/plugins/vis_type_timeseries/public/application/components/svg/bomb_icon.js rename to src/plugins/vis_types/timeseries/public/application/components/svg/bomb_icon.js diff --git a/src/plugins/vis_type_timeseries/public/application/components/svg/fire_icon.js b/src/plugins/vis_types/timeseries/public/application/components/svg/fire_icon.js similarity index 100% rename from src/plugins/vis_type_timeseries/public/application/components/svg/fire_icon.js rename to src/plugins/vis_types/timeseries/public/application/components/svg/fire_icon.js diff --git a/src/plugins/vis_type_timeseries/public/application/components/timeseries_visualization.scss b/src/plugins/vis_types/timeseries/public/application/components/timeseries_visualization.scss similarity index 100% rename from src/plugins/vis_type_timeseries/public/application/components/timeseries_visualization.scss rename to src/plugins/vis_types/timeseries/public/application/components/timeseries_visualization.scss diff --git a/src/plugins/vis_type_timeseries/public/application/components/timeseries_visualization.tsx b/src/plugins/vis_types/timeseries/public/application/components/timeseries_visualization.tsx similarity index 98% rename from src/plugins/vis_type_timeseries/public/application/components/timeseries_visualization.tsx rename to src/plugins/vis_types/timeseries/public/application/components/timeseries_visualization.tsx index b1722d4098587..a73f9c6a5e092 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/timeseries_visualization.tsx +++ b/src/plugins/vis_types/timeseries/public/application/components/timeseries_visualization.tsx @@ -27,7 +27,7 @@ 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 type { IndexPattern } from '../../../../../data/common'; interface TimeseriesVisualizationProps { className?: string; diff --git a/src/plugins/vis_type_timeseries/public/application/components/use_index_patter_mode_callout.tsx b/src/plugins/vis_types/timeseries/public/application/components/use_index_patter_mode_callout.tsx similarity index 100% rename from src/plugins/vis_type_timeseries/public/application/components/use_index_patter_mode_callout.tsx rename to src/plugins/vis_types/timeseries/public/application/components/use_index_patter_mode_callout.tsx diff --git a/src/plugins/vis_type_timeseries/public/application/components/vis_editor.tsx b/src/plugins/vis_types/timeseries/public/application/components/vis_editor.tsx similarity index 95% rename from src/plugins/vis_type_timeseries/public/application/components/vis_editor.tsx rename to src/plugins/vis_types/timeseries/public/application/components/vis_editor.tsx index 5e4ff436ff1e6..9e46427e33c2e 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/vis_editor.tsx +++ b/src/plugins/vis_types/timeseries/public/application/components/vis_editor.tsx @@ -15,11 +15,11 @@ import type { IUiSettingsClient } from 'kibana/public'; import type { Vis, VisualizeEmbeddableContract, -} from '../../../../../plugins/visualizations/public'; -import { KibanaContextProvider } from '../../../../../plugins/kibana_react/public'; -import { Storage } from '../../../../../plugins/kibana_utils/public'; +} from '../../../../../../plugins/visualizations/public'; +import { KibanaContextProvider } from '../../../../../../plugins/kibana_react/public'; +import { Storage } from '../../../../../../plugins/kibana_utils/public'; -import type { TimeRange } from '../../../../../plugins/data/public'; +import type { TimeRange } from '../../../../../../plugins/data/public'; import type { IndexPatternValue, TimeseriesVisData } from '../../../common/types'; // @ts-expect-error @@ -32,7 +32,7 @@ import { fetchFields, VisFields } from '../lib/fetch_fields'; import { getDataStart, getCoreStart } from '../../services'; import type { TimeseriesVisParams } from '../../types'; import { UseIndexPatternModeCallout } from './use_index_patter_mode_callout'; -import type { EditorRenderProps } from '../../../../visualize/public'; +import type { EditorRenderProps } from '../../../../../visualize/public'; const VIS_STATE_DEBOUNCE_DELAY = 200; const APP_NAME = 'VisEditor'; diff --git a/src/plugins/vis_type_timeseries/public/application/components/vis_editor_lazy.tsx b/src/plugins/vis_types/timeseries/public/application/components/vis_editor_lazy.tsx similarity index 100% rename from src/plugins/vis_type_timeseries/public/application/components/vis_editor_lazy.tsx rename to src/plugins/vis_types/timeseries/public/application/components/vis_editor_lazy.tsx diff --git a/src/plugins/vis_type_timeseries/public/application/components/vis_editor_visualization.js b/src/plugins/vis_types/timeseries/public/application/components/vis_editor_visualization.js similarity index 100% rename from src/plugins/vis_type_timeseries/public/application/components/vis_editor_visualization.js rename to src/plugins/vis_types/timeseries/public/application/components/vis_editor_visualization.js diff --git a/src/plugins/vis_type_timeseries/public/application/components/vis_picker.tsx b/src/plugins/vis_types/timeseries/public/application/components/vis_picker.tsx similarity index 100% rename from src/plugins/vis_type_timeseries/public/application/components/vis_picker.tsx rename to src/plugins/vis_types/timeseries/public/application/components/vis_picker.tsx diff --git a/src/plugins/vis_type_timeseries/public/application/components/vis_types/_index.scss b/src/plugins/vis_types/timeseries/public/application/components/vis_types/_index.scss similarity index 100% rename from src/plugins/vis_type_timeseries/public/application/components/vis_types/_index.scss rename to src/plugins/vis_types/timeseries/public/application/components/vis_types/_index.scss diff --git a/src/plugins/vis_type_timeseries/public/application/components/vis_types/_vis_types.scss b/src/plugins/vis_types/timeseries/public/application/components/vis_types/_vis_types.scss similarity index 100% rename from src/plugins/vis_type_timeseries/public/application/components/vis_types/_vis_types.scss rename to src/plugins/vis_types/timeseries/public/application/components/vis_types/_vis_types.scss diff --git a/src/plugins/vis_type_timeseries/public/application/components/vis_types/gauge/series.js b/src/plugins/vis_types/timeseries/public/application/components/vis_types/gauge/series.js similarity index 100% rename from src/plugins/vis_type_timeseries/public/application/components/vis_types/gauge/series.js rename to src/plugins/vis_types/timeseries/public/application/components/vis_types/gauge/series.js diff --git a/src/plugins/vis_type_timeseries/public/application/components/vis_types/gauge/series.test.js b/src/plugins/vis_types/timeseries/public/application/components/vis_types/gauge/series.test.js similarity index 100% rename from src/plugins/vis_type_timeseries/public/application/components/vis_types/gauge/series.test.js rename to src/plugins/vis_types/timeseries/public/application/components/vis_types/gauge/series.test.js diff --git a/src/plugins/vis_type_timeseries/public/application/components/vis_types/gauge/vis.js b/src/plugins/vis_types/timeseries/public/application/components/vis_types/gauge/vis.js similarity index 100% rename from src/plugins/vis_type_timeseries/public/application/components/vis_types/gauge/vis.js rename to src/plugins/vis_types/timeseries/public/application/components/vis_types/gauge/vis.js diff --git a/src/plugins/vis_type_timeseries/public/application/components/vis_types/index.ts b/src/plugins/vis_types/timeseries/public/application/components/vis_types/index.ts similarity index 96% rename from src/plugins/vis_type_timeseries/public/application/components/vis_types/index.ts rename to src/plugins/vis_types/timeseries/public/application/components/vis_types/index.ts index b2e40940b8001..653b2985ed1ae 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/vis_types/index.ts +++ b/src/plugins/vis_types/timeseries/public/application/components/vis_types/index.ts @@ -14,7 +14,7 @@ import { PaletteRegistry } from 'src/plugins/charts/public'; import { TimeseriesVisParams } from '../../../types'; import type { TimeseriesVisData, PanelData } from '../../../../common/types'; -import type { FieldFormatMap } from '../../../../../data/common'; +import type { FieldFormatMap } from '../../../../../../data/common'; /** * Lazy load each visualization type, since the only one is presented on the screen at the same time. diff --git a/src/plugins/vis_type_timeseries/public/application/components/vis_types/markdown/_markdown.scss b/src/plugins/vis_types/timeseries/public/application/components/vis_types/markdown/_markdown.scss similarity index 100% rename from src/plugins/vis_type_timeseries/public/application/components/vis_types/markdown/_markdown.scss rename to src/plugins/vis_types/timeseries/public/application/components/vis_types/markdown/_markdown.scss diff --git a/src/plugins/vis_type_timeseries/public/application/components/vis_types/markdown/series.js b/src/plugins/vis_types/timeseries/public/application/components/vis_types/markdown/series.js similarity index 100% rename from src/plugins/vis_type_timeseries/public/application/components/vis_types/markdown/series.js rename to src/plugins/vis_types/timeseries/public/application/components/vis_types/markdown/series.js diff --git a/src/plugins/vis_type_timeseries/public/application/components/vis_types/markdown/vis.js b/src/plugins/vis_types/timeseries/public/application/components/vis_types/markdown/vis.js similarity index 97% rename from src/plugins/vis_type_timeseries/public/application/components/vis_types/markdown/vis.js rename to src/plugins/vis_types/timeseries/public/application/components/vis_types/markdown/vis.js index fc7019bd38293..49fdbcd98501c 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/vis_types/markdown/vis.js +++ b/src/plugins/vis_types/timeseries/public/application/components/vis_types/markdown/vis.js @@ -11,7 +11,7 @@ import React from 'react'; import classNames from 'classnames'; import uuid from 'uuid'; import { get } from 'lodash'; -import { Markdown } from '../../../../../../../plugins/kibana_react/public'; +import { Markdown } from '../../../../../../../../plugins/kibana_react/public'; import { ErrorComponent } from '../../error'; import { replaceVars } from '../../lib/replace_vars'; diff --git a/src/plugins/vis_type_timeseries/public/application/components/vis_types/metric/series.js b/src/plugins/vis_types/timeseries/public/application/components/vis_types/metric/series.js similarity index 100% rename from src/plugins/vis_type_timeseries/public/application/components/vis_types/metric/series.js rename to src/plugins/vis_types/timeseries/public/application/components/vis_types/metric/series.js diff --git a/src/plugins/vis_type_timeseries/public/application/components/vis_types/metric/series.test.js b/src/plugins/vis_types/timeseries/public/application/components/vis_types/metric/series.test.js similarity index 100% rename from src/plugins/vis_type_timeseries/public/application/components/vis_types/metric/series.test.js rename to src/plugins/vis_types/timeseries/public/application/components/vis_types/metric/series.test.js diff --git a/src/plugins/vis_type_timeseries/public/application/components/vis_types/metric/vis.js b/src/plugins/vis_types/timeseries/public/application/components/vis_types/metric/vis.js similarity index 100% rename from src/plugins/vis_type_timeseries/public/application/components/vis_types/metric/vis.js rename to src/plugins/vis_types/timeseries/public/application/components/vis_types/metric/vis.js diff --git a/src/plugins/vis_type_timeseries/public/application/components/vis_types/table/config.js b/src/plugins/vis_types/timeseries/public/application/components/vis_types/table/config.js similarity index 100% rename from src/plugins/vis_type_timeseries/public/application/components/vis_types/table/config.js rename to src/plugins/vis_types/timeseries/public/application/components/vis_types/table/config.js diff --git a/src/plugins/vis_type_timeseries/public/application/components/vis_types/table/is_sortable.js b/src/plugins/vis_types/timeseries/public/application/components/vis_types/table/is_sortable.js similarity index 100% rename from src/plugins/vis_type_timeseries/public/application/components/vis_types/table/is_sortable.js rename to src/plugins/vis_types/timeseries/public/application/components/vis_types/table/is_sortable.js diff --git a/src/plugins/vis_type_timeseries/public/application/components/vis_types/table/series.js b/src/plugins/vis_types/timeseries/public/application/components/vis_types/table/series.js similarity index 100% rename from src/plugins/vis_type_timeseries/public/application/components/vis_types/table/series.js rename to src/plugins/vis_types/timeseries/public/application/components/vis_types/table/series.js diff --git a/src/plugins/vis_type_timeseries/public/application/components/vis_types/table/vis.js b/src/plugins/vis_types/timeseries/public/application/components/vis_types/table/vis.js similarity index 98% rename from src/plugins/vis_type_timeseries/public/application/components/vis_types/table/vis.js rename to src/plugins/vis_types/timeseries/public/application/components/vis_types/table/vis.js index 21d7de9f1d880..7b1db4b362647 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/vis_types/table/vis.js +++ b/src/plugins/vis_types/timeseries/public/application/components/vis_types/table/vis.js @@ -10,14 +10,14 @@ import _, { isArray, last, get } from 'lodash'; import React, { Component } from 'react'; import { parse as parseUrl } from 'url'; import PropTypes from 'prop-types'; -import { RedirectAppLinks } from '../../../../../../kibana_react/public'; +import { RedirectAppLinks } from '../../../../../../../kibana_react/public'; import { getMetricsField } from '../../lib/get_metrics_field'; import { createTickFormatter } from '../../lib/tick_formatter'; import { createFieldFormatter } from '../../lib/create_field_formatter'; import { isSortable } from './is_sortable'; import { EuiToolTip, EuiIcon } from '@elastic/eui'; import { replaceVars } from '../../lib/replace_vars'; -import { FIELD_FORMAT_IDS } from '../../../../../../../plugins/field_formats/common'; +import { FIELD_FORMAT_IDS } from '../../../../../../../../plugins/field_formats/common'; import { FormattedMessage } from '@kbn/i18n/react'; import { getFieldFormats, getCoreStart } from '../../../../services'; import { DATA_FORMATTERS } from '../../../../../common/enums'; diff --git a/src/plugins/vis_type_timeseries/public/application/components/vis_types/timeseries/config.js b/src/plugins/vis_types/timeseries/public/application/components/vis_types/timeseries/config.js similarity index 100% rename from src/plugins/vis_type_timeseries/public/application/components/vis_types/timeseries/config.js rename to src/plugins/vis_types/timeseries/public/application/components/vis_types/timeseries/config.js diff --git a/src/plugins/vis_type_timeseries/public/application/components/vis_types/timeseries/series.js b/src/plugins/vis_types/timeseries/public/application/components/vis_types/timeseries/series.js similarity index 100% rename from src/plugins/vis_type_timeseries/public/application/components/vis_types/timeseries/series.js rename to src/plugins/vis_types/timeseries/public/application/components/vis_types/timeseries/series.js diff --git a/src/plugins/vis_type_timeseries/public/application/components/vis_types/timeseries/vis.js b/src/plugins/vis_types/timeseries/public/application/components/vis_types/timeseries/vis.js similarity index 99% rename from src/plugins/vis_type_timeseries/public/application/components/vis_types/timeseries/vis.js rename to src/plugins/vis_types/timeseries/public/application/components/vis_types/timeseries/vis.js index fed295fef9d30..75a8f11e640df 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/vis_types/timeseries/vis.js +++ b/src/plugins/vis_types/timeseries/public/application/components/vis_types/timeseries/vis.js @@ -18,7 +18,7 @@ import { createTickFormatter } from '../../lib/tick_formatter'; import { createFieldFormatter } from '../../lib/create_field_formatter'; import { checkIfSeriesHaveSameFormatters } from '../../lib/check_if_series_have_same_formatters'; import { TimeSeries } from '../../../visualizations/views/timeseries'; -import { MarkdownSimple } from '../../../../../../../plugins/kibana_react/public'; +import { MarkdownSimple } from '../../../../../../../../plugins/kibana_react/public'; import { replaceVars } from '../../lib/replace_vars'; import { getInterval } from '../../lib/get_interval'; import { createIntervalBasedFormatter } from '../../lib/create_interval_based_formatter'; diff --git a/src/plugins/vis_type_timeseries/public/application/components/vis_types/timeseries/vis.test.js b/src/plugins/vis_types/timeseries/public/application/components/vis_types/timeseries/vis.test.js similarity index 96% rename from src/plugins/vis_type_timeseries/public/application/components/vis_types/timeseries/vis.test.js rename to src/plugins/vis_types/timeseries/public/application/components/vis_types/timeseries/vis.test.js index d6e7484e903bf..cf4c327df3d77 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/vis_types/timeseries/vis.test.js +++ b/src/plugins/vis_types/timeseries/public/application/components/vis_types/timeseries/vis.test.js @@ -12,11 +12,11 @@ import { TimeSeries } from '../../../visualizations/views/timeseries'; import TimeseriesVisualization from './vis'; import { setFieldFormats } from '../../../../services'; import { createFieldFormatter } from '../../lib/create_field_formatter'; -import { FORMATS_UI_SETTINGS } from '../../../../../../field_formats/common'; -import { METRIC_TYPES } from '../../../../../../data/common'; -import { getFieldFormatsRegistry } from '../../../../../../data/public/test_utils'; +import { FORMATS_UI_SETTINGS } from '../../../../../../../field_formats/common'; +import { METRIC_TYPES } from '../../../../../../../data/common'; +import { getFieldFormatsRegistry } from '../../../../../../../data/public/test_utils'; -jest.mock('../../../../../../data/public/services', () => ({ +jest.mock('../../../../../../../data/public/services', () => ({ getUiSettings: () => ({ get: jest.fn() }), })); diff --git a/src/plugins/vis_type_timeseries/public/application/components/vis_types/top_n/series.js b/src/plugins/vis_types/timeseries/public/application/components/vis_types/top_n/series.js similarity index 100% rename from src/plugins/vis_type_timeseries/public/application/components/vis_types/top_n/series.js rename to src/plugins/vis_types/timeseries/public/application/components/vis_types/top_n/series.js diff --git a/src/plugins/vis_type_timeseries/public/application/components/vis_types/top_n/vis.js b/src/plugins/vis_types/timeseries/public/application/components/vis_types/top_n/vis.js similarity index 100% rename from src/plugins/vis_type_timeseries/public/application/components/vis_types/top_n/vis.js rename to src/plugins/vis_types/timeseries/public/application/components/vis_types/top_n/vis.js diff --git a/src/plugins/vis_type_timeseries/public/application/components/vis_with_splits.js b/src/plugins/vis_types/timeseries/public/application/components/vis_with_splits.js similarity index 100% rename from src/plugins/vis_type_timeseries/public/application/components/vis_with_splits.js rename to src/plugins/vis_types/timeseries/public/application/components/vis_with_splits.js diff --git a/src/plugins/vis_type_timeseries/public/application/components/yes_no.test.tsx b/src/plugins/vis_types/timeseries/public/application/components/yes_no.test.tsx similarity index 100% rename from src/plugins/vis_type_timeseries/public/application/components/yes_no.test.tsx rename to src/plugins/vis_types/timeseries/public/application/components/yes_no.test.tsx diff --git a/src/plugins/vis_type_timeseries/public/application/components/yes_no.tsx b/src/plugins/vis_types/timeseries/public/application/components/yes_no.tsx similarity index 100% rename from src/plugins/vis_type_timeseries/public/application/components/yes_no.tsx rename to src/plugins/vis_types/timeseries/public/application/components/yes_no.tsx diff --git a/src/plugins/vis_type_timeseries/public/application/contexts/form_validation_context.ts b/src/plugins/vis_types/timeseries/public/application/contexts/form_validation_context.ts similarity index 100% rename from src/plugins/vis_type_timeseries/public/application/contexts/form_validation_context.ts rename to src/plugins/vis_types/timeseries/public/application/contexts/form_validation_context.ts diff --git a/src/plugins/vis_type_timeseries/public/application/contexts/panel_model_context.ts b/src/plugins/vis_types/timeseries/public/application/contexts/panel_model_context.ts similarity index 100% rename from src/plugins/vis_type_timeseries/public/application/contexts/panel_model_context.ts rename to src/plugins/vis_types/timeseries/public/application/contexts/panel_model_context.ts diff --git a/src/plugins/vis_type_timeseries/public/application/contexts/query_input_bar_context.ts b/src/plugins/vis_types/timeseries/public/application/contexts/query_input_bar_context.ts similarity index 100% rename from src/plugins/vis_type_timeseries/public/application/contexts/query_input_bar_context.ts rename to src/plugins/vis_types/timeseries/public/application/contexts/query_input_bar_context.ts diff --git a/src/plugins/vis_type_timeseries/public/application/contexts/vis_data_context.ts b/src/plugins/vis_types/timeseries/public/application/contexts/vis_data_context.ts similarity index 100% rename from src/plugins/vis_type_timeseries/public/application/contexts/vis_data_context.ts rename to src/plugins/vis_types/timeseries/public/application/contexts/vis_data_context.ts diff --git a/src/plugins/vis_type_timeseries/public/application/editor_controller.tsx b/src/plugins/vis_types/timeseries/public/application/editor_controller.tsx similarity index 100% rename from src/plugins/vis_type_timeseries/public/application/editor_controller.tsx rename to src/plugins/vis_types/timeseries/public/application/editor_controller.tsx diff --git a/src/plugins/vis_type_timeseries/public/application/index.scss b/src/plugins/vis_types/timeseries/public/application/index.scss similarity index 100% rename from src/plugins/vis_type_timeseries/public/application/index.scss rename to src/plugins/vis_types/timeseries/public/application/index.scss diff --git a/src/plugins/vis_type_timeseries/public/application/lib/check_ui_restrictions.js b/src/plugins/vis_types/timeseries/public/application/lib/check_ui_restrictions.js similarity index 100% rename from src/plugins/vis_type_timeseries/public/application/lib/check_ui_restrictions.js rename to src/plugins/vis_types/timeseries/public/application/lib/check_ui_restrictions.js diff --git a/src/plugins/vis_type_timeseries/public/application/lib/compute_gradient_final_color.test.ts b/src/plugins/vis_types/timeseries/public/application/lib/compute_gradient_final_color.test.ts similarity index 100% rename from src/plugins/vis_type_timeseries/public/application/lib/compute_gradient_final_color.test.ts rename to src/plugins/vis_types/timeseries/public/application/lib/compute_gradient_final_color.test.ts diff --git a/src/plugins/vis_type_timeseries/public/application/lib/compute_gradient_final_color.ts b/src/plugins/vis_types/timeseries/public/application/lib/compute_gradient_final_color.ts similarity index 100% rename from src/plugins/vis_type_timeseries/public/application/lib/compute_gradient_final_color.ts rename to src/plugins/vis_types/timeseries/public/application/lib/compute_gradient_final_color.ts diff --git a/src/plugins/vis_type_timeseries/public/application/lib/fetch_fields.ts b/src/plugins/vis_types/timeseries/public/application/lib/fetch_fields.ts similarity index 100% rename from src/plugins/vis_type_timeseries/public/application/lib/fetch_fields.ts rename to src/plugins/vis_types/timeseries/public/application/lib/fetch_fields.ts diff --git a/src/plugins/vis_type_timeseries/public/application/lib/get_split_by_terms_color.test.ts b/src/plugins/vis_types/timeseries/public/application/lib/get_split_by_terms_color.test.ts similarity index 97% rename from src/plugins/vis_type_timeseries/public/application/lib/get_split_by_terms_color.test.ts rename to src/plugins/vis_types/timeseries/public/application/lib/get_split_by_terms_color.test.ts index 0a78e525796ff..c447acb74dc37 100644 --- a/src/plugins/vis_type_timeseries/public/application/lib/get_split_by_terms_color.test.ts +++ b/src/plugins/vis_types/timeseries/public/application/lib/get_split_by_terms_color.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 { chartPluginMock } from '../../../../charts/public/mocks'; +import { chartPluginMock } from '../../../../../charts/public/mocks'; import { getSplitByTermsColor, SplitByTermsColorProps } from './get_split_by_terms_color'; const chartsRegistry = chartPluginMock.createPaletteRegistry(); diff --git a/src/plugins/vis_type_timeseries/public/application/lib/get_split_by_terms_color.ts b/src/plugins/vis_types/timeseries/public/application/lib/get_split_by_terms_color.ts similarity index 100% rename from src/plugins/vis_type_timeseries/public/application/lib/get_split_by_terms_color.ts rename to src/plugins/vis_types/timeseries/public/application/lib/get_split_by_terms_color.ts diff --git a/src/plugins/vis_type_timeseries/public/application/lib/get_timezone.ts b/src/plugins/vis_types/timeseries/public/application/lib/get_timezone.ts similarity index 100% rename from src/plugins/vis_type_timeseries/public/application/lib/get_timezone.ts rename to src/plugins/vis_types/timeseries/public/application/lib/get_timezone.ts diff --git a/src/plugins/vis_type_timeseries/public/application/lib/index.ts b/src/plugins/vis_types/timeseries/public/application/lib/index.ts similarity index 100% rename from src/plugins/vis_type_timeseries/public/application/lib/index.ts rename to src/plugins/vis_types/timeseries/public/application/lib/index.ts diff --git a/src/plugins/vis_type_timeseries/public/application/lib/rainbow_colors.ts b/src/plugins/vis_types/timeseries/public/application/lib/rainbow_colors.ts similarity index 100% rename from src/plugins/vis_type_timeseries/public/application/lib/rainbow_colors.ts rename to src/plugins/vis_types/timeseries/public/application/lib/rainbow_colors.ts diff --git a/src/plugins/vis_type_timeseries/public/application/lib/set_is_reversed.js b/src/plugins/vis_types/timeseries/public/application/lib/set_is_reversed.js similarity index 100% rename from src/plugins/vis_type_timeseries/public/application/lib/set_is_reversed.js rename to src/plugins/vis_types/timeseries/public/application/lib/set_is_reversed.js diff --git a/src/plugins/vis_type_timeseries/public/application/visualizations/constants/chart.ts b/src/plugins/vis_types/timeseries/public/application/visualizations/constants/chart.ts similarity index 100% rename from src/plugins/vis_type_timeseries/public/application/visualizations/constants/chart.ts rename to src/plugins/vis_types/timeseries/public/application/visualizations/constants/chart.ts diff --git a/src/plugins/vis_type_timeseries/public/application/visualizations/constants/icons.ts b/src/plugins/vis_types/timeseries/public/application/visualizations/constants/icons.ts similarity index 100% rename from src/plugins/vis_type_timeseries/public/application/visualizations/constants/icons.ts rename to src/plugins/vis_types/timeseries/public/application/visualizations/constants/icons.ts diff --git a/src/plugins/vis_type_timeseries/public/application/visualizations/constants/index.ts b/src/plugins/vis_types/timeseries/public/application/visualizations/constants/index.ts similarity index 100% rename from src/plugins/vis_type_timeseries/public/application/visualizations/constants/index.ts rename to src/plugins/vis_types/timeseries/public/application/visualizations/constants/index.ts diff --git a/src/plugins/vis_type_timeseries/public/application/visualizations/lib/calc_dimensions.js b/src/plugins/vis_types/timeseries/public/application/visualizations/lib/calc_dimensions.js similarity index 100% rename from src/plugins/vis_type_timeseries/public/application/visualizations/lib/calc_dimensions.js rename to src/plugins/vis_types/timeseries/public/application/visualizations/lib/calc_dimensions.js diff --git a/src/plugins/vis_type_timeseries/public/application/visualizations/lib/calculate_coordinates.js b/src/plugins/vis_types/timeseries/public/application/visualizations/lib/calculate_coordinates.js similarity index 100% rename from src/plugins/vis_type_timeseries/public/application/visualizations/lib/calculate_coordinates.js rename to src/plugins/vis_types/timeseries/public/application/visualizations/lib/calculate_coordinates.js diff --git a/src/plugins/vis_type_timeseries/public/application/visualizations/lib/get_value_by.js b/src/plugins/vis_types/timeseries/public/application/visualizations/lib/get_value_by.js similarity index 100% rename from src/plugins/vis_type_timeseries/public/application/visualizations/lib/get_value_by.js rename to src/plugins/vis_types/timeseries/public/application/visualizations/lib/get_value_by.js diff --git a/src/plugins/vis_type_timeseries/public/application/visualizations/views/_annotation.scss b/src/plugins/vis_types/timeseries/public/application/visualizations/views/_annotation.scss similarity index 100% rename from src/plugins/vis_type_timeseries/public/application/visualizations/views/_annotation.scss rename to src/plugins/vis_types/timeseries/public/application/visualizations/views/_annotation.scss diff --git a/src/plugins/vis_type_timeseries/public/application/visualizations/views/_gauge.scss b/src/plugins/vis_types/timeseries/public/application/visualizations/views/_gauge.scss similarity index 100% rename from src/plugins/vis_type_timeseries/public/application/visualizations/views/_gauge.scss rename to src/plugins/vis_types/timeseries/public/application/visualizations/views/_gauge.scss diff --git a/src/plugins/vis_type_timeseries/public/application/visualizations/views/_index.scss b/src/plugins/vis_types/timeseries/public/application/visualizations/views/_index.scss similarity index 100% rename from src/plugins/vis_type_timeseries/public/application/visualizations/views/_index.scss rename to src/plugins/vis_types/timeseries/public/application/visualizations/views/_index.scss diff --git a/src/plugins/vis_type_timeseries/public/application/visualizations/views/_metric.scss b/src/plugins/vis_types/timeseries/public/application/visualizations/views/_metric.scss similarity index 100% rename from src/plugins/vis_type_timeseries/public/application/visualizations/views/_metric.scss rename to src/plugins/vis_types/timeseries/public/application/visualizations/views/_metric.scss diff --git a/src/plugins/vis_type_timeseries/public/application/visualizations/views/_top_n.scss b/src/plugins/vis_types/timeseries/public/application/visualizations/views/_top_n.scss similarity index 100% rename from src/plugins/vis_type_timeseries/public/application/visualizations/views/_top_n.scss rename to src/plugins/vis_types/timeseries/public/application/visualizations/views/_top_n.scss diff --git a/src/plugins/vis_type_timeseries/public/application/visualizations/views/annotation.js b/src/plugins/vis_types/timeseries/public/application/visualizations/views/annotation.js similarity index 100% rename from src/plugins/vis_type_timeseries/public/application/visualizations/views/annotation.js rename to src/plugins/vis_types/timeseries/public/application/visualizations/views/annotation.js diff --git a/src/plugins/vis_type_timeseries/public/application/visualizations/views/gauge.js b/src/plugins/vis_types/timeseries/public/application/visualizations/views/gauge.js similarity index 100% rename from src/plugins/vis_type_timeseries/public/application/visualizations/views/gauge.js rename to src/plugins/vis_types/timeseries/public/application/visualizations/views/gauge.js diff --git a/src/plugins/vis_type_timeseries/public/application/visualizations/views/gauge_vis.js b/src/plugins/vis_types/timeseries/public/application/visualizations/views/gauge_vis.js similarity index 100% rename from src/plugins/vis_type_timeseries/public/application/visualizations/views/gauge_vis.js rename to src/plugins/vis_types/timeseries/public/application/visualizations/views/gauge_vis.js diff --git a/src/plugins/vis_type_timeseries/public/application/visualizations/views/metric.js b/src/plugins/vis_types/timeseries/public/application/visualizations/views/metric.js similarity index 100% rename from src/plugins/vis_type_timeseries/public/application/visualizations/views/metric.js rename to src/plugins/vis_types/timeseries/public/application/visualizations/views/metric.js diff --git a/src/plugins/vis_type_timeseries/public/application/visualizations/views/timeseries/decorators/__snapshots__/area_decorator.test.js.snap b/src/plugins/vis_types/timeseries/public/application/visualizations/views/timeseries/decorators/__snapshots__/area_decorator.test.js.snap similarity index 100% rename from src/plugins/vis_type_timeseries/public/application/visualizations/views/timeseries/decorators/__snapshots__/area_decorator.test.js.snap rename to src/plugins/vis_types/timeseries/public/application/visualizations/views/timeseries/decorators/__snapshots__/area_decorator.test.js.snap diff --git a/src/plugins/vis_type_timeseries/public/application/visualizations/views/timeseries/decorators/__snapshots__/bar_decorator.test.js.snap b/src/plugins/vis_types/timeseries/public/application/visualizations/views/timeseries/decorators/__snapshots__/bar_decorator.test.js.snap similarity index 100% rename from src/plugins/vis_type_timeseries/public/application/visualizations/views/timeseries/decorators/__snapshots__/bar_decorator.test.js.snap rename to src/plugins/vis_types/timeseries/public/application/visualizations/views/timeseries/decorators/__snapshots__/bar_decorator.test.js.snap diff --git a/src/plugins/vis_type_timeseries/public/application/visualizations/views/timeseries/decorators/area_decorator.js b/src/plugins/vis_types/timeseries/public/application/visualizations/views/timeseries/decorators/area_decorator.js similarity index 100% rename from src/plugins/vis_type_timeseries/public/application/visualizations/views/timeseries/decorators/area_decorator.js rename to src/plugins/vis_types/timeseries/public/application/visualizations/views/timeseries/decorators/area_decorator.js diff --git a/src/plugins/vis_type_timeseries/public/application/visualizations/views/timeseries/decorators/area_decorator.test.js b/src/plugins/vis_types/timeseries/public/application/visualizations/views/timeseries/decorators/area_decorator.test.js similarity index 100% rename from src/plugins/vis_type_timeseries/public/application/visualizations/views/timeseries/decorators/area_decorator.test.js rename to src/plugins/vis_types/timeseries/public/application/visualizations/views/timeseries/decorators/area_decorator.test.js diff --git a/src/plugins/vis_type_timeseries/public/application/visualizations/views/timeseries/decorators/bar_decorator.js b/src/plugins/vis_types/timeseries/public/application/visualizations/views/timeseries/decorators/bar_decorator.js similarity index 100% rename from src/plugins/vis_type_timeseries/public/application/visualizations/views/timeseries/decorators/bar_decorator.js rename to src/plugins/vis_types/timeseries/public/application/visualizations/views/timeseries/decorators/bar_decorator.js diff --git a/src/plugins/vis_type_timeseries/public/application/visualizations/views/timeseries/decorators/bar_decorator.test.js b/src/plugins/vis_types/timeseries/public/application/visualizations/views/timeseries/decorators/bar_decorator.test.js similarity index 100% rename from src/plugins/vis_type_timeseries/public/application/visualizations/views/timeseries/decorators/bar_decorator.test.js rename to src/plugins/vis_types/timeseries/public/application/visualizations/views/timeseries/decorators/bar_decorator.test.js diff --git a/src/plugins/vis_type_timeseries/public/application/visualizations/views/timeseries/index.js b/src/plugins/vis_types/timeseries/public/application/visualizations/views/timeseries/index.js similarity index 99% rename from src/plugins/vis_type_timeseries/public/application/visualizations/views/timeseries/index.js rename to src/plugins/vis_types/timeseries/public/application/visualizations/views/timeseries/index.js index 1edaf38ef403c..6f6ddbbb7c414 100644 --- a/src/plugins/vis_type_timeseries/public/application/visualizations/views/timeseries/index.js +++ b/src/plugins/vis_types/timeseries/public/application/visualizations/views/timeseries/index.js @@ -32,7 +32,7 @@ import { getBaseTheme, getChartClasses } from './utils/theme'; import { TOOLTIP_MODES } from '../../../../../common/enums'; import { getValueOrEmpty } from '../../../../../common/empty_label'; import { getSplitByTermsColor } from '../../../lib/get_split_by_terms_color'; -import { renderEndzoneTooltip, useActiveCursor } from '../../../../../../charts/public'; +import { renderEndzoneTooltip, useActiveCursor } from '../../../../../../../charts/public'; import { getAxisLabelString } from '../../../components/lib/get_axis_label_string'; import { calculateDomainForSeries } from './utils/series_domain_calculation'; diff --git a/src/plugins/vis_type_timeseries/public/application/visualizations/views/timeseries/model/__snapshots__/charts.test.js.snap b/src/plugins/vis_types/timeseries/public/application/visualizations/views/timeseries/model/__snapshots__/charts.test.js.snap similarity index 100% rename from src/plugins/vis_type_timeseries/public/application/visualizations/views/timeseries/model/__snapshots__/charts.test.js.snap rename to src/plugins/vis_types/timeseries/public/application/visualizations/views/timeseries/model/__snapshots__/charts.test.js.snap diff --git a/src/plugins/vis_type_timeseries/public/application/visualizations/views/timeseries/model/charts.js b/src/plugins/vis_types/timeseries/public/application/visualizations/views/timeseries/model/charts.js similarity index 100% rename from src/plugins/vis_type_timeseries/public/application/visualizations/views/timeseries/model/charts.js rename to src/plugins/vis_types/timeseries/public/application/visualizations/views/timeseries/model/charts.js diff --git a/src/plugins/vis_type_timeseries/public/application/visualizations/views/timeseries/model/charts.test.js b/src/plugins/vis_types/timeseries/public/application/visualizations/views/timeseries/model/charts.test.js similarity index 100% rename from src/plugins/vis_type_timeseries/public/application/visualizations/views/timeseries/model/charts.test.js rename to src/plugins/vis_types/timeseries/public/application/visualizations/views/timeseries/model/charts.test.js diff --git a/src/plugins/vis_type_timeseries/public/application/visualizations/views/timeseries/utils/__snapshots__/series_styles.test.js.snap b/src/plugins/vis_types/timeseries/public/application/visualizations/views/timeseries/utils/__snapshots__/series_styles.test.js.snap similarity index 100% rename from src/plugins/vis_type_timeseries/public/application/visualizations/views/timeseries/utils/__snapshots__/series_styles.test.js.snap rename to src/plugins/vis_types/timeseries/public/application/visualizations/views/timeseries/utils/__snapshots__/series_styles.test.js.snap diff --git a/src/plugins/vis_type_timeseries/public/application/visualizations/views/timeseries/utils/series_domain_calculation.ts b/src/plugins/vis_types/timeseries/public/application/visualizations/views/timeseries/utils/series_domain_calculation.ts similarity index 100% rename from src/plugins/vis_type_timeseries/public/application/visualizations/views/timeseries/utils/series_domain_calculation.ts rename to src/plugins/vis_types/timeseries/public/application/visualizations/views/timeseries/utils/series_domain_calculation.ts diff --git a/src/plugins/vis_type_timeseries/public/application/visualizations/views/timeseries/utils/series_domain_calculations.test.ts b/src/plugins/vis_types/timeseries/public/application/visualizations/views/timeseries/utils/series_domain_calculations.test.ts similarity index 93% rename from src/plugins/vis_type_timeseries/public/application/visualizations/views/timeseries/utils/series_domain_calculations.test.ts rename to src/plugins/vis_types/timeseries/public/application/visualizations/views/timeseries/utils/series_domain_calculations.test.ts index 5b502636003f0..53157286328e2 100644 --- a/src/plugins/vis_type_timeseries/public/application/visualizations/views/timeseries/utils/series_domain_calculations.test.ts +++ b/src/plugins/vis_types/timeseries/public/application/visualizations/views/timeseries/utils/series_domain_calculations.test.ts @@ -7,7 +7,7 @@ */ import { calculateDomainForSeries } from './series_domain_calculation'; -import { PanelData } from 'src/plugins/vis_type_timeseries/common/types'; +import { PanelData } from 'src/plugins/vis_types/timeseries/common/types'; describe('calculateDomainForSeries', () => { it('should return 0 for domainStart and 3 for domainEnd', () => { diff --git a/src/plugins/vis_type_timeseries/public/application/visualizations/views/timeseries/utils/series_styles.js b/src/plugins/vis_types/timeseries/public/application/visualizations/views/timeseries/utils/series_styles.js similarity index 100% rename from src/plugins/vis_type_timeseries/public/application/visualizations/views/timeseries/utils/series_styles.js rename to src/plugins/vis_types/timeseries/public/application/visualizations/views/timeseries/utils/series_styles.js diff --git a/src/plugins/vis_type_timeseries/public/application/visualizations/views/timeseries/utils/series_styles.test.js b/src/plugins/vis_types/timeseries/public/application/visualizations/views/timeseries/utils/series_styles.test.js similarity index 100% rename from src/plugins/vis_type_timeseries/public/application/visualizations/views/timeseries/utils/series_styles.test.js rename to src/plugins/vis_types/timeseries/public/application/visualizations/views/timeseries/utils/series_styles.test.js diff --git a/src/plugins/vis_type_timeseries/public/application/visualizations/views/timeseries/utils/stack_format.js b/src/plugins/vis_types/timeseries/public/application/visualizations/views/timeseries/utils/stack_format.js similarity index 100% rename from src/plugins/vis_type_timeseries/public/application/visualizations/views/timeseries/utils/stack_format.js rename to src/plugins/vis_types/timeseries/public/application/visualizations/views/timeseries/utils/stack_format.js diff --git a/src/plugins/vis_type_timeseries/public/application/visualizations/views/timeseries/utils/stack_format.test.js b/src/plugins/vis_types/timeseries/public/application/visualizations/views/timeseries/utils/stack_format.test.js similarity index 100% rename from src/plugins/vis_type_timeseries/public/application/visualizations/views/timeseries/utils/stack_format.test.js rename to src/plugins/vis_types/timeseries/public/application/visualizations/views/timeseries/utils/stack_format.test.js diff --git a/src/plugins/vis_type_timeseries/public/application/visualizations/views/timeseries/utils/theme.test.ts b/src/plugins/vis_types/timeseries/public/application/visualizations/views/timeseries/utils/theme.test.ts similarity index 100% rename from src/plugins/vis_type_timeseries/public/application/visualizations/views/timeseries/utils/theme.test.ts rename to src/plugins/vis_types/timeseries/public/application/visualizations/views/timeseries/utils/theme.test.ts diff --git a/src/plugins/vis_type_timeseries/public/application/visualizations/views/timeseries/utils/theme.ts b/src/plugins/vis_types/timeseries/public/application/visualizations/views/timeseries/utils/theme.ts similarity index 100% rename from src/plugins/vis_type_timeseries/public/application/visualizations/views/timeseries/utils/theme.ts rename to src/plugins/vis_types/timeseries/public/application/visualizations/views/timeseries/utils/theme.ts diff --git a/src/plugins/vis_type_timeseries/public/application/visualizations/views/top_n.js b/src/plugins/vis_types/timeseries/public/application/visualizations/views/top_n.js similarity index 100% rename from src/plugins/vis_type_timeseries/public/application/visualizations/views/top_n.js rename to src/plugins/vis_types/timeseries/public/application/visualizations/views/top_n.js diff --git a/src/plugins/vis_type_timeseries/public/index.ts b/src/plugins/vis_types/timeseries/public/index.ts similarity index 88% rename from src/plugins/vis_type_timeseries/public/index.ts rename to src/plugins/vis_types/timeseries/public/index.ts index 0ab10581c48c2..a3180678b53a0 100644 --- a/src/plugins/vis_type_timeseries/public/index.ts +++ b/src/plugins/vis_types/timeseries/public/index.ts @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import { PluginInitializerContext } from '../../../core/public'; +import { PluginInitializerContext } from '../../../../core/public'; import { MetricsPlugin as Plugin } from './plugin'; export function plugin(initializerContext: PluginInitializerContext) { diff --git a/src/plugins/vis_type_timeseries/public/metrics_fn.ts b/src/plugins/vis_types/timeseries/public/metrics_fn.ts similarity index 93% rename from src/plugins/vis_type_timeseries/public/metrics_fn.ts rename to src/plugins/vis_types/timeseries/public/metrics_fn.ts index fe1c4722762ae..23c196ebe1149 100644 --- a/src/plugins/vis_type_timeseries/public/metrics_fn.ts +++ b/src/plugins/vis_types/timeseries/public/metrics_fn.ts @@ -7,8 +7,8 @@ */ import { i18n } from '@kbn/i18n'; -import { KibanaContext } from '../../data/public'; -import { ExpressionFunctionDefinition, Render } from '../../expressions/public'; +import { KibanaContext } from '../../../data/public'; +import { ExpressionFunctionDefinition, Render } from '../../../expressions/public'; import type { TimeseriesVisData } from '../common/types'; import { metricsRequestHandler } from './request_handler'; diff --git a/src/plugins/vis_type_timeseries/public/metrics_type.ts b/src/plugins/vis_types/timeseries/public/metrics_type.ts similarity index 98% rename from src/plugins/vis_type_timeseries/public/metrics_type.ts rename to src/plugins/vis_types/timeseries/public/metrics_type.ts index 5d4a61c1edb82..64970d9730eee 100644 --- a/src/plugins/vis_type_timeseries/public/metrics_type.ts +++ b/src/plugins/vis_types/timeseries/public/metrics_type.ts @@ -19,7 +19,7 @@ import { VisGroups, VisParams, VisTypeDefinition, -} from '../../visualizations/public'; +} from '../../../visualizations/public'; import { getDataStart } from './services'; import type { TimeseriesVisDefaultParams, TimeseriesVisParams } from './types'; diff --git a/src/plugins/vis_type_timeseries/public/plugin.ts b/src/plugins/vis_types/timeseries/public/plugin.ts similarity index 86% rename from src/plugins/vis_type_timeseries/public/plugin.ts rename to src/plugins/vis_types/timeseries/public/plugin.ts index 3cd090c7da829..d6d83caa6eb0f 100644 --- a/src/plugins/vis_type_timeseries/public/plugin.ts +++ b/src/plugins/vis_types/timeseries/public/plugin.ts @@ -7,9 +7,9 @@ */ import { PluginInitializerContext, CoreSetup, CoreStart, Plugin } from 'kibana/public'; -import { Plugin as ExpressionsPublicPlugin } from '../../expressions/public'; -import { VisualizationsSetup } from '../../visualizations/public'; -import { VisualizePluginSetup } from '../../visualize/public'; +import { Plugin as ExpressionsPublicPlugin } from '../../../expressions/public'; +import { VisualizationsSetup } from '../../../visualizations/public'; +import { VisualizePluginSetup } from '../../../visualize/public'; import { EditorController, TSVB_EDITOR_NAME } from './application/editor_controller'; import { createMetricsFn } from './metrics_fn'; @@ -22,8 +22,8 @@ import { setDataStart, setCharts, } from './services'; -import { DataPublicPluginStart } from '../../data/public'; -import { ChartsPluginStart } from '../../charts/public'; +import { DataPublicPluginStart } from '../../../data/public'; +import { ChartsPluginStart } from '../../../charts/public'; import { getTimeseriesVisRenderer } from './timeseries_vis_renderer'; /** @internal */ diff --git a/src/plugins/vis_type_timeseries/public/request_handler.ts b/src/plugins/vis_types/timeseries/public/request_handler.ts similarity index 97% rename from src/plugins/vis_type_timeseries/public/request_handler.ts rename to src/plugins/vis_types/timeseries/public/request_handler.ts index 0a110dd65d5e9..e9037c0b84a5e 100644 --- a/src/plugins/vis_type_timeseries/public/request_handler.ts +++ b/src/plugins/vis_types/timeseries/public/request_handler.ts @@ -12,7 +12,7 @@ import { ROUTES } from '../common/constants'; import type { TimeseriesVisParams } from './types'; import type { TimeseriesVisData } from '../common/types'; -import type { KibanaContext } from '../../data/public'; +import type { KibanaContext } from '../../../data/public'; interface MetricsRequestHandlerParams { input: KibanaContext | null; diff --git a/src/plugins/vis_type_timeseries/public/services.ts b/src/plugins/vis_types/timeseries/public/services.ts similarity index 84% rename from src/plugins/vis_type_timeseries/public/services.ts rename to src/plugins/vis_types/timeseries/public/services.ts index ba7ab4a25c8a2..f76a9ed7c6389 100644 --- a/src/plugins/vis_type_timeseries/public/services.ts +++ b/src/plugins/vis_types/timeseries/public/services.ts @@ -7,9 +7,9 @@ */ import { I18nStart, IUiSettingsClient, CoreStart } from 'src/core/public'; -import { createGetterSetter } from '../../kibana_utils/public'; -import { ChartsPluginStart } from '../../charts/public'; -import { DataPublicPluginStart } from '../../data/public'; +import { createGetterSetter } from '../../../kibana_utils/public'; +import { ChartsPluginStart } from '../../../charts/public'; +import { DataPublicPluginStart } from '../../../data/public'; export const [getUISettings, setUISettings] = createGetterSetter('UISettings'); diff --git a/src/plugins/vis_type_timeseries/public/test_utils/index.ts b/src/plugins/vis_types/timeseries/public/test_utils/index.ts similarity index 100% rename from src/plugins/vis_type_timeseries/public/test_utils/index.ts rename to src/plugins/vis_types/timeseries/public/test_utils/index.ts diff --git a/src/plugins/vis_type_timeseries/public/timeseries_vis_renderer.tsx b/src/plugins/vis_types/timeseries/public/timeseries_vis_renderer.tsx similarity index 94% rename from src/plugins/vis_type_timeseries/public/timeseries_vis_renderer.tsx rename to src/plugins/vis_types/timeseries/public/timeseries_vis_renderer.tsx index 9a19ddc285ebb..34cc1dc347ef8 100644 --- a/src/plugins/vis_type_timeseries/public/timeseries_vis_renderer.tsx +++ b/src/plugins/vis_types/timeseries/public/timeseries_vis_renderer.tsx @@ -14,14 +14,14 @@ import { I18nProvider } from '@kbn/i18n/react'; import { IUiSettingsClient } from 'kibana/public'; import { fetchIndexPattern } from '../common/index_patterns_utils'; -import { VisualizationContainer, PersistedState } from '../../visualizations/public'; +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'; +import type { ExpressionRenderDefinition } from '../../../expressions/common'; import type { TimeseriesRenderValue } from './metrics_fn'; const TimeseriesVisualization = lazy( diff --git a/src/plugins/vis_type_timeseries/public/to_ast.ts b/src/plugins/vis_types/timeseries/public/to_ast.ts similarity index 91% rename from src/plugins/vis_type_timeseries/public/to_ast.ts rename to src/plugins/vis_types/timeseries/public/to_ast.ts index c0c0a5b1546a9..91fc7465f122f 100644 --- a/src/plugins/vis_type_timeseries/public/to_ast.ts +++ b/src/plugins/vis_types/timeseries/public/to_ast.ts @@ -6,8 +6,8 @@ * Side Public License, v 1. */ -import { buildExpression, buildExpressionFunction } from '../../expressions/public'; -import type { Vis } from '../../visualizations/public'; +import { buildExpression, buildExpressionFunction } from '../../../expressions/public'; +import type { Vis } from '../../../visualizations/public'; import type { TimeseriesExpressionFunctionDefinition } from './metrics_fn'; import type { TimeseriesVisParams } from './types'; diff --git a/src/plugins/vis_type_timeseries/public/types.ts b/src/plugins/vis_types/timeseries/public/types.ts similarity index 100% rename from src/plugins/vis_type_timeseries/public/types.ts rename to src/plugins/vis_types/timeseries/public/types.ts diff --git a/src/plugins/vis_type_timeseries/server/config.ts b/src/plugins/vis_types/timeseries/server/config.ts similarity index 100% rename from src/plugins/vis_type_timeseries/server/config.ts rename to src/plugins/vis_types/timeseries/server/config.ts diff --git a/src/plugins/vis_type_timeseries/server/index.ts b/src/plugins/vis_types/timeseries/server/index.ts similarity index 98% rename from src/plugins/vis_type_timeseries/server/index.ts rename to src/plugins/vis_types/timeseries/server/index.ts index a78ddade30965..0890b37e77926 100644 --- a/src/plugins/vis_type_timeseries/server/index.ts +++ b/src/plugins/vis_types/timeseries/server/index.ts @@ -15,7 +15,7 @@ export { VisTypeTimeseriesSetup } from './plugin'; export const config: PluginConfigDescriptor = { deprecations: ({ unused, renameFromRoot }) => [ // In Kibana v7.8 plugin id was renamed from 'metrics' to 'vis_type_timeseries': - renameFromRoot('metrics.enabled', 'vis_type_timeseries.enabled', { silent: true }), + renameFromRoot('metrics.enabled', 'vis_type_timeseries.enabled'), renameFromRoot('metrics.chartResolution', 'vis_type_timeseries.chartResolution', { silent: true, }), diff --git a/src/plugins/vis_type_timeseries/server/lib/get_fields.ts b/src/plugins/vis_types/timeseries/server/lib/get_fields.ts similarity index 100% rename from src/plugins/vis_type_timeseries/server/lib/get_fields.ts rename to src/plugins/vis_types/timeseries/server/lib/get_fields.ts diff --git a/src/plugins/vis_type_timeseries/server/lib/get_vis_data.ts b/src/plugins/vis_types/timeseries/server/lib/get_vis_data.ts similarity index 100% rename from src/plugins/vis_type_timeseries/server/lib/get_vis_data.ts rename to src/plugins/vis_types/timeseries/server/lib/get_vis_data.ts diff --git a/src/plugins/vis_type_timeseries/server/lib/search_strategies/capabilities/default_search_capabilities.test.ts b/src/plugins/vis_types/timeseries/server/lib/search_strategies/capabilities/default_search_capabilities.test.ts similarity index 100% rename from src/plugins/vis_type_timeseries/server/lib/search_strategies/capabilities/default_search_capabilities.test.ts rename to src/plugins/vis_types/timeseries/server/lib/search_strategies/capabilities/default_search_capabilities.test.ts diff --git a/src/plugins/vis_type_timeseries/server/lib/search_strategies/capabilities/default_search_capabilities.ts b/src/plugins/vis_types/timeseries/server/lib/search_strategies/capabilities/default_search_capabilities.ts similarity index 100% rename from src/plugins/vis_type_timeseries/server/lib/search_strategies/capabilities/default_search_capabilities.ts rename to src/plugins/vis_types/timeseries/server/lib/search_strategies/capabilities/default_search_capabilities.ts diff --git a/src/plugins/vis_type_timeseries/server/lib/search_strategies/capabilities/rollup_search_capabilities.test.ts b/src/plugins/vis_types/timeseries/server/lib/search_strategies/capabilities/rollup_search_capabilities.test.ts similarity index 100% rename from src/plugins/vis_type_timeseries/server/lib/search_strategies/capabilities/rollup_search_capabilities.test.ts rename to src/plugins/vis_types/timeseries/server/lib/search_strategies/capabilities/rollup_search_capabilities.test.ts diff --git a/src/plugins/vis_type_timeseries/server/lib/search_strategies/capabilities/rollup_search_capabilities.ts b/src/plugins/vis_types/timeseries/server/lib/search_strategies/capabilities/rollup_search_capabilities.ts similarity index 100% rename from src/plugins/vis_type_timeseries/server/lib/search_strategies/capabilities/rollup_search_capabilities.ts rename to src/plugins/vis_types/timeseries/server/lib/search_strategies/capabilities/rollup_search_capabilities.ts diff --git a/src/plugins/vis_type_timeseries/server/lib/search_strategies/index.ts b/src/plugins/vis_types/timeseries/server/lib/search_strategies/index.ts similarity index 100% rename from src/plugins/vis_type_timeseries/server/lib/search_strategies/index.ts rename to src/plugins/vis_types/timeseries/server/lib/search_strategies/index.ts diff --git a/src/plugins/vis_type_timeseries/server/lib/search_strategies/lib/cached_index_pattern_fetcher.test.ts b/src/plugins/vis_types/timeseries/server/lib/search_strategies/lib/cached_index_pattern_fetcher.test.ts similarity index 100% rename from src/plugins/vis_type_timeseries/server/lib/search_strategies/lib/cached_index_pattern_fetcher.test.ts rename to src/plugins/vis_types/timeseries/server/lib/search_strategies/lib/cached_index_pattern_fetcher.test.ts diff --git a/src/plugins/vis_type_timeseries/server/lib/search_strategies/lib/cached_index_pattern_fetcher.ts b/src/plugins/vis_types/timeseries/server/lib/search_strategies/lib/cached_index_pattern_fetcher.ts similarity index 95% rename from src/plugins/vis_type_timeseries/server/lib/search_strategies/lib/cached_index_pattern_fetcher.ts rename to src/plugins/vis_types/timeseries/server/lib/search_strategies/lib/cached_index_pattern_fetcher.ts index 5f989a50ca639..2a3738878c97a 100644 --- a/src/plugins/vis_type_timeseries/server/lib/search_strategies/lib/cached_index_pattern_fetcher.ts +++ b/src/plugins/vis_types/timeseries/server/lib/search_strategies/lib/cached_index_pattern_fetcher.ts @@ -8,7 +8,7 @@ import { getIndexPatternKey, fetchIndexPattern } from '../../../../common/index_patterns_utils'; -import type { IndexPatternsService } from '../../../../../data/server'; +import type { IndexPatternsService } from '../../../../../../data/server'; import type { IndexPatternValue, FetchedIndexPattern } from '../../../../common/types'; export const getCachedIndexPatternFetcher = ( diff --git a/src/plugins/vis_type_timeseries/server/lib/search_strategies/lib/fields_fetcher.ts b/src/plugins/vis_types/timeseries/server/lib/search_strategies/lib/fields_fetcher.ts similarity index 95% rename from src/plugins/vis_type_timeseries/server/lib/search_strategies/lib/fields_fetcher.ts rename to src/plugins/vis_types/timeseries/server/lib/search_strategies/lib/fields_fetcher.ts index 9563a8fbece23..cf7bc42fc6db3 100644 --- a/src/plugins/vis_type_timeseries/server/lib/search_strategies/lib/fields_fetcher.ts +++ b/src/plugins/vis_types/timeseries/server/lib/search_strategies/lib/fields_fetcher.ts @@ -10,7 +10,7 @@ import { getIndexPatternKey } from '../../../../common/index_patterns_utils'; import type { VisTypeTimeseriesVisDataRequest } from '../../../types'; import type { SearchStrategy, SearchCapabilities } from '../index'; -import type { IndexPatternsService } from '../../../../../data/common'; +import type { IndexPatternsService } from '../../../../../../data/common'; import type { CachedIndexPatternFetcher } from './cached_index_pattern_fetcher'; import type { IndexPatternValue } from '../../../../common/types'; diff --git a/src/plugins/vis_type_timeseries/server/lib/search_strategies/lib/interval_helper.test.ts b/src/plugins/vis_types/timeseries/server/lib/search_strategies/lib/interval_helper.test.ts similarity index 100% rename from src/plugins/vis_type_timeseries/server/lib/search_strategies/lib/interval_helper.test.ts rename to src/plugins/vis_types/timeseries/server/lib/search_strategies/lib/interval_helper.test.ts diff --git a/src/plugins/vis_type_timeseries/server/lib/search_strategies/lib/interval_helper.ts b/src/plugins/vis_types/timeseries/server/lib/search_strategies/lib/interval_helper.ts similarity index 100% rename from src/plugins/vis_type_timeseries/server/lib/search_strategies/lib/interval_helper.ts rename to src/plugins/vis_types/timeseries/server/lib/search_strategies/lib/interval_helper.ts diff --git a/src/plugins/vis_type_timeseries/server/lib/search_strategies/search_strategies_registry.test.ts b/src/plugins/vis_types/timeseries/server/lib/search_strategies/search_strategies_registry.test.ts similarity index 100% rename from src/plugins/vis_type_timeseries/server/lib/search_strategies/search_strategies_registry.test.ts rename to src/plugins/vis_types/timeseries/server/lib/search_strategies/search_strategies_registry.test.ts diff --git a/src/plugins/vis_type_timeseries/server/lib/search_strategies/search_strategy_registry.ts b/src/plugins/vis_types/timeseries/server/lib/search_strategies/search_strategy_registry.ts similarity index 100% rename from src/plugins/vis_type_timeseries/server/lib/search_strategies/search_strategy_registry.ts rename to src/plugins/vis_types/timeseries/server/lib/search_strategies/search_strategy_registry.ts diff --git a/src/plugins/vis_type_timeseries/server/lib/search_strategies/strategies/abstract_search_strategy.test.ts b/src/plugins/vis_types/timeseries/server/lib/search_strategies/strategies/abstract_search_strategy.test.ts similarity index 95% rename from src/plugins/vis_type_timeseries/server/lib/search_strategies/strategies/abstract_search_strategy.test.ts rename to src/plugins/vis_types/timeseries/server/lib/search_strategies/strategies/abstract_search_strategy.test.ts index 0a9b3d0047c80..6216bce00fc7d 100644 --- a/src/plugins/vis_type_timeseries/server/lib/search_strategies/strategies/abstract_search_strategy.test.ts +++ b/src/plugins/vis_types/timeseries/server/lib/search_strategies/strategies/abstract_search_strategy.test.ts @@ -6,11 +6,11 @@ * Side Public License, v 1. */ -import { IndexPatternsService } from '../../../../../data/common'; +import { IndexPatternsService } from '../../../../../../data/common'; import { from } from 'rxjs'; import { AbstractSearchStrategy } from './abstract_search_strategy'; -import type { FieldSpec } from '../../../../../data/common'; +import type { FieldSpec } from '../../../../../../data/common'; import type { CachedIndexPatternFetcher } from '../lib/cached_index_pattern_fetcher'; import type { VisTypeTimeseriesRequestHandlerContext, diff --git a/src/plugins/vis_type_timeseries/server/lib/search_strategies/strategies/abstract_search_strategy.ts b/src/plugins/vis_types/timeseries/server/lib/search_strategies/strategies/abstract_search_strategy.ts similarity index 96% rename from src/plugins/vis_type_timeseries/server/lib/search_strategies/strategies/abstract_search_strategy.ts rename to src/plugins/vis_types/timeseries/server/lib/search_strategies/strategies/abstract_search_strategy.ts index 26c3a6c7c8bf7..bce07d2cdb300 100644 --- a/src/plugins/vis_type_timeseries/server/lib/search_strategies/strategies/abstract_search_strategy.ts +++ b/src/plugins/vis_types/timeseries/server/lib/search_strategies/strategies/abstract_search_strategy.ts @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import { IndexPatternsService } from '../../../../../data/server'; +import { IndexPatternsService } from '../../../../../../data/server'; import { toSanitizedFieldType } from '../../../../common/fields_utils'; import type { FetchedIndexPattern } from '../../../../common/types'; diff --git a/src/plugins/vis_type_timeseries/server/lib/search_strategies/strategies/default_search_strategy.test.ts b/src/plugins/vis_types/timeseries/server/lib/search_strategies/strategies/default_search_strategy.test.ts similarity index 100% rename from src/plugins/vis_type_timeseries/server/lib/search_strategies/strategies/default_search_strategy.test.ts rename to src/plugins/vis_types/timeseries/server/lib/search_strategies/strategies/default_search_strategy.test.ts diff --git a/src/plugins/vis_type_timeseries/server/lib/search_strategies/strategies/default_search_strategy.ts b/src/plugins/vis_types/timeseries/server/lib/search_strategies/strategies/default_search_strategy.ts similarity index 95% rename from src/plugins/vis_type_timeseries/server/lib/search_strategies/strategies/default_search_strategy.ts rename to src/plugins/vis_types/timeseries/server/lib/search_strategies/strategies/default_search_strategy.ts index 34892ec797c0b..0fa92b5f061fa 100644 --- a/src/plugins/vis_type_timeseries/server/lib/search_strategies/strategies/default_search_strategy.ts +++ b/src/plugins/vis_types/timeseries/server/lib/search_strategies/strategies/default_search_strategy.ts @@ -9,7 +9,7 @@ import { AbstractSearchStrategy } from './abstract_search_strategy'; import { DefaultSearchCapabilities } from '../capabilities/default_search_capabilities'; -import type { IndexPatternsService } from '../../../../../data/server'; +import type { IndexPatternsService } from '../../../../../../data/server'; import type { FetchedIndexPattern } from '../../../../common/types'; import type { VisTypeTimeseriesRequestHandlerContext, diff --git a/src/plugins/vis_type_timeseries/server/lib/search_strategies/strategies/index.ts b/src/plugins/vis_types/timeseries/server/lib/search_strategies/strategies/index.ts similarity index 100% rename from src/plugins/vis_type_timeseries/server/lib/search_strategies/strategies/index.ts rename to src/plugins/vis_types/timeseries/server/lib/search_strategies/strategies/index.ts diff --git a/src/plugins/vis_type_timeseries/server/lib/search_strategies/strategies/rollup_search_strategy.test.ts b/src/plugins/vis_types/timeseries/server/lib/search_strategies/strategies/rollup_search_strategy.test.ts similarity index 98% rename from src/plugins/vis_type_timeseries/server/lib/search_strategies/strategies/rollup_search_strategy.test.ts rename to src/plugins/vis_types/timeseries/server/lib/search_strategies/strategies/rollup_search_strategy.test.ts index 85b8bcdf57b93..4d2608e3519ed 100644 --- a/src/plugins/vis_type_timeseries/server/lib/search_strategies/strategies/rollup_search_strategy.test.ts +++ b/src/plugins/vis_types/timeseries/server/lib/search_strategies/strategies/rollup_search_strategy.test.ts @@ -8,7 +8,7 @@ import { RollupSearchStrategy } from './rollup_search_strategy'; -import type { IndexPatternsService } from '../../../../../data/common'; +import type { IndexPatternsService } from '../../../../../../data/common'; import type { CachedIndexPatternFetcher } from '../lib/cached_index_pattern_fetcher'; import type { VisTypeTimeseriesRequestHandlerContext, diff --git a/src/plugins/vis_type_timeseries/server/lib/search_strategies/strategies/rollup_search_strategy.ts b/src/plugins/vis_types/timeseries/server/lib/search_strategies/strategies/rollup_search_strategy.ts similarity index 96% rename from src/plugins/vis_type_timeseries/server/lib/search_strategies/strategies/rollup_search_strategy.ts rename to src/plugins/vis_types/timeseries/server/lib/search_strategies/strategies/rollup_search_strategy.ts index f68c877cf7a3f..903e7f239f824 100644 --- a/src/plugins/vis_type_timeseries/server/lib/search_strategies/strategies/rollup_search_strategy.ts +++ b/src/plugins/vis_types/timeseries/server/lib/search_strategies/strategies/rollup_search_strategy.ts @@ -6,7 +6,10 @@ * Side Public License, v 1. */ -import { getCapabilitiesForRollupIndices, IndexPatternsService } from '../../../../../data/server'; +import { + getCapabilitiesForRollupIndices, + IndexPatternsService, +} from '../../../../../../data/server'; import { AbstractSearchStrategy } from './abstract_search_strategy'; import { RollupSearchCapabilities } from '../capabilities/rollup_search_capabilities'; diff --git a/src/plugins/vis_type_timeseries/server/lib/vis_data/annotations/build_request_body.ts b/src/plugins/vis_types/timeseries/server/lib/vis_data/annotations/build_request_body.ts similarity index 100% rename from src/plugins/vis_type_timeseries/server/lib/vis_data/annotations/build_request_body.ts rename to src/plugins/vis_types/timeseries/server/lib/vis_data/annotations/build_request_body.ts diff --git a/src/plugins/vis_type_timeseries/server/lib/vis_data/annotations/get_request_params.ts b/src/plugins/vis_types/timeseries/server/lib/vis_data/annotations/get_request_params.ts similarity index 100% rename from src/plugins/vis_type_timeseries/server/lib/vis_data/annotations/get_request_params.ts rename to src/plugins/vis_types/timeseries/server/lib/vis_data/annotations/get_request_params.ts diff --git a/src/plugins/vis_type_timeseries/server/lib/vis_data/build_processor_function.test.ts b/src/plugins/vis_types/timeseries/server/lib/vis_data/build_processor_function.test.ts similarity index 100% rename from src/plugins/vis_type_timeseries/server/lib/vis_data/build_processor_function.test.ts rename to src/plugins/vis_types/timeseries/server/lib/vis_data/build_processor_function.test.ts diff --git a/src/plugins/vis_type_timeseries/server/lib/vis_data/build_processor_function.ts b/src/plugins/vis_types/timeseries/server/lib/vis_data/build_processor_function.ts similarity index 100% rename from src/plugins/vis_type_timeseries/server/lib/vis_data/build_processor_function.ts rename to src/plugins/vis_types/timeseries/server/lib/vis_data/build_processor_function.ts diff --git a/src/plugins/vis_type_timeseries/server/lib/vis_data/get_annotations.ts b/src/plugins/vis_types/timeseries/server/lib/vis_data/get_annotations.ts similarity index 100% rename from src/plugins/vis_type_timeseries/server/lib/vis_data/get_annotations.ts rename to src/plugins/vis_types/timeseries/server/lib/vis_data/get_annotations.ts diff --git a/src/plugins/vis_type_timeseries/server/lib/vis_data/get_interval_and_timefield.test.ts b/src/plugins/vis_types/timeseries/server/lib/vis_data/get_interval_and_timefield.test.ts similarity index 100% rename from src/plugins/vis_type_timeseries/server/lib/vis_data/get_interval_and_timefield.test.ts rename to src/plugins/vis_types/timeseries/server/lib/vis_data/get_interval_and_timefield.test.ts diff --git a/src/plugins/vis_type_timeseries/server/lib/vis_data/get_interval_and_timefield.ts b/src/plugins/vis_types/timeseries/server/lib/vis_data/get_interval_and_timefield.ts similarity index 100% rename from src/plugins/vis_type_timeseries/server/lib/vis_data/get_interval_and_timefield.ts rename to src/plugins/vis_types/timeseries/server/lib/vis_data/get_interval_and_timefield.ts diff --git a/src/plugins/vis_type_timeseries/server/lib/vis_data/get_series_data.ts b/src/plugins/vis_types/timeseries/server/lib/vis_data/get_series_data.ts similarity index 100% rename from src/plugins/vis_type_timeseries/server/lib/vis_data/get_series_data.ts rename to src/plugins/vis_types/timeseries/server/lib/vis_data/get_series_data.ts diff --git a/src/plugins/vis_type_timeseries/server/lib/vis_data/get_table_data.ts b/src/plugins/vis_types/timeseries/server/lib/vis_data/get_table_data.ts similarity index 100% rename from src/plugins/vis_type_timeseries/server/lib/vis_data/get_table_data.ts rename to src/plugins/vis_types/timeseries/server/lib/vis_data/get_table_data.ts diff --git a/src/plugins/vis_type_timeseries/server/lib/vis_data/handle_error_response.test.ts b/src/plugins/vis_types/timeseries/server/lib/vis_data/handle_error_response.test.ts similarity index 100% rename from src/plugins/vis_type_timeseries/server/lib/vis_data/handle_error_response.test.ts rename to src/plugins/vis_types/timeseries/server/lib/vis_data/handle_error_response.test.ts diff --git a/src/plugins/vis_type_timeseries/server/lib/vis_data/handle_error_response.ts b/src/plugins/vis_types/timeseries/server/lib/vis_data/handle_error_response.ts similarity index 100% rename from src/plugins/vis_type_timeseries/server/lib/vis_data/handle_error_response.ts rename to src/plugins/vis_types/timeseries/server/lib/vis_data/handle_error_response.ts diff --git a/src/plugins/vis_type_timeseries/server/lib/vis_data/helpers/__snapshots__/bucket_transform.test.js.snap b/src/plugins/vis_types/timeseries/server/lib/vis_data/helpers/__snapshots__/bucket_transform.test.js.snap similarity index 100% rename from src/plugins/vis_type_timeseries/server/lib/vis_data/helpers/__snapshots__/bucket_transform.test.js.snap rename to src/plugins/vis_types/timeseries/server/lib/vis_data/helpers/__snapshots__/bucket_transform.test.js.snap diff --git a/src/plugins/vis_type_timeseries/server/lib/vis_data/helpers/bucket_transform.js b/src/plugins/vis_types/timeseries/server/lib/vis_data/helpers/bucket_transform.js similarity index 100% rename from src/plugins/vis_type_timeseries/server/lib/vis_data/helpers/bucket_transform.js rename to src/plugins/vis_types/timeseries/server/lib/vis_data/helpers/bucket_transform.js diff --git a/src/plugins/vis_type_timeseries/server/lib/vis_data/helpers/bucket_transform.test.js b/src/plugins/vis_types/timeseries/server/lib/vis_data/helpers/bucket_transform.test.js similarity index 100% rename from src/plugins/vis_type_timeseries/server/lib/vis_data/helpers/bucket_transform.test.js rename to src/plugins/vis_types/timeseries/server/lib/vis_data/helpers/bucket_transform.test.js diff --git a/src/plugins/vis_type_timeseries/server/lib/vis_data/helpers/check_aggs.ts b/src/plugins/vis_types/timeseries/server/lib/vis_data/helpers/check_aggs.ts similarity index 100% rename from src/plugins/vis_type_timeseries/server/lib/vis_data/helpers/check_aggs.ts rename to src/plugins/vis_types/timeseries/server/lib/vis_data/helpers/check_aggs.ts diff --git a/src/plugins/vis_type_timeseries/server/lib/vis_data/helpers/format_key.ts b/src/plugins/vis_types/timeseries/server/lib/vis_data/helpers/format_key.ts similarity index 100% rename from src/plugins/vis_type_timeseries/server/lib/vis_data/helpers/format_key.ts rename to src/plugins/vis_types/timeseries/server/lib/vis_data/helpers/format_key.ts diff --git a/src/plugins/vis_type_timeseries/server/lib/vis_data/helpers/get_active_series.ts b/src/plugins/vis_types/timeseries/server/lib/vis_data/helpers/get_active_series.ts similarity index 100% rename from src/plugins/vis_type_timeseries/server/lib/vis_data/helpers/get_active_series.ts rename to src/plugins/vis_types/timeseries/server/lib/vis_data/helpers/get_active_series.ts diff --git a/src/plugins/vis_type_timeseries/server/lib/vis_data/helpers/get_agg_value.js b/src/plugins/vis_types/timeseries/server/lib/vis_data/helpers/get_agg_value.js similarity index 97% rename from src/plugins/vis_type_timeseries/server/lib/vis_data/helpers/get_agg_value.js rename to src/plugins/vis_types/timeseries/server/lib/vis_data/helpers/get_agg_value.js index 6756a8f7fc85a..eac2c4af65291 100644 --- a/src/plugins/vis_type_timeseries/server/lib/vis_data/helpers/get_agg_value.js +++ b/src/plugins/vis_types/timeseries/server/lib/vis_data/helpers/get_agg_value.js @@ -8,7 +8,7 @@ import { get, max, min, sum, noop } from 'lodash'; import { toPercentileNumber } from '../../../../common/to_percentile_number'; -import { METRIC_TYPES } from '../../../../../data/common'; +import { METRIC_TYPES } from '../../../../../../data/common'; import { TSVB_METRIC_TYPES } from '../../../../common/enums'; import { getAggByPredicate } from '../../../../common/agg_utils'; diff --git a/src/plugins/vis_type_timeseries/server/lib/vis_data/helpers/get_agg_value.test.js b/src/plugins/vis_types/timeseries/server/lib/vis_data/helpers/get_agg_value.test.js similarity index 100% rename from src/plugins/vis_type_timeseries/server/lib/vis_data/helpers/get_agg_value.test.js rename to src/plugins/vis_types/timeseries/server/lib/vis_data/helpers/get_agg_value.test.js diff --git a/src/plugins/vis_type_timeseries/server/lib/vis_data/helpers/get_bucket_size.test.ts b/src/plugins/vis_types/timeseries/server/lib/vis_data/helpers/get_bucket_size.test.ts similarity index 100% rename from src/plugins/vis_type_timeseries/server/lib/vis_data/helpers/get_bucket_size.test.ts rename to src/plugins/vis_types/timeseries/server/lib/vis_data/helpers/get_bucket_size.test.ts diff --git a/src/plugins/vis_type_timeseries/server/lib/vis_data/helpers/get_bucket_size.ts b/src/plugins/vis_types/timeseries/server/lib/vis_data/helpers/get_bucket_size.ts similarity index 98% rename from src/plugins/vis_type_timeseries/server/lib/vis_data/helpers/get_bucket_size.ts rename to src/plugins/vis_types/timeseries/server/lib/vis_data/helpers/get_bucket_size.ts index 7f5874c0763f5..e02b403c8ba13 100644 --- a/src/plugins/vis_type_timeseries/server/lib/vis_data/helpers/get_bucket_size.ts +++ b/src/plugins/vis_types/timeseries/server/lib/vis_data/helpers/get_bucket_size.ts @@ -15,7 +15,7 @@ import { } from './unit_to_seconds'; import { getTimerange } from './get_timerange'; import { INTERVAL_STRING_RE, GTE_INTERVAL_RE } from '../../../../common/interval_regexp'; -import { search } from '../../../../../data/server'; +import { search } from '../../../../../../data/server'; import type { SearchCapabilities } from '../../search_strategies'; import type { VisTypeTimeseriesVisDataRequest } from '../../../types'; diff --git a/src/plugins/vis_type_timeseries/server/lib/vis_data/helpers/get_buckets_path.test.ts b/src/plugins/vis_types/timeseries/server/lib/vis_data/helpers/get_buckets_path.test.ts similarity index 100% rename from src/plugins/vis_type_timeseries/server/lib/vis_data/helpers/get_buckets_path.test.ts rename to src/plugins/vis_types/timeseries/server/lib/vis_data/helpers/get_buckets_path.test.ts diff --git a/src/plugins/vis_type_timeseries/server/lib/vis_data/helpers/get_buckets_path.ts b/src/plugins/vis_types/timeseries/server/lib/vis_data/helpers/get_buckets_path.ts similarity index 96% rename from src/plugins/vis_type_timeseries/server/lib/vis_data/helpers/get_buckets_path.ts rename to src/plugins/vis_types/timeseries/server/lib/vis_data/helpers/get_buckets_path.ts index be8755584e2c7..06df19ab73856 100644 --- a/src/plugins/vis_type_timeseries/server/lib/vis_data/helpers/get_buckets_path.ts +++ b/src/plugins/vis_types/timeseries/server/lib/vis_data/helpers/get_buckets_path.ts @@ -8,7 +8,7 @@ import { startsWith } from 'lodash'; import { toPercentileNumber } from '../../../../common/to_percentile_number'; -import { METRIC_TYPES } from '../../../../../data/common'; +import { METRIC_TYPES } from '../../../../../../data/common'; import { TSVB_METRIC_TYPES } from '../../../../common/enums'; import type { Metric } from '../../../../common/types'; diff --git a/src/plugins/vis_type_timeseries/server/lib/vis_data/helpers/get_default_decoration.js b/src/plugins/vis_types/timeseries/server/lib/vis_data/helpers/get_default_decoration.js similarity index 100% rename from src/plugins/vis_type_timeseries/server/lib/vis_data/helpers/get_default_decoration.js rename to src/plugins/vis_types/timeseries/server/lib/vis_data/helpers/get_default_decoration.js diff --git a/src/plugins/vis_type_timeseries/server/lib/vis_data/helpers/get_default_decoration.test.js b/src/plugins/vis_types/timeseries/server/lib/vis_data/helpers/get_default_decoration.test.js similarity index 100% rename from src/plugins/vis_type_timeseries/server/lib/vis_data/helpers/get_default_decoration.test.js rename to src/plugins/vis_types/timeseries/server/lib/vis_data/helpers/get_default_decoration.test.js diff --git a/src/plugins/vis_type_timeseries/server/lib/vis_data/helpers/get_es_query_uisettings.ts b/src/plugins/vis_types/timeseries/server/lib/vis_data/helpers/get_es_query_uisettings.ts similarity index 93% rename from src/plugins/vis_type_timeseries/server/lib/vis_data/helpers/get_es_query_uisettings.ts rename to src/plugins/vis_types/timeseries/server/lib/vis_data/helpers/get_es_query_uisettings.ts index c5ceac5cd7dd5..07b030782d6d7 100644 --- a/src/plugins/vis_type_timeseries/server/lib/vis_data/helpers/get_es_query_uisettings.ts +++ b/src/plugins/vis_types/timeseries/server/lib/vis_data/helpers/get_es_query_uisettings.ts @@ -7,7 +7,7 @@ */ import { IUiSettingsClient } from 'kibana/server'; -import { UI_SETTINGS } from '../../../../../data/server'; +import { UI_SETTINGS } from '../../../../../../data/server'; export async function getEsQueryConfig(uiSettings: IUiSettingsClient) { const allowLeadingWildcards = await uiSettings.get(UI_SETTINGS.QUERY_ALLOW_LEADING_WILDCARDS); diff --git a/src/plugins/vis_type_timeseries/server/lib/vis_data/helpers/get_last_metric.test.ts b/src/plugins/vis_types/timeseries/server/lib/vis_data/helpers/get_last_metric.test.ts similarity index 100% rename from src/plugins/vis_type_timeseries/server/lib/vis_data/helpers/get_last_metric.test.ts rename to src/plugins/vis_types/timeseries/server/lib/vis_data/helpers/get_last_metric.test.ts diff --git a/src/plugins/vis_type_timeseries/server/lib/vis_data/helpers/get_last_metric.ts b/src/plugins/vis_types/timeseries/server/lib/vis_data/helpers/get_last_metric.ts similarity index 100% rename from src/plugins/vis_type_timeseries/server/lib/vis_data/helpers/get_last_metric.ts rename to src/plugins/vis_types/timeseries/server/lib/vis_data/helpers/get_last_metric.ts diff --git a/src/plugins/vis_type_timeseries/server/lib/vis_data/helpers/get_sibling_agg_value.js b/src/plugins/vis_types/timeseries/server/lib/vis_data/helpers/get_sibling_agg_value.js similarity index 100% rename from src/plugins/vis_type_timeseries/server/lib/vis_data/helpers/get_sibling_agg_value.js rename to src/plugins/vis_types/timeseries/server/lib/vis_data/helpers/get_sibling_agg_value.js diff --git a/src/plugins/vis_type_timeseries/server/lib/vis_data/helpers/get_sibling_agg_value.test.js b/src/plugins/vis_types/timeseries/server/lib/vis_data/helpers/get_sibling_agg_value.test.js similarity index 100% rename from src/plugins/vis_type_timeseries/server/lib/vis_data/helpers/get_sibling_agg_value.test.js rename to src/plugins/vis_types/timeseries/server/lib/vis_data/helpers/get_sibling_agg_value.test.js diff --git a/src/plugins/vis_type_timeseries/server/lib/vis_data/helpers/get_splits.test.js b/src/plugins/vis_types/timeseries/server/lib/vis_data/helpers/get_splits.test.js similarity index 100% rename from src/plugins/vis_type_timeseries/server/lib/vis_data/helpers/get_splits.test.js rename to src/plugins/vis_types/timeseries/server/lib/vis_data/helpers/get_splits.test.js diff --git a/src/plugins/vis_type_timeseries/server/lib/vis_data/helpers/get_splits.ts b/src/plugins/vis_types/timeseries/server/lib/vis_data/helpers/get_splits.ts similarity index 100% rename from src/plugins/vis_type_timeseries/server/lib/vis_data/helpers/get_splits.ts rename to src/plugins/vis_types/timeseries/server/lib/vis_data/helpers/get_splits.ts diff --git a/src/plugins/vis_type_timeseries/server/lib/vis_data/helpers/get_timerange.test.ts b/src/plugins/vis_types/timeseries/server/lib/vis_data/helpers/get_timerange.test.ts similarity index 100% rename from src/plugins/vis_type_timeseries/server/lib/vis_data/helpers/get_timerange.test.ts rename to src/plugins/vis_types/timeseries/server/lib/vis_data/helpers/get_timerange.test.ts diff --git a/src/plugins/vis_type_timeseries/server/lib/vis_data/helpers/get_timerange.ts b/src/plugins/vis_types/timeseries/server/lib/vis_data/helpers/get_timerange.ts similarity index 100% rename from src/plugins/vis_type_timeseries/server/lib/vis_data/helpers/get_timerange.ts rename to src/plugins/vis_types/timeseries/server/lib/vis_data/helpers/get_timerange.ts diff --git a/src/plugins/vis_type_timeseries/server/lib/vis_data/helpers/get_timerange_mode.ts b/src/plugins/vis_types/timeseries/server/lib/vis_data/helpers/get_timerange_mode.ts similarity index 100% rename from src/plugins/vis_type_timeseries/server/lib/vis_data/helpers/get_timerange_mode.ts rename to src/plugins/vis_types/timeseries/server/lib/vis_data/helpers/get_timerange_mode.ts diff --git a/src/plugins/vis_type_timeseries/server/lib/vis_data/helpers/index.ts b/src/plugins/vis_types/timeseries/server/lib/vis_data/helpers/index.ts similarity index 100% rename from src/plugins/vis_type_timeseries/server/lib/vis_data/helpers/index.ts rename to src/plugins/vis_types/timeseries/server/lib/vis_data/helpers/index.ts diff --git a/src/plugins/vis_type_timeseries/server/lib/vis_data/helpers/map_empty_to_zero.test.ts b/src/plugins/vis_types/timeseries/server/lib/vis_data/helpers/map_empty_to_zero.test.ts similarity index 96% rename from src/plugins/vis_type_timeseries/server/lib/vis_data/helpers/map_empty_to_zero.test.ts rename to src/plugins/vis_types/timeseries/server/lib/vis_data/helpers/map_empty_to_zero.test.ts index d52b6b38a7bd7..fd061cd363695 100644 --- a/src/plugins/vis_type_timeseries/server/lib/vis_data/helpers/map_empty_to_zero.test.ts +++ b/src/plugins/vis_types/timeseries/server/lib/vis_data/helpers/map_empty_to_zero.test.ts @@ -7,7 +7,7 @@ */ import { mapEmptyToZero } from './map_empty_to_zero'; -import { METRIC_TYPES } from '../../../../../data/common'; +import { METRIC_TYPES } from '../../../../../../data/common'; import { TSVB_METRIC_TYPES } from '../../../../common/enums'; describe('mapEmptyToZero(metric, buckets)', () => { diff --git a/src/plugins/vis_type_timeseries/server/lib/vis_data/helpers/map_empty_to_zero.ts b/src/plugins/vis_types/timeseries/server/lib/vis_data/helpers/map_empty_to_zero.ts similarity index 94% rename from src/plugins/vis_type_timeseries/server/lib/vis_data/helpers/map_empty_to_zero.ts rename to src/plugins/vis_types/timeseries/server/lib/vis_data/helpers/map_empty_to_zero.ts index a035d566d130e..26014b229bae9 100644 --- a/src/plugins/vis_type_timeseries/server/lib/vis_data/helpers/map_empty_to_zero.ts +++ b/src/plugins/vis_types/timeseries/server/lib/vis_data/helpers/map_empty_to_zero.ts @@ -8,7 +8,7 @@ // @ts-expect-error not typed yet import { getAggValue } from './get_agg_value'; -import { METRIC_TYPES } from '../../../../../data/common'; +import { METRIC_TYPES } from '../../../../../../data/common'; import type { Metric } from '../../../../common/types'; import type { PanelDataArray } from '../../../../common/types/vis_data'; diff --git a/src/plugins/vis_type_timeseries/server/lib/vis_data/helpers/moving_fn_scripts.js b/src/plugins/vis_types/timeseries/server/lib/vis_data/helpers/moving_fn_scripts.js similarity index 100% rename from src/plugins/vis_type_timeseries/server/lib/vis_data/helpers/moving_fn_scripts.js rename to src/plugins/vis_types/timeseries/server/lib/vis_data/helpers/moving_fn_scripts.js diff --git a/src/plugins/vis_type_timeseries/server/lib/vis_data/helpers/moving_fn_scripts.test.js b/src/plugins/vis_types/timeseries/server/lib/vis_data/helpers/moving_fn_scripts.test.js similarity index 100% rename from src/plugins/vis_type_timeseries/server/lib/vis_data/helpers/moving_fn_scripts.test.js rename to src/plugins/vis_types/timeseries/server/lib/vis_data/helpers/moving_fn_scripts.test.js diff --git a/src/plugins/vis_type_timeseries/server/lib/vis_data/helpers/overwrite.ts b/src/plugins/vis_types/timeseries/server/lib/vis_data/helpers/overwrite.ts similarity index 100% rename from src/plugins/vis_type_timeseries/server/lib/vis_data/helpers/overwrite.ts rename to src/plugins/vis_types/timeseries/server/lib/vis_data/helpers/overwrite.ts diff --git a/src/plugins/vis_type_timeseries/server/lib/vis_data/helpers/parse_interval.js b/src/plugins/vis_types/timeseries/server/lib/vis_data/helpers/parse_interval.js similarity index 100% rename from src/plugins/vis_type_timeseries/server/lib/vis_data/helpers/parse_interval.js rename to src/plugins/vis_types/timeseries/server/lib/vis_data/helpers/parse_interval.js diff --git a/src/plugins/vis_type_timeseries/server/lib/vis_data/helpers/timestamp.test.ts b/src/plugins/vis_types/timeseries/server/lib/vis_data/helpers/timestamp.test.ts similarity index 100% rename from src/plugins/vis_type_timeseries/server/lib/vis_data/helpers/timestamp.test.ts rename to src/plugins/vis_types/timeseries/server/lib/vis_data/helpers/timestamp.test.ts diff --git a/src/plugins/vis_type_timeseries/server/lib/vis_data/helpers/timestamp.ts b/src/plugins/vis_types/timeseries/server/lib/vis_data/helpers/timestamp.ts similarity index 100% rename from src/plugins/vis_type_timeseries/server/lib/vis_data/helpers/timestamp.ts rename to src/plugins/vis_types/timeseries/server/lib/vis_data/helpers/timestamp.ts diff --git a/src/plugins/vis_type_timeseries/server/lib/vis_data/helpers/unit_to_seconds.test.ts b/src/plugins/vis_types/timeseries/server/lib/vis_data/helpers/unit_to_seconds.test.ts similarity index 100% rename from src/plugins/vis_type_timeseries/server/lib/vis_data/helpers/unit_to_seconds.test.ts rename to src/plugins/vis_types/timeseries/server/lib/vis_data/helpers/unit_to_seconds.test.ts diff --git a/src/plugins/vis_type_timeseries/server/lib/vis_data/helpers/unit_to_seconds.ts b/src/plugins/vis_types/timeseries/server/lib/vis_data/helpers/unit_to_seconds.ts similarity index 100% rename from src/plugins/vis_type_timeseries/server/lib/vis_data/helpers/unit_to_seconds.ts rename to src/plugins/vis_types/timeseries/server/lib/vis_data/helpers/unit_to_seconds.ts diff --git a/src/plugins/vis_type_timeseries/server/lib/vis_data/offset_time.js b/src/plugins/vis_types/timeseries/server/lib/vis_data/offset_time.js similarity index 100% rename from src/plugins/vis_type_timeseries/server/lib/vis_data/offset_time.js rename to src/plugins/vis_types/timeseries/server/lib/vis_data/offset_time.js diff --git a/src/plugins/vis_type_timeseries/server/lib/vis_data/offset_time.test.js b/src/plugins/vis_types/timeseries/server/lib/vis_data/offset_time.test.js similarity index 100% rename from src/plugins/vis_type_timeseries/server/lib/vis_data/offset_time.test.js rename to src/plugins/vis_types/timeseries/server/lib/vis_data/offset_time.test.js diff --git a/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/annotations/date_histogram.ts b/src/plugins/vis_types/timeseries/server/lib/vis_data/request_processors/annotations/date_histogram.ts similarity index 95% rename from src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/annotations/date_histogram.ts rename to src/plugins/vis_types/timeseries/server/lib/vis_data/request_processors/annotations/date_histogram.ts index 258dc2d541376..a52e15eb90fee 100644 --- a/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/annotations/date_histogram.ts +++ b/src/plugins/vis_types/timeseries/server/lib/vis_data/request_processors/annotations/date_histogram.ts @@ -10,7 +10,7 @@ import { overwrite } from '../../helpers'; import { getBucketSize, getTimerange } from '../../helpers'; import { validateField } from '../../../../../common/fields_utils'; -import { search, UI_SETTINGS } from '../../../../../../../plugins/data/server'; +import { search, UI_SETTINGS } from '../../../../../../../../plugins/data/server'; import type { AnnotationsRequestProcessorsFunction } from './types'; diff --git a/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/annotations/index.ts b/src/plugins/vis_types/timeseries/server/lib/vis_data/request_processors/annotations/index.ts similarity index 100% rename from src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/annotations/index.ts rename to src/plugins/vis_types/timeseries/server/lib/vis_data/request_processors/annotations/index.ts diff --git a/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/annotations/query.ts b/src/plugins/vis_types/timeseries/server/lib/vis_data/request_processors/annotations/query.ts similarity index 97% rename from src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/annotations/query.ts rename to src/plugins/vis_types/timeseries/server/lib/vis_data/request_processors/annotations/query.ts index 53fe51329acb3..f1b11a780fec8 100644 --- a/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/annotations/query.ts +++ b/src/plugins/vis_types/timeseries/server/lib/vis_data/request_processors/annotations/query.ts @@ -8,7 +8,7 @@ import { getBucketSize, getTimerange, overwrite } from '../../helpers'; import { validateField } from '../../../../../common/fields_utils'; -import { esQuery, UI_SETTINGS } from '../../../../../../data/server'; +import { esQuery, UI_SETTINGS } from '../../../../../../../data/server'; import type { AnnotationsRequestProcessorsFunction } from './types'; diff --git a/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/annotations/top_hits.ts b/src/plugins/vis_types/timeseries/server/lib/vis_data/request_processors/annotations/top_hits.ts similarity index 100% rename from src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/annotations/top_hits.ts rename to src/plugins/vis_types/timeseries/server/lib/vis_data/request_processors/annotations/top_hits.ts diff --git a/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/annotations/types.ts b/src/plugins/vis_types/timeseries/server/lib/vis_data/request_processors/annotations/types.ts similarity index 100% rename from src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/annotations/types.ts rename to src/plugins/vis_types/timeseries/server/lib/vis_data/request_processors/annotations/types.ts diff --git a/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/date_histogram.js b/src/plugins/vis_types/timeseries/server/lib/vis_data/request_processors/series/date_histogram.js similarity index 97% rename from src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/date_histogram.js rename to src/plugins/vis_types/timeseries/server/lib/vis_data/request_processors/series/date_histogram.js index 6349a75993aa8..696bea7d6421b 100644 --- a/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/date_histogram.js +++ b/src/plugins/vis_types/timeseries/server/lib/vis_data/request_processors/series/date_histogram.js @@ -10,7 +10,7 @@ import { overwrite } from '../../helpers'; import { getBucketSize } from '../../helpers/get_bucket_size'; import { offsetTime } from '../../offset_time'; import { isLastValueTimerangeMode } from '../../helpers/get_timerange_mode'; -import { search, UI_SETTINGS } from '../../../../../../../plugins/data/server'; +import { search, UI_SETTINGS } from '../../../../../../../../plugins/data/server'; import { AGG_TYPE, getAggsByType } from '../../../../../common/agg_utils'; import { TSVB_METRIC_TYPES } from '../../../../../common/enums'; diff --git a/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/date_histogram.test.js b/src/plugins/vis_types/timeseries/server/lib/vis_data/request_processors/series/date_histogram.test.js similarity index 99% rename from src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/date_histogram.test.js rename to src/plugins/vis_types/timeseries/server/lib/vis_data/request_processors/series/date_histogram.test.js index b09b2c28d77e3..6a7f09a49f26a 100644 --- a/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/date_histogram.test.js +++ b/src/plugins/vis_types/timeseries/server/lib/vis_data/request_processors/series/date_histogram.test.js @@ -9,7 +9,7 @@ import { DefaultSearchCapabilities } from '../../../search_strategies/capabilities/default_search_capabilities'; import { dateHistogram } from './date_histogram'; import { getIntervalAndTimefield } from '../../get_interval_and_timefield'; -import { UI_SETTINGS } from '../../../../../../data/common'; +import { UI_SETTINGS } from '../../../../../../../data/common'; describe('dateHistogram(req, panel, series)', () => { let panel; diff --git a/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/filter_ratios.js b/src/plugins/vis_types/timeseries/server/lib/vis_data/request_processors/series/filter_ratios.js similarity index 97% rename from src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/filter_ratios.js rename to src/plugins/vis_types/timeseries/server/lib/vis_data/request_processors/series/filter_ratios.js index d45943f6f21ac..32e9a1ea0aa90 100644 --- a/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/filter_ratios.js +++ b/src/plugins/vis_types/timeseries/server/lib/vis_data/request_processors/series/filter_ratios.js @@ -8,7 +8,7 @@ import { bucketTransform } from '../../helpers/bucket_transform'; import { overwrite } from '../../helpers'; -import { esQuery } from '../../../../../../data/server'; +import { esQuery } from '../../../../../../../data/server'; const filter = (metric) => metric.type === 'filter_ratio'; diff --git a/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/filter_ratios.test.js b/src/plugins/vis_types/timeseries/server/lib/vis_data/request_processors/series/filter_ratios.test.js similarity index 100% rename from src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/filter_ratios.test.js rename to src/plugins/vis_types/timeseries/server/lib/vis_data/request_processors/series/filter_ratios.test.js diff --git a/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/index.js b/src/plugins/vis_types/timeseries/server/lib/vis_data/request_processors/series/index.js similarity index 100% rename from src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/index.js rename to src/plugins/vis_types/timeseries/server/lib/vis_data/request_processors/series/index.js diff --git a/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/metric_buckets.js b/src/plugins/vis_types/timeseries/server/lib/vis_data/request_processors/series/metric_buckets.js similarity index 100% rename from src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/metric_buckets.js rename to src/plugins/vis_types/timeseries/server/lib/vis_data/request_processors/series/metric_buckets.js diff --git a/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/metric_buckets.test.js b/src/plugins/vis_types/timeseries/server/lib/vis_data/request_processors/series/metric_buckets.test.js similarity index 100% rename from src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/metric_buckets.test.js rename to src/plugins/vis_types/timeseries/server/lib/vis_data/request_processors/series/metric_buckets.test.js diff --git a/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/normalize_query.js b/src/plugins/vis_types/timeseries/server/lib/vis_data/request_processors/series/normalize_query.js similarity index 100% rename from src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/normalize_query.js rename to src/plugins/vis_types/timeseries/server/lib/vis_data/request_processors/series/normalize_query.js diff --git a/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/normalize_query.test.js b/src/plugins/vis_types/timeseries/server/lib/vis_data/request_processors/series/normalize_query.test.js similarity index 100% rename from src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/normalize_query.test.js rename to src/plugins/vis_types/timeseries/server/lib/vis_data/request_processors/series/normalize_query.test.js diff --git a/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/positive_rate.js b/src/plugins/vis_types/timeseries/server/lib/vis_data/request_processors/series/positive_rate.js similarity index 97% rename from src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/positive_rate.js rename to src/plugins/vis_types/timeseries/server/lib/vis_data/request_processors/series/positive_rate.js index 91016384794c4..86c5311f51dcb 100644 --- a/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/positive_rate.js +++ b/src/plugins/vis_types/timeseries/server/lib/vis_data/request_processors/series/positive_rate.js @@ -9,7 +9,7 @@ import { getBucketSize } from '../../helpers/get_bucket_size'; import { bucketTransform } from '../../helpers/bucket_transform'; import { overwrite } from '../../helpers'; -import { UI_SETTINGS } from '../../../../../../data/common'; +import { UI_SETTINGS } from '../../../../../../../data/common'; export const filter = (metric) => metric.type === 'positive_rate'; diff --git a/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/positive_rate.test.js b/src/plugins/vis_types/timeseries/server/lib/vis_data/request_processors/series/positive_rate.test.js similarity index 100% rename from src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/positive_rate.test.js rename to src/plugins/vis_types/timeseries/server/lib/vis_data/request_processors/series/positive_rate.test.js diff --git a/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/query.js b/src/plugins/vis_types/timeseries/server/lib/vis_data/request_processors/series/query.js similarity index 96% rename from src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/query.js rename to src/plugins/vis_types/timeseries/server/lib/vis_data/request_processors/series/query.js index 5031a0f2ec185..f745a7bf7ad74 100644 --- a/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/query.js +++ b/src/plugins/vis_types/timeseries/server/lib/vis_data/request_processors/series/query.js @@ -7,7 +7,7 @@ */ import { offsetTime } from '../../offset_time'; -import { esQuery } from '../../../../../../data/server'; +import { esQuery } from '../../../../../../../data/server'; export function query( req, diff --git a/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/query.test.js b/src/plugins/vis_types/timeseries/server/lib/vis_data/request_processors/series/query.test.js similarity index 100% rename from src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/query.test.js rename to src/plugins/vis_types/timeseries/server/lib/vis_data/request_processors/series/query.test.js diff --git a/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/sibling_buckets.js b/src/plugins/vis_types/timeseries/server/lib/vis_data/request_processors/series/sibling_buckets.js similarity index 100% rename from src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/sibling_buckets.js rename to src/plugins/vis_types/timeseries/server/lib/vis_data/request_processors/series/sibling_buckets.js diff --git a/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/sibling_buckets.test.js b/src/plugins/vis_types/timeseries/server/lib/vis_data/request_processors/series/sibling_buckets.test.js similarity index 100% rename from src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/sibling_buckets.test.js rename to src/plugins/vis_types/timeseries/server/lib/vis_data/request_processors/series/sibling_buckets.test.js diff --git a/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/split_by_everything.js b/src/plugins/vis_types/timeseries/server/lib/vis_data/request_processors/series/split_by_everything.js similarity index 100% rename from src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/split_by_everything.js rename to src/plugins/vis_types/timeseries/server/lib/vis_data/request_processors/series/split_by_everything.js diff --git a/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/split_by_everything.test.js b/src/plugins/vis_types/timeseries/server/lib/vis_data/request_processors/series/split_by_everything.test.js similarity index 100% rename from src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/split_by_everything.test.js rename to src/plugins/vis_types/timeseries/server/lib/vis_data/request_processors/series/split_by_everything.test.js diff --git a/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/split_by_filter.js b/src/plugins/vis_types/timeseries/server/lib/vis_data/request_processors/series/split_by_filter.js similarity index 92% rename from src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/split_by_filter.js rename to src/plugins/vis_types/timeseries/server/lib/vis_data/request_processors/series/split_by_filter.js index 01e1b9f8d1dce..b7e7754d05f81 100644 --- a/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/split_by_filter.js +++ b/src/plugins/vis_types/timeseries/server/lib/vis_data/request_processors/series/split_by_filter.js @@ -7,7 +7,7 @@ */ import { overwrite } from '../../helpers'; -import { esQuery } from '../../../../../../data/server'; +import { esQuery } from '../../../../../../../data/server'; export function splitByFilter(req, panel, series, esQueryConfig, seriesIndex) { return (next) => (doc) => { diff --git a/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/split_by_filter.test.js b/src/plugins/vis_types/timeseries/server/lib/vis_data/request_processors/series/split_by_filter.test.js similarity index 100% rename from src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/split_by_filter.test.js rename to src/plugins/vis_types/timeseries/server/lib/vis_data/request_processors/series/split_by_filter.test.js diff --git a/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/split_by_filters.js b/src/plugins/vis_types/timeseries/server/lib/vis_data/request_processors/series/split_by_filters.js similarity index 93% rename from src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/split_by_filters.js rename to src/plugins/vis_types/timeseries/server/lib/vis_data/request_processors/series/split_by_filters.js index 77b9ccc5880fe..5e8def5db771e 100644 --- a/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/split_by_filters.js +++ b/src/plugins/vis_types/timeseries/server/lib/vis_data/request_processors/series/split_by_filters.js @@ -7,7 +7,7 @@ */ import { overwrite } from '../../helpers'; -import { esQuery } from '../../../../../../data/server'; +import { esQuery } from '../../../../../../../data/server'; export function splitByFilters(req, panel, series, esQueryConfig, seriesIndex) { return (next) => (doc) => { diff --git a/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/split_by_filters.test.js b/src/plugins/vis_types/timeseries/server/lib/vis_data/request_processors/series/split_by_filters.test.js similarity index 100% rename from src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/split_by_filters.test.js rename to src/plugins/vis_types/timeseries/server/lib/vis_data/request_processors/series/split_by_filters.test.js diff --git a/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/split_by_terms.js b/src/plugins/vis_types/timeseries/server/lib/vis_data/request_processors/series/split_by_terms.js similarity index 100% rename from src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/split_by_terms.js rename to src/plugins/vis_types/timeseries/server/lib/vis_data/request_processors/series/split_by_terms.js diff --git a/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/split_by_terms.test.js b/src/plugins/vis_types/timeseries/server/lib/vis_data/request_processors/series/split_by_terms.test.js similarity index 100% rename from src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/split_by_terms.test.js rename to src/plugins/vis_types/timeseries/server/lib/vis_data/request_processors/series/split_by_terms.test.js diff --git a/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/table/calculate_agg_root.ts b/src/plugins/vis_types/timeseries/server/lib/vis_data/request_processors/table/calculate_agg_root.ts similarity index 100% rename from src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/table/calculate_agg_root.ts rename to src/plugins/vis_types/timeseries/server/lib/vis_data/request_processors/table/calculate_agg_root.ts diff --git a/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/table/date_histogram.ts b/src/plugins/vis_types/timeseries/server/lib/vis_data/request_processors/table/date_histogram.ts similarity index 97% rename from src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/table/date_histogram.ts rename to src/plugins/vis_types/timeseries/server/lib/vis_data/request_processors/table/date_histogram.ts index 5dee812dd4c56..c02f661c3aedc 100644 --- a/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/table/date_histogram.ts +++ b/src/plugins/vis_types/timeseries/server/lib/vis_data/request_processors/table/date_histogram.ts @@ -8,7 +8,7 @@ import { overwrite, getBucketSize, isLastValueTimerangeMode, getTimerange } from '../../helpers'; import { calculateAggRoot } from './calculate_agg_root'; -import { search, UI_SETTINGS } from '../../../../../../../plugins/data/server'; +import { search, UI_SETTINGS } from '../../../../../../../../plugins/data/server'; import { AGG_TYPE, getAggsByType } from '../../../../../common/agg_utils'; import { TSVB_METRIC_TYPES } from '../../../../../common/enums'; diff --git a/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/table/filter_ratios.ts b/src/plugins/vis_types/timeseries/server/lib/vis_data/request_processors/table/filter_ratios.ts similarity index 97% rename from src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/table/filter_ratios.ts rename to src/plugins/vis_types/timeseries/server/lib/vis_data/request_processors/table/filter_ratios.ts index e8fb684ef4b6c..b6036a03ba464 100644 --- a/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/table/filter_ratios.ts +++ b/src/plugins/vis_types/timeseries/server/lib/vis_data/request_processors/table/filter_ratios.ts @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import { esQuery } from '../../../../../../data/server'; +import { esQuery } from '../../../../../../../data/server'; import { overwrite, bucketTransform } from '../../helpers'; import { calculateAggRoot } from './calculate_agg_root'; diff --git a/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/table/index.ts b/src/plugins/vis_types/timeseries/server/lib/vis_data/request_processors/table/index.ts similarity index 100% rename from src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/table/index.ts rename to src/plugins/vis_types/timeseries/server/lib/vis_data/request_processors/table/index.ts diff --git a/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/table/metric_buckets.ts b/src/plugins/vis_types/timeseries/server/lib/vis_data/request_processors/table/metric_buckets.ts similarity index 100% rename from src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/table/metric_buckets.ts rename to src/plugins/vis_types/timeseries/server/lib/vis_data/request_processors/table/metric_buckets.ts diff --git a/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/table/normalize_query.test.ts b/src/plugins/vis_types/timeseries/server/lib/vis_data/request_processors/table/normalize_query.test.ts similarity index 100% rename from src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/table/normalize_query.test.ts rename to src/plugins/vis_types/timeseries/server/lib/vis_data/request_processors/table/normalize_query.test.ts diff --git a/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/table/normalize_query.ts b/src/plugins/vis_types/timeseries/server/lib/vis_data/request_processors/table/normalize_query.ts similarity index 100% rename from src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/table/normalize_query.ts rename to src/plugins/vis_types/timeseries/server/lib/vis_data/request_processors/table/normalize_query.ts diff --git a/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/table/pivot.ts b/src/plugins/vis_types/timeseries/server/lib/vis_data/request_processors/table/pivot.ts similarity index 100% rename from src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/table/pivot.ts rename to src/plugins/vis_types/timeseries/server/lib/vis_data/request_processors/table/pivot.ts diff --git a/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/table/positive_rate.ts b/src/plugins/vis_types/timeseries/server/lib/vis_data/request_processors/table/positive_rate.ts similarity index 95% rename from src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/table/positive_rate.ts rename to src/plugins/vis_types/timeseries/server/lib/vis_data/request_processors/table/positive_rate.ts index 85a817e124aa1..e03a65ffebbd1 100644 --- a/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/table/positive_rate.ts +++ b/src/plugins/vis_types/timeseries/server/lib/vis_data/request_processors/table/positive_rate.ts @@ -8,7 +8,7 @@ import { getBucketSize } from '../../helpers/get_bucket_size'; import { calculateAggRoot } from './calculate_agg_root'; -import { UI_SETTINGS } from '../../../../../../data/common'; +import { UI_SETTINGS } from '../../../../../../../data/common'; import type { TableRequestProcessorsFunction } from './types'; diff --git a/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/table/query.ts b/src/plugins/vis_types/timeseries/server/lib/vis_data/request_processors/table/query.ts similarity index 96% rename from src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/table/query.ts rename to src/plugins/vis_types/timeseries/server/lib/vis_data/request_processors/table/query.ts index 5ddf83e1134b3..76c4649ee5738 100644 --- a/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/table/query.ts +++ b/src/plugins/vis_types/timeseries/server/lib/vis_data/request_processors/table/query.ts @@ -7,7 +7,7 @@ */ import { getTimerange, overwrite } from '../../helpers'; -import { esQuery } from '../../../../../../data/server'; +import { esQuery } from '../../../../../../../data/server'; import type { TableRequestProcessorsFunction } from './types'; export const query: TableRequestProcessorsFunction = diff --git a/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/table/sibling_buckets.ts b/src/plugins/vis_types/timeseries/server/lib/vis_data/request_processors/table/sibling_buckets.ts similarity index 100% rename from src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/table/sibling_buckets.ts rename to src/plugins/vis_types/timeseries/server/lib/vis_data/request_processors/table/sibling_buckets.ts diff --git a/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/table/split_by_everything.ts b/src/plugins/vis_types/timeseries/server/lib/vis_data/request_processors/table/split_by_everything.ts similarity index 94% rename from src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/table/split_by_everything.ts rename to src/plugins/vis_types/timeseries/server/lib/vis_data/request_processors/table/split_by_everything.ts index cca86a1960fcd..e0e1485f6a8ff 100644 --- a/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/table/split_by_everything.ts +++ b/src/plugins/vis_types/timeseries/server/lib/vis_data/request_processors/table/split_by_everything.ts @@ -7,7 +7,7 @@ */ import { overwrite } from '../../helpers'; -import { esQuery } from '../../../../../../data/server'; +import { esQuery } from '../../../../../../../data/server'; import type { TableRequestProcessorsFunction } from './types'; diff --git a/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/table/split_by_terms.ts b/src/plugins/vis_types/timeseries/server/lib/vis_data/request_processors/table/split_by_terms.ts similarity index 95% rename from src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/table/split_by_terms.ts rename to src/plugins/vis_types/timeseries/server/lib/vis_data/request_processors/table/split_by_terms.ts index 35b0ba50a05b9..1a4e0f6ceb1b5 100644 --- a/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/table/split_by_terms.ts +++ b/src/plugins/vis_types/timeseries/server/lib/vis_data/request_processors/table/split_by_terms.ts @@ -7,7 +7,7 @@ */ import { overwrite } from '../../helpers'; -import { esQuery } from '../../../../../../data/server'; +import { esQuery } from '../../../../../../../data/server'; import type { TableRequestProcessorsFunction } from './types'; diff --git a/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/table/types.ts b/src/plugins/vis_types/timeseries/server/lib/vis_data/request_processors/table/types.ts similarity index 95% rename from src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/table/types.ts rename to src/plugins/vis_types/timeseries/server/lib/vis_data/request_processors/table/types.ts index d47d3fe34c9b1..6bb4dfa55426a 100644 --- a/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/table/types.ts +++ b/src/plugins/vis_types/timeseries/server/lib/vis_data/request_processors/table/types.ts @@ -8,7 +8,7 @@ import type { IUiSettingsClient } from 'kibana/server'; import type { FetchedIndexPattern, Panel } from '../../../../../common/types'; -import type { EsQueryConfig } from '../../../../../../data/common'; +import type { EsQueryConfig } from '../../../../../../../data/common'; import type { SearchCapabilities } from '../../../search_strategies'; import type { VisTypeTimeseriesVisDataRequest } from '../../../../types'; diff --git a/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/types.ts b/src/plugins/vis_types/timeseries/server/lib/vis_data/request_processors/types.ts similarity index 100% rename from src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/types.ts rename to src/plugins/vis_types/timeseries/server/lib/vis_data/request_processors/types.ts diff --git a/src/plugins/vis_type_timeseries/server/lib/vis_data/response_processors/annotations/buckets.ts b/src/plugins/vis_types/timeseries/server/lib/vis_data/response_processors/annotations/buckets.ts similarity index 100% rename from src/plugins/vis_type_timeseries/server/lib/vis_data/response_processors/annotations/buckets.ts rename to src/plugins/vis_types/timeseries/server/lib/vis_data/response_processors/annotations/buckets.ts diff --git a/src/plugins/vis_type_timeseries/server/lib/vis_data/response_processors/annotations/filter.test.ts b/src/plugins/vis_types/timeseries/server/lib/vis_data/response_processors/annotations/filter.test.ts similarity index 100% rename from src/plugins/vis_type_timeseries/server/lib/vis_data/response_processors/annotations/filter.test.ts rename to src/plugins/vis_types/timeseries/server/lib/vis_data/response_processors/annotations/filter.test.ts diff --git a/src/plugins/vis_type_timeseries/server/lib/vis_data/response_processors/annotations/filter.ts b/src/plugins/vis_types/timeseries/server/lib/vis_data/response_processors/annotations/filter.ts similarity index 100% rename from src/plugins/vis_type_timeseries/server/lib/vis_data/response_processors/annotations/filter.ts rename to src/plugins/vis_types/timeseries/server/lib/vis_data/response_processors/annotations/filter.ts diff --git a/src/plugins/vis_type_timeseries/server/lib/vis_data/response_processors/annotations/index.ts b/src/plugins/vis_types/timeseries/server/lib/vis_data/response_processors/annotations/index.ts similarity index 100% rename from src/plugins/vis_type_timeseries/server/lib/vis_data/response_processors/annotations/index.ts rename to src/plugins/vis_types/timeseries/server/lib/vis_data/response_processors/annotations/index.ts diff --git a/src/plugins/vis_type_timeseries/server/lib/vis_data/response_processors/series/_series_agg.js b/src/plugins/vis_types/timeseries/server/lib/vis_data/response_processors/series/_series_agg.js similarity index 100% rename from src/plugins/vis_type_timeseries/server/lib/vis_data/response_processors/series/_series_agg.js rename to src/plugins/vis_types/timeseries/server/lib/vis_data/response_processors/series/_series_agg.js diff --git a/src/plugins/vis_type_timeseries/server/lib/vis_data/response_processors/series/_series_agg.test.js b/src/plugins/vis_types/timeseries/server/lib/vis_data/response_processors/series/_series_agg.test.js similarity index 100% rename from src/plugins/vis_type_timeseries/server/lib/vis_data/response_processors/series/_series_agg.test.js rename to src/plugins/vis_types/timeseries/server/lib/vis_data/response_processors/series/_series_agg.test.js diff --git a/src/plugins/vis_type_timeseries/server/lib/vis_data/response_processors/series/drop_last_bucket.js b/src/plugins/vis_types/timeseries/server/lib/vis_data/response_processors/series/drop_last_bucket.js similarity index 100% rename from src/plugins/vis_type_timeseries/server/lib/vis_data/response_processors/series/drop_last_bucket.js rename to src/plugins/vis_types/timeseries/server/lib/vis_data/response_processors/series/drop_last_bucket.js diff --git a/src/plugins/vis_type_timeseries/server/lib/vis_data/response_processors/series/format_label.ts b/src/plugins/vis_types/timeseries/server/lib/vis_data/response_processors/series/format_label.ts similarity index 96% rename from src/plugins/vis_type_timeseries/server/lib/vis_data/response_processors/series/format_label.ts rename to src/plugins/vis_types/timeseries/server/lib/vis_data/response_processors/series/format_label.ts index 7908cbccb9845..6d824c1c7f43e 100644 --- a/src/plugins/vis_type_timeseries/server/lib/vis_data/response_processors/series/format_label.ts +++ b/src/plugins/vis_types/timeseries/server/lib/vis_data/response_processors/series/format_label.ts @@ -9,7 +9,7 @@ import { KBN_FIELD_TYPES } from '@kbn/field-types'; import { BUCKET_TYPES, PANEL_TYPES } from '../../../../../common/enums'; import type { Panel, PanelData, Series } from '../../../../../common/types'; -import type { FieldFormatsRegistry } from '../../../../../../field_formats/common'; +import type { FieldFormatsRegistry } from '../../../../../../../field_formats/common'; import type { createFieldsFetcher } from '../../../search_strategies/lib/fields_fetcher'; import type { CachedIndexPatternFetcher } from '../../../search_strategies/lib/cached_index_pattern_fetcher'; diff --git a/src/plugins/vis_type_timeseries/server/lib/vis_data/response_processors/series/index.js b/src/plugins/vis_types/timeseries/server/lib/vis_data/response_processors/series/index.js similarity index 100% rename from src/plugins/vis_type_timeseries/server/lib/vis_data/response_processors/series/index.js rename to src/plugins/vis_types/timeseries/server/lib/vis_data/response_processors/series/index.js diff --git a/src/plugins/vis_type_timeseries/server/lib/vis_data/response_processors/series/math.js b/src/plugins/vis_types/timeseries/server/lib/vis_data/response_processors/series/math.js similarity index 100% rename from src/plugins/vis_type_timeseries/server/lib/vis_data/response_processors/series/math.js rename to src/plugins/vis_types/timeseries/server/lib/vis_data/response_processors/series/math.js diff --git a/src/plugins/vis_type_timeseries/server/lib/vis_data/response_processors/series/math.test.js b/src/plugins/vis_types/timeseries/server/lib/vis_data/response_processors/series/math.test.js similarity index 100% rename from src/plugins/vis_type_timeseries/server/lib/vis_data/response_processors/series/math.test.js rename to src/plugins/vis_types/timeseries/server/lib/vis_data/response_processors/series/math.test.js diff --git a/src/plugins/vis_type_timeseries/server/lib/vis_data/response_processors/series/percentile.js b/src/plugins/vis_types/timeseries/server/lib/vis_data/response_processors/series/percentile.js similarity index 100% rename from src/plugins/vis_type_timeseries/server/lib/vis_data/response_processors/series/percentile.js rename to src/plugins/vis_types/timeseries/server/lib/vis_data/response_processors/series/percentile.js diff --git a/src/plugins/vis_type_timeseries/server/lib/vis_data/response_processors/series/percentile.test.js b/src/plugins/vis_types/timeseries/server/lib/vis_data/response_processors/series/percentile.test.js similarity index 100% rename from src/plugins/vis_type_timeseries/server/lib/vis_data/response_processors/series/percentile.test.js rename to src/plugins/vis_types/timeseries/server/lib/vis_data/response_processors/series/percentile.test.js diff --git a/src/plugins/vis_type_timeseries/server/lib/vis_data/response_processors/series/percentile_rank.js b/src/plugins/vis_types/timeseries/server/lib/vis_data/response_processors/series/percentile_rank.js similarity index 100% rename from src/plugins/vis_type_timeseries/server/lib/vis_data/response_processors/series/percentile_rank.js rename to src/plugins/vis_types/timeseries/server/lib/vis_data/response_processors/series/percentile_rank.js diff --git a/src/plugins/vis_type_timeseries/server/lib/vis_data/response_processors/series/percentile_rank.test.ts b/src/plugins/vis_types/timeseries/server/lib/vis_data/response_processors/series/percentile_rank.test.ts similarity index 100% rename from src/plugins/vis_type_timeseries/server/lib/vis_data/response_processors/series/percentile_rank.test.ts rename to src/plugins/vis_types/timeseries/server/lib/vis_data/response_processors/series/percentile_rank.test.ts diff --git a/src/plugins/vis_type_timeseries/server/lib/vis_data/response_processors/series/series_agg.js b/src/plugins/vis_types/timeseries/server/lib/vis_data/response_processors/series/series_agg.js similarity index 100% rename from src/plugins/vis_type_timeseries/server/lib/vis_data/response_processors/series/series_agg.js rename to src/plugins/vis_types/timeseries/server/lib/vis_data/response_processors/series/series_agg.js diff --git a/src/plugins/vis_type_timeseries/server/lib/vis_data/response_processors/series/series_agg.test.js b/src/plugins/vis_types/timeseries/server/lib/vis_data/response_processors/series/series_agg.test.js similarity index 100% rename from src/plugins/vis_type_timeseries/server/lib/vis_data/response_processors/series/series_agg.test.js rename to src/plugins/vis_types/timeseries/server/lib/vis_data/response_processors/series/series_agg.test.js diff --git a/src/plugins/vis_type_timeseries/server/lib/vis_data/response_processors/series/std_deviation_bands.js b/src/plugins/vis_types/timeseries/server/lib/vis_data/response_processors/series/std_deviation_bands.js similarity index 100% rename from src/plugins/vis_type_timeseries/server/lib/vis_data/response_processors/series/std_deviation_bands.js rename to src/plugins/vis_types/timeseries/server/lib/vis_data/response_processors/series/std_deviation_bands.js diff --git a/src/plugins/vis_type_timeseries/server/lib/vis_data/response_processors/series/std_deviation_bands.test.js b/src/plugins/vis_types/timeseries/server/lib/vis_data/response_processors/series/std_deviation_bands.test.js similarity index 100% rename from src/plugins/vis_type_timeseries/server/lib/vis_data/response_processors/series/std_deviation_bands.test.js rename to src/plugins/vis_types/timeseries/server/lib/vis_data/response_processors/series/std_deviation_bands.test.js diff --git a/src/plugins/vis_type_timeseries/server/lib/vis_data/response_processors/series/std_deviation_sibling.js b/src/plugins/vis_types/timeseries/server/lib/vis_data/response_processors/series/std_deviation_sibling.js similarity index 100% rename from src/plugins/vis_type_timeseries/server/lib/vis_data/response_processors/series/std_deviation_sibling.js rename to src/plugins/vis_types/timeseries/server/lib/vis_data/response_processors/series/std_deviation_sibling.js diff --git a/src/plugins/vis_type_timeseries/server/lib/vis_data/response_processors/series/std_deviation_sibling.test.js b/src/plugins/vis_types/timeseries/server/lib/vis_data/response_processors/series/std_deviation_sibling.test.js similarity index 100% rename from src/plugins/vis_type_timeseries/server/lib/vis_data/response_processors/series/std_deviation_sibling.test.js rename to src/plugins/vis_types/timeseries/server/lib/vis_data/response_processors/series/std_deviation_sibling.test.js diff --git a/src/plugins/vis_type_timeseries/server/lib/vis_data/response_processors/series/std_metric.js b/src/plugins/vis_types/timeseries/server/lib/vis_data/response_processors/series/std_metric.js similarity index 100% rename from src/plugins/vis_type_timeseries/server/lib/vis_data/response_processors/series/std_metric.js rename to src/plugins/vis_types/timeseries/server/lib/vis_data/response_processors/series/std_metric.js diff --git a/src/plugins/vis_type_timeseries/server/lib/vis_data/response_processors/series/std_metric.test.js b/src/plugins/vis_types/timeseries/server/lib/vis_data/response_processors/series/std_metric.test.js similarity index 100% rename from src/plugins/vis_type_timeseries/server/lib/vis_data/response_processors/series/std_metric.test.js rename to src/plugins/vis_types/timeseries/server/lib/vis_data/response_processors/series/std_metric.test.js diff --git a/src/plugins/vis_type_timeseries/server/lib/vis_data/response_processors/series/std_sibling.js b/src/plugins/vis_types/timeseries/server/lib/vis_data/response_processors/series/std_sibling.js similarity index 100% rename from src/plugins/vis_type_timeseries/server/lib/vis_data/response_processors/series/std_sibling.js rename to src/plugins/vis_types/timeseries/server/lib/vis_data/response_processors/series/std_sibling.js diff --git a/src/plugins/vis_type_timeseries/server/lib/vis_data/response_processors/series/std_sibling.test.js b/src/plugins/vis_types/timeseries/server/lib/vis_data/response_processors/series/std_sibling.test.js similarity index 100% rename from src/plugins/vis_type_timeseries/server/lib/vis_data/response_processors/series/std_sibling.test.js rename to src/plugins/vis_types/timeseries/server/lib/vis_data/response_processors/series/std_sibling.test.js diff --git a/src/plugins/vis_type_timeseries/server/lib/vis_data/response_processors/series/time_shift.js b/src/plugins/vis_types/timeseries/server/lib/vis_data/response_processors/series/time_shift.js similarity index 100% rename from src/plugins/vis_type_timeseries/server/lib/vis_data/response_processors/series/time_shift.js rename to src/plugins/vis_types/timeseries/server/lib/vis_data/response_processors/series/time_shift.js diff --git a/src/plugins/vis_type_timeseries/server/lib/vis_data/response_processors/series/time_shift.test.js b/src/plugins/vis_types/timeseries/server/lib/vis_data/response_processors/series/time_shift.test.js similarity index 100% rename from src/plugins/vis_type_timeseries/server/lib/vis_data/response_processors/series/time_shift.test.js rename to src/plugins/vis_types/timeseries/server/lib/vis_data/response_processors/series/time_shift.test.js diff --git a/src/plugins/vis_type_timeseries/server/lib/vis_data/response_processors/table/_series_agg.js b/src/plugins/vis_types/timeseries/server/lib/vis_data/response_processors/table/_series_agg.js similarity index 100% rename from src/plugins/vis_type_timeseries/server/lib/vis_data/response_processors/table/_series_agg.js rename to src/plugins/vis_types/timeseries/server/lib/vis_data/response_processors/table/_series_agg.js diff --git a/src/plugins/vis_type_timeseries/server/lib/vis_data/response_processors/table/drop_last_bucket.ts b/src/plugins/vis_types/timeseries/server/lib/vis_data/response_processors/table/drop_last_bucket.ts similarity index 100% rename from src/plugins/vis_type_timeseries/server/lib/vis_data/response_processors/table/drop_last_bucket.ts rename to src/plugins/vis_types/timeseries/server/lib/vis_data/response_processors/table/drop_last_bucket.ts diff --git a/src/plugins/vis_type_timeseries/server/lib/vis_data/response_processors/table/index.ts b/src/plugins/vis_types/timeseries/server/lib/vis_data/response_processors/table/index.ts similarity index 100% rename from src/plugins/vis_type_timeseries/server/lib/vis_data/response_processors/table/index.ts rename to src/plugins/vis_types/timeseries/server/lib/vis_data/response_processors/table/index.ts diff --git a/src/plugins/vis_type_timeseries/server/lib/vis_data/response_processors/table/math.ts b/src/plugins/vis_types/timeseries/server/lib/vis_data/response_processors/table/math.ts similarity index 100% rename from src/plugins/vis_type_timeseries/server/lib/vis_data/response_processors/table/math.ts rename to src/plugins/vis_types/timeseries/server/lib/vis_data/response_processors/table/math.ts diff --git a/src/plugins/vis_type_timeseries/server/lib/vis_data/response_processors/table/percentile.ts b/src/plugins/vis_types/timeseries/server/lib/vis_data/response_processors/table/percentile.ts similarity index 100% rename from src/plugins/vis_type_timeseries/server/lib/vis_data/response_processors/table/percentile.ts rename to src/plugins/vis_types/timeseries/server/lib/vis_data/response_processors/table/percentile.ts diff --git a/src/plugins/vis_type_timeseries/server/lib/vis_data/response_processors/table/percentile_rank.ts b/src/plugins/vis_types/timeseries/server/lib/vis_data/response_processors/table/percentile_rank.ts similarity index 100% rename from src/plugins/vis_type_timeseries/server/lib/vis_data/response_processors/table/percentile_rank.ts rename to src/plugins/vis_types/timeseries/server/lib/vis_data/response_processors/table/percentile_rank.ts diff --git a/src/plugins/vis_type_timeseries/server/lib/vis_data/response_processors/table/series_agg.ts b/src/plugins/vis_types/timeseries/server/lib/vis_data/response_processors/table/series_agg.ts similarity index 100% rename from src/plugins/vis_type_timeseries/server/lib/vis_data/response_processors/table/series_agg.ts rename to src/plugins/vis_types/timeseries/server/lib/vis_data/response_processors/table/series_agg.ts diff --git a/src/plugins/vis_type_timeseries/server/lib/vis_data/response_processors/table/std_metric.ts b/src/plugins/vis_types/timeseries/server/lib/vis_data/response_processors/table/std_metric.ts similarity index 100% rename from src/plugins/vis_type_timeseries/server/lib/vis_data/response_processors/table/std_metric.ts rename to src/plugins/vis_types/timeseries/server/lib/vis_data/response_processors/table/std_metric.ts diff --git a/src/plugins/vis_type_timeseries/server/lib/vis_data/response_processors/table/std_sibling.ts b/src/plugins/vis_types/timeseries/server/lib/vis_data/response_processors/table/std_sibling.ts similarity index 100% rename from src/plugins/vis_type_timeseries/server/lib/vis_data/response_processors/table/std_sibling.ts rename to src/plugins/vis_types/timeseries/server/lib/vis_data/response_processors/table/std_sibling.ts diff --git a/src/plugins/vis_type_timeseries/server/lib/vis_data/response_processors/table/types.ts b/src/plugins/vis_types/timeseries/server/lib/vis_data/response_processors/table/types.ts similarity index 100% rename from src/plugins/vis_type_timeseries/server/lib/vis_data/response_processors/table/types.ts rename to src/plugins/vis_types/timeseries/server/lib/vis_data/response_processors/table/types.ts diff --git a/src/plugins/vis_type_timeseries/server/lib/vis_data/series/build_request_body.test.ts b/src/plugins/vis_types/timeseries/server/lib/vis_data/series/build_request_body.test.ts similarity index 100% rename from src/plugins/vis_type_timeseries/server/lib/vis_data/series/build_request_body.test.ts rename to src/plugins/vis_types/timeseries/server/lib/vis_data/series/build_request_body.test.ts diff --git a/src/plugins/vis_type_timeseries/server/lib/vis_data/series/build_request_body.ts b/src/plugins/vis_types/timeseries/server/lib/vis_data/series/build_request_body.ts similarity index 100% rename from src/plugins/vis_type_timeseries/server/lib/vis_data/series/build_request_body.ts rename to src/plugins/vis_types/timeseries/server/lib/vis_data/series/build_request_body.ts diff --git a/src/plugins/vis_type_timeseries/server/lib/vis_data/series/get_request_params.ts b/src/plugins/vis_types/timeseries/server/lib/vis_data/series/get_request_params.ts similarity index 100% rename from src/plugins/vis_type_timeseries/server/lib/vis_data/series/get_request_params.ts rename to src/plugins/vis_types/timeseries/server/lib/vis_data/series/get_request_params.ts diff --git a/src/plugins/vis_type_timeseries/server/lib/vis_data/series/handle_response_body.ts b/src/plugins/vis_types/timeseries/server/lib/vis_data/series/handle_response_body.ts similarity index 96% rename from src/plugins/vis_type_timeseries/server/lib/vis_data/series/handle_response_body.ts rename to src/plugins/vis_types/timeseries/server/lib/vis_data/series/handle_response_body.ts index 78e9f971a61dd..415844abeedaf 100644 --- a/src/plugins/vis_type_timeseries/server/lib/vis_data/series/handle_response_body.ts +++ b/src/plugins/vis_types/timeseries/server/lib/vis_data/series/handle_response_body.ts @@ -17,7 +17,7 @@ import { FieldsFetcherServices, } from '../../search_strategies/lib/fields_fetcher'; import { VisTypeTimeseriesVisDataRequest } from '../../../types'; -import type { FieldFormatsRegistry } from '../../../../../field_formats/common'; +import type { FieldFormatsRegistry } from '../../../../../../field_formats/common'; export function handleResponseBody( panel: Panel, diff --git a/src/plugins/vis_type_timeseries/server/lib/vis_data/table/build_request_body.ts b/src/plugins/vis_types/timeseries/server/lib/vis_data/table/build_request_body.ts similarity index 100% rename from src/plugins/vis_type_timeseries/server/lib/vis_data/table/build_request_body.ts rename to src/plugins/vis_types/timeseries/server/lib/vis_data/table/build_request_body.ts diff --git a/src/plugins/vis_type_timeseries/server/lib/vis_data/table/build_response_body.ts b/src/plugins/vis_types/timeseries/server/lib/vis_data/table/build_response_body.ts similarity index 100% rename from src/plugins/vis_type_timeseries/server/lib/vis_data/table/build_response_body.ts rename to src/plugins/vis_types/timeseries/server/lib/vis_data/table/build_response_body.ts diff --git a/src/plugins/vis_type_timeseries/server/lib/vis_data/table/process_bucket.test.ts b/src/plugins/vis_types/timeseries/server/lib/vis_data/table/process_bucket.test.ts similarity index 100% rename from src/plugins/vis_type_timeseries/server/lib/vis_data/table/process_bucket.test.ts rename to src/plugins/vis_types/timeseries/server/lib/vis_data/table/process_bucket.test.ts diff --git a/src/plugins/vis_type_timeseries/server/lib/vis_data/table/process_bucket.ts b/src/plugins/vis_types/timeseries/server/lib/vis_data/table/process_bucket.ts similarity index 100% rename from src/plugins/vis_type_timeseries/server/lib/vis_data/table/process_bucket.ts rename to src/plugins/vis_types/timeseries/server/lib/vis_data/table/process_bucket.ts diff --git a/src/plugins/vis_type_timeseries/server/plugin.ts b/src/plugins/vis_types/timeseries/server/plugin.ts similarity index 94% rename from src/plugins/vis_type_timeseries/server/plugin.ts rename to src/plugins/vis_types/timeseries/server/plugin.ts index d2ecb07c0273d..5347d9ab7bfbc 100644 --- a/src/plugins/vis_type_timeseries/server/plugin.ts +++ b/src/plugins/vis_types/timeseries/server/plugin.ts @@ -20,9 +20,9 @@ import { Server } from '@hapi/hapi'; import { first, map } from 'rxjs/operators'; import { VisTypeTimeseriesConfig } from './config'; import { getVisData } from './lib/get_vis_data'; -import { UsageCollectionSetup } from '../../usage_collection/server'; -import { PluginStart } from '../../data/server'; -import { IndexPatternsService } from '../../data/common'; +import { UsageCollectionSetup } from '../../../usage_collection/server'; +import { PluginStart } from '../../../data/server'; +import { IndexPatternsService } from '../../../data/common'; import { visDataRoutes } from './routes/vis'; import { fieldsRoutes } from './routes/fields'; import { getUiSettings } from './ui_settings'; @@ -30,7 +30,7 @@ import type { VisTypeTimeseriesRequestHandlerContext, VisTypeTimeseriesVisDataRequest, } from './types'; -import type { FieldFormatsRegistry } from '../../field_formats/common'; +import type { FieldFormatsRegistry } from '../../../field_formats/common'; import { SearchStrategyRegistry, diff --git a/src/plugins/vis_type_timeseries/server/routes/fields.ts b/src/plugins/vis_types/timeseries/server/routes/fields.ts similarity index 100% rename from src/plugins/vis_type_timeseries/server/routes/fields.ts rename to src/plugins/vis_types/timeseries/server/routes/fields.ts diff --git a/src/plugins/vis_type_timeseries/server/routes/vis.ts b/src/plugins/vis_types/timeseries/server/routes/vis.ts similarity index 100% rename from src/plugins/vis_type_timeseries/server/routes/vis.ts rename to src/plugins/vis_types/timeseries/server/routes/vis.ts diff --git a/src/plugins/vis_type_timeseries/server/types.ts b/src/plugins/vis_types/timeseries/server/types.ts similarity index 95% rename from src/plugins/vis_type_timeseries/server/types.ts rename to src/plugins/vis_types/timeseries/server/types.ts index 40ced72933012..ab01f09c75f1e 100644 --- a/src/plugins/vis_type_timeseries/server/types.ts +++ b/src/plugins/vis_types/timeseries/server/types.ts @@ -10,8 +10,8 @@ import { Observable } from 'rxjs'; import { EsQueryConfig } from '@kbn/es-query'; import { SharedGlobalConfig } from 'kibana/server'; import type { IRouter, IUiSettingsClient, KibanaRequest } from 'src/core/server'; -import type { DataRequestHandlerContext, IndexPatternsService } from '../../data/server'; -import type { FieldFormatsRegistry } from '../../field_formats/common'; +import type { DataRequestHandlerContext, IndexPatternsService } from '../../../data/server'; +import type { FieldFormatsRegistry } from '../../../field_formats/common'; import type { Series, VisPayload } from '../common/types'; import type { SearchStrategyRegistry } from './lib/search_strategies'; import type { CachedIndexPatternFetcher } from './lib/search_strategies/lib/cached_index_pattern_fetcher'; diff --git a/src/plugins/vis_type_timeseries/server/ui_settings.ts b/src/plugins/vis_types/timeseries/server/ui_settings.ts similarity index 100% rename from src/plugins/vis_type_timeseries/server/ui_settings.ts rename to src/plugins/vis_types/timeseries/server/ui_settings.ts diff --git a/src/plugins/vis_type_timeseries/server/usage_collector/get_usage_collector.mock.ts b/src/plugins/vis_types/timeseries/server/usage_collector/get_usage_collector.mock.ts similarity index 100% rename from src/plugins/vis_type_timeseries/server/usage_collector/get_usage_collector.mock.ts rename to src/plugins/vis_types/timeseries/server/usage_collector/get_usage_collector.mock.ts diff --git a/src/plugins/vis_type_timeseries/server/usage_collector/get_usage_collector.test.ts b/src/plugins/vis_types/timeseries/server/usage_collector/get_usage_collector.test.ts similarity index 95% rename from src/plugins/vis_type_timeseries/server/usage_collector/get_usage_collector.test.ts rename to src/plugins/vis_types/timeseries/server/usage_collector/get_usage_collector.test.ts index 91daf09121f18..aac6d879f48fd 100644 --- a/src/plugins/vis_type_timeseries/server/usage_collector/get_usage_collector.test.ts +++ b/src/plugins/vis_types/timeseries/server/usage_collector/get_usage_collector.test.ts @@ -7,8 +7,11 @@ */ import { getStats } from './get_usage_collector'; -import { createCollectorFetchContextMock } from '../../../usage_collection/server/mocks'; -import type { SavedObjectsClientContract, SavedObjectsFindResponse } from '../../../../core/server'; +import { createCollectorFetchContextMock } from '../../../../usage_collection/server/mocks'; +import type { + SavedObjectsClientContract, + SavedObjectsFindResponse, +} from '../../../../../core/server'; import { TIME_RANGE_DATA_MODES } from '../../common/enums'; const mockedSavedObject = { diff --git a/src/plugins/vis_type_timeseries/server/usage_collector/get_usage_collector.ts b/src/plugins/vis_types/timeseries/server/usage_collector/get_usage_collector.ts similarity index 93% rename from src/plugins/vis_type_timeseries/server/usage_collector/get_usage_collector.ts rename to src/plugins/vis_types/timeseries/server/usage_collector/get_usage_collector.ts index a8d64afaf8cee..8309d51a9d56d 100644 --- a/src/plugins/vis_type_timeseries/server/usage_collector/get_usage_collector.ts +++ b/src/plugins/vis_types/timeseries/server/usage_collector/get_usage_collector.ts @@ -7,14 +7,14 @@ */ import { TIME_RANGE_DATA_MODES } from '../../common/enums'; -import { findByValueEmbeddables } from '../../../dashboard/server'; +import { findByValueEmbeddables } from '../../../../dashboard/server'; import type { SavedObjectsClientContract, ISavedObjectsRepository, SavedObjectsFindResult, -} from '../../../../core/server'; -import type { SavedVisState } from '../../../visualizations/common'; +} from '../../../../../core/server'; +import type { SavedVisState } from '../../../../visualizations/common'; export interface TimeseriesUsage { timeseries_use_last_value_mode_total: number; diff --git a/src/plugins/vis_type_timeseries/server/usage_collector/index.ts b/src/plugins/vis_types/timeseries/server/usage_collector/index.ts similarity index 100% rename from src/plugins/vis_type_timeseries/server/usage_collector/index.ts rename to src/plugins/vis_types/timeseries/server/usage_collector/index.ts diff --git a/src/plugins/vis_type_timeseries/server/usage_collector/register_timeseries_collector.test.ts b/src/plugins/vis_types/timeseries/server/usage_collector/register_timeseries_collector.test.ts similarity index 92% rename from src/plugins/vis_type_timeseries/server/usage_collector/register_timeseries_collector.test.ts rename to src/plugins/vis_types/timeseries/server/usage_collector/register_timeseries_collector.test.ts index 0dfe5ae5f9351..26a74821fe5ae 100644 --- a/src/plugins/vis_type_timeseries/server/usage_collector/register_timeseries_collector.test.ts +++ b/src/plugins/vis_types/timeseries/server/usage_collector/register_timeseries_collector.test.ts @@ -7,8 +7,8 @@ */ import { mockStats, mockGetStats } from './get_usage_collector.mock'; -import { createUsageCollectionSetupMock } from '../../../usage_collection/server/mocks'; -import { createCollectorFetchContextMock } from '../../../usage_collection/server/mocks'; +import { createUsageCollectionSetupMock } from '../../../../usage_collection/server/mocks'; +import { createCollectorFetchContextMock } from '../../../../usage_collection/server/mocks'; import { registerTimeseriesUsageCollector } from './register_timeseries_collector'; describe('registerTimeseriesUsageCollector', () => { diff --git a/src/plugins/vis_type_timeseries/server/usage_collector/register_timeseries_collector.ts b/src/plugins/vis_types/timeseries/server/usage_collector/register_timeseries_collector.ts similarity index 92% rename from src/plugins/vis_type_timeseries/server/usage_collector/register_timeseries_collector.ts rename to src/plugins/vis_types/timeseries/server/usage_collector/register_timeseries_collector.ts index 7e9294f03ba1c..6fccd7ef30171 100644 --- a/src/plugins/vis_type_timeseries/server/usage_collector/register_timeseries_collector.ts +++ b/src/plugins/vis_types/timeseries/server/usage_collector/register_timeseries_collector.ts @@ -7,7 +7,7 @@ */ import { getStats, TimeseriesUsage } from './get_usage_collector'; -import type { UsageCollectionSetup } from '../../../usage_collection/server'; +import type { UsageCollectionSetup } from '../../../../usage_collection/server'; export function registerTimeseriesUsageCollector(collectorSet: UsageCollectionSetup) { const collector = collectorSet.makeUsageCollector({ diff --git a/src/plugins/vis_types/timeseries/tsconfig.json b/src/plugins/vis_types/timeseries/tsconfig.json new file mode 100644 index 0000000000000..f336a467e0c81 --- /dev/null +++ b/src/plugins/vis_types/timeseries/tsconfig.json @@ -0,0 +1,26 @@ +{ + "extends": "../../../../tsconfig.base.json", + "compilerOptions": { + "outDir": "./target/types", + "emitDeclarationOnly": true, + "declaration": true, + "declarationMap": true + }, + "include": [ + "common/**/*", + "public/**/*", + "server/**/*", + "*.ts" + ], + "references": [ + { "path": "../../../core/tsconfig.json" }, + { "path": "../../charts/tsconfig.json" }, + { "path": "../../data/tsconfig.json" }, + { "path": "../../expressions/tsconfig.json" }, + { "path": "../../visualizations/tsconfig.json" }, + { "path": "../../visualize/tsconfig.json" }, + { "path": "../../kibana_utils/tsconfig.json" }, + { "path": "../../kibana_react/tsconfig.json" }, + { "path": "../../usage_collection/tsconfig.json" }, + ] +} diff --git a/src/plugins/vis_types/vega/public/data_model/search_api.ts b/src/plugins/vis_types/vega/public/data_model/search_api.ts index e00cf647930a8..11302ad65d56b 100644 --- a/src/plugins/vis_types/vega/public/data_model/search_api.ts +++ b/src/plugins/vis_types/vega/public/data_model/search_api.ts @@ -95,7 +95,16 @@ export class SearchAPI { } ) .pipe( - tap((data) => this.inspectSearchResult(data, requestResponders[requestId])), + tap( + (data) => this.inspectSearchResult(data, requestResponders[requestId]), + (err) => + this.inspectSearchResult( + { + rawResponse: err?.err, + }, + requestResponders[requestId] + ) + ), map((data) => ({ name: requestId, rawResponse: data.rawResponse, diff --git a/src/plugins/vis_types/vega/public/data_model/vega_parser.test.js b/src/plugins/vis_types/vega/public/data_model/vega_parser.test.js index be356ea4e05ce..cfeed174307ac 100644 --- a/src/plugins/vis_types/vega/public/data_model/vega_parser.test.js +++ b/src/plugins/vis_types/vega/public/data_model/vega_parser.test.js @@ -5,8 +5,8 @@ * in compliance with, at your election, the Elastic License 2.0 or the Server * Side Public License, v 1. */ - import { cloneDeep } from 'lodash'; +import 'jest-canvas-mock'; import { euiThemeVars } from '@kbn/ui-shared-deps-src/theme'; import { VegaParser } from './vega_parser'; import { bypassExternalUrlCheck } from '../vega_view/vega_base_view'; diff --git a/src/plugins/vis_types/vega/public/vega_view/vega_map_view/map_service_settings.ts b/src/plugins/vis_types/vega/public/vega_view/vega_map_view/map_service_settings.ts index 3399d0628ad65..9772e693358b6 100644 --- a/src/plugins/vis_types/vega/public/vega_view/vega_map_view/map_service_settings.ts +++ b/src/plugins/vis_types/vega/public/vega_view/vega_map_view/map_service_settings.ts @@ -66,6 +66,11 @@ export class MapServiceSettings { tileApiUrl: this.config.emsTileApiUrl, landingPageUrl: this.config.emsLandingPageUrl, }); + + // Allow zooms > 10 for Vega Maps + // any kibana user, regardless of distribution, should get all zoom levels + // use `sspl` license to indicate this + this.emsClient.addQueryParams({ license: 'sspl' }); } public async getTmsService(tmsTileLayer: string) { diff --git a/src/plugins/vis_types/vega/public/vega_view/vega_map_view/view.test.ts b/src/plugins/vis_types/vega/public/vega_view/vega_map_view/view.test.ts index d3d0b6cb0411e..8ca2b2bd26eed 100644 --- a/src/plugins/vis_types/vega/public/vega_view/vega_map_view/view.test.ts +++ b/src/plugins/vis_types/vega/public/vega_view/vega_map_view/view.test.ts @@ -124,6 +124,8 @@ describe('vega_map_view/view', () => { } as unknown as VegaViewParams); } + let mockedConsoleLog: jest.SpyInstance; + beforeEach(() => { vegaParser = new VegaParser( JSON.stringify(vegaMap), @@ -137,10 +139,13 @@ describe('vega_map_view/view', () => { {}, mockGetServiceSettings ); + mockedConsoleLog = jest.spyOn(console, 'log'); // mocked console.log to avoid messages in the console when running tests + mockedConsoleLog.mockImplementation(() => {}); // comment this line when console logging for debugging }); afterEach(() => { jest.clearAllMocks(); + mockedConsoleLog.mockRestore(); }); test('should be added TmsRasterLayer and do not use tmsService if mapStyle is "user_configured"', async () => { diff --git a/src/plugins/vis_types/vega/public/vega_view/vega_map_view/view.ts b/src/plugins/vis_types/vega/public/vega_view/vega_map_view/view.ts index cf5bf15d15051..777806d90d9a6 100644 --- a/src/plugins/vis_types/vega/public/vega_view/vega_map_view/view.ts +++ b/src/plugins/vis_types/vega/public/vega_view/vega_map_view/view.ts @@ -72,7 +72,7 @@ export class VegaMapView extends VegaBaseView { const { zoom, maxZoom, minZoom } = validateZoomSettings( this._parser.mapConfig, defaults, - this.onWarn + this.onWarn.bind(this) ); const { signals } = this._vegaStateRestorer.restore() || {}; diff --git a/src/plugins/vis_types/vega/public/vega_visualization.test.js b/src/plugins/vis_types/vega/public/vega_visualization.test.js index 05a88880822ca..dd76e2d470004 100644 --- a/src/plugins/vis_types/vega/public/vega_visualization.test.js +++ b/src/plugins/vis_types/vega/public/vega_visualization.test.js @@ -81,7 +81,11 @@ describe('VegaVisualizations', () => { mockWidth.mockRestore(); mockHeight.mockRestore(); }); + test('should show vegalite graph and update on resize (may fail in dev env)', async () => { + const mockedConsoleLog = jest.spyOn(console, 'log'); // mocked console.log to avoid messages in the console when running tests + mockedConsoleLog.mockImplementation(() => {}); // comment this line when console logging for debugging comment this line + let vegaVis; try { vegaVis = new VegaVisualization(domNode, jest.fn()); @@ -111,6 +115,8 @@ describe('VegaVisualizations', () => { } finally { vegaVis.destroy(); } + expect(console.log).toBeCalledTimes(2); + mockedConsoleLog.mockRestore(); }); test('should show vega graph (may fail in dev env)', async () => { @@ -130,7 +136,6 @@ describe('VegaVisualizations', () => { mockGetServiceSettings ); await vegaParser.parseAsync(); - await vegaVis.render(vegaParser); expect(domNode.innerHTML).toMatchSnapshot(); } finally { diff --git a/src/plugins/vis_types/xy/config.ts b/src/plugins/vis_types/xy/config.ts new file mode 100644 index 0000000000000..b831d26854c30 --- /dev/null +++ b/src/plugins/vis_types/xy/config.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 { schema, TypeOf } from '@kbn/config-schema'; + +export const configSchema = schema.object({ + enabled: schema.boolean({ defaultValue: true }), +}); + +export type ConfigSchema = TypeOf; diff --git a/src/plugins/vis_types/xy/kibana.json b/src/plugins/vis_types/xy/kibana.json index 1666a346e3482..1606af5944ad3 100644 --- a/src/plugins/vis_types/xy/kibana.json +++ b/src/plugins/vis_types/xy/kibana.json @@ -2,7 +2,7 @@ "id": "visTypeXy", "version": "kibana", "ui": true, - "server": false, + "server": true, "requiredPlugins": ["charts", "data", "expressions", "visualizations", "usageCollection"], "requiredBundles": ["kibanaUtils", "visDefaultEditor"], "extraPublicDirs": ["common/index"], diff --git a/src/plugins/vis_types/xy/server/index.ts b/src/plugins/vis_types/xy/server/index.ts new file mode 100644 index 0000000000000..9dfa405ee27b8 --- /dev/null +++ b/src/plugins/vis_types/xy/server/index.ts @@ -0,0 +1,17 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 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 { PluginConfigDescriptor } from 'src/core/server'; +import { configSchema, ConfigSchema } from '../config'; +import { VisTypeXYServerPlugin } from './plugin'; + +export const config: PluginConfigDescriptor = { + schema: configSchema, +}; + +export const plugin = () => new VisTypeXYServerPlugin(); diff --git a/src/plugins/data/common/data_views/lib/is_default.ts b/src/plugins/vis_types/xy/server/plugin.ts similarity index 65% rename from src/plugins/data/common/data_views/lib/is_default.ts rename to src/plugins/vis_types/xy/server/plugin.ts index 5a50d2862c58b..5cb0687cf1889 100644 --- a/src/plugins/data/common/data_views/lib/is_default.ts +++ b/src/plugins/vis_types/xy/server/plugin.ts @@ -6,9 +6,14 @@ * Side Public License, v 1. */ -import { IIndexPattern } from '../..'; +import { Plugin } from 'src/core/server'; -export const isDefault = (indexPattern: IIndexPattern) => { - // Default index patterns don't have `type` defined. - return !indexPattern.type; -}; +export class VisTypeXYServerPlugin implements Plugin { + public setup() { + return {}; + } + + public start() { + return {}; + } +} diff --git a/src/plugins/vis_types/xy/tsconfig.json b/src/plugins/vis_types/xy/tsconfig.json index f1f65b6218e82..ab3f3d1252ed8 100644 --- a/src/plugins/vis_types/xy/tsconfig.json +++ b/src/plugins/vis_types/xy/tsconfig.json @@ -9,7 +9,8 @@ "include": [ "common/**/*", "public/**/*", - "server/**/*" + "server/**/*", + "*.ts" ], "references": [ { "path": "../../../core/tsconfig.json" }, diff --git a/src/plugins/visualizations/public/embeddable/visualize_embeddable.ts b/src/plugins/visualizations/public/embeddable/visualize_embeddable.ts index 75809bd06ba4a..f5a7349b633eb 100644 --- a/src/plugins/visualizations/public/embeddable/visualize_embeddable.ts +++ b/src/plugins/visualizations/public/embeddable/visualize_embeddable.ts @@ -187,7 +187,11 @@ export class VisualizeEmbeddable if (!adapters) return; return this.deps.start().plugins.inspector.open(adapters, { - title: this.getTitle(), + title: + this.getTitle() || + i18n.translate('visualizations.embeddable.inspectorTitle', { + defaultMessage: 'Inspector', + }), }); }; diff --git a/src/plugins/visualize/public/application/components/visualize_no_match.tsx b/src/plugins/visualize/public/application/components/visualize_no_match.tsx index 3b735eb23671c..ad993af430086 100644 --- a/src/plugins/visualize/public/application/components/visualize_no_match.tsx +++ b/src/plugins/visualize/public/application/components/visualize_no_match.tsx @@ -24,7 +24,7 @@ export const VisualizeNoMatch = () => { services.restorePreviousUrl(); const { navigated } = services.urlForwarding.navigateToLegacyKibanaUrl( - services.history.location.pathname + services.history.location.pathname + services.history.location.search ); if (!navigated) { diff --git a/src/plugins/visualize/public/application/utils/get_table_columns.tsx b/src/plugins/visualize/public/application/utils/get_table_columns.tsx index 8c8ecaf9a448a..69383005deb07 100644 --- a/src/plugins/visualize/public/application/utils/get_table_columns.tsx +++ b/src/plugins/visualize/public/application/utils/get_table_columns.tsx @@ -8,7 +8,15 @@ import React from 'react'; import { METRIC_TYPE } from '@kbn/analytics'; -import { EuiBetaBadge, EuiButton, EuiEmptyPrompt, EuiIcon, EuiLink, EuiBadge } from '@elastic/eui'; +import { + EuiBetaBadge, + EuiButton, + EuiEmptyPrompt, + EuiIcon, + EuiLink, + EuiBadge, + EuiBasicTableColumn, +} from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import { ApplicationStart } from 'kibana/public'; @@ -86,61 +94,62 @@ export const getTableColumns = ( application: ApplicationStart, kbnUrlStateStorage: IKbnUrlStateStorage, taggingApi?: SavedObjectsTaggingApi -) => [ - { - field: 'title', - name: i18n.translate('visualize.listing.table.titleColumnName', { - defaultMessage: 'Title', - }), - sortable: true, - render: (field: string, { editApp, editUrl, title, error, type }: VisualizationListItem) => - // In case an error occurs i.e. the vis has wrong type, we render the vis but without the link - !error ? ( - - {/* eslint-disable-next-line @elastic/eui/href-or-on-click */} - { - doTelemetryForAddEvent(typeof type === 'string' ? type : type?.name); - }} - data-test-subj={`visListingTitleLink-${title.split(' ').join('-')}`} - > - {field} - - - ) : ( - field - ), - }, - { - field: 'typeTitle', - name: i18n.translate('visualize.listing.table.typeColumnName', { - defaultMessage: 'Type', - }), - sortable: true, - render: (field: string, record: VisualizationListItem) => - !record.error ? ( - - {renderItemTypeIcon(record)} - {record.typeTitle} - {getBadge(record)} - - ) : ( - - {record.error} - - ), - }, - { - field: 'description', - name: i18n.translate('visualize.listing.table.descriptionColumnName', { - defaultMessage: 'Description', - }), - sortable: true, - render: (field: string, record: VisualizationListItem) => {record.description}, - }, - ...(taggingApi ? [taggingApi.ui.getTableColumnDefinition()] : []), -]; +) => + [ + { + field: 'title', + name: i18n.translate('visualize.listing.table.titleColumnName', { + defaultMessage: 'Title', + }), + sortable: true, + render: (field: string, { editApp, editUrl, title, error, type }: VisualizationListItem) => + // In case an error occurs i.e. the vis has wrong type, we render the vis but without the link + !error ? ( + + {/* eslint-disable-next-line @elastic/eui/href-or-on-click */} + { + doTelemetryForAddEvent(typeof type === 'string' ? type : type?.name); + }} + data-test-subj={`visListingTitleLink-${title.split(' ').join('-')}`} + > + {field} + + + ) : ( + field + ), + }, + { + field: 'typeTitle', + name: i18n.translate('visualize.listing.table.typeColumnName', { + defaultMessage: 'Type', + }), + sortable: true, + render: (field: string, record: VisualizationListItem) => + !record.error ? ( + + {renderItemTypeIcon(record)} + {record.typeTitle} + {getBadge(record)} + + ) : ( + + {record.error} + + ), + }, + { + field: 'description', + name: i18n.translate('visualize.listing.table.descriptionColumnName', { + defaultMessage: 'Description', + }), + sortable: true, + render: (field: string, record: VisualizationListItem) => {record.description}, + }, + ...(taggingApi ? [taggingApi.ui.getTableColumnDefinition()] : []), + ] as unknown as Array>>; export const getNoItemsMessage = (createItem: () => void) => ( { + describe.skip('Dashboard', () => { const dashboardName = 'Dashboard Listing A11y'; const clonedDashboardName = 'Dashboard Listing A11y Copy'; diff --git a/test/functional/apps/dashboard/bwc_shared_urls.ts b/test/functional/apps/dashboard/bwc_shared_urls.ts index d40cf03327fd3..569cd8e2a67d5 100644 --- a/test/functional/apps/dashboard/bwc_shared_urls.ts +++ b/test/functional/apps/dashboard/bwc_shared_urls.ts @@ -86,6 +86,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { }); describe('6.0 urls', () => { + let savedDashboardId: string; + it('loads an unsaved dashboard', async function () { const url = `${kibanaLegacyBaseUrl}#/dashboard?${urlQuery}`; log.debug(`Navigating to ${url}`); @@ -106,8 +108,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { storeTimeWithDashboard: true, }); - const id = await PageObjects.dashboard.getDashboardIdFromCurrentUrl(); - const url = `${kibanaLegacyBaseUrl}#/dashboard/${id}`; + savedDashboardId = await PageObjects.dashboard.getDashboardIdFromCurrentUrl(); + const url = `${kibanaLegacyBaseUrl}#/dashboard/${savedDashboardId}`; log.debug(`Navigating to ${url}`); await browser.get(url, true); await PageObjects.header.waitUntilLoadingHasFinished(); @@ -121,6 +123,22 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await dashboardExpect.selectedLegendColorCount('#F9D9F9', 5); }); + it('loads a saved dashboard with query via dashboard_no_match', async function () { + await PageObjects.dashboard.gotoDashboardLandingPage(); + const currentUrl = await browser.getCurrentUrl(); + const dashboardBaseUrl = currentUrl.substring(0, currentUrl.indexOf('/app/dashboards')); + const url = `${dashboardBaseUrl}/app/dashboards#/dashboard/${savedDashboardId}?_a=(query:(language:kuery,query:'boop'))`; + log.debug(`Navigating to ${url}`); + await browser.get(url); + await PageObjects.header.waitUntilLoadingHasFinished(); + + const query = await queryBar.getQueryString(); + expect(query).to.equal('boop'); + + await dashboardExpect.panelCount(2); + await PageObjects.dashboard.waitForRenderComplete(); + }); + it('uiState in url takes precedence over saved dashboard state', async function () { const id = await PageObjects.dashboard.getDashboardIdFromCurrentUrl(); const updatedQuery = urlQuery.replace(/F9D9F9/g, '000000'); diff --git a/test/functional/apps/dashboard/dashboard_unsaved_state.ts b/test/functional/apps/dashboard/dashboard_unsaved_state.ts index 6b71dd34b76f8..c2da82a96cd0c 100644 --- a/test/functional/apps/dashboard/dashboard_unsaved_state.ts +++ b/test/functional/apps/dashboard/dashboard_unsaved_state.ts @@ -12,6 +12,9 @@ import { FtrProviderContext } from '../../ftr_provider_context'; export default function ({ getService, getPageObjects }: FtrProviderContext) { const PageObjects = getPageObjects(['dashboard', 'header', 'visualize', 'settings', 'common']); + const browser = getService('browser'); + const queryBar = getService('queryBar'); + const filterBar = getService('filterBar'); const esArchiver = getService('esArchiver'); const testSubjects = getService('testSubjects'); const kibanaServer = getService('kibanaServer'); @@ -19,9 +22,10 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { let originalPanelCount = 0; let unsavedPanelCount = 0; + const testQuery = 'Test Query'; - // FLAKY: https://github.com/elastic/kibana/issues/91191 - describe.skip('dashboard unsaved panels', () => { + // FLAKY https://github.com/elastic/kibana/issues/112812 + describe.skip('dashboard unsaved state', () => { before(async () => { await esArchiver.load('test/functional/fixtures/es_archiver/dashboard/current/kibana'); await kibanaServer.uiSettings.replace({ @@ -31,79 +35,123 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await PageObjects.dashboard.preserveCrossAppState(); await PageObjects.dashboard.loadSavedDashboard('few panels'); await PageObjects.header.waitUntilLoadingHasFinished(); + await PageObjects.dashboard.waitForRenderComplete(); originalPanelCount = await PageObjects.dashboard.getPanelCount(); }); - it('does not show unsaved changes badge when there are no unsaved changes', async () => { - await testSubjects.missingOrFail('dashboardUnsavedChangesBadge'); - }); + describe('view mode state', () => { + before(async () => { + await queryBar.setQuery(testQuery); + await filterBar.addFilter('bytes', 'exists'); + await queryBar.submitQuery(); + }); - it('shows the unsaved changes badge after adding panels', async () => { - await PageObjects.dashboard.switchToEditMode(); - // add an area chart by value - await dashboardAddPanel.clickEditorMenuButton(); - await dashboardAddPanel.clickAggBasedVisualizations(); - await PageObjects.visualize.clickAreaChart(); - await PageObjects.visualize.clickNewSearch(); - await PageObjects.visualize.saveVisualizationAndReturn(); + const validateQueryAndFilter = async () => { + const query = await queryBar.getQueryString(); + expect(query).to.eql(testQuery); + const filterCount = await filterBar.getFilterCount(); + expect(filterCount).to.eql(1); + }; + + it('persists after navigating to the listing page and back', async () => { + await PageObjects.header.waitUntilLoadingHasFinished(); + await PageObjects.dashboard.gotoDashboardLandingPage(); + await PageObjects.header.waitUntilLoadingHasFinished(); + await PageObjects.dashboard.loadSavedDashboard('few panels'); + await PageObjects.dashboard.waitForRenderComplete(); + await validateQueryAndFilter(); + }); - // add a metric by reference - await dashboardAddPanel.addVisualization('Rendering-Test: metric'); + it('persists after navigating to Visualize and back', async () => { + await PageObjects.header.waitUntilLoadingHasFinished(); + await PageObjects.visualize.gotoVisualizationLandingPage(); + await PageObjects.header.waitUntilLoadingHasFinished(); + await PageObjects.common.navigateToApp('dashboards'); + await PageObjects.dashboard.loadSavedDashboard('few panels'); + await PageObjects.dashboard.waitForRenderComplete(); + await validateQueryAndFilter(); + }); - await PageObjects.header.waitUntilLoadingHasFinished(); - await testSubjects.existOrFail('dashboardUnsavedChangesBadge'); - }); + it('persists after a hard refresh', async () => { + await browser.refresh(); + const alert = await browser.getAlert(); + await alert?.accept(); + await PageObjects.dashboard.waitForRenderComplete(); + await validateQueryAndFilter(); + }); - it('has correct number of panels', async () => { - unsavedPanelCount = await PageObjects.dashboard.getPanelCount(); - expect(unsavedPanelCount).to.eql(originalPanelCount + 2); + after(async () => { + // discard changes made in view mode + await PageObjects.dashboard.switchToEditMode(); + await PageObjects.dashboard.clickCancelOutOfEditMode(); + }); }); - it('retains unsaved panel count after navigating to listing page and back', async () => { - await PageObjects.header.waitUntilLoadingHasFinished(); - await PageObjects.dashboard.gotoDashboardLandingPage(); - await PageObjects.header.waitUntilLoadingHasFinished(); - await PageObjects.dashboard.loadSavedDashboard('few panels'); - await PageObjects.dashboard.switchToEditMode(); - const currentPanelCount = await PageObjects.dashboard.getPanelCount(); - expect(currentPanelCount).to.eql(unsavedPanelCount); - }); + describe('edit mode state', () => { + const addPanels = async () => { + // add an area chart by value + await dashboardAddPanel.clickEditorMenuButton(); + await dashboardAddPanel.clickAggBasedVisualizations(); + await PageObjects.visualize.clickAreaChart(); + await PageObjects.visualize.clickNewSearch(); + await PageObjects.visualize.saveVisualizationAndReturn(); + + // add a metric by reference + await dashboardAddPanel.addVisualization('Rendering-Test: metric'); + }; + + it('does not show unsaved changes badge when there are no unsaved changes', async () => { + await testSubjects.missingOrFail('dashboardUnsavedChangesBadge'); + }); - it('retains unsaved panel count after navigating to another app and back', async () => { - await PageObjects.header.waitUntilLoadingHasFinished(); - await PageObjects.visualize.gotoVisualizationLandingPage(); - await PageObjects.header.waitUntilLoadingHasFinished(); - await PageObjects.common.navigateToApp('dashboards'); - await PageObjects.dashboard.loadSavedDashboard('few panels'); - await PageObjects.dashboard.switchToEditMode(); - const currentPanelCount = await PageObjects.dashboard.getPanelCount(); - expect(currentPanelCount).to.eql(unsavedPanelCount); - }); + it('shows the unsaved changes badge after adding panels', async () => { + await PageObjects.dashboard.switchToEditMode(); + await addPanels(); + await PageObjects.header.waitUntilLoadingHasFinished(); + await testSubjects.existOrFail('dashboardUnsavedChangesBadge'); + }); - it('resets to original panel count upon entering view mode', async () => { - await PageObjects.header.waitUntilLoadingHasFinished(); - await PageObjects.dashboard.clickCancelOutOfEditMode(); - await PageObjects.header.waitUntilLoadingHasFinished(); - const currentPanelCount = await PageObjects.dashboard.getPanelCount(); - expect(currentPanelCount).to.eql(originalPanelCount); - }); + it('has correct number of panels', async () => { + unsavedPanelCount = await PageObjects.dashboard.getPanelCount(); + expect(unsavedPanelCount).to.eql(originalPanelCount + 2); + }); - it('shows unsaved changes badge in view mode if changes have not been discarded', async () => { - await testSubjects.existOrFail('dashboardUnsavedChangesBadge'); - }); + it('retains unsaved panel count after navigating to listing page and back', async () => { + await PageObjects.header.waitUntilLoadingHasFinished(); + await PageObjects.dashboard.gotoDashboardLandingPage(); + await PageObjects.header.waitUntilLoadingHasFinished(); + await PageObjects.dashboard.loadSavedDashboard('few panels'); + const currentPanelCount = await PageObjects.dashboard.getPanelCount(); + expect(currentPanelCount).to.eql(unsavedPanelCount); + }); - it('retains unsaved panel count after returning to edit mode', async () => { - await PageObjects.header.waitUntilLoadingHasFinished(); - await PageObjects.dashboard.switchToEditMode(); - await PageObjects.header.waitUntilLoadingHasFinished(); - const currentPanelCount = await PageObjects.dashboard.getPanelCount(); - expect(currentPanelCount).to.eql(unsavedPanelCount); - }); + it('retains unsaved panel count after navigating to another app and back', async () => { + await PageObjects.header.waitUntilLoadingHasFinished(); + await PageObjects.visualize.gotoVisualizationLandingPage(); + await PageObjects.header.waitUntilLoadingHasFinished(); + await PageObjects.common.navigateToApp('dashboards'); + await PageObjects.dashboard.loadSavedDashboard('few panels'); + const currentPanelCount = await PageObjects.dashboard.getPanelCount(); + expect(currentPanelCount).to.eql(unsavedPanelCount); + }); - it('does not show unsaved changes badge after saving', async () => { - await PageObjects.dashboard.saveDashboard('Unsaved State Test'); - await PageObjects.header.waitUntilLoadingHasFinished(); - await testSubjects.missingOrFail('dashboardUnsavedChangesBadge'); + it('resets to original panel count after discarding changes', async () => { + await PageObjects.header.waitUntilLoadingHasFinished(); + await PageObjects.dashboard.clickCancelOutOfEditMode(); + await PageObjects.header.waitUntilLoadingHasFinished(); + const currentPanelCount = await PageObjects.dashboard.getPanelCount(); + expect(currentPanelCount).to.eql(originalPanelCount); + expect(PageObjects.dashboard.getIsInViewMode()).to.eql(true); + }); + + it('does not show unsaved changes badge after saving', async () => { + await PageObjects.dashboard.switchToEditMode(); + await addPanels(); + await PageObjects.header.waitUntilLoadingHasFinished(); + await PageObjects.dashboard.saveDashboard('Unsaved State Test'); + await PageObjects.header.waitUntilLoadingHasFinished(); + await testSubjects.missingOrFail('dashboardUnsavedChangesBadge'); + }); }); }); } diff --git a/test/functional/apps/discover/_runtime_fields_editor.ts b/test/functional/apps/discover/_runtime_fields_editor.ts index 642743d3a0377..4757807cb7ac1 100644 --- a/test/functional/apps/discover/_runtime_fields_editor.ts +++ b/test/functional/apps/discover/_runtime_fields_editor.ts @@ -31,7 +31,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await fieldEditor.save(); }; - describe('discover integration with runtime fields editor', function describeIndexTests() { + // Failing: https://github.com/elastic/kibana/issues/111922 + describe.skip('discover integration with runtime fields editor', function describeIndexTests() { before(async function () { await security.testUser.setRoles(['kibana_admin', 'test_logstash_reader']); await esArchiver.loadIfNeeded('test/functional/fixtures/es_archiver/logstash_functional'); diff --git a/test/functional/apps/visualize/_timelion.ts b/test/functional/apps/visualize/_timelion.ts index ea8cb8b13ba49..bb85b6821df31 100644 --- a/test/functional/apps/visualize/_timelion.ts +++ b/test/functional/apps/visualize/_timelion.ts @@ -18,6 +18,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { 'timelion', 'common', ]); + const security = getService('security'); const monacoEditor = getService('monacoEditor'); const kibanaServer = getService('kibanaServer'); const elasticChart = getService('elasticChart'); @@ -26,6 +27,11 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { describe('Timelion visualization', () => { before(async () => { + await security.testUser.setRoles([ + 'kibana_admin', + 'long_window_logstash', + 'test_logstash_reader', + ]); await kibanaServer.uiSettings.update({ 'timelion:legacyChartsLibrary': false, }); @@ -277,17 +283,20 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { it('should show field suggestions for split argument when index pattern set', async () => { await monacoEditor.setCodeEditorValue(''); await monacoEditor.typeCodeEditorValue( - '.es(index=logstash-*, timefield=@timestamp ,split=', + '.es(index=logstash-*, timefield=@timestamp, split=', 'timelionCodeEditor' ); + // wait for split fields to load + await common.sleep(300); const suggestions = await timelion.getSuggestionItemsText(); + expect(suggestions.length).not.to.eql(0); expect(suggestions[0].includes('@message.raw')).to.eql(true); }); it('should show field suggestions for metric argument when index pattern set', async () => { await monacoEditor.typeCodeEditorValue( - '.es(index=logstash-*, timefield=@timestamp ,metric=avg:', + '.es(index=logstash-*, timefield=@timestamp, metric=avg:', 'timelionCodeEditor' ); const suggestions = await timelion.getSuggestionItemsText(); diff --git a/test/functional/apps/visualize/_tsvb_time_series.ts b/test/functional/apps/visualize/_tsvb_time_series.ts index 21bee2d16442f..968c979dfa89d 100644 --- a/test/functional/apps/visualize/_tsvb_time_series.ts +++ b/test/functional/apps/visualize/_tsvb_time_series.ts @@ -17,6 +17,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { 'timeToVisualize', 'dashboard', ]); + const security = getService('security'); const testSubjects = getService('testSubjects'); const retry = getService('retry'); const filterBar = getService('filterBar'); @@ -27,6 +28,11 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { describe('visual builder', function describeIndexTests() { before(async () => { + await security.testUser.setRoles([ + 'kibana_admin', + 'long_window_logstash', + 'test_logstash_reader', + ]); await visualize.initTests(); }); beforeEach(async () => { diff --git a/test/functional/fixtures/es_archiver/saved_objects_management/visible_in_management/data.json b/test/functional/fixtures/es_archiver/saved_objects_management/visible_in_management/data.json new file mode 100644 index 0000000000000..236d7bbff7215 --- /dev/null +++ b/test/functional/fixtures/es_archiver/saved_objects_management/visible_in_management/data.json @@ -0,0 +1,20 @@ +{ + "type": "doc", + "value": { + "id": "test-not-visible-in-management:vim-1", + "index": ".kibana", + "source": { + "coreMigrationVersion": "7.14.0", + "references": [ + ], + "test-not-visible-in-management": { + "enabled": true, + "title": "vim-1" + }, + "type": "test-not-visible-in-management", + "updated_at": "2018-12-21T00:43:07.096Z" + }, + "type": "_doc" + } +} + diff --git a/test/functional/fixtures/es_archiver/saved_objects_management/visible_in_management/mappings.json b/test/functional/fixtures/es_archiver/saved_objects_management/visible_in_management/mappings.json new file mode 100644 index 0000000000000..3c0a975a30be0 --- /dev/null +++ b/test/functional/fixtures/es_archiver/saved_objects_management/visible_in_management/mappings.json @@ -0,0 +1,477 @@ +{ + "type": "index", + "value": { + "aliases": { + ".kibana_$KIBANA_PACKAGE_VERSION": {}, + ".kibana": {} + }, + "index": ".kibana_$KIBANA_PACKAGE_VERSION_001", + "mappings": { + "_meta": { + "migrationMappingPropertyHashes": { + "application_usage_daily": "43b8830d5d0df85a6823d290885fc9fd", + "application_usage_totals": "3d1b76c39bfb2cc8296b024d73854724", + "application_usage_transactional": "3d1b76c39bfb2cc8296b024d73854724", + "config": "c63748b75f39d0c54de12d12c1ccbc20", + "core-usage-stats": "3d1b76c39bfb2cc8296b024d73854724", + "coreMigrationVersion": "2f4316de49999235636386fe51dc06c1", + "dashboard": "40554caf09725935e2c02e02563a2d07", + "index-pattern": "45915a1ad866812242df474eb0479052", + "kql-telemetry": "d12a98a6f19a2d273696597547e064ee", + "legacy-url-alias": "6155300fd11a00e23d5cbaa39f0fce0a", + "migrationVersion": "4a1746014a75ade3a714e1db5763276f", + "namespace": "2f4316de49999235636386fe51dc06c1", + "namespaces": "2f4316de49999235636386fe51dc06c1", + "originId": "2f4316de49999235636386fe51dc06c1", + "query": "11aaeb7f5f7fa5bb43f25e18ce26e7d9", + "references": "7997cf5a56cc02bdc9c93361bde732b0", + "sample-data-telemetry": "7d3cfeb915303c9641c59681967ffeb4", + "search": "db2c00e39b36f40930a3b9fc71c823e1", + "search-telemetry": "3d1b76c39bfb2cc8296b024d73854724", + "telemetry": "36a616f7026dfa617d6655df850fe16d", + "type": "2f4316de49999235636386fe51dc06c1", + "ui-counter": "0d409297dc5ebe1e3a1da691c6ee32e3", + "ui-metric": "0d409297dc5ebe1e3a1da691c6ee32e3", + "updated_at": "00da57df13e94e9d98437d13ace4bfe0", + "url": "c7f66a0df8b1b52f17c28c4adb111105", + "usage-counters": "8cc260bdceffec4ffc3ad165c97dc1b4", + "visualization": "f819cf6636b75c9e76ba733a0c6ef355" + } + }, + "dynamic": "strict", + "properties": { + "apm-telemetry": { + "dynamic": "false", + "type": "object" + }, + "application_usage_daily": { + "dynamic": "false", + "properties": { + "timestamp": { + "type": "date" + } + } + }, + "application_usage_totals": { + "dynamic": "false", + "type": "object" + }, + "application_usage_transactional": { + "dynamic": "false", + "type": "object" + }, + "canvas-workpad": { + "dynamic": "false", + "type": "object" + }, + "config": { + "dynamic": "false", + "properties": { + "buildNum": { + "type": "keyword" + } + } + }, + "core-usage-stats": { + "dynamic": "false", + "type": "object" + }, + "coreMigrationVersion": { + "type": "keyword" + }, + "dashboard": { + "properties": { + "description": { + "type": "text" + }, + "hits": { + "doc_values": false, + "index": false, + "type": "integer" + }, + "kibanaSavedObjectMeta": { + "properties": { + "searchSourceJSON": { + "index": false, + "type": "text" + } + } + }, + "optionsJSON": { + "index": false, + "type": "text" + }, + "panelsJSON": { + "index": false, + "type": "text" + }, + "refreshInterval": { + "properties": { + "display": { + "doc_values": false, + "index": false, + "type": "keyword" + }, + "pause": { + "doc_values": false, + "index": false, + "type": "boolean" + }, + "section": { + "doc_values": false, + "index": false, + "type": "integer" + }, + "value": { + "doc_values": false, + "index": false, + "type": "integer" + } + } + }, + "timeFrom": { + "doc_values": false, + "index": false, + "type": "keyword" + }, + "timeRestore": { + "doc_values": false, + "index": false, + "type": "boolean" + }, + "timeTo": { + "doc_values": false, + "index": false, + "type": "keyword" + }, + "title": { + "type": "text" + }, + "version": { + "type": "integer" + } + } + }, + "graph-workspace": { + "dynamic": "false", + "type": "object" + }, + "index-pattern": { + "dynamic": "false", + "properties": { + "title": { + "type": "text" + }, + "type": { + "type": "keyword" + } + } + }, + "kql-telemetry": { + "properties": { + "optInCount": { + "type": "long" + }, + "optOutCount": { + "type": "long" + } + } + }, + "legacy-url-alias": { + "dynamic": "false", + "properties": { + "disabled": { + "type": "boolean" + }, + "sourceId": { + "type": "keyword" + }, + "targetType": { + "type": "keyword" + } + } + }, + "map": { + "dynamic": "false", + "type": "object" + }, + "migrationVersion": { + "dynamic": "true", + "type": "object" + }, + "namespace": { + "type": "keyword" + }, + "namespaces": { + "type": "keyword" + }, + "originId": { + "type": "keyword" + }, + "query": { + "properties": { + "description": { + "type": "text" + }, + "filters": { + "enabled": false, + "type": "object" + }, + "query": { + "properties": { + "language": { + "type": "keyword" + }, + "query": { + "index": false, + "type": "keyword" + } + } + }, + "timefilter": { + "enabled": false, + "type": "object" + }, + "title": { + "type": "text" + } + } + }, + "references": { + "properties": { + "id": { + "type": "keyword" + }, + "name": { + "type": "keyword" + }, + "type": { + "type": "keyword" + } + }, + "type": "nested" + }, + "sample-data-telemetry": { + "properties": { + "installCount": { + "type": "long" + }, + "unInstallCount": { + "type": "long" + } + } + }, + "search": { + "properties": { + "columns": { + "doc_values": false, + "index": false, + "type": "keyword" + }, + "description": { + "type": "text" + }, + "grid": { + "enabled": false, + "type": "object" + }, + "hideChart": { + "doc_values": false, + "index": false, + "type": "boolean" + }, + "hits": { + "doc_values": false, + "index": false, + "type": "integer" + }, + "kibanaSavedObjectMeta": { + "properties": { + "searchSourceJSON": { + "index": false, + "type": "text" + } + } + }, + "sort": { + "doc_values": false, + "index": false, + "type": "keyword" + }, + "title": { + "type": "text" + }, + "version": { + "type": "integer" + } + } + }, + "search-telemetry": { + "dynamic": "false", + "type": "object" + }, + "server": { + "dynamic": "false", + "type": "object" + }, + "space": { + "dynamic": "false", + "type": "object" + }, + "spaceId": { + "type": "keyword" + }, + "telemetry": { + "properties": { + "allowChangingOptInStatus": { + "type": "boolean" + }, + "enabled": { + "type": "boolean" + }, + "lastReported": { + "type": "date" + }, + "lastVersionChecked": { + "type": "keyword" + }, + "reportFailureCount": { + "type": "integer" + }, + "reportFailureVersion": { + "type": "keyword" + }, + "sendUsageFrom": { + "type": "keyword" + }, + "userHasSeenNotice": { + "type": "boolean" + } + } + }, + "test-export-add": { + "dynamic": "false", + "type": "object" + }, + "test-export-add-dep": { + "dynamic": "false", + "type": "object" + }, + "test-export-invalid-transform": { + "dynamic": "false", + "type": "object" + }, + "test-export-transform": { + "dynamic": "false", + "type": "object" + }, + "test-export-transform-error": { + "dynamic": "false", + "type": "object" + }, + "test-visible-in-management": { + "dynamic": "false", + "type": "object" + }, + "test-not-visible-in-management": { + "dynamic": "false", + "type": "object" + }, + "type": { + "type": "keyword" + }, + "ui-counter": { + "properties": { + "count": { + "type": "integer" + } + } + }, + "ui-metric": { + "properties": { + "count": { + "type": "integer" + } + } + }, + "updated_at": { + "type": "date" + }, + "url": { + "properties": { + "accessCount": { + "type": "long" + }, + "accessDate": { + "type": "date" + }, + "createDate": { + "type": "date" + }, + "url": { + "fields": { + "keyword": { + "ignore_above": 2048, + "type": "keyword" + } + }, + "type": "text" + } + } + }, + "usage-counters": { + "dynamic": "false", + "properties": { + "domainId": { + "type": "keyword" + } + } + }, + "visualization": { + "properties": { + "description": { + "type": "text" + }, + "kibanaSavedObjectMeta": { + "properties": { + "searchSourceJSON": { + "index": false, + "type": "text" + } + } + }, + "savedSearchRefName": { + "doc_values": false, + "index": false, + "type": "keyword" + }, + "title": { + "type": "text" + }, + "uiStateJSON": { + "index": false, + "type": "text" + }, + "version": { + "type": "integer" + }, + "visState": { + "index": false, + "type": "text" + } + } + } + } + }, + "settings": { + "index": { + "auto_expand_replicas": "0-1", + "number_of_replicas": "0", + "number_of_shards": "1", + "priority": "10", + "refresh_interval": "1s", + "routing_partition_size": "1" + } + } + } +} diff --git a/test/functional/page_objects/visual_builder_page.ts b/test/functional/page_objects/visual_builder_page.ts index c324de1231b7d..f5628f60e5a9c 100644 --- a/test/functional/page_objects/visual_builder_page.ts +++ b/test/functional/page_objects/visual_builder_page.ts @@ -284,7 +284,8 @@ export class VisualBuilderPageObject extends FtrService { const drilldownEl = await this.testSubjects.find('drilldownUrl'); await drilldownEl.clearValue(); - await drilldownEl.type(value); + await drilldownEl.type(value, { charByChar: true }); + await this.header.waitUntilLoadingHasFinished(); } /** diff --git a/test/functional/services/saved_query_management_component.ts b/test/functional/services/saved_query_management_component.ts index 37c6a45557f2f..a216f8cb0469e 100644 --- a/test/functional/services/saved_query_management_component.ts +++ b/test/functional/services/saved_query_management_component.ts @@ -166,8 +166,9 @@ export class SavedQueryManagementComponentService extends FtrService { const isOpenAlready = await this.testSubjects.exists('saved-query-management-popover'); if (isOpenAlready) return; + await this.testSubjects.click('saved-query-management-popover-button'); + await this.retry.waitFor('saved query management popover to have any text', async () => { - await this.testSubjects.click('saved-query-management-popover-button'); const queryText = await this.testSubjects.getVisibleText('saved-query-management-popover'); return queryText.length > 0; }); diff --git a/test/interpreter_functional/test_suites/run_pipeline/esaggs_timeshift.ts b/test/interpreter_functional/test_suites/run_pipeline/esaggs_timeshift.ts index 0bc32672d41b9..244d07d2cfc82 100644 --- a/test/interpreter_functional/test_suites/run_pipeline/esaggs_timeshift.ts +++ b/test/interpreter_functional/test_suites/run_pipeline/esaggs_timeshift.ts @@ -37,7 +37,8 @@ export default function ({ }: FtrProviderContext & { updateBaselines: boolean }) { let expectExpression: ExpectExpression; - describe('esaggs timeshift tests', () => { + // FLAKY https://github.com/elastic/kibana/issues/107028 + describe.skip('esaggs timeshift tests', () => { before(() => { expectExpression = expectExpressionProvider({ getService, updateBaselines }); }); diff --git a/test/plugin_functional/plugins/saved_object_export_transforms/server/plugin.ts b/test/plugin_functional/plugins/saved_object_export_transforms/server/plugin.ts index 15afdb229b1fd..0cb6a5ba8eb5d 100644 --- a/test/plugin_functional/plugins/saved_object_export_transforms/server/plugin.ts +++ b/test/plugin_functional/plugins/saved_object_export_transforms/server/plugin.ts @@ -176,6 +176,42 @@ export class SavedObjectExportTransformsPlugin implements Plugin { }, }, }); + + // example of a SO type with `visibleInManagement: false` + savedObjects.registerType<{ enabled: boolean; title: string }>({ + name: 'test-not-visible-in-management', + hidden: false, + namespaceType: 'single', + mappings: { + properties: { + title: { type: 'text' }, + enabled: { type: 'boolean' }, + }, + }, + management: { + defaultSearchField: 'title', + importableAndExportable: true, + visibleInManagement: false, + }, + }); + + // example of a SO type with `visibleInManagement: true` + savedObjects.registerType<{ enabled: boolean; title: string }>({ + name: 'test-visible-in-management', + hidden: false, + namespaceType: 'single', + mappings: { + properties: { + title: { type: 'text' }, + enabled: { type: 'boolean' }, + }, + }, + management: { + defaultSearchField: 'title', + importableAndExportable: true, + visibleInManagement: true, + }, + }); } public start() {} diff --git a/test/plugin_functional/test_suites/saved_objects_management/exports/_import_non_visible_in_management.ndjson b/test/plugin_functional/test_suites/saved_objects_management/exports/_import_non_visible_in_management.ndjson new file mode 100644 index 0000000000000..754848a99d03d --- /dev/null +++ b/test/plugin_functional/test_suites/saved_objects_management/exports/_import_non_visible_in_management.ndjson @@ -0,0 +1 @@ +{"attributes": { "title": "Saved object type that is not visible in management" }, "id":"ff3773b0-9ate-11e7-ahb3-3dcb94193fab", "references":[], "type":"test-not-visible-in-management", "version":1} diff --git a/test/plugin_functional/test_suites/saved_objects_management/index.ts b/test/plugin_functional/test_suites/saved_objects_management/index.ts index 03ac96b9a11f6..b7730a95d3c71 100644 --- a/test/plugin_functional/test_suites/saved_objects_management/index.ts +++ b/test/plugin_functional/test_suites/saved_objects_management/index.ts @@ -16,5 +16,6 @@ export default function ({ loadTestFile }: PluginFunctionalProviderContext) { loadTestFile(require.resolve('./export_transform')); loadTestFile(require.resolve('./import_warnings')); loadTestFile(require.resolve('./hidden_types')); + loadTestFile(require.resolve('./visible_in_management')); }); } diff --git a/test/plugin_functional/test_suites/saved_objects_management/visible_in_management.ts b/test/plugin_functional/test_suites/saved_objects_management/visible_in_management.ts new file mode 100644 index 0000000000000..dd43ba80a8e8e --- /dev/null +++ b/test/plugin_functional/test_suites/saved_objects_management/visible_in_management.ts @@ -0,0 +1,113 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { join } from 'path'; +import expect from '@kbn/expect'; +import type { Response } from 'supertest'; +import type { PluginFunctionalProviderContext } from '../../services'; +import { SavedObject } from '../../../../src/core/types'; + +function parseNdJson(input: string): Array> { + return input.split('\n').map((str) => JSON.parse(str)); +} + +export default function ({ getService }: PluginFunctionalProviderContext) { + const supertest = getService('supertest'); + const esArchiver = getService('esArchiver'); + + describe('types with `visibleInManagement` ', () => { + before(async () => { + await esArchiver.load( + 'test/functional/fixtures/es_archiver/saved_objects_management/visible_in_management' + ); + }); + + after(async () => { + await esArchiver.unload( + 'test/functional/fixtures/es_archiver/saved_objects_management/visible_in_management' + ); + }); + + describe('export', () => { + it('allows to export them directly by id', async () => { + await supertest + .post('/api/saved_objects/_export') + .set('kbn-xsrf', 'true') + .send({ + objects: [ + { + type: 'test-not-visible-in-management', + id: 'vim-1', + }, + ], + excludeExportDetails: true, + }) + .expect(200) + .then((resp) => { + const objects = parseNdJson(resp.text); + expect(objects.map((obj) => obj.id)).to.eql(['vim-1']); + }); + }); + + it('allows to export them directly by type', async () => { + await supertest + .post('/api/saved_objects/_export') + .set('kbn-xsrf', 'true') + .send({ + type: ['test-not-visible-in-management'], + excludeExportDetails: true, + }) + .expect(200) + .then((resp) => { + const objects = parseNdJson(resp.text); + expect(objects.map((obj) => obj.id)).to.eql(['vim-1']); + }); + }); + }); + + describe('import', () => { + it('allows to import them', async () => { + await supertest + .post('/api/saved_objects/_import') + .set('kbn-xsrf', 'true') + .attach('file', join(__dirname, './exports/_import_non_visible_in_management.ndjson')) + .expect(200) + .then((resp) => { + expect(resp.body).to.eql({ + success: true, + successCount: 1, + successResults: [ + { + id: 'ff3773b0-9ate-11e7-ahb3-3dcb94193fab', + meta: { + title: 'Saved object type that is not visible in management', + }, + type: 'test-not-visible-in-management', + }, + ], + warnings: [], + }); + }); + }); + }); + + describe('savedObjects management APIS', () => { + it('GET /api/kibana/management/saved_objects/_allowed_types should only return types that are `visibleInManagement: true`', async () => + await supertest + .get('/api/kibana/management/saved_objects/_allowed_types') + .set('kbn-xsrf', 'true') + .expect(200) + .then((response: Response) => { + const { types } = response.body; + expect(types.includes('test-is-exportable')).to.eql(true); + expect(types.includes('test-visible-in-management')).to.eql(true); + expect(types.includes('test-not-visible-in-management')).to.eql(false); + })); + }); + }); +} diff --git a/test/scripts/jenkins_storybook.sh b/test/scripts/jenkins_storybook.sh index 00cc0d78599dd..17ca46b0097b1 100755 --- a/test/scripts/jenkins_storybook.sh +++ b/test/scripts/jenkins_storybook.sh @@ -20,6 +20,7 @@ yarn storybook --site expression_repeat_image yarn storybook --site expression_reveal_image yarn storybook --site expression_shape yarn storybook --site expression_tagcloud +yarn storybook --site fleet yarn storybook --site infra yarn storybook --site security_solution yarn storybook --site ui_actions_enhanced diff --git a/test/scripts/test/jest_integration.sh b/test/scripts/test/jest_integration.sh index aaffdb2fd9e90..89390657d1b48 100755 --- a/test/scripts/test/jest_integration.sh +++ b/test/scripts/test/jest_integration.sh @@ -3,4 +3,4 @@ source src/dev/ci_setup/setup_env.sh checks-reporter-with-killswitch "Jest Integration Tests" \ - node scripts/jest_integration --ci + node --max-old-space-size=5120 scripts/jest_integration --ci diff --git a/x-pack/plugins/actions/common/index.ts b/x-pack/plugins/actions/common/index.ts index cff876b5995a1..1e51adf3e9d09 100644 --- a/x-pack/plugins/actions/common/index.ts +++ b/x-pack/plugins/actions/common/index.ts @@ -15,3 +15,10 @@ export * from './rewrite_request_case'; export const BASE_ACTION_API_PATH = '/api/actions'; export const INTERNAL_BASE_ACTION_API_PATH = '/internal/actions'; export const ACTIONS_FEATURE_ID = 'actions'; + +// supported values for `service` in addition to nodemailer's list of well-known services +export enum AdditionalEmailServices { + ELASTIC_CLOUD = 'elastic_cloud', + EXCHANGE = 'exchange_server', + OTHER = 'other', +} diff --git a/x-pack/plugins/actions/server/actions_client.test.ts b/x-pack/plugins/actions/server/actions_client.test.ts index a341cdf58b9e2..7549d2ecaab77 100644 --- a/x-pack/plugins/actions/server/actions_client.test.ts +++ b/x-pack/plugins/actions/server/actions_client.test.ts @@ -20,6 +20,7 @@ import { licenseStateMock } from './lib/license_state.mock'; import { licensingMock } from '../../licensing/server/mocks'; import { httpServerMock } from '../../../../src/core/server/mocks'; import { auditServiceMock } from '../../security/server/audit/index.mock'; +import { usageCountersServiceMock } from 'src/plugins/usage_collection/server/usage_counters/usage_counters_service.mock'; import { elasticsearchServiceMock, @@ -28,7 +29,12 @@ import { import { actionExecutorMock } from './lib/action_executor.mock'; import uuid from 'uuid'; import { ActionsAuthorization } from './authorization/actions_authorization'; +import { + getAuthorizationModeBySource, + AuthorizationMode, +} from './authorization/get_authorization_mode_by_source'; import { actionsAuthorizationMock } from './authorization/actions_authorization.mock'; +import { trackLegacyRBACExemption } from './lib/track_legacy_rbac_exemption'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths import { elasticsearchClientMock } from '../../../../src/core/server/elasticsearch/client/mocks'; @@ -38,6 +44,22 @@ jest.mock('../../../../src/core/server/saved_objects/service/lib/utils', () => ( }, })); +jest.mock('./lib/track_legacy_rbac_exemption', () => ({ + trackLegacyRBACExemption: jest.fn(), +})); + +jest.mock('./authorization/get_authorization_mode_by_source', () => { + return { + getAuthorizationModeBySource: jest.fn(() => { + return 1; + }), + AuthorizationMode: { + Legacy: 0, + RBAC: 1, + }, + }; +}); + const defaultKibanaIndex = '.kibana'; const unsecuredSavedObjectsClient = savedObjectsClientMock.create(); const scopedClusterClient = elasticsearchServiceMock.createScopedClusterClient(); @@ -47,6 +69,8 @@ const executionEnqueuer = jest.fn(); const ephemeralExecutionEnqueuer = jest.fn(); const request = httpServerMock.createKibanaRequest(); const auditLogger = auditServiceMock.create().asScoped(request); +const mockUsageCountersSetup = usageCountersServiceMock.createSetupContract(); +const mockUsageCounter = mockUsageCountersSetup.createUsageCounter('test'); const mockTaskManager = taskManagerMock.createSetup(); @@ -82,6 +106,7 @@ beforeEach(() => { request, authorization: authorization as unknown as ActionsAuthorization, auditLogger, + usageCounter: mockUsageCounter, }); }); @@ -1640,6 +1665,9 @@ describe('update()', () => { describe('execute()', () => { describe('authorization', () => { test('ensures user is authorised to excecute actions', async () => { + (getAuthorizationModeBySource as jest.Mock).mockImplementationOnce(() => { + return AuthorizationMode.RBAC; + }); await actionsClient.execute({ actionId: 'action-id', params: { @@ -1650,6 +1678,9 @@ describe('execute()', () => { }); test('throws when user is not authorised to create the type of action', async () => { + (getAuthorizationModeBySource as jest.Mock).mockImplementationOnce(() => { + return AuthorizationMode.RBAC; + }); authorization.ensureAuthorized.mockRejectedValue( new Error(`Unauthorized to execute all actions`) ); @@ -1665,6 +1696,21 @@ describe('execute()', () => { expect(authorization.ensureAuthorized).toHaveBeenCalledWith('execute'); }); + + test('tracks legacy RBAC', async () => { + (getAuthorizationModeBySource as jest.Mock).mockImplementationOnce(() => { + return AuthorizationMode.Legacy; + }); + + await actionsClient.execute({ + actionId: 'action-id', + params: { + name: 'my name', + }, + }); + + expect(trackLegacyRBACExemption as jest.Mock).toBeCalledWith('execute', mockUsageCounter); + }); }); test('calls the actionExecutor with the appropriate parameters', async () => { @@ -1756,6 +1802,9 @@ describe('execute()', () => { describe('enqueueExecution()', () => { describe('authorization', () => { test('ensures user is authorised to excecute actions', async () => { + (getAuthorizationModeBySource as jest.Mock).mockImplementationOnce(() => { + return AuthorizationMode.RBAC; + }); await actionsClient.enqueueExecution({ id: uuid.v4(), params: {}, @@ -1766,6 +1815,9 @@ describe('enqueueExecution()', () => { }); test('throws when user is not authorised to create the type of action', async () => { + (getAuthorizationModeBySource as jest.Mock).mockImplementationOnce(() => { + return AuthorizationMode.RBAC; + }); authorization.ensureAuthorized.mockRejectedValue( new Error(`Unauthorized to execute all actions`) ); @@ -1781,6 +1833,24 @@ describe('enqueueExecution()', () => { expect(authorization.ensureAuthorized).toHaveBeenCalledWith('execute'); }); + + test('tracks legacy RBAC', async () => { + (getAuthorizationModeBySource as jest.Mock).mockImplementationOnce(() => { + return AuthorizationMode.Legacy; + }); + + await actionsClient.enqueueExecution({ + id: uuid.v4(), + params: {}, + spaceId: 'default', + apiKey: null, + }); + + expect(trackLegacyRBACExemption as jest.Mock).toBeCalledWith( + 'enqueueExecution', + mockUsageCounter + ); + }); }); test('calls the executionEnqueuer with the appropriate parameters', async () => { diff --git a/x-pack/plugins/actions/server/actions_client.ts b/x-pack/plugins/actions/server/actions_client.ts index d6f6037ecd8b8..b391e50283ad1 100644 --- a/x-pack/plugins/actions/server/actions_client.ts +++ b/x-pack/plugins/actions/server/actions_client.ts @@ -7,6 +7,7 @@ import Boom from '@hapi/boom'; import type { estypes } from '@elastic/elasticsearch'; +import { UsageCounter } from 'src/plugins/usage_collection/server'; import { i18n } from '@kbn/i18n'; import { omitBy, isUndefined } from 'lodash'; @@ -42,6 +43,7 @@ import { } from './authorization/get_authorization_mode_by_source'; import { connectorAuditEvent, ConnectorAuditAction } from './lib/audit_events'; import { RunNowResult } from '../../task_manager/server'; +import { trackLegacyRBACExemption } from './lib/track_legacy_rbac_exemption'; // We are assuming there won't be many actions. This is why we will load // all the actions in advance and assume the total count to not go over 10000. @@ -74,6 +76,7 @@ interface ConstructorOptions { request: KibanaRequest; authorization: ActionsAuthorization; auditLogger?: AuditLogger; + usageCounter?: UsageCounter; } export interface UpdateOptions { @@ -93,6 +96,7 @@ export class ActionsClient { private readonly executionEnqueuer: ExecutionEnqueuer; private readonly ephemeralExecutionEnqueuer: ExecutionEnqueuer; private readonly auditLogger?: AuditLogger; + private readonly usageCounter?: UsageCounter; constructor({ actionTypeRegistry, @@ -106,6 +110,7 @@ export class ActionsClient { request, authorization, auditLogger, + usageCounter, }: ConstructorOptions) { this.actionTypeRegistry = actionTypeRegistry; this.unsecuredSavedObjectsClient = unsecuredSavedObjectsClient; @@ -118,6 +123,7 @@ export class ActionsClient { this.request = request; this.authorization = authorization; this.auditLogger = auditLogger; + this.usageCounter = usageCounter; } /** @@ -478,6 +484,8 @@ export class ActionsClient { AuthorizationMode.RBAC ) { await this.authorization.ensureAuthorized('execute'); + } else { + trackLegacyRBACExemption('execute', this.usageCounter); } return this.actionExecutor.execute({ actionId, @@ -495,6 +503,8 @@ export class ActionsClient { AuthorizationMode.RBAC ) { await this.authorization.ensureAuthorized('execute'); + } else { + trackLegacyRBACExemption('enqueueExecution', this.usageCounter); } return this.executionEnqueuer(this.unsecuredSavedObjectsClient, options); } @@ -506,6 +516,8 @@ export class ActionsClient { AuthorizationMode.RBAC ) { await this.authorization.ensureAuthorized('execute'); + } else { + trackLegacyRBACExemption('ephemeralEnqueuedExecution', this.usageCounter); } return this.ephemeralExecutionEnqueuer(this.unsecuredSavedObjectsClient, options); } diff --git a/x-pack/plugins/actions/server/builtin_action_types/email.ts b/x-pack/plugins/actions/server/builtin_action_types/email.ts index d10046341b268..fcd003286d5bb 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/email.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/email.ts @@ -17,6 +17,7 @@ import { Logger } from '../../../../../src/core/server'; import { ActionType, ActionTypeExecutorOptions, ActionTypeExecutorResult } from '../types'; import { ActionsConfigurationUtilities } from '../actions_config'; import { renderMustacheString, renderMustacheObject } from '../lib/mustache_renderer'; +import { AdditionalEmailServices } from '../../common'; export type EmailActionType = ActionType< ActionTypeConfigType, @@ -33,13 +34,6 @@ export type EmailActionTypeExecutorOptions = ActionTypeExecutorOptions< // config definition export type ActionTypeConfigType = TypeOf; -// supported values for `service` in addition to nodemailer's list of well-known services -export enum AdditionalEmailServices { - ELASTIC_CLOUD = 'elastic_cloud', - EXCHANGE = 'exchange_server', - OTHER = 'other', -} - // these values for `service` require users to fill in host/port/secure export const CUSTOM_HOST_PORT_SERVICES: string[] = [AdditionalEmailServices.OTHER]; diff --git a/x-pack/plugins/actions/server/builtin_action_types/lib/request_oauth_client_credentials_token.ts b/x-pack/plugins/actions/server/builtin_action_types/lib/request_oauth_client_credentials_token.ts index 09080ee0c0063..b632cdf5f5219 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/lib/request_oauth_client_credentials_token.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/lib/request_oauth_client_credentials_token.ts @@ -7,6 +7,7 @@ import qs from 'query-string'; import axios from 'axios'; +import stringify from 'json-stable-stringify'; import { Logger } from '../../../../../../src/core/server'; import { request } from './axios_utils'; import { ActionsConfigurationUtilities } from '../../actions_config'; @@ -59,7 +60,7 @@ export async function requestOAuthClientCredentialsToken( expiresIn: res.data.expires_in, }; } else { - const errString = JSON.stringify(res.data); + const errString = stringify(res.data); logger.warn( `error thrown getting the access token from ${tokenUrl} for clientID: ${clientId}: ${errString}` ); diff --git a/x-pack/plugins/actions/server/builtin_action_types/lib/send_email.ts b/x-pack/plugins/actions/server/builtin_action_types/lib/send_email.ts index ea3c0f91b6a5c..53c70fddc5a09 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/lib/send_email.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/lib/send_email.ts @@ -13,10 +13,10 @@ import { Logger } from '../../../../../../src/core/server'; import { ActionsConfigurationUtilities } from '../../actions_config'; import { CustomHostSettings } from '../../config'; import { getNodeSSLOptions, getSSLSettingsFromConfig } from './get_node_ssl_options'; -import { AdditionalEmailServices } from '../email'; import { sendEmailGraphApi } from './send_email_graph_api'; import { requestOAuthClientCredentialsToken } from './request_oauth_client_credentials_token'; import { ProxySettings } from '../../types'; +import { AdditionalEmailServices } from '../../../common'; // an email "service" which doesn't actually send, just returns what it would send export const JSON_TRANSPORT_SERVICE = '__json'; diff --git a/x-pack/plugins/actions/server/builtin_action_types/lib/send_email_graph_api.ts b/x-pack/plugins/actions/server/builtin_action_types/lib/send_email_graph_api.ts index 10e9a3bc8d27c..ea1579095bb97 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/lib/send_email_graph_api.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/lib/send_email_graph_api.ts @@ -5,6 +5,8 @@ * 2.0. */ +// @ts-expect-error missing type def +import stringify from 'json-stringify-safe'; import axios, { AxiosResponse } from 'axios'; import { Logger } from '../../../../../../src/core/server'; import { request } from './axios_utils'; @@ -41,9 +43,9 @@ export async function sendEmailGraphApi( validateStatus: () => true, }); if (res.status === 202) { - return res; + return res.data; } - const errString = JSON.stringify(res.data); + const errString = stringify(res.data); logger.warn( `error thrown sending Microsoft Exchange email for clientID: ${sendEmailOptions.options.transport.clientId}: ${errString}` ); diff --git a/x-pack/plugins/actions/server/lib/track_legacy_rbac_exemption.test.ts b/x-pack/plugins/actions/server/lib/track_legacy_rbac_exemption.test.ts new file mode 100644 index 0000000000000..ffd8e7f17c11f --- /dev/null +++ b/x-pack/plugins/actions/server/lib/track_legacy_rbac_exemption.test.ts @@ -0,0 +1,32 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import { usageCountersServiceMock } from 'src/plugins/usage_collection/server/usage_counters/usage_counters_service.mock'; +import { trackLegacyRBACExemption } from './track_legacy_rbac_exemption'; + +describe('trackLegacyRBACExemption', () => { + it('should call `usageCounter.incrementCounter`', () => { + const mockUsageCountersSetup = usageCountersServiceMock.createSetupContract(); + const mockUsageCounter = mockUsageCountersSetup.createUsageCounter('test'); + + trackLegacyRBACExemption('test', mockUsageCounter); + expect(mockUsageCounter.incrementCounter).toHaveBeenCalledWith({ + counterName: `source_test`, + counterType: 'legacyRBACExemption', + incrementBy: 1, + }); + }); + + it('should do nothing if no usage counter is provided', () => { + let err; + try { + trackLegacyRBACExemption('test', undefined); + } catch (e) { + err = e; + } + expect(err).toBeUndefined(); + }); +}); diff --git a/x-pack/plugins/actions/server/lib/track_legacy_rbac_exemption.ts b/x-pack/plugins/actions/server/lib/track_legacy_rbac_exemption.ts new file mode 100644 index 0000000000000..73c859c4cd21e --- /dev/null +++ b/x-pack/plugins/actions/server/lib/track_legacy_rbac_exemption.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. + */ + +import { UsageCounter } from 'src/plugins/usage_collection/server'; + +export function trackLegacyRBACExemption(source: string, usageCounter?: UsageCounter) { + if (usageCounter) { + usageCounter.incrementCounter({ + counterName: `source_${source}`, + counterType: 'legacyRBACExemption', + incrementBy: 1, + }); + } +} diff --git a/x-pack/plugins/actions/server/plugin.ts b/x-pack/plugins/actions/server/plugin.ts index fe133ddb6f0ac..78808b669d9e9 100644 --- a/x-pack/plugins/actions/server/plugin.ts +++ b/x-pack/plugins/actions/server/plugin.ts @@ -6,7 +6,7 @@ */ import type { PublicMethodsOf } from '@kbn/utility-types'; -import { UsageCollectionSetup } from 'src/plugins/usage_collection/server'; +import { UsageCollectionSetup, UsageCounter } from 'src/plugins/usage_collection/server'; import { PluginInitializerContext, Plugin, @@ -151,6 +151,7 @@ export class ActionsPlugin implements Plugin(), this.licenseState, - usageCounter + this.usageCounter ); // Cleanup failed execution task definition @@ -367,6 +368,7 @@ export class ActionsPlugin implements Plugin ({ verifyApiAccess: jest.fn(), })); +jest.mock('./lib/track_legacy_terminology', () => ({ + trackLegacyTerminology: jest.fn(), +})); + beforeEach(() => { jest.resetAllMocks(); }); @@ -147,4 +155,39 @@ describe('aggregateRulesRoute', () => { expect(verifyApiAccess).toHaveBeenCalledWith(licenseState); }); + + it('should track calls with deprecated param values', async () => { + const licenseState = licenseStateMock.create(); + const router = httpServiceMock.createRouter(); + + aggregateRulesRoute(router, licenseState, mockUsageCounter); + const aggregateResult = { + alertExecutionStatus: { + ok: 15, + error: 2, + active: 23, + pending: 1, + unknown: 0, + }, + }; + rulesClient.aggregate.mockResolvedValueOnce(aggregateResult); + const [, handler] = router.get.mock.calls[0]; + const [context, req, res] = mockHandlerArguments( + { rulesClient }, + { + params: {}, + query: { + search_fields: ['alertTypeId:1', 'message:foo'], + search: 'alertTypeId:2', + }, + }, + ['ok'] + ); + await handler(context, req, res); + expect(trackLegacyTerminology).toHaveBeenCalledTimes(1); + expect((trackLegacyTerminology as jest.Mock).mock.calls[0][0]).toStrictEqual([ + 'alertTypeId:2', + ['alertTypeId:1', 'message:foo'], + ]); + }); }); diff --git a/x-pack/plugins/alerting/server/routes/aggregate_rules.ts b/x-pack/plugins/alerting/server/routes/aggregate_rules.ts index f91231f9ac48a..84c03e21ff36e 100644 --- a/x-pack/plugins/alerting/server/routes/aggregate_rules.ts +++ b/x-pack/plugins/alerting/server/routes/aggregate_rules.ts @@ -7,10 +7,12 @@ import { IRouter } from 'kibana/server'; import { schema } from '@kbn/config-schema'; +import { UsageCounter } from 'src/plugins/usage_collection/server'; import { ILicenseState } from '../lib'; import { AggregateResult, AggregateOptions } from '../rules_client'; import { RewriteResponseCase, RewriteRequestCase, verifyAccessAndContext } from './lib'; import { AlertingRequestHandlerContext, INTERNAL_BASE_ALERTING_API_PATH } from '../types'; +import { trackLegacyTerminology } from './lib/track_legacy_terminology'; // config definition const querySchema = schema.object({ @@ -53,7 +55,8 @@ const rewriteBodyRes: RewriteResponseCase = ({ export const aggregateRulesRoute = ( router: IRouter, - licenseState: ILicenseState + licenseState: ILicenseState, + usageCounter?: UsageCounter ) => { router.get( { @@ -69,6 +72,10 @@ export const aggregateRulesRoute = ( ...req.query, has_reference: req.query.has_reference || undefined, }); + trackLegacyTerminology( + [req.query.search, req.query.search_fields].filter(Boolean) as string[], + usageCounter + ); const aggregateResult = await rulesClient.aggregate({ options }); return res.ok({ body: rewriteBodyRes(aggregateResult), diff --git a/x-pack/plugins/alerting/server/routes/find_rules.test.ts b/x-pack/plugins/alerting/server/routes/find_rules.test.ts index e0da7b0645748..692fdca6c95ae 100644 --- a/x-pack/plugins/alerting/server/routes/find_rules.test.ts +++ b/x-pack/plugins/alerting/server/routes/find_rules.test.ts @@ -4,20 +4,27 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ - +import { usageCountersServiceMock } from 'src/plugins/usage_collection/server/usage_counters/usage_counters_service.mock'; import { findRulesRoute } from './find_rules'; import { httpServiceMock } from 'src/core/server/mocks'; import { licenseStateMock } from '../lib/license_state.mock'; import { verifyApiAccess } from '../lib/license_api_access'; import { mockHandlerArguments } from './_mock_handler_arguments'; import { rulesClientMock } from '../rules_client.mock'; +import { trackLegacyTerminology } from './lib/track_legacy_terminology'; const rulesClient = rulesClientMock.create(); +const mockUsageCountersSetup = usageCountersServiceMock.createSetupContract(); +const mockUsageCounter = mockUsageCountersSetup.createUsageCounter('test'); jest.mock('../lib/license_api_access.ts', () => ({ verifyApiAccess: jest.fn(), })); +jest.mock('./lib/track_legacy_terminology', () => ({ + trackLegacyTerminology: jest.fn(), +})); + beforeEach(() => { jest.resetAllMocks(); }); @@ -145,4 +152,74 @@ describe('findRulesRoute', () => { expect(verifyApiAccess).toHaveBeenCalledWith(licenseState); }); + + it('should track calls with deprecated param values', async () => { + const licenseState = licenseStateMock.create(); + const router = httpServiceMock.createRouter(); + + findRulesRoute(router, licenseState, mockUsageCounter); + const findResult = { + page: 1, + perPage: 1, + total: 0, + data: [], + }; + rulesClient.find.mockResolvedValueOnce(findResult); + const [, handler] = router.get.mock.calls[0]; + const [context, req, res] = mockHandlerArguments( + { rulesClient }, + { + params: {}, + query: { + search_fields: ['alertTypeId:1', 'message:foo'], + search: 'alertTypeId:2', + sort_field: 'alertTypeId', + }, + }, + ['ok'] + ); + await handler(context, req, res); + expect(trackLegacyTerminology).toHaveBeenCalledTimes(1); + expect((trackLegacyTerminology as jest.Mock).mock.calls[0][0]).toStrictEqual([ + 'alertTypeId:2', + ['alertTypeId:1', 'message:foo'], + 'alertTypeId', + ]); + }); + + it('should track calls to deprecated functionality', async () => { + const licenseState = licenseStateMock.create(); + const router = httpServiceMock.createRouter(); + + findRulesRoute(router, licenseState, mockUsageCounter); + + const findResult = { + page: 1, + perPage: 1, + total: 0, + data: [], + }; + rulesClient.find.mockResolvedValueOnce(findResult); + + const [, handler] = router.get.mock.calls[0]; + const [context, req, res] = mockHandlerArguments( + { rulesClient }, + { + params: {}, + query: { + fields: ['foo', 'bar'], + per_page: 1, + page: 1, + default_search_operator: 'OR', + }, + }, + ['ok'] + ); + await handler(context, req, res); + expect(mockUsageCounter.incrementCounter).toHaveBeenCalledWith({ + counterName: `alertingFieldsUsage`, + counterType: 'alertingFieldsUsage', + incrementBy: 1, + }); + }); }); diff --git a/x-pack/plugins/alerting/server/routes/find_rules.ts b/x-pack/plugins/alerting/server/routes/find_rules.ts index ef62341290228..a4a066728555d 100644 --- a/x-pack/plugins/alerting/server/routes/find_rules.ts +++ b/x-pack/plugins/alerting/server/routes/find_rules.ts @@ -7,11 +7,13 @@ import { omit } from 'lodash'; import { IRouter } from 'kibana/server'; +import { UsageCounter } from 'src/plugins/usage_collection/server'; import { schema } from '@kbn/config-schema'; import { ILicenseState } from '../lib'; import { FindOptions, FindResult } from '../rules_client'; import { RewriteRequestCase, RewriteResponseCase, verifyAccessAndContext } from './lib'; import { AlertTypeParams, AlertingRequestHandlerContext, BASE_ALERTING_API_PATH } from '../types'; +import { trackLegacyTerminology } from './lib/track_legacy_terminology'; // query definition const querySchema = schema.object({ @@ -107,7 +109,8 @@ const rewriteBodyRes: RewriteResponseCase> = ({ export const findRulesRoute = ( router: IRouter, - licenseState: ILicenseState + licenseState: ILicenseState, + usageCounter?: UsageCounter ) => { router.get( { @@ -120,12 +123,27 @@ export const findRulesRoute = ( verifyAccessAndContext(licenseState, async function (context, req, res) { const rulesClient = context.alerting.getRulesClient(); + trackLegacyTerminology( + [req.query.search, req.query.search_fields, req.query.sort_field].filter( + Boolean + ) as string[], + usageCounter + ); + const options = rewriteQueryReq({ ...req.query, has_reference: req.query.has_reference || undefined, search_fields: searchFieldsAsArray(req.query.search_fields), }); + if (req.query.fields) { + usageCounter?.incrementCounter({ + counterName: `alertingFieldsUsage`, + counterType: 'alertingFieldsUsage', + incrementBy: 1, + }); + } + const findResult = await rulesClient.find({ options }); return res.ok({ body: rewriteBodyRes(findResult), diff --git a/x-pack/plugins/alerting/server/routes/index.ts b/x-pack/plugins/alerting/server/routes/index.ts index 703cce7517665..6579bbc005c5c 100644 --- a/x-pack/plugins/alerting/server/routes/index.ts +++ b/x-pack/plugins/alerting/server/routes/index.ts @@ -38,7 +38,7 @@ export interface RouteOptions { } export function defineRoutes(opts: RouteOptions) { - const { router, licenseState, encryptedSavedObjects } = opts; + const { router, licenseState, encryptedSavedObjects, usageCounter } = opts; defineLegacyRoutes(opts); createRuleRoute(opts); @@ -49,7 +49,7 @@ export function defineRoutes(opts: RouteOptions) { aggregateRulesRoute(router, licenseState); disableRuleRoute(router, licenseState); enableRuleRoute(router, licenseState); - findRulesRoute(router, licenseState); + findRulesRoute(router, licenseState, usageCounter); getRuleAlertSummaryRoute(router, licenseState); getRuleStateRoute(router, licenseState); healthRoute(router, licenseState, encryptedSavedObjects); diff --git a/x-pack/plugins/alerting/server/routes/legacy/aggregate.test.ts b/x-pack/plugins/alerting/server/routes/legacy/aggregate.test.ts index d08e970eef69d..cab779a42ce20 100644 --- a/x-pack/plugins/alerting/server/routes/legacy/aggregate.test.ts +++ b/x-pack/plugins/alerting/server/routes/legacy/aggregate.test.ts @@ -12,6 +12,7 @@ import { verifyApiAccess } from '../../lib/license_api_access'; import { mockHandlerArguments } from './../_mock_handler_arguments'; import { rulesClientMock } from '../../rules_client.mock'; import { trackLegacyRouteUsage } from '../../lib/track_legacy_route_usage'; +import { trackLegacyTerminology } from '../lib/track_legacy_terminology'; import { usageCountersServiceMock } from 'src/plugins/usage_collection/server/usage_counters/usage_counters_service.mock'; const rulesClient = rulesClientMock.create(); @@ -26,6 +27,10 @@ jest.mock('../../lib/license_api_access.ts', () => ({ verifyApiAccess: jest.fn(), })); +jest.mock('../lib/track_legacy_terminology', () => ({ + trackLegacyTerminology: jest.fn(), +})); + beforeEach(() => { jest.resetAllMocks(); }); @@ -166,4 +171,29 @@ describe('aggregateAlertRoute', () => { await handler(context, req, res); expect(trackLegacyRouteUsage).toHaveBeenCalledWith('aggregate', mockUsageCounter); }); + + it('should track calls with deprecated param values', async () => { + const licenseState = licenseStateMock.create(); + const router = httpServiceMock.createRouter(); + + aggregateAlertRoute(router, licenseState, mockUsageCounter); + const [, handler] = router.get.mock.calls[0]; + const [context, req, res] = mockHandlerArguments( + { rulesClient }, + { + params: {}, + query: { + search_fields: ['alertTypeId:1', 'message:foo'], + search: 'alertTypeId:2', + }, + }, + ['ok'] + ); + await handler(context, req, res); + expect(trackLegacyTerminology).toHaveBeenCalledTimes(1); + expect((trackLegacyTerminology as jest.Mock).mock.calls[0][0]).toStrictEqual([ + 'alertTypeId:2', + ['alertTypeId:1', 'message:foo'], + ]); + }); }); diff --git a/x-pack/plugins/alerting/server/routes/legacy/aggregate.ts b/x-pack/plugins/alerting/server/routes/legacy/aggregate.ts index d1e8d98a95409..bd4aa7750738c 100644 --- a/x-pack/plugins/alerting/server/routes/legacy/aggregate.ts +++ b/x-pack/plugins/alerting/server/routes/legacy/aggregate.ts @@ -14,6 +14,7 @@ import { LEGACY_BASE_ALERT_API_PATH } from '../../../common'; import { renameKeys } from './../lib/rename_keys'; import { FindOptions } from '../../rules_client'; import { trackLegacyRouteUsage } from '../../lib/track_legacy_route_usage'; +import { trackLegacyTerminology } from '../lib/track_legacy_terminology'; // config definition const querySchema = schema.object({ @@ -55,6 +56,10 @@ export const aggregateAlertRoute = ( const rulesClient = context.alerting.getRulesClient(); trackLegacyRouteUsage('aggregate', usageCounter); + trackLegacyTerminology( + [req.query.search, req.query.search_fields].filter(Boolean) as string[], + usageCounter + ); const query = req.query; const renameMap = { diff --git a/x-pack/plugins/alerting/server/routes/legacy/find.test.ts b/x-pack/plugins/alerting/server/routes/legacy/find.test.ts index de12c62f1e4fd..ef346fab1e621 100644 --- a/x-pack/plugins/alerting/server/routes/legacy/find.test.ts +++ b/x-pack/plugins/alerting/server/routes/legacy/find.test.ts @@ -12,6 +12,7 @@ import { verifyApiAccess } from '../../lib/license_api_access'; import { mockHandlerArguments } from './../_mock_handler_arguments'; import { rulesClientMock } from '../../rules_client.mock'; import { trackLegacyRouteUsage } from '../../lib/track_legacy_route_usage'; +import { trackLegacyTerminology } from '../lib/track_legacy_terminology'; const rulesClient = rulesClientMock.create(); @@ -23,6 +24,10 @@ jest.mock('../../lib/track_legacy_route_usage', () => ({ trackLegacyRouteUsage: jest.fn(), })); +jest.mock('../lib/track_legacy_terminology', () => ({ + trackLegacyTerminology: jest.fn(), +})); + beforeEach(() => { jest.resetAllMocks(); }); @@ -160,4 +165,59 @@ describe('findAlertRoute', () => { await handler(context, req, res); expect(trackLegacyRouteUsage).toHaveBeenCalledWith('find', mockUsageCounter); }); + + it('should track calls with deprecated param values', async () => { + const licenseState = licenseStateMock.create(); + const router = httpServiceMock.createRouter(); + const mockUsageCountersSetup = usageCountersServiceMock.createSetupContract(); + const mockUsageCounter = mockUsageCountersSetup.createUsageCounter('test'); + + findAlertRoute(router, licenseState, mockUsageCounter); + const [, handler] = router.get.mock.calls[0]; + const [context, req, res] = mockHandlerArguments( + { rulesClient }, + { + params: {}, + query: { + search_fields: ['alertTypeId:1', 'message:foo'], + search: 'alertTypeId:2', + sort_field: 'alertTypeId', + }, + }, + ['ok'] + ); + await handler(context, req, res); + expect(trackLegacyTerminology).toHaveBeenCalledTimes(1); + expect((trackLegacyTerminology as jest.Mock).mock.calls[0][0]).toStrictEqual([ + 'alertTypeId:2', + ['alertTypeId:1', 'message:foo'], + 'alertTypeId', + ]); + }); + + it('should track calls to deprecated functionality', async () => { + const licenseState = licenseStateMock.create(); + const router = httpServiceMock.createRouter(); + const mockUsageCountersSetup = usageCountersServiceMock.createSetupContract(); + const mockUsageCounter = mockUsageCountersSetup.createUsageCounter('test'); + + findAlertRoute(router, licenseState, mockUsageCounter); + const [, handler] = router.get.mock.calls[0]; + const [context, req, res] = mockHandlerArguments( + { rulesClient }, + { + params: {}, + query: { + fields: ['foo', 'bar'], + }, + }, + ['ok'] + ); + await handler(context, req, res); + expect(mockUsageCounter.incrementCounter).toHaveBeenCalledWith({ + counterName: `legacyAlertingFieldsUsage`, + counterType: 'alertingFieldsUsage', + incrementBy: 1, + }); + }); }); diff --git a/x-pack/plugins/alerting/server/routes/legacy/find.ts b/x-pack/plugins/alerting/server/routes/legacy/find.ts index f915f0e15afb6..328fade491642 100644 --- a/x-pack/plugins/alerting/server/routes/legacy/find.ts +++ b/x-pack/plugins/alerting/server/routes/legacy/find.ts @@ -15,6 +15,7 @@ import { LEGACY_BASE_ALERT_API_PATH } from '../../../common'; import { renameKeys } from './../lib/rename_keys'; import { FindOptions } from '../../rules_client'; import { trackLegacyRouteUsage } from '../../lib/track_legacy_route_usage'; +import { trackLegacyTerminology } from '../lib/track_legacy_terminology'; // config definition const querySchema = schema.object({ @@ -59,6 +60,12 @@ export const findAlertRoute = ( return res.badRequest({ body: 'RouteHandlerContext is not registered for alerting' }); } trackLegacyRouteUsage('find', usageCounter); + trackLegacyTerminology( + [req.query.search, req.query.search_fields, req.query.sort_field].filter( + Boolean + ) as string[], + usageCounter + ); const rulesClient = context.alerting.getRulesClient(); const query = req.query; @@ -82,6 +89,14 @@ export const findAlertRoute = ( : [query.search_fields]; } + if (query.fields) { + usageCounter?.incrementCounter({ + counterName: `legacyAlertingFieldsUsage`, + counterType: 'alertingFieldsUsage', + incrementBy: 1, + }); + } + const findResult = await rulesClient.find({ options }); return res.ok({ body: findResult, diff --git a/x-pack/plugins/alerting/server/routes/lib/track_legacy_terminology.test.ts b/x-pack/plugins/alerting/server/routes/lib/track_legacy_terminology.test.ts new file mode 100644 index 0000000000000..5c6838e144aa2 --- /dev/null +++ b/x-pack/plugins/alerting/server/routes/lib/track_legacy_terminology.test.ts @@ -0,0 +1,48 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import { usageCountersServiceMock } from 'src/plugins/usage_collection/server/usage_counters/usage_counters_service.mock'; +import { trackLegacyTerminology, LEGACY_TERMS } from './track_legacy_terminology'; + +describe('trackLegacyTerminology', () => { + it('should call `usageCounter.incrementCounter`', () => { + const mockUsageCountersSetup = usageCountersServiceMock.createSetupContract(); + const mockUsageCounter = mockUsageCountersSetup.createUsageCounter('test'); + + trackLegacyTerminology( + ['shouldNotMatch', LEGACY_TERMS.map((lt) => `${lt}foo`)], + mockUsageCounter + ); + expect(mockUsageCounter.incrementCounter).toHaveBeenCalledTimes(LEGACY_TERMS.length); + LEGACY_TERMS.forEach((legacyTerm, index) => { + expect((mockUsageCounter.incrementCounter as jest.Mock).mock.calls[index][0]).toStrictEqual({ + counterName: `legacyTerm_${legacyTerm}`, + counterType: 'legacyTerminology', + incrementBy: 1, + }); + }); + }); + + it('should do nothing if no usage counter is provided', () => { + let err; + try { + trackLegacyTerminology(['test'], undefined); + } catch (e) { + err = e; + } + expect(err).toBeUndefined(); + }); + + it('should do nothing if no terms are provided', () => { + let err; + try { + trackLegacyTerminology([], undefined); + } catch (e) { + err = e; + } + expect(err).toBeUndefined(); + }); +}); diff --git a/x-pack/plugins/alerting/server/routes/lib/track_legacy_terminology.ts b/x-pack/plugins/alerting/server/routes/lib/track_legacy_terminology.ts new file mode 100644 index 0000000000000..e825cb990fc4d --- /dev/null +++ b/x-pack/plugins/alerting/server/routes/lib/track_legacy_terminology.ts @@ -0,0 +1,35 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import { flatten } from 'lodash'; +import { UsageCounter } from 'src/plugins/usage_collection/server'; + +export const LEGACY_TERMS = ['alertTypeId', 'actionTypeId']; + +export function trackLegacyTerminology( + terms: Array, + usageCounter?: UsageCounter +) { + if (!usageCounter) { + return null; + } + + if (!terms || terms.length === 0) { + return null; + } + + for (const legacyTerm of LEGACY_TERMS) { + for (const term of flatten(terms)) { + if (term.includes(legacyTerm)) { + usageCounter.incrementCounter({ + counterName: `legacyTerm_${legacyTerm}`, + counterType: 'legacyTerminology', + incrementBy: 1, + }); + } + } + } +} 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 505fd5c1020b3..bbc77bcabca52 100644 --- a/x-pack/plugins/apm/public/components/app/RumDashboard/RumHome.tsx +++ b/x-pack/plugins/apm/public/components/app/RumDashboard/RumHome.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import React from 'react'; +import React, { Fragment } from 'react'; import { i18n } from '@kbn/i18n'; import { EuiFlexGroup, EuiTitle, EuiFlexItem } from '@elastic/eui'; import { RumOverview } from '../RumDashboard'; @@ -18,6 +18,7 @@ import { UserPercentile } from './UserPercentile'; import { useBreakpoints } from '../../../hooks/use_breakpoints'; import { KibanaPageTemplateProps } from '../../../../../../../src/plugins/kibana_react/public'; import { useHasRumData } from './hooks/useHasRumData'; +import { EmptyStateLoading } from './empty_state_loading'; export const UX_LABEL = i18n.translate('xpack.apm.ux.title', { defaultMessage: 'Dashboard', @@ -29,7 +30,7 @@ export function RumHome() { const { isSmall, isXXL } = useBreakpoints(); - const { data: rumHasData } = useHasRumData(); + const { data: rumHasData, status } = useHasRumData(); const envStyle = isSmall ? {} : { maxWidth: 500 }; @@ -58,31 +59,38 @@ export function RumHome() { } : undefined; + const isLoading = status === 'loading'; + return ( - - , -
- -
, - , - , - ], - } - : { children: } - } - > - -
-
+ + + , +
+ +
, + , + , + ], + } + : { children: } + } + > + {isLoading && } +
+ +
+
+
+
); } diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdownMap/useLayerList.ts b/x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdownMap/useLayerList.ts index f1b5b67da21f1..ce42b530b80f5 100644 --- a/x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdownMap/useLayerList.ts +++ b/x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdownMap/useLayerList.ts @@ -15,6 +15,7 @@ import { COLOR_MAP_TYPE, FIELD_ORIGIN, LABEL_BORDER_SIZES, + LAYER_TYPE, SOURCE_TYPES, STYLE_TYPE, SYMBOLIZE_AS_TYPES, @@ -154,7 +155,7 @@ export function useLayerList() { maxZoom: 24, alpha: 0.75, visible: true, - type: 'VECTOR', + type: LAYER_TYPE.VECTOR, }; ES_TERM_SOURCE_REGION.whereQuery = getWhereQuery(serviceName!); @@ -178,7 +179,7 @@ export function useLayerList() { maxZoom: 24, alpha: 0.75, visible: true, - type: 'VECTOR', + type: LAYER_TYPE.VECTOR, }; return [ diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/empty_state_loading.tsx b/x-pack/plugins/apm/public/components/app/RumDashboard/empty_state_loading.tsx new file mode 100644 index 0000000000000..b02672721ce8e --- /dev/null +++ b/x-pack/plugins/apm/public/components/app/RumDashboard/empty_state_loading.tsx @@ -0,0 +1,35 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { + EuiEmptyPrompt, + EuiLoadingSpinner, + EuiSpacer, + EuiTitle, +} from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import React, { Fragment } from 'react'; + +export function EmptyStateLoading() { + return ( + + + + +

+ {i18n.translate('xpack.apm.emptyState.loadingMessage', { + defaultMessage: 'Loading…', + })} +

+
+ + } + /> + ); +} diff --git a/x-pack/plugins/apm/public/components/app/correlations/custom_fields.tsx b/x-pack/plugins/apm/public/components/app/correlations/custom_fields.tsx deleted file mode 100644 index 9a13c44602c2d..0000000000000 --- a/x-pack/plugins/apm/public/components/app/correlations/custom_fields.tsx +++ /dev/null @@ -1,166 +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 { - EuiFlexGroup, - EuiFlexItem, - EuiAccordion, - EuiComboBox, - EuiFormRow, - EuiLink, - EuiSelect, - EuiSpacer, -} from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; -import { FormattedMessage } from '@kbn/i18n/react'; -import React, { useEffect, useState } from 'react'; -import { useFieldNames } from './use_field_names'; -import { ElasticDocsLink } from '../../shared/Links/ElasticDocsLink'; -import { useUiTracker } from '../../../../../observability/public'; - -interface Props { - fieldNames: string[]; - setFieldNames: (fieldNames: string[]) => void; - setDurationPercentile?: (value: PercentileOption) => void; - showThreshold?: boolean; - durationPercentile?: PercentileOption; -} - -export type PercentileOption = 50 | 75 | 99; -const percentilOptions: PercentileOption[] = [50, 75, 99]; - -export function CustomFields({ - fieldNames, - setFieldNames, - setDurationPercentile = () => {}, - showThreshold = false, - durationPercentile = 75, -}: Props) { - const trackApmEvent = useUiTracker({ app: 'apm' }); - const { defaultFieldNames, getSuggestions } = useFieldNames(); - const [suggestedFieldNames, setSuggestedFieldNames] = useState( - getSuggestions('') - ); - - useEffect(() => { - if (suggestedFieldNames.length) { - return; - } - setSuggestedFieldNames(getSuggestions('')); - }, [getSuggestions, suggestedFieldNames]); - - return ( - - - - {showThreshold && ( - - - ({ - value: percentile, - text: i18n.translate( - 'xpack.apm.correlations.customize.thresholdPercentile', - { - defaultMessage: '{percentile}th percentile', - values: { percentile }, - } - ), - }))} - onChange={(e) => { - setDurationPercentile( - parseInt(e.target.value, 10) as PercentileOption - ); - }} - /> - - - )} - - { - setFieldNames(defaultFieldNames); - }} - > - {i18n.translate( - 'xpack.apm.correlations.customize.fieldHelpTextReset', - { defaultMessage: 'reset' } - )} - - ), - docsLink: ( - - {i18n.translate( - 'xpack.apm.correlations.customize.fieldHelpTextDocsLink', - { - defaultMessage: - 'Learn more about the default fields.', - } - )} - - ), - }} - /> - } - > - ({ label }))} - onChange={(options) => { - const nextFieldNames = options.map((option) => option.label); - setFieldNames(nextFieldNames); - trackApmEvent({ metric: 'customize_correlations_fields' }); - }} - onCreateOption={(term) => { - const nextFieldNames = [...fieldNames, term]; - setFieldNames(nextFieldNames); - }} - onSearchChange={(searchValue) => { - setSuggestedFieldNames(getSuggestions(searchValue)); - }} - options={suggestedFieldNames.map((label) => ({ label }))} - /> - - - - - ); -} diff --git a/x-pack/plugins/apm/public/components/app/correlations/use_field_names.ts b/x-pack/plugins/apm/public/components/app/correlations/use_field_names.ts deleted file mode 100644 index ff88808c51d15..0000000000000 --- a/x-pack/plugins/apm/public/components/app/correlations/use_field_names.ts +++ /dev/null @@ -1,74 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { memoize } from 'lodash'; -import { useEffect, useMemo, useState } from 'react'; -import { isRumAgentName } from '../../../../common/agent_name'; -import { useApmServiceContext } from '../../../context/apm_service/use_apm_service_context'; -import { useDynamicIndexPatternFetcher } from '../../../hooks/use_dynamic_index_pattern'; - -interface IndexPattern { - fields: Array<{ name: string; esTypes: string[] }>; -} - -export function useFieldNames() { - const { agentName } = useApmServiceContext(); - const isRumAgent = isRumAgentName(agentName); - const { indexPattern } = useDynamicIndexPatternFetcher(); - - const [defaultFieldNames, setDefaultFieldNames] = useState( - getDefaultFieldNames(indexPattern, isRumAgent) - ); - - const getSuggestions = useMemo( - () => - memoize((searchValue: string) => - getMatchingFieldNames(indexPattern, searchValue) - ), - [indexPattern] - ); - - useEffect(() => { - setDefaultFieldNames(getDefaultFieldNames(indexPattern, isRumAgent)); - }, [indexPattern, isRumAgent]); - - return { defaultFieldNames, getSuggestions }; -} - -function getMatchingFieldNames( - indexPattern: IndexPattern | undefined, - inputValue: string -) { - if (!indexPattern) { - return []; - } - return indexPattern.fields - .filter( - ({ name, esTypes }) => - name.startsWith(inputValue) && esTypes[0] === 'keyword' // only show fields of type 'keyword' - ) - .map(({ name }) => name); -} - -function getDefaultFieldNames( - indexPattern: IndexPattern | undefined, - isRumAgent: boolean -) { - const labelFields = getMatchingFieldNames(indexPattern, 'labels.').slice( - 0, - 6 - ); - return isRumAgent - ? [ - ...labelFields, - 'user_agent.name', - 'user_agent.os.name', - 'url.original', - ...getMatchingFieldNames(indexPattern, 'user.').slice(0, 6), - ] - : [...labelFields, 'service.version', 'service.node.name', 'host.ip']; -} 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 e8a159f23ee3d..535fb777166bb 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 @@ -256,6 +256,7 @@ export function TransactionDistributionChart({ /> {data.map((d, i) => ( - + {value} diff --git a/x-pack/plugins/apm/public/hooks/use_search_strategy.ts b/x-pack/plugins/apm/public/hooks/use_search_strategy.ts index 32fc31ad80fe4..ca8d28b106f84 100644 --- a/x-pack/plugins/apm/public/hooks/use_search_strategy.ts +++ b/x-pack/plugins/apm/public/hooks/use_search_strategy.ts @@ -156,8 +156,8 @@ export function useSearchStrategy< setRawResponse(response.rawResponse); setFetchState({ isRunning: response.isRunning || false, - loaded: response.loaded, - total: response.total, + ...(response.loaded ? { loaded: response.loaded } : {}), + ...(response.total ? { total: response.total } : {}), }); if (isCompleteResponse(response)) { diff --git a/x-pack/plugins/apm/server/index.ts b/x-pack/plugins/apm/server/index.ts index 6ba412bd22029..2c21ff17f779b 100644 --- a/x-pack/plugins/apm/server/index.ts +++ b/x-pack/plugins/apm/server/index.ts @@ -6,56 +6,62 @@ */ import { schema, TypeOf } from '@kbn/config-schema'; -import { PluginInitializerContext } from 'src/core/server'; +import { + PluginInitializerContext, + PluginConfigDescriptor, +} from 'src/core/server'; import { APMOSSConfig } from 'src/plugins/apm_oss/server'; import { APMPlugin } from './plugin'; import { SearchAggregatedTransactionSetting } from '../common/aggregated_transactions'; +const configSchema = schema.object({ + enabled: schema.boolean({ defaultValue: true }), + serviceMapEnabled: schema.boolean({ defaultValue: true }), + serviceMapFingerprintBucketSize: schema.number({ defaultValue: 100 }), + serviceMapTraceIdBucketSize: schema.number({ defaultValue: 65 }), + serviceMapFingerprintGlobalBucketSize: schema.number({ + defaultValue: 1000, + }), + serviceMapTraceIdGlobalBucketSize: schema.number({ defaultValue: 6 }), + serviceMapMaxTracesPerRequest: schema.number({ defaultValue: 50 }), + autocreateApmIndexPattern: schema.boolean({ defaultValue: true }), + ui: schema.object({ + enabled: schema.boolean({ defaultValue: true }), + transactionGroupBucketSize: schema.number({ defaultValue: 1000 }), + maxTraceItems: schema.number({ defaultValue: 1000 }), + }), + searchAggregatedTransactions: schema.oneOf( + [ + schema.literal(SearchAggregatedTransactionSetting.auto), + schema.literal(SearchAggregatedTransactionSetting.always), + schema.literal(SearchAggregatedTransactionSetting.never), + ], + { defaultValue: SearchAggregatedTransactionSetting.auto } + ), + telemetryCollectionEnabled: schema.boolean({ defaultValue: true }), + metricsInterval: schema.number({ defaultValue: 30 }), + maxServiceEnvironments: schema.number({ defaultValue: 100 }), + maxServiceSelection: schema.number({ defaultValue: 50 }), + profilingEnabled: schema.boolean({ defaultValue: false }), + agent: schema.object({ + migrations: schema.object({ + enabled: schema.boolean({ defaultValue: false }), + }), + }), +}); + // plugin config -export const config = { +export const config: PluginConfigDescriptor = { + deprecations: ({ deprecate }) => [deprecate('enabled', '8.0.0')], exposeToBrowser: { serviceMapEnabled: true, ui: true, profilingEnabled: true, }, - schema: schema.object({ - enabled: schema.boolean({ defaultValue: true }), - serviceMapEnabled: schema.boolean({ defaultValue: true }), - serviceMapFingerprintBucketSize: schema.number({ defaultValue: 100 }), - serviceMapTraceIdBucketSize: schema.number({ defaultValue: 65 }), - serviceMapFingerprintGlobalBucketSize: schema.number({ - defaultValue: 1000, - }), - serviceMapTraceIdGlobalBucketSize: schema.number({ defaultValue: 6 }), - serviceMapMaxTracesPerRequest: schema.number({ defaultValue: 50 }), - autocreateApmIndexPattern: schema.boolean({ defaultValue: true }), - ui: schema.object({ - enabled: schema.boolean({ defaultValue: true }), - transactionGroupBucketSize: schema.number({ defaultValue: 1000 }), - maxTraceItems: schema.number({ defaultValue: 1000 }), - }), - searchAggregatedTransactions: schema.oneOf( - [ - schema.literal(SearchAggregatedTransactionSetting.auto), - schema.literal(SearchAggregatedTransactionSetting.always), - schema.literal(SearchAggregatedTransactionSetting.never), - ], - { defaultValue: SearchAggregatedTransactionSetting.auto } - ), - telemetryCollectionEnabled: schema.boolean({ defaultValue: true }), - metricsInterval: schema.number({ defaultValue: 30 }), - maxServiceEnvironments: schema.number({ defaultValue: 100 }), - maxServiceSelection: schema.number({ defaultValue: 50 }), - profilingEnabled: schema.boolean({ defaultValue: false }), - agent: schema.object({ - migrations: schema.object({ - enabled: schema.boolean({ defaultValue: false }), - }), - }), - }), + schema: configSchema, }; -export type APMXPackConfig = TypeOf; +export type APMXPackConfig = TypeOf; export type APMConfig = ReturnType; // plugin config and ui indices settings diff --git a/x-pack/plugins/apm/server/lib/search_strategies/queries/query_histogram_interval.test.ts b/x-pack/plugins/apm/server/lib/search_strategies/queries/query_histogram_interval.test.ts deleted file mode 100644 index e6cf926d20bd7..0000000000000 --- a/x-pack/plugins/apm/server/lib/search_strategies/queries/query_histogram_interval.test.ts +++ /dev/null @@ -1,111 +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 { estypes } from '@elastic/elasticsearch'; - -import type { ElasticsearchClient } from 'src/core/server'; -import { ENVIRONMENT_ALL } from '../../../../common/environment_filter_values'; - -import { - fetchTransactionDurationHistogramInterval, - getHistogramIntervalRequest, -} from './query_histogram_interval'; - -const params = { - index: 'apm-*', - start: '2020', - end: '2021', - includeFrozen: false, - environment: ENVIRONMENT_ALL.value, - kuery: '', -}; - -describe('query_histogram_interval', () => { - describe('getHistogramIntervalRequest', () => { - it('returns the request body for the transaction duration ranges aggregation', () => { - const req = getHistogramIntervalRequest(params); - - expect(req).toEqual({ - body: { - aggs: { - transaction_duration_max: { - max: { - field: 'transaction.duration.us', - }, - }, - transaction_duration_min: { - min: { - field: 'transaction.duration.us', - }, - }, - }, - query: { - bool: { - filter: [ - { - term: { - 'processor.event': 'transaction', - }, - }, - { - range: { - '@timestamp': { - format: 'epoch_millis', - gte: 1577836800000, - lte: 1609459200000, - }, - }, - }, - ], - }, - }, - size: 0, - }, - index: params.index, - ignore_throttled: !params.includeFrozen, - ignore_unavailable: true, - }); - }); - }); - - describe('fetchTransactionDurationHistogramInterval', () => { - it('fetches the interval duration for histograms', async () => { - const esClientSearchMock = jest.fn( - ( - req: estypes.SearchRequest - ): { - body: estypes.SearchResponse; - } => { - return { - body: { - aggregations: { - transaction_duration_max: { - value: 10000, - }, - transaction_duration_min: { - value: 10, - }, - }, - } as unknown as estypes.SearchResponse, - }; - } - ); - - const esClientMock = { - search: esClientSearchMock, - } as unknown as ElasticsearchClient; - - const resp = await fetchTransactionDurationHistogramInterval( - esClientMock, - params - ); - - expect(resp).toEqual(10); - expect(esClientSearchMock).toHaveBeenCalledTimes(1); - }); - }); -}); diff --git a/x-pack/plugins/apm/server/lib/search_strategies/queries/query_histogram_interval.ts b/x-pack/plugins/apm/server/lib/search_strategies/queries/query_histogram_interval.ts deleted file mode 100644 index 906105003b716..0000000000000 --- a/x-pack/plugins/apm/server/lib/search_strategies/queries/query_histogram_interval.ts +++ /dev/null @@ -1,57 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import type { estypes } from '@elastic/elasticsearch'; - -import type { ElasticsearchClient } from 'src/core/server'; - -import { TRANSACTION_DURATION } from '../../../../common/elasticsearch_fieldnames'; -import type { SearchStrategyParams } from '../../../../common/search_strategies/types'; - -import { getQueryWithParams } from './get_query_with_params'; -import { getRequestBase } from './get_request_base'; - -const HISTOGRAM_INTERVALS = 1000; - -export const getHistogramIntervalRequest = ( - params: SearchStrategyParams -): estypes.SearchRequest => ({ - ...getRequestBase(params), - body: { - query: getQueryWithParams({ params }), - size: 0, - aggs: { - transaction_duration_min: { min: { field: TRANSACTION_DURATION } }, - transaction_duration_max: { max: { field: TRANSACTION_DURATION } }, - }, - }, -}); - -export const fetchTransactionDurationHistogramInterval = async ( - esClient: ElasticsearchClient, - params: SearchStrategyParams -): Promise => { - const resp = await esClient.search(getHistogramIntervalRequest(params)); - - if (resp.body.aggregations === undefined) { - throw new Error( - 'fetchTransactionDurationHistogramInterval failed, did not return aggregations.' - ); - } - - const transactionDurationDelta = - ( - resp.body.aggregations - .transaction_duration_max as estypes.AggregationsValueAggregate - ).value - - ( - resp.body.aggregations - .transaction_duration_min as estypes.AggregationsValueAggregate - ).value; - - return transactionDurationDelta / (HISTOGRAM_INTERVALS - 1); -}; 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 index b56ab83f547ff..6e03c879f9b97 100644 --- 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 @@ -57,17 +57,6 @@ const clientSearchMock = ( aggregations = { transaction_duration_percentiles: { values: {} } }; } - // fetchTransactionDurationHistogramInterval - if ( - aggs.transaction_duration_min !== undefined && - aggs.transaction_duration_max !== undefined - ) { - aggregations = { - transaction_duration_min: { value: 0 }, - transaction_duration_max: { value: 1234 }, - }; - } - // fetchTransactionDurationCorrelation if (aggs.logspace_ranges !== undefined) { aggregations = { logspace_ranges: { buckets: [] } }; diff --git a/x-pack/plugins/apm/server/routes/correlations.ts b/x-pack/plugins/apm/server/routes/correlations.ts deleted file mode 100644 index 4728aa2e8d3f6..0000000000000 --- a/x-pack/plugins/apm/server/routes/correlations.ts +++ /dev/null @@ -1,214 +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 { i18n } from '@kbn/i18n'; -import * as t from 'io-ts'; -import { isActivePlatinumLicense } from '../../common/license_check'; -import { getCorrelationsForFailedTransactions } from '../lib/correlations/errors/get_correlations_for_failed_transactions'; -import { getOverallErrorTimeseries } from '../lib/correlations/errors/get_overall_error_timeseries'; -import { getCorrelationsForSlowTransactions } from '../lib/correlations/latency/get_correlations_for_slow_transactions'; -import { getOverallLatencyDistribution } from '../lib/correlations/latency/get_overall_latency_distribution'; -import { setupRequest } from '../lib/helpers/setup_request'; -import { createApmServerRoute } from './create_apm_server_route'; -import { createApmServerRouteRepository } from './create_apm_server_route_repository'; -import { environmentRt, kueryRt, rangeRt } from './default_api_types'; - -const INVALID_LICENSE = i18n.translate( - 'xpack.apm.significanTerms.license.text', - { - defaultMessage: - 'To use the correlations API, you must be subscribed to an Elastic Platinum license.', - } -); - -const correlationsLatencyDistributionRoute = createApmServerRoute({ - endpoint: 'GET /api/apm/correlations/latency/overall_distribution', - params: t.type({ - query: t.intersection([ - t.partial({ - serviceName: t.string, - transactionName: t.string, - transactionType: t.string, - }), - environmentRt, - kueryRt, - rangeRt, - ]), - }), - options: { tags: ['access:apm'] }, - handler: async (resources) => { - const { context, params } = resources; - if (!isActivePlatinumLicense(context.licensing.license)) { - throw Boom.forbidden(INVALID_LICENSE); - } - const setup = await setupRequest(resources); - const { - environment, - kuery, - serviceName, - transactionType, - transactionName, - } = params.query; - - return getOverallLatencyDistribution({ - environment, - kuery, - serviceName, - transactionType, - transactionName, - setup, - }); - }, -}); - -const correlationsForSlowTransactionsRoute = createApmServerRoute({ - endpoint: 'GET /api/apm/correlations/latency/slow_transactions', - params: t.type({ - query: t.intersection([ - t.partial({ - serviceName: t.string, - transactionName: t.string, - transactionType: t.string, - }), - t.type({ - durationPercentile: t.string, - fieldNames: t.string, - maxLatency: t.string, - distributionInterval: t.string, - }), - environmentRt, - kueryRt, - rangeRt, - ]), - }), - options: { tags: ['access:apm'] }, - handler: async (resources) => { - const { context, params } = resources; - - if (!isActivePlatinumLicense(context.licensing.license)) { - throw Boom.forbidden(INVALID_LICENSE); - } - const setup = await setupRequest(resources); - const { - environment, - kuery, - serviceName, - transactionType, - transactionName, - durationPercentile, - fieldNames, - maxLatency, - distributionInterval, - } = params.query; - - return getCorrelationsForSlowTransactions({ - environment, - kuery, - serviceName, - transactionType, - transactionName, - durationPercentile: parseInt(durationPercentile, 10), - fieldNames: fieldNames.split(','), - setup, - maxLatency: parseInt(maxLatency, 10), - distributionInterval: parseInt(distributionInterval, 10), - }); - }, -}); - -const correlationsErrorDistributionRoute = createApmServerRoute({ - endpoint: 'GET /api/apm/correlations/errors/overall_timeseries', - params: t.type({ - query: t.intersection([ - t.partial({ - serviceName: t.string, - transactionName: t.string, - transactionType: t.string, - }), - environmentRt, - kueryRt, - rangeRt, - ]), - }), - options: { tags: ['access:apm'] }, - handler: async (resources) => { - const { params, context } = resources; - - if (!isActivePlatinumLicense(context.licensing.license)) { - throw Boom.forbidden(INVALID_LICENSE); - } - const setup = await setupRequest(resources); - const { - environment, - kuery, - serviceName, - transactionType, - transactionName, - } = params.query; - - return getOverallErrorTimeseries({ - environment, - kuery, - serviceName, - transactionType, - transactionName, - setup, - }); - }, -}); - -const correlationsForFailedTransactionsRoute = createApmServerRoute({ - endpoint: 'GET /api/apm/correlations/errors/failed_transactions', - params: t.type({ - query: t.intersection([ - t.partial({ - serviceName: t.string, - transactionName: t.string, - transactionType: t.string, - }), - t.type({ - fieldNames: t.string, - }), - environmentRt, - kueryRt, - rangeRt, - ]), - }), - options: { tags: ['access:apm'] }, - handler: async (resources) => { - const { context, params } = resources; - if (!isActivePlatinumLicense(context.licensing.license)) { - throw Boom.forbidden(INVALID_LICENSE); - } - const setup = await setupRequest(resources); - const { - environment, - kuery, - serviceName, - transactionType, - transactionName, - fieldNames, - } = params.query; - - return getCorrelationsForFailedTransactions({ - environment, - kuery, - serviceName, - transactionType, - transactionName, - fieldNames: fieldNames.split(','), - setup, - }); - }, -}); - -export const correlationsRouteRepository = createApmServerRouteRepository() - .add(correlationsLatencyDistributionRoute) - .add(correlationsForSlowTransactionsRoute) - .add(correlationsErrorDistributionRoute) - .add(correlationsForFailedTransactionsRoute); 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 index 9bc9108da9055..09756e30d9682 100644 --- 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 @@ -12,7 +12,6 @@ import type { import { PickByValue } from 'utility-types'; import { alertsChartPreviewRouteRepository } from './alerts/chart_preview'; import { backendsRouteRepository } from './backends'; -import { correlationsRouteRepository } from './correlations'; import { createApmServerRouteRepository } from './create_apm_server_route_repository'; import { environmentsRouteRepository } from './environments'; import { errorsRouteRepository } from './errors'; @@ -49,7 +48,6 @@ const getTypedGlobalApmServerRouteRepository = () => { .merge(traceRouteRepository) .merge(transactionRouteRepository) .merge(alertsChartPreviewRouteRepository) - .merge(correlationsRouteRepository) .merge(agentConfigurationRouteRepository) .merge(anomalyDetectionRouteRepository) .merge(apmIndicesRouteRepository) diff --git a/x-pack/plugins/canvas/public/application.tsx b/x-pack/plugins/canvas/public/application.tsx index 30b2d78a6b1fe..f2fe944bfd45d 100644 --- a/x-pack/plugins/canvas/public/application.tsx +++ b/x-pack/plugins/canvas/public/application.tsx @@ -98,11 +98,10 @@ export const initializeCanvas = async ( setupPlugins: CanvasSetupDeps, startPlugins: CanvasStartDeps, registries: SetupRegistries, - appUpdater: BehaviorSubject, - pluginServices: PluginServices + appUpdater: BehaviorSubject ) => { await startLegacyServices(coreSetup, coreStart, setupPlugins, startPlugins, appUpdater); - const { expressions } = pluginServices.getServices(); + const { expressions } = setupPlugins; // Adding these functions here instead of in plugin.ts. // Some of these functions have deep dependencies into Canvas, which was bulking up the size diff --git a/x-pack/plugins/canvas/public/plugin.tsx b/x-pack/plugins/canvas/public/plugin.tsx index 555cedb6b16a1..bd5d884f1485c 100644 --- a/x-pack/plugins/canvas/public/plugin.tsx +++ b/x-pack/plugins/canvas/public/plugin.tsx @@ -132,8 +132,7 @@ export class CanvasPlugin setupPlugins, startPlugins, registries, - this.appUpdater, - pluginServices + this.appUpdater ); const unmount = renderApp({ coreStart, startPlugins, params, canvasStore, pluginServices }); diff --git a/x-pack/plugins/canvas/public/services/expressions.ts b/x-pack/plugins/canvas/public/services/expressions.ts index a1af0fba50a5c..01bb0adb17711 100644 --- a/x-pack/plugins/canvas/public/services/expressions.ts +++ b/x-pack/plugins/canvas/public/services/expressions.ts @@ -5,6 +5,6 @@ * 2.0. */ -import { ExpressionsService } from '../../../../../src/plugins/expressions/public'; +import { ExpressionsServiceStart } from '../../../../../src/plugins/expressions/public'; -export type CanvasExpressionsService = ExpressionsService; +export type CanvasExpressionsService = ExpressionsServiceStart; diff --git a/x-pack/plugins/canvas/public/services/kibana/expressions.ts b/x-pack/plugins/canvas/public/services/kibana/expressions.ts index 4e3bb52a5d449..780de5309d97e 100644 --- a/x-pack/plugins/canvas/public/services/kibana/expressions.ts +++ b/x-pack/plugins/canvas/public/services/kibana/expressions.ts @@ -16,4 +16,4 @@ export type CanvasExpressionsServiceFactory = KibanaPluginServiceFactory< >; export const expressionsServiceFactory: CanvasExpressionsServiceFactory = ({ startPlugins }) => - startPlugins.expressions.fork(); + startPlugins.expressions; diff --git a/x-pack/plugins/canvas/server/saved_objects/workpad_references.ts b/x-pack/plugins/canvas/server/saved_objects/workpad_references.ts index b0d20add2f79a..e9eefa1bdb3f4 100644 --- a/x-pack/plugins/canvas/server/saved_objects/workpad_references.ts +++ b/x-pack/plugins/canvas/server/saved_objects/workpad_references.ts @@ -6,14 +6,15 @@ */ import { fromExpression, toExpression } from '@kbn/interpreter/common'; +import { PersistableStateService } from '../../../../../src/plugins/kibana_utils/common'; import { SavedObjectReference } from '../../../../../src/core/server'; import { WorkpadAttributes } from '../routes/workpad/workpad_attributes'; -import { ExpressionsServerSetup } from '../../../../../src/plugins/expressions/server'; +import type { ExpressionAstExpression } from '../../../../../src/plugins/expressions'; export const extractReferences = ( workpad: WorkpadAttributes, - expressions: ExpressionsServerSetup + expressions: PersistableStateService ): { workpad: WorkpadAttributes; references: SavedObjectReference[] } => { // We need to find every element in the workpad and extract references const references: SavedObjectReference[] = []; @@ -42,7 +43,7 @@ export const extractReferences = ( export const injectReferences = ( workpad: WorkpadAttributes, references: SavedObjectReference[], - expressions: ExpressionsServerSetup + expressions: PersistableStateService ) => { const pages = workpad.pages.map((page) => { const elements = page.elements.map((element) => { diff --git a/x-pack/plugins/cases/common/api/cases/case.ts b/x-pack/plugins/cases/common/api/cases/case.ts index 37a491cdad4c0..cd26ca0bab977 100644 --- a/x-pack/plugins/cases/common/api/cases/case.ts +++ b/x-pack/plugins/cases/common/api/cases/case.ts @@ -87,8 +87,11 @@ const CaseBasicRt = rt.type({ owner: rt.string, }); -export const CaseExternalServiceBasicRt = rt.type({ - connector_id: rt.union([rt.string, rt.null]), +/** + * This represents the push to service UserAction. It lacks the connector_id because that is stored in a different field + * within the user action object in the API response. + */ +export const CaseUserActionExternalServiceRt = rt.type({ connector_name: rt.string, external_id: rt.string, external_title: rt.string, @@ -97,7 +100,14 @@ export const CaseExternalServiceBasicRt = rt.type({ pushed_by: UserRT, }); -const CaseFullExternalServiceRt = rt.union([CaseExternalServiceBasicRt, rt.null]); +export const CaseExternalServiceBasicRt = rt.intersection([ + rt.type({ + connector_id: rt.union([rt.string, rt.null]), + }), + CaseUserActionExternalServiceRt, +]); + +export const CaseFullExternalServiceRt = rt.union([CaseExternalServiceBasicRt, rt.null]); export const CaseAttributesRt = rt.intersection([ CaseBasicRt, @@ -244,6 +254,16 @@ export const CaseResponseRt = rt.intersection([ }), ]); +export const CaseResolveResponseRt = rt.intersection([ + rt.type({ + case: CaseResponseRt, + outcome: rt.union([rt.literal('exactMatch'), rt.literal('aliasMatch'), rt.literal('conflict')]), + }), + rt.partial({ + alias_target_id: rt.string, + }), +]); + export const CasesFindResponseRt = rt.intersection([ rt.type({ cases: rt.array(CaseResponseRt), @@ -309,6 +329,7 @@ export type CaseAttributes = rt.TypeOf; export type CasesClientPostRequest = rt.TypeOf; export type CasePostRequest = rt.TypeOf; export type CaseResponse = rt.TypeOf; +export type CaseResolveResponse = rt.TypeOf; export type CasesResponse = rt.TypeOf; export type CasesFindRequest = rt.TypeOf; export type CasesByAlertIDRequest = rt.TypeOf; diff --git a/x-pack/plugins/cases/common/api/cases/user_actions.ts b/x-pack/plugins/cases/common/api/cases/user_actions.ts index 03912c550d77a..e86ce5248a6f9 100644 --- a/x-pack/plugins/cases/common/api/cases/user_actions.ts +++ b/x-pack/plugins/cases/common/api/cases/user_actions.ts @@ -34,7 +34,6 @@ const UserActionRt = rt.union([ rt.literal('push-to-service'), ]); -// TO DO change state to status const CaseUserActionBasicRT = rt.type({ action_field: UserActionFieldRt, action: UserActionRt, @@ -51,6 +50,8 @@ const CaseUserActionResponseRT = rt.intersection([ action_id: rt.string, case_id: rt.string, comment_id: rt.union([rt.string, rt.null]), + new_val_connector_id: rt.union([rt.string, rt.null]), + old_val_connector_id: rt.union([rt.string, rt.null]), }), rt.partial({ sub_case_id: rt.string }), ]); diff --git a/x-pack/plugins/cases/common/api/connectors/index.ts b/x-pack/plugins/cases/common/api/connectors/index.ts index 77af90b5d08cb..2b3483b4f6184 100644 --- a/x-pack/plugins/cases/common/api/connectors/index.ts +++ b/x-pack/plugins/cases/common/api/connectors/index.ts @@ -84,14 +84,22 @@ export const ConnectorTypeFieldsRt = rt.union([ ConnectorSwimlaneTypeFieldsRt, ]); +/** + * This type represents the connector's format when it is encoded within a user action. + */ +export const CaseUserActionConnectorRt = rt.intersection([ + rt.type({ name: rt.string }), + ConnectorTypeFieldsRt, +]); + export const CaseConnectorRt = rt.intersection([ rt.type({ id: rt.string, - name: rt.string, }), - ConnectorTypeFieldsRt, + CaseUserActionConnectorRt, ]); +export type CaseUserActionConnector = rt.TypeOf; export type CaseConnector = rt.TypeOf; export type ConnectorTypeFields = rt.TypeOf; export type ConnectorJiraTypeFields = rt.TypeOf; diff --git a/x-pack/plugins/cases/common/index.ts b/x-pack/plugins/cases/common/index.ts index 5305318cc9aa6..d38b1a779981c 100644 --- a/x-pack/plugins/cases/common/index.ts +++ b/x-pack/plugins/cases/common/index.ts @@ -12,3 +12,4 @@ export * from './constants'; export * from './api'; export * from './ui/types'; export * from './utils/connectors_api'; +export * from './utils/user_actions'; diff --git a/x-pack/plugins/cases/common/ui/types.ts b/x-pack/plugins/cases/common/ui/types.ts index bf4ec0da6ee56..948b203af14a8 100644 --- a/x-pack/plugins/cases/common/ui/types.ts +++ b/x-pack/plugins/cases/common/ui/types.ts @@ -66,7 +66,9 @@ export interface CaseUserActions { caseId: string; commentId: string | null; newValue: string | null; + newValConnectorId: string | null; oldValue: string | null; + oldValConnectorId: string | null; } export interface CaseExternalService { @@ -112,6 +114,12 @@ export interface Case extends BasicCase { type: CaseType; } +export interface ResolvedCase { + case: Case; + outcome: 'exactMatch' | 'aliasMatch' | 'conflict'; + aliasTargetId?: string; +} + export interface QueryParams { page: number; perPage: number; diff --git a/x-pack/plugins/cases/common/utils/user_actions.ts b/x-pack/plugins/cases/common/utils/user_actions.ts new file mode 100644 index 0000000000000..7de0d7066eaed --- /dev/null +++ b/x-pack/plugins/cases/common/utils/user_actions.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 function isCreateConnector(action?: string, actionFields?: string[]): boolean { + return action === 'create' && actionFields != null && actionFields.includes('connector'); +} + +export function isUpdateConnector(action?: string, actionFields?: string[]): boolean { + return action === 'update' && actionFields != null && actionFields.includes('connector'); +} + +export function isPush(action?: string, actionFields?: string[]): boolean { + return action === 'push-to-service' && actionFields != null && actionFields.includes('pushed'); +} diff --git a/x-pack/plugins/cases/public/common/lib/kibana/kibana_react.mock.ts b/x-pack/plugins/cases/public/common/lib/kibana/kibana_react.mock.ts index 18370be61bdf1..09f0215f5629f 100644 --- a/x-pack/plugins/cases/public/common/lib/kibana/kibana_react.mock.ts +++ b/x-pack/plugins/cases/public/common/lib/kibana/kibana_react.mock.ts @@ -13,6 +13,7 @@ import { KibanaContextProvider } from '../../../../../../../src/plugins/kibana_r import { StartServices } from '../../../types'; import { EuiTheme } from '../../../../../../../src/plugins/kibana_react/common'; import { securityMock } from '../../../../../security/public/mocks'; +import { spacesPluginMock } from '../../../../../spaces/public/mocks'; import { triggersActionsUiMock } from '../../../../../triggers_actions_ui/public/mocks'; export const createStartServicesMock = (): StartServices => @@ -25,6 +26,7 @@ export const createStartServicesMock = (): StartServices => }, security: securityMock.createStart(), triggersActionsUi: triggersActionsUiMock.createStart(), + spaces: spacesPluginMock.createStartContract(), } as unknown as StartServices); export const createWithKibanaMock = () => { diff --git a/x-pack/plugins/security_solution/server/lib/timeline/saved_object_mappings/migrations/index.ts b/x-pack/plugins/cases/public/common/user_actions/index.ts similarity index 72% rename from x-pack/plugins/security_solution/server/lib/timeline/saved_object_mappings/migrations/index.ts rename to x-pack/plugins/cases/public/common/user_actions/index.ts index e4c8858321e14..507455f7102a7 100644 --- a/x-pack/plugins/security_solution/server/lib/timeline/saved_object_mappings/migrations/index.ts +++ b/x-pack/plugins/cases/public/common/user_actions/index.ts @@ -5,5 +5,4 @@ * 2.0. */ -export { timelinesMigrations } from './timelines'; -export { notesMigrations } from './notes'; +export * from './parsers'; diff --git a/x-pack/plugins/cases/public/common/user_actions/parsers.test.ts b/x-pack/plugins/cases/public/common/user_actions/parsers.test.ts new file mode 100644 index 0000000000000..c6d13cc41686c --- /dev/null +++ b/x-pack/plugins/cases/public/common/user_actions/parsers.test.ts @@ -0,0 +1,86 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { ConnectorTypes, noneConnectorId } from '../../../common'; +import { parseStringAsConnector, parseStringAsExternalService } from './parsers'; + +describe('user actions utility functions', () => { + describe('parseStringAsConnector', () => { + it('return null if the data is null', () => { + expect(parseStringAsConnector('', null)).toBeNull(); + }); + + it('return null if the data is not a json object', () => { + expect(parseStringAsConnector('', 'blah')).toBeNull(); + }); + + it('return null if the data is not a valid connector', () => { + expect(parseStringAsConnector('', JSON.stringify({ a: '1' }))).toBeNull(); + }); + + it('return null if id is null but the data is a connector other than none', () => { + expect( + parseStringAsConnector( + null, + JSON.stringify({ type: ConnectorTypes.jira, name: '', fields: null }) + ) + ).toBeNull(); + }); + + it('return the id as the none connector if the data is the none connector', () => { + expect( + parseStringAsConnector( + null, + JSON.stringify({ type: ConnectorTypes.none, name: '', fields: null }) + ) + ).toEqual({ id: noneConnectorId, type: ConnectorTypes.none, name: '', fields: null }); + }); + + it('returns a decoded connector with the specified id', () => { + expect( + parseStringAsConnector( + 'a', + JSON.stringify({ type: ConnectorTypes.jira, name: 'hi', fields: null }) + ) + ).toEqual({ id: 'a', type: ConnectorTypes.jira, name: 'hi', fields: null }); + }); + }); + + describe('parseStringAsExternalService', () => { + it('returns null when the data is null', () => { + expect(parseStringAsExternalService('', null)).toBeNull(); + }); + + it('returns null when the data is not valid json', () => { + expect(parseStringAsExternalService('', 'blah')).toBeNull(); + }); + + it('returns null when the data is not a valid external service object', () => { + expect(parseStringAsExternalService('', JSON.stringify({ a: '1' }))).toBeNull(); + }); + + it('returns the decoded external service with the connector_id field added', () => { + const externalServiceInfo = { + connector_name: 'name', + external_id: '1', + external_title: 'title', + external_url: 'abc', + pushed_at: '1', + pushed_by: { + username: 'a', + email: 'a@a.com', + full_name: 'a', + }, + }; + + expect(parseStringAsExternalService('500', JSON.stringify(externalServiceInfo))).toEqual({ + ...externalServiceInfo, + connector_id: '500', + }); + }); + }); +}); diff --git a/x-pack/plugins/cases/public/common/user_actions/parsers.ts b/x-pack/plugins/cases/public/common/user_actions/parsers.ts new file mode 100644 index 0000000000000..dfea22443aa51 --- /dev/null +++ b/x-pack/plugins/cases/public/common/user_actions/parsers.ts @@ -0,0 +1,77 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { + CaseUserActionConnectorRt, + CaseConnector, + ConnectorTypes, + noneConnectorId, + CaseFullExternalService, + CaseUserActionExternalServiceRt, +} from '../../../common'; + +export const parseStringAsConnector = ( + id: string | null, + encodedData: string | null +): CaseConnector | null => { + if (encodedData == null) { + return null; + } + + const decodedConnector = parseString(encodedData); + + if (!CaseUserActionConnectorRt.is(decodedConnector)) { + return null; + } + + if (id == null && decodedConnector.type === ConnectorTypes.none) { + return { + ...decodedConnector, + id: noneConnectorId, + }; + } else if (id == null) { + return null; + } else { + // id does not equal null or undefined and the connector type does not equal none + // so return the connector with its id + return { + ...decodedConnector, + id, + }; + } +}; + +const parseString = (params: string | null): unknown | null => { + if (params == null) { + return null; + } + + try { + return JSON.parse(params); + } catch { + return null; + } +}; + +export const parseStringAsExternalService = ( + id: string | null, + encodedData: string | null +): CaseFullExternalService => { + if (encodedData == null) { + return null; + } + + const decodedExternalService = parseString(encodedData); + if (!CaseUserActionExternalServiceRt.is(decodedExternalService)) { + return null; + } + + return { + ...decodedExternalService, + connector_id: id, + }; +}; diff --git a/x-pack/plugins/cases/public/components/case_view/index.test.tsx b/x-pack/plugins/cases/public/components/case_view/index.test.tsx index f12c8ba098d43..6fc9e1719e1cf 100644 --- a/x-pack/plugins/cases/public/components/case_view/index.test.tsx +++ b/x-pack/plugins/cases/public/components/case_view/index.test.tsx @@ -18,6 +18,7 @@ import { getAlertUserAction, } from '../../containers/mock'; import { TestProviders } from '../../common/mock'; +import { SpacesApi } from '../../../../spaces/public'; import { useUpdateCase } from '../../containers/use_update_case'; import { useGetCase } from '../../containers/use_get_case'; import { useGetCaseUserActions } from '../../containers/use_get_case_user_actions'; @@ -47,6 +48,13 @@ const useConnectorsMock = useConnectors as jest.Mock; const usePostPushToServiceMock = usePostPushToService as jest.Mock; const useKibanaMock = useKibana as jest.Mocked; +const spacesUiApiMock = { + redirectLegacyUrl: jest.fn().mockResolvedValue(undefined), + components: { + getLegacyUrlConflict: jest.fn().mockReturnValue(
), + }, +}; + const alertsHit = [ { _id: 'alert-id-1', @@ -138,6 +146,7 @@ describe('CaseView ', () => { isLoading: false, isError: false, data, + resolveOutcome: 'exactMatch', updateCase, fetchCase, }; @@ -174,6 +183,7 @@ describe('CaseView ', () => { actionTypeTitle: '.servicenow', iconClass: 'logoSecurity', }); + useKibanaMock().services.spaces = { ui: spacesUiApiMock } as unknown as SpacesApi; }); it('should render CaseComponent', async () => { @@ -395,36 +405,7 @@ describe('CaseView ', () => { })); const wrapper = mount( - + ); await waitFor(() => { @@ -439,36 +420,7 @@ describe('CaseView ', () => { })); const wrapper = mount( - + ); await waitFor(() => { @@ -477,43 +429,66 @@ describe('CaseView ', () => { }); it('should return case view when data is there', async () => { - (useGetCase as jest.Mock).mockImplementation(() => defaultGetCase); + (useGetCase as jest.Mock).mockImplementation(() => ({ + ...defaultGetCase, + resolveOutcome: 'exactMatch', + })); const wrapper = mount( - + ); await waitFor(() => { expect(wrapper.find('[data-test-subj="case-view-title"]').exists()).toBeTruthy(); + expect(spacesUiApiMock.components.getLegacyUrlConflict).not.toHaveBeenCalled(); + expect(spacesUiApiMock.redirectLegacyUrl).not.toHaveBeenCalled(); + }); + }); + + it('should redirect case view when resolves to alias match', async () => { + const resolveAliasId = `${defaultGetCase.data.id}_2`; + (useGetCase as jest.Mock).mockImplementation(() => ({ + ...defaultGetCase, + resolveOutcome: 'aliasMatch', + resolveAliasId, + })); + const wrapper = mount( + + + + ); + await waitFor(() => { + expect(wrapper.find('[data-test-subj="case-view-title"]').exists()).toBeTruthy(); + expect(spacesUiApiMock.components.getLegacyUrlConflict).not.toHaveBeenCalled(); + expect(spacesUiApiMock.redirectLegacyUrl).toHaveBeenCalledWith( + `cases/${resolveAliasId}`, + 'case' + ); + }); + }); + + it('should redirect case view when resolves to conflict', async () => { + const resolveAliasId = `${defaultGetCase.data.id}_2`; + (useGetCase as jest.Mock).mockImplementation(() => ({ + ...defaultGetCase, + resolveOutcome: 'conflict', + resolveAliasId, + })); + const wrapper = mount( + + + + ); + await waitFor(() => { + expect(wrapper.find('[data-test-subj="case-view-title"]').exists()).toBeTruthy(); + expect(wrapper.find('[data-test-subj="conflict-component"]').exists()).toBeTruthy(); + expect(spacesUiApiMock.redirectLegacyUrl).not.toHaveBeenCalled(); + expect(spacesUiApiMock.components.getLegacyUrlConflict).toHaveBeenCalledWith({ + objectNoun: 'case', + currentObjectId: defaultGetCase.data.id, + otherObjectId: resolveAliasId, + otherObjectPath: `cases/${resolveAliasId}`, + }); }); }); @@ -521,41 +496,12 @@ describe('CaseView ', () => { (useGetCase as jest.Mock).mockImplementation(() => defaultGetCase); const wrapper = mount( - + ); wrapper.find('[data-test-subj="case-refresh"]').first().simulate('click'); await waitFor(() => { - expect(fetchCaseUserActions).toBeCalledWith('1234', 'resilient-2', undefined); + expect(fetchCaseUserActions).toBeCalledWith(caseProps.caseData.id, 'resilient-2', undefined); expect(fetchCase).toBeCalled(); }); }); 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 bb0b894238b9d..81e7607c9011f 100644 --- a/x-pack/plugins/cases/public/components/case_view/index.tsx +++ b/x-pack/plugins/cases/public/components/case_view/index.tsx @@ -40,6 +40,7 @@ import { CasesNavigation } from '../links'; import { OwnerProvider } from '../owner_context'; import { getConnectorById } from '../utils'; import { DoesNotExist } from './does_not_exist'; +import { useKibana } from '../../common/lib/kibana'; export interface CaseViewComponentProps { allCasesNavigation: CasesNavigation; @@ -499,6 +500,14 @@ export const CaseComponent = React.memo( } ); +export const CaseViewLoading = () => ( + + + + + +); + export const CaseView = React.memo( ({ allCasesNavigation, @@ -518,27 +527,59 @@ export const CaseView = React.memo( refreshRef, hideSyncAlerts, }: CaseViewProps) => { - const { data, isLoading, isError, fetchCase, updateCase } = useGetCase(caseId, subCaseId); - if (isError) { - return ; - } - if (isLoading) { - return ( - - - - - - ); - } - if (onCaseDataSuccess && data) { - onCaseDataSuccess(data); - } + const { data, resolveOutcome, resolveAliasId, isLoading, isError, fetchCase, updateCase } = + useGetCase(caseId, subCaseId); + const { spaces: spacesApi, http } = useKibana().services; - return ( + useEffect(() => { + if (onCaseDataSuccess && data) { + onCaseDataSuccess(data); + } + }, [data, onCaseDataSuccess]); + + useEffect(() => { + if (spacesApi && resolveOutcome === 'aliasMatch' && resolveAliasId != null) { + // CAUTION: the path /cases/:detailName is working in both Observability (/app/observability/cases/:detailName) and + // Security Solutions (/app/security/cases/:detailName) plugins. This will need to be changed if this component is loaded + // under any another path, passing a path builder function by props from every parent plugin. + const newPath = http.basePath.prepend( + `cases/${resolveAliasId}${window.location.search}${window.location.hash}` + ); + spacesApi.ui.redirectLegacyUrl(newPath, i18n.CASE); + } + }, [resolveOutcome, resolveAliasId, spacesApi, http]); + + const getLegacyUrlConflictCallout = useCallback(() => { + // This function returns a callout component *if* we have encountered a "legacy URL conflict" scenario + if (data && spacesApi && resolveOutcome === 'conflict' && resolveAliasId != null) { + // We have resolved to one object, but another object has a legacy URL alias associated with this ID/page. We should display a + // callout with a warning for the user, and provide a way for them to navigate to the other object. + const otherObjectId = resolveAliasId; // This is always defined if outcome === 'conflict' + // CAUTION: the path /cases/:detailName is working in both Observability (/app/observability/cases/:detailName) and + // Security Solutions (/app/security/cases/:detailName) plugins. This will need to be changed if this component is loaded + // under any another path, passing a path builder function by props from every parent plugin. + const otherObjectPath = http.basePath.prepend( + `cases/${otherObjectId}${window.location.search}${window.location.hash}` + ); + return spacesApi.ui.components.getLegacyUrlConflict({ + objectNoun: i18n.CASE, + currentObjectId: data.id, + otherObjectId, + otherObjectPath, + }); + } + return null; + }, [data, resolveAliasId, resolveOutcome, spacesApi, http.basePath]); + + return isError ? ( + + ) : isLoading ? ( + + ) : ( data && ( + {getLegacyUrlConflictCallout()} { + describe('getConnectorFieldsFromUserActions', () => { + it('returns null when it cannot find the connector id', () => { + expect(getConnectorFieldsFromUserActions('a', [])).toBeNull(); + }); + + it('returns null when the value fields are not valid encoded fields', () => { + expect( + getConnectorFieldsFromUserActions('a', [createUserAction({ newValue: 'a', oldValue: 'a' })]) + ).toBeNull(); + }); + + it('returns null when it cannot find the connector id in a non empty array', () => { + expect( + getConnectorFieldsFromUserActions('a', [ + createUserAction({ + newValue: JSON.stringify({ a: '1' }), + oldValue: JSON.stringify({ a: '1' }), + }), + ]) + ).toBeNull(); + }); + + it('returns the fields when it finds the connector id in the new value', () => { + expect( + getConnectorFieldsFromUserActions('a', [ + createUserAction({ + newValue: createEncodedJiraConnector(), + oldValue: JSON.stringify({ a: '1' }), + newValConnectorId: 'a', + }), + ]) + ).toEqual(defaultJiraFields); + }); + + it('returns the fields when it finds the connector id in the new value and the old value is null', () => { + expect( + getConnectorFieldsFromUserActions('a', [ + createUserAction({ + newValue: createEncodedJiraConnector(), + newValConnectorId: 'a', + }), + ]) + ).toEqual(defaultJiraFields); + }); + + it('returns the fields when it finds the connector id in the old value', () => { + const expectedFields = { ...defaultJiraFields, issueType: '5' }; + + expect( + getConnectorFieldsFromUserActions('id-to-find', [ + createUserAction({ + newValue: createEncodedJiraConnector(), + oldValue: createEncodedJiraConnector({ + fields: expectedFields, + }), + newValConnectorId: 'b', + oldValConnectorId: 'id-to-find', + }), + ]) + ).toEqual(expectedFields); + }); + + it('returns the fields when it finds the connector id in the second user action', () => { + const expectedFields = { ...defaultJiraFields, issueType: '5' }; + + expect( + getConnectorFieldsFromUserActions('id-to-find', [ + createUserAction({ + newValue: createEncodedJiraConnector(), + oldValue: createEncodedJiraConnector(), + newValConnectorId: 'b', + oldValConnectorId: 'a', + }), + createUserAction({ + newValue: createEncodedJiraConnector(), + oldValue: createEncodedJiraConnector({ fields: expectedFields }), + newValConnectorId: 'b', + oldValConnectorId: 'id-to-find', + }), + ]) + ).toEqual(expectedFields); + }); + + it('ignores a parse failure and finds the right user action', () => { + expect( + getConnectorFieldsFromUserActions('none', [ + createUserAction({ + newValue: 'b', + newValConnectorId: null, + }), + createUserAction({ + newValue: createEncodedJiraConnector({ + type: ConnectorTypes.none, + name: '', + fields: null, + }), + newValConnectorId: null, + }), + ]) + ).toBeNull(); + }); + + it('returns null when the id matches but the encoded value is null', () => { + expect( + getConnectorFieldsFromUserActions('b', [ + createUserAction({ + newValue: null, + newValConnectorId: 'b', + }), + ]) + ).toBeNull(); + }); + + it('returns null when the action fields is not of length 1', () => { + expect( + getConnectorFieldsFromUserActions('id-to-find', [ + createUserAction({ + newValue: JSON.stringify({ a: '1', fields: { hello: '1' } }), + oldValue: JSON.stringify({ a: '1', fields: { hi: '2' } }), + newValConnectorId: 'b', + oldValConnectorId: 'id-to-find', + actionField: ['connector', 'connector'], + }), + ]) + ).toBeNull(); + }); + + it('matches the none connector the searched for id is none', () => { + expect( + getConnectorFieldsFromUserActions('none', [ + createUserAction({ + newValue: createEncodedJiraConnector({ + type: ConnectorTypes.none, + name: '', + fields: null, + }), + newValConnectorId: null, + }), + ]) + ).toBeNull(); + }); + }); +}); + +function createUserAction(fields: Partial): CaseUserActions { + return { + action: 'update', + actionAt: '', + actionBy: {}, + actionField: ['connector'], + actionId: '', + caseId: '', + commentId: '', + newValConnectorId: null, + oldValConnectorId: null, + newValue: null, + oldValue: null, + ...fields, + }; +} + +function createEncodedJiraConnector(fields?: Partial): string { + return JSON.stringify({ + type: ConnectorTypes.jira, + name: 'name', + fields: defaultJiraFields, + ...fields, + }); +} + +const defaultJiraFields = { + issueType: '1', + parent: null, + priority: null, +}; diff --git a/x-pack/plugins/cases/public/components/edit_connector/helpers.ts b/x-pack/plugins/cases/public/components/edit_connector/helpers.ts index 36eb3f58c8aaf..b97035c458aca 100644 --- a/x-pack/plugins/cases/public/components/edit_connector/helpers.ts +++ b/x-pack/plugins/cases/public/components/edit_connector/helpers.ts @@ -5,23 +5,33 @@ * 2.0. */ +import { ConnectorTypeFields } from '../../../common'; import { CaseUserActions } from '../../containers/types'; +import { parseStringAsConnector } from '../../common/user_actions'; -export const getConnectorFieldsFromUserActions = (id: string, userActions: CaseUserActions[]) => { +export const getConnectorFieldsFromUserActions = ( + id: string, + userActions: CaseUserActions[] +): ConnectorTypeFields['fields'] => { try { for (const action of [...userActions].reverse()) { if (action.actionField.length === 1 && action.actionField[0] === 'connector') { - if (action.oldValue && action.newValue) { - const oldValue = JSON.parse(action.oldValue); - const newValue = JSON.parse(action.newValue); + const parsedNewConnector = parseStringAsConnector( + action.newValConnectorId, + action.newValue + ); - if (newValue.id === id) { - return newValue.fields; - } + if (parsedNewConnector && id === parsedNewConnector.id) { + return parsedNewConnector.fields; + } + + const parsedOldConnector = parseStringAsConnector( + action.oldValConnectorId, + action.oldValue + ); - if (oldValue.id === id) { - return oldValue.fields; - } + if (parsedOldConnector && id === parsedOldConnector.id) { + return parsedOldConnector.fields; } } } diff --git a/x-pack/plugins/cases/public/components/user_action_tree/helpers.test.tsx b/x-pack/plugins/cases/public/components/user_action_tree/helpers.test.tsx index b49a010cff38f..841f0d36bbf17 100644 --- a/x-pack/plugins/cases/public/components/user_action_tree/helpers.test.tsx +++ b/x-pack/plugins/cases/public/components/user_action_tree/helpers.test.tsx @@ -8,7 +8,7 @@ import React from 'react'; import { mount } from 'enzyme'; -import { CaseStatuses } from '../../../common'; +import { CaseStatuses, ConnectorTypes } from '../../../common'; import { basicPush, getUserAction } from '../../containers/mock'; import { getLabelTitle, @@ -129,7 +129,7 @@ describe('User action tree helpers', () => { `${i18n.PUSHED_NEW_INCIDENT} ${basicPush.connectorName}` ); expect(wrapper.find(`[data-test-subj="pushed-value"]`).first().prop('href')).toEqual( - JSON.parse(action.newValue).external_url + JSON.parse(action.newValue!).external_url ); }); @@ -142,50 +142,74 @@ describe('User action tree helpers', () => { `${i18n.UPDATE_INCIDENT} ${basicPush.connectorName}` ); expect(wrapper.find(`[data-test-subj="pushed-value"]`).first().prop('href')).toEqual( - JSON.parse(action.newValue).external_url + JSON.parse(action.newValue!).external_url ); }); - it('label title generated for update connector - change connector', () => { - const action = { - ...getUserAction(['connector'], 'update'), - oldValue: JSON.stringify({ id: 'servicenow-1' }), - newValue: JSON.stringify({ id: 'resilient-2' }), - }; - const result: string | JSX.Element = getConnectorLabelTitle({ - action, - connectors, - }); - - expect(result).toEqual('selected My Connector 2 as incident management system'); - }); - - it('label title generated for update connector - change connector to none', () => { - const action = { - ...getUserAction(['connector'], 'update'), - oldValue: JSON.stringify({ id: 'servicenow-1' }), - newValue: JSON.stringify({ id: 'none' }), - }; - const result: string | JSX.Element = getConnectorLabelTitle({ - action, - connectors, + describe('getConnectorLabelTitle', () => { + it('returns an empty string when the encoded old value is null', () => { + const result = getConnectorLabelTitle({ + action: getUserAction(['connector'], 'update', { oldValue: null }), + connectors, + }); + + expect(result).toEqual(''); + }); + + it('returns an empty string when the encoded new value is null', () => { + const result = getConnectorLabelTitle({ + action: getUserAction(['connector'], 'update', { newValue: null }), + connectors, + }); + + expect(result).toEqual(''); + }); + + it('returns the change connector label', () => { + const result: string | JSX.Element = getConnectorLabelTitle({ + action: getUserAction(['connector'], 'update', { + oldValue: JSON.stringify({ + type: ConnectorTypes.serviceNowITSM, + name: 'a', + fields: null, + }), + oldValConnectorId: 'servicenow-1', + newValue: JSON.stringify({ type: ConnectorTypes.resilient, name: 'a', fields: null }), + newValConnectorId: 'resilient-2', + }), + connectors, + }); + + expect(result).toEqual('selected My Connector 2 as incident management system'); + }); + + it('returns the removed connector label', () => { + const result: string | JSX.Element = getConnectorLabelTitle({ + action: getUserAction(['connector'], 'update', { + oldValue: JSON.stringify({ type: ConnectorTypes.serviceNowITSM, name: '', fields: null }), + oldValConnectorId: 'servicenow-1', + newValue: JSON.stringify({ type: ConnectorTypes.none, name: '', fields: null }), + newValConnectorId: 'none', + }), + connectors, + }); + + expect(result).toEqual('removed external incident management system'); + }); + + it('returns the connector fields changed label', () => { + const result: string | JSX.Element = getConnectorLabelTitle({ + action: getUserAction(['connector'], 'update', { + oldValue: JSON.stringify({ type: ConnectorTypes.serviceNowITSM, name: '', fields: null }), + oldValConnectorId: 'servicenow-1', + newValue: JSON.stringify({ type: ConnectorTypes.serviceNowITSM, name: '', fields: null }), + newValConnectorId: 'servicenow-1', + }), + connectors, + }); + + expect(result).toEqual('changed connector field'); }); - - expect(result).toEqual('removed external incident management system'); - }); - - it('label title generated for update connector - field change', () => { - const action = { - ...getUserAction(['connector'], 'update'), - oldValue: JSON.stringify({ id: 'servicenow-1' }), - newValue: JSON.stringify({ id: 'servicenow-1' }), - }; - const result: string | JSX.Element = getConnectorLabelTitle({ - action, - connectors, - }); - - expect(result).toEqual('changed connector field'); }); describe('toStringArray', () => { 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 744b14926b358..2eb44f91190c6 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 @@ -23,10 +23,11 @@ import { CommentType, Comment, CommentRequestActionsType, + noneConnectorId, } from '../../../common'; import { CaseUserActions } from '../../containers/types'; import { CaseServices } from '../../containers/use_get_case_user_actions'; -import { parseString } from '../../containers/utils'; +import { parseStringAsConnector, parseStringAsExternalService } from '../../common/user_actions'; import { Tags } from '../tag_list/tags'; import { UserActionUsernameWithAvatar } from './user_action_username_with_avatar'; import { UserActionTimestamp } from './user_action_timestamp'; @@ -97,23 +98,27 @@ export const getConnectorLabelTitle = ({ action: CaseUserActions; connectors: ActionConnector[]; }) => { - const oldValue = parseString(`${action.oldValue}`); - const newValue = parseString(`${action.newValue}`); + const oldConnector = parseStringAsConnector(action.oldValConnectorId, action.oldValue); + const newConnector = parseStringAsConnector(action.newValConnectorId, action.newValue); - if (oldValue === null || newValue === null) { + if (!oldConnector || !newConnector) { return ''; } - // Connector changed - if (oldValue.id !== newValue.id) { - const newConnector = connectors.find((c) => c.id === newValue.id); - return newValue.id != null && newValue.id !== 'none' && newConnector != null - ? i18n.SELECTED_THIRD_PARTY(newConnector.name) - : i18n.REMOVED_THIRD_PARTY; - } else { - // Field changed + // if the ids are the same, assume we just changed the fields + if (oldConnector.id === newConnector.id) { return i18n.CHANGED_CONNECTOR_FIELD; } + + // ids are not the same so check and see if the id is a valid connector and then return its name + // if the connector id is the none connector value then it must have been removed + const newConnectorActionInfo = connectors.find((c) => c.id === newConnector.id); + if (newConnector.id !== noneConnectorId && newConnectorActionInfo != null) { + return i18n.SELECTED_THIRD_PARTY(newConnectorActionInfo.name); + } + + // it wasn't a valid connector or it was the none connector, so it must have been removed + return i18n.REMOVED_THIRD_PARTY; }; const getTagsLabelTitle = (action: CaseUserActions) => { @@ -133,7 +138,8 @@ const getTagsLabelTitle = (action: CaseUserActions) => { }; export const getPushedServiceLabelTitle = (action: CaseUserActions, firstPush: boolean) => { - const pushedVal = JSON.parse(action.newValue ?? '') as CaseFullExternalService; + const externalService = parseStringAsExternalService(action.newValConnectorId, action.newValue); + return ( {`${firstPush ? i18n.PUSHED_NEW_INCIDENT : i18n.UPDATE_INCIDENT} ${ - pushedVal?.connector_name + externalService?.connector_name }`} - - {pushedVal?.external_title} + + {externalService?.external_title} @@ -157,20 +163,19 @@ export const getPushedServiceLabelTitle = (action: CaseUserActions, firstPush: b export const getPushInfo = ( caseServices: CaseServices, - // a JSON parse failure will result in null for parsedValue - parsedValue: { connector_id: string | null; connector_name: string } | null, + externalService: CaseFullExternalService | undefined, index: number ) => - parsedValue != null && parsedValue.connector_id != null + externalService != null && externalService.connector_id != null ? { - firstPush: caseServices[parsedValue.connector_id]?.firstPushIndex === index, - parsedConnectorId: parsedValue.connector_id, - parsedConnectorName: parsedValue.connector_name, + firstPush: caseServices[externalService.connector_id]?.firstPushIndex === index, + parsedConnectorId: externalService.connector_id, + parsedConnectorName: externalService.connector_name, } : { firstPush: false, - parsedConnectorId: 'none', - parsedConnectorName: 'none', + parsedConnectorId: noneConnectorId, + parsedConnectorName: noneConnectorId, }; const getUpdateActionIcon = (actionField: string): string => { 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 784817229caf9..7ea415324194c 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 @@ -35,7 +35,7 @@ import { Ecs, } from '../../../common'; import { CaseServices } from '../../containers/use_get_case_user_actions'; -import { parseString } from '../../containers/utils'; +import { parseStringAsExternalService } from '../../common/user_actions'; import { OnUpdateFields } from '../case_view'; import { getConnectorLabelTitle, @@ -512,10 +512,14 @@ export const UserActionTree = React.memo( // Pushed information if (action.actionField.length === 1 && action.actionField[0] === 'pushed') { - const parsedValue = parseString(`${action.newValue}`); + const parsedExternalService = parseStringAsExternalService( + action.newValConnectorId, + action.newValue + ); + const { firstPush, parsedConnectorId, parsedConnectorName } = getPushInfo( caseServices, - parsedValue, + parsedExternalService, index ); diff --git a/x-pack/plugins/cases/public/containers/__mocks__/api.ts b/x-pack/plugins/cases/public/containers/__mocks__/api.ts index 6db92829bb8d6..96e75a96ca115 100644 --- a/x-pack/plugins/cases/public/containers/__mocks__/api.ts +++ b/x-pack/plugins/cases/public/containers/__mocks__/api.ts @@ -21,6 +21,7 @@ import { basicCase, basicCaseCommentPatch, basicCasePost, + basicResolvedCase, casesStatus, caseUserActions, pushedCase, @@ -33,6 +34,7 @@ import { CommentRequest, User, CaseStatuses, + ResolvedCase, } from '../../../common'; export const getCase = async ( @@ -41,6 +43,12 @@ export const getCase = async ( signal: AbortSignal ): Promise => Promise.resolve(basicCase); +export const resolveCase = async ( + caseId: string, + includeComments: boolean = true, + signal: AbortSignal +): Promise => Promise.resolve(basicResolvedCase); + export const getCasesStatus = async (signal: AbortSignal): Promise => Promise.resolve(casesStatus); diff --git a/x-pack/plugins/cases/public/containers/api.test.tsx b/x-pack/plugins/cases/public/containers/api.test.tsx index e47930e81fe6b..654ade308ed44 100644 --- a/x-pack/plugins/cases/public/containers/api.test.tsx +++ b/x-pack/plugins/cases/public/containers/api.test.tsx @@ -30,6 +30,7 @@ import { postCase, postComment, pushCase, + resolveCase, } from './api'; import { @@ -68,7 +69,7 @@ describe('Case Configuration API', () => { }); const data = ['1', '2']; - test('check url, method, signal', async () => { + test('should be called with correct check url, method, signal', async () => { await deleteCases(data, abortCtrl.signal); expect(fetchMock).toHaveBeenCalledWith(`${CASES_URL}`, { method: 'DELETE', @@ -77,7 +78,7 @@ describe('Case Configuration API', () => { }); }); - test('happy path', async () => { + test('should return correct response', async () => { const resp = await deleteCases(data, abortCtrl.signal); expect(resp).toEqual(''); }); @@ -89,7 +90,7 @@ describe('Case Configuration API', () => { fetchMock.mockResolvedValue(actionLicenses); }); - test('check url, method, signal', async () => { + test('should be called with correct check url, method, signal', async () => { await getActionLicense(abortCtrl.signal); expect(fetchMock).toHaveBeenCalledWith(`/api/actions/connector_types`, { method: 'GET', @@ -97,7 +98,7 @@ describe('Case Configuration API', () => { }); }); - test('happy path', async () => { + test('should return correct response', async () => { const resp = await getActionLicense(abortCtrl.signal); expect(resp).toEqual(actionLicenses); }); @@ -110,7 +111,7 @@ describe('Case Configuration API', () => { }); const data = basicCase.id; - test('check url, method, signal', async () => { + test('should be called with correct check url, method, signal', async () => { await getCase(data, true, abortCtrl.signal); expect(fetchMock).toHaveBeenCalledWith(`${CASES_URL}/${basicCase.id}`, { method: 'GET', @@ -119,18 +120,46 @@ describe('Case Configuration API', () => { }); }); - test('happy path', async () => { + test('should return correct response', async () => { const resp = await getCase(data, true, abortCtrl.signal); expect(resp).toEqual(basicCase); }); }); + describe('resolveCase', () => { + const targetAliasId = '12345'; + const basicResolveCase = { + outcome: 'aliasMatch', + case: basicCaseSnake, + }; + const caseId = basicCase.id; + + beforeEach(() => { + fetchMock.mockClear(); + fetchMock.mockResolvedValue({ ...basicResolveCase, target_alias_id: targetAliasId }); + }); + + test('should be called with correct check url, method, signal', async () => { + await resolveCase(caseId, true, abortCtrl.signal); + expect(fetchMock).toHaveBeenCalledWith(`${CASES_URL}/${caseId}/resolve`, { + method: 'GET', + query: { includeComments: true }, + signal: abortCtrl.signal, + }); + }); + + test('should return correct response', async () => { + const resp = await resolveCase(caseId, true, abortCtrl.signal); + expect(resp).toEqual({ ...basicResolveCase, case: basicCase, targetAliasId }); + }); + }); + describe('getCases', () => { beforeEach(() => { fetchMock.mockClear(); fetchMock.mockResolvedValue(allCasesSnake); }); - test('check url, method, signal', async () => { + test('should be called with correct check url, method, signal', async () => { await getCases({ filterOptions: { ...DEFAULT_FILTER_OPTIONS, owner: [SECURITY_SOLUTION_OWNER] }, queryParams: DEFAULT_QUERY_PARAMS, @@ -148,7 +177,7 @@ describe('Case Configuration API', () => { }); }); - test('correctly applies filters', async () => { + test('should applies correct filters', async () => { await getCases({ filterOptions: { ...DEFAULT_FILTER_OPTIONS, @@ -175,7 +204,7 @@ describe('Case Configuration API', () => { }); }); - test('tags with weird chars get handled gracefully', async () => { + test('should handle tags with weird chars', async () => { const weirdTags: string[] = ['(', '"double"']; await getCases({ @@ -204,7 +233,7 @@ describe('Case Configuration API', () => { }); }); - test('happy path', async () => { + test('should return correct response', async () => { const resp = await getCases({ filterOptions: { ...DEFAULT_FILTER_OPTIONS, owner: [SECURITY_SOLUTION_OWNER] }, queryParams: DEFAULT_QUERY_PARAMS, @@ -219,7 +248,7 @@ describe('Case Configuration API', () => { fetchMock.mockClear(); fetchMock.mockResolvedValue(casesStatusSnake); }); - test('check url, method, signal', async () => { + test('should be called with correct check url, method, signal', async () => { await getCasesStatus(abortCtrl.signal, [SECURITY_SOLUTION_OWNER]); expect(fetchMock).toHaveBeenCalledWith(`${CASES_URL}/status`, { method: 'GET', @@ -228,7 +257,7 @@ describe('Case Configuration API', () => { }); }); - test('happy path', async () => { + test('should return correct response', async () => { const resp = await getCasesStatus(abortCtrl.signal, [SECURITY_SOLUTION_OWNER]); expect(resp).toEqual(casesStatus); }); @@ -240,7 +269,7 @@ describe('Case Configuration API', () => { fetchMock.mockResolvedValue(caseUserActionsSnake); }); - test('check url, method, signal', async () => { + test('should be called with correct check url, method, signal', async () => { await getCaseUserActions(basicCase.id, abortCtrl.signal); expect(fetchMock).toHaveBeenCalledWith(`${CASES_URL}/${basicCase.id}/user_actions`, { method: 'GET', @@ -248,7 +277,7 @@ describe('Case Configuration API', () => { }); }); - test('happy path', async () => { + test('should return correct response', async () => { const resp = await getCaseUserActions(basicCase.id, abortCtrl.signal); expect(resp).toEqual(caseUserActions); }); @@ -260,7 +289,7 @@ describe('Case Configuration API', () => { fetchMock.mockResolvedValue(respReporters); }); - test('check url, method, signal', async () => { + test('should be called with correct check url, method, signal', async () => { await getReporters(abortCtrl.signal, [SECURITY_SOLUTION_OWNER]); expect(fetchMock).toHaveBeenCalledWith(`${CASES_URL}/reporters`, { method: 'GET', @@ -271,7 +300,7 @@ describe('Case Configuration API', () => { }); }); - test('happy path', async () => { + test('should return correct response', async () => { const resp = await getReporters(abortCtrl.signal, [SECURITY_SOLUTION_OWNER]); expect(resp).toEqual(respReporters); }); @@ -283,7 +312,7 @@ describe('Case Configuration API', () => { fetchMock.mockResolvedValue(tags); }); - test('check url, method, signal', async () => { + test('should be called with correct check url, method, signal', async () => { await getTags(abortCtrl.signal, [SECURITY_SOLUTION_OWNER]); expect(fetchMock).toHaveBeenCalledWith(`${CASES_URL}/tags`, { method: 'GET', @@ -294,7 +323,7 @@ describe('Case Configuration API', () => { }); }); - test('happy path', async () => { + test('should return correct response', async () => { const resp = await getTags(abortCtrl.signal, [SECURITY_SOLUTION_OWNER]); expect(resp).toEqual(tags); }); @@ -306,7 +335,7 @@ describe('Case Configuration API', () => { fetchMock.mockResolvedValue([basicCaseSnake]); }); const data = { description: 'updated description' }; - test('check url, method, signal', async () => { + test('should be called with correct check url, method, signal', async () => { await patchCase(basicCase.id, data, basicCase.version, abortCtrl.signal); expect(fetchMock).toHaveBeenCalledWith(`${CASES_URL}`, { method: 'PATCH', @@ -317,7 +346,7 @@ describe('Case Configuration API', () => { }); }); - test('happy path', async () => { + test('should return correct response', async () => { const resp = await patchCase( basicCase.id, { description: 'updated description' }, @@ -341,7 +370,7 @@ describe('Case Configuration API', () => { }, ]; - test('check url, method, signal', async () => { + test('should be called with correct check url, method, signal', async () => { await patchCasesStatus(data, abortCtrl.signal); expect(fetchMock).toHaveBeenCalledWith(`${CASES_URL}`, { method: 'PATCH', @@ -350,7 +379,7 @@ describe('Case Configuration API', () => { }); }); - test('happy path', async () => { + test('should return correct response', async () => { const resp = await patchCasesStatus(data, abortCtrl.signal); expect(resp).toEqual({ ...cases }); }); @@ -362,7 +391,7 @@ describe('Case Configuration API', () => { fetchMock.mockResolvedValue(basicCaseSnake); }); - test('check url, method, signal', async () => { + test('should be called with correct check url, method, signal', async () => { await patchComment({ caseId: basicCase.id, commentId: basicCase.comments[0].id, @@ -384,7 +413,7 @@ describe('Case Configuration API', () => { }); }); - test('happy path', async () => { + test('should return correct response', async () => { const resp = await patchComment({ caseId: basicCase.id, commentId: basicCase.comments[0].id, @@ -418,7 +447,7 @@ describe('Case Configuration API', () => { owner: SECURITY_SOLUTION_OWNER, }; - test('check url, method, signal', async () => { + test('should be called with correct check url, method, signal', async () => { await postCase(data, abortCtrl.signal); expect(fetchMock).toHaveBeenCalledWith(`${CASES_URL}`, { method: 'POST', @@ -427,7 +456,7 @@ describe('Case Configuration API', () => { }); }); - test('happy path', async () => { + test('should return correct response', async () => { const resp = await postCase(data, abortCtrl.signal); expect(resp).toEqual(basicCase); }); @@ -444,7 +473,7 @@ describe('Case Configuration API', () => { type: CommentType.user as const, }; - test('check url, method, signal', async () => { + test('should be called with correct check url, method, signal', async () => { await postComment(data, basicCase.id, abortCtrl.signal); expect(fetchMock).toHaveBeenCalledWith(`${CASES_URL}/${basicCase.id}/comments`, { method: 'POST', @@ -453,7 +482,7 @@ describe('Case Configuration API', () => { }); }); - test('happy path', async () => { + test('should return correct response', async () => { const resp = await postComment(data, basicCase.id, abortCtrl.signal); expect(resp).toEqual(basicCase); }); @@ -467,7 +496,7 @@ describe('Case Configuration API', () => { fetchMock.mockResolvedValue(pushedCaseSnake); }); - test('check url, method, signal', async () => { + test('should be called with correct check url, method, signal', async () => { await pushCase(basicCase.id, connectorId, abortCtrl.signal); expect(fetchMock).toHaveBeenCalledWith( `${CASES_URL}/${basicCase.id}/connector/${connectorId}/_push`, @@ -479,7 +508,7 @@ describe('Case Configuration API', () => { ); }); - test('happy path', async () => { + test('should return correct response', async () => { const resp = await pushCase(basicCase.id, connectorId, abortCtrl.signal); expect(resp).toEqual(pushedCase); }); diff --git a/x-pack/plugins/cases/public/containers/api.ts b/x-pack/plugins/cases/public/containers/api.ts index 51a68376936af..75e8c8f58705d 100644 --- a/x-pack/plugins/cases/public/containers/api.ts +++ b/x-pack/plugins/cases/public/containers/api.ts @@ -14,6 +14,7 @@ import { CasePatchRequest, CasePostRequest, CaseResponse, + CaseResolveResponse, CASES_URL, CasesFindResponse, CasesResponse, @@ -35,6 +36,7 @@ import { SubCaseResponse, SubCasesResponse, User, + ResolvedCase, } from '../../common'; import { getAllConnectorTypesUrl } from '../../common/utils/connectors_api'; @@ -61,6 +63,7 @@ import { decodeCasesFindResponse, decodeCasesStatusResponse, decodeCaseUserActionsResponse, + decodeCaseResolveResponse, } from './utils'; export const getCase = async ( @@ -78,6 +81,24 @@ export const getCase = async ( return convertToCamelCase(decodeCaseResponse(response)); }; +export const resolveCase = async ( + caseId: string, + includeComments: boolean = true, + signal: AbortSignal +): Promise => { + const response = await KibanaServices.get().http.fetch( + getCaseDetailsUrl(caseId) + '/resolve', + { + method: 'GET', + query: { + includeComments, + }, + signal, + } + ); + return convertToCamelCase(decodeCaseResolveResponse(response)); +}; + export const getSubCase = async ( caseId: string, subCaseId: string, diff --git a/x-pack/plugins/cases/public/containers/mock.ts b/x-pack/plugins/cases/public/containers/mock.ts index c955bb34240e2..f7d1daabd60ea 100644 --- a/x-pack/plugins/cases/public/containers/mock.ts +++ b/x-pack/plugins/cases/public/containers/mock.ts @@ -9,6 +9,7 @@ import { ActionLicense, AllCases, Case, CasesStatus, CaseUserActions, Comment } import { AssociationType, + CaseUserActionConnector, CaseResponse, CasesFindResponse, CasesResponse, @@ -19,6 +20,10 @@ import { CommentResponse, CommentType, ConnectorTypes, + ResolvedCase, + isCreateConnector, + isPush, + isUpdateConnector, SECURITY_SOLUTION_OWNER, UserAction, UserActionField, @@ -159,6 +164,12 @@ export const basicCase: Case = { subCaseIds: [], }; +export const basicResolvedCase: ResolvedCase = { + case: basicCase, + outcome: 'aliasMatch', + aliasTargetId: `${basicCase.id}_2`, +}; + export const collectionCase: Case = { type: CaseType.collection, owner: SECURITY_SOLUTION_OWNER, @@ -240,7 +251,9 @@ export const pushedCase: Case = { const basicAction = { actionAt: basicCreatedAt, actionBy: elasticUser, + oldValConnectorId: null, oldValue: null, + newValConnectorId: null, newValue: 'what a cool value', caseId: basicCaseId, commentId: null, @@ -308,12 +321,7 @@ export const basicCaseSnake: CaseResponse = { closed_at: null, closed_by: null, comments: [basicCommentSnake], - connector: { - id: 'none', - name: 'My Connector', - type: ConnectorTypes.none, - fields: null, - }, + connector: { id: 'none', name: 'My Connector', type: ConnectorTypes.none, fields: null }, created_at: basicCreatedAt, created_by: elasticUserSnake, external_service: null, @@ -328,8 +336,8 @@ export const casesStatusSnake: CasesStatusResponse = { count_open_cases: 20, }; +export const pushConnectorId = '123'; export const pushSnake = { - connector_id: '123', connector_name: 'connector name', external_id: 'external_id', external_title: 'external title', @@ -350,7 +358,7 @@ export const pushedCaseSnake = { type: ConnectorTypes.jira, fields: null, }, - external_service: basicPushSnake, + external_service: { ...basicPushSnake, connector_id: pushConnectorId }, }; export const reporters: string[] = ['alexis', 'kim', 'maria', 'steph']; @@ -385,17 +393,20 @@ const basicActionSnake = { comment_id: null, owner: SECURITY_SOLUTION_OWNER, }; -export const getUserActionSnake = (af: UserActionField, a: UserAction) => ({ - ...basicActionSnake, - action_id: `${af[0]}-${a}`, - action_field: af, - action: a, - comment_id: af[0] === 'comment' ? basicCommentId : null, - new_value: - a === 'push-to-service' && af[0] === 'pushed' - ? JSON.stringify(basicPushSnake) - : basicAction.newValue, -}); +export const getUserActionSnake = (af: UserActionField, a: UserAction) => { + const isPushToService = a === 'push-to-service' && af[0] === 'pushed'; + + return { + ...basicActionSnake, + action_id: `${af[0]}-${a}`, + action_field: af, + action: a, + comment_id: af[0] === 'comment' ? basicCommentId : null, + new_value: isPushToService ? JSON.stringify(basicPushSnake) : basicAction.newValue, + new_val_connector_id: isPushToService ? pushConnectorId : null, + old_val_connector_id: null, + }; +}; export const caseUserActionsSnake: CaseUserActionsResponse = [ getUserActionSnake(['description'], 'create'), @@ -405,17 +416,76 @@ export const caseUserActionsSnake: CaseUserActionsResponse = [ // user actions -export const getUserAction = (af: UserActionField, a: UserAction) => ({ - ...basicAction, - actionId: `${af[0]}-${a}`, - actionField: af, - action: a, - commentId: af[0] === 'comment' ? basicCommentId : null, - newValue: - a === 'push-to-service' && af[0] === 'pushed' - ? JSON.stringify(basicPushSnake) - : basicAction.newValue, -}); +export const getUserAction = ( + af: UserActionField, + a: UserAction, + overrides?: Partial +): CaseUserActions => { + return { + ...basicAction, + actionId: `${af[0]}-${a}`, + actionField: af, + action: a, + commentId: af[0] === 'comment' ? basicCommentId : null, + ...getValues(a, af, overrides), + }; +}; + +const getValues = ( + userAction: UserAction, + actionFields: UserActionField, + overrides?: Partial +): Partial => { + if (isCreateConnector(userAction, actionFields)) { + return { + newValue: + overrides?.newValue === undefined ? JSON.stringify(basicCaseSnake) : overrides.newValue, + newValConnectorId: overrides?.newValConnectorId ?? null, + oldValue: null, + oldValConnectorId: null, + }; + } else if (isUpdateConnector(userAction, actionFields)) { + return { + newValue: + overrides?.newValue === undefined + ? JSON.stringify({ name: 'My Connector', type: ConnectorTypes.none, fields: null }) + : overrides.newValue, + newValConnectorId: overrides?.newValConnectorId ?? null, + oldValue: + overrides?.oldValue === undefined + ? JSON.stringify({ name: 'My Connector2', type: ConnectorTypes.none, fields: null }) + : overrides.oldValue, + oldValConnectorId: overrides?.oldValConnectorId ?? null, + }; + } else if (isPush(userAction, actionFields)) { + return { + newValue: + overrides?.newValue === undefined ? JSON.stringify(basicPushSnake) : overrides?.newValue, + newValConnectorId: + overrides?.newValConnectorId === undefined ? pushConnectorId : overrides.newValConnectorId, + oldValue: overrides?.oldValue ?? null, + oldValConnectorId: overrides?.oldValConnectorId ?? null, + }; + } else { + return { + newValue: overrides?.newValue === undefined ? basicAction.newValue : overrides.newValue, + newValConnectorId: overrides?.newValConnectorId ?? null, + oldValue: overrides?.oldValue ?? null, + oldValConnectorId: overrides?.oldValConnectorId ?? null, + }; + } +}; + +export const getJiraConnectorWithoutId = (overrides?: Partial) => { + return JSON.stringify({ + name: 'jira1', + type: ConnectorTypes.jira, + ...jiraFields, + ...overrides, + }); +}; + +export const jiraFields = { fields: { issueType: '10006', priority: null, parent: null } }; export const getAlertUserAction = () => ({ ...basicAction, diff --git a/x-pack/plugins/cases/public/containers/use_get_case.test.tsx b/x-pack/plugins/cases/public/containers/use_get_case.test.tsx index c88f530709c8a..e825e232aebdc 100644 --- a/x-pack/plugins/cases/public/containers/use_get_case.test.tsx +++ b/x-pack/plugins/cases/public/containers/use_get_case.test.tsx @@ -7,7 +7,7 @@ import { renderHook, act } from '@testing-library/react-hooks'; import { useGetCase, UseGetCase } from './use_get_case'; -import { basicCase } from './mock'; +import { basicCase, basicResolvedCase } from './mock'; import * as api from './api'; jest.mock('./api'); @@ -28,6 +28,7 @@ describe('useGetCase', () => { await waitForNextUpdate(); expect(result.current).toEqual({ data: null, + resolveOutcome: null, isLoading: false, isError: false, fetchCase: result.current.fetchCase, @@ -36,13 +37,13 @@ describe('useGetCase', () => { }); }); - it('calls getCase with correct arguments', async () => { - const spyOnGetCase = jest.spyOn(api, 'getCase'); + it('calls resolveCase with correct arguments', async () => { + const spyOnResolveCase = jest.spyOn(api, 'resolveCase'); await act(async () => { const { waitForNextUpdate } = renderHook(() => useGetCase(basicCase.id)); await waitForNextUpdate(); await waitForNextUpdate(); - expect(spyOnGetCase).toBeCalledWith(basicCase.id, true, abortCtrl.signal); + expect(spyOnResolveCase).toBeCalledWith(basicCase.id, true, abortCtrl.signal); }); }); @@ -55,6 +56,8 @@ describe('useGetCase', () => { await waitForNextUpdate(); expect(result.current).toEqual({ data: basicCase, + resolveOutcome: basicResolvedCase.outcome, + resolveAliasId: basicResolvedCase.aliasTargetId, isLoading: false, isError: false, fetchCase: result.current.fetchCase, @@ -64,7 +67,7 @@ describe('useGetCase', () => { }); it('refetch case', async () => { - const spyOnGetCase = jest.spyOn(api, 'getCase'); + const spyOnResolveCase = jest.spyOn(api, 'resolveCase'); await act(async () => { const { result, waitForNextUpdate } = renderHook(() => useGetCase(basicCase.id) @@ -72,7 +75,7 @@ describe('useGetCase', () => { await waitForNextUpdate(); await waitForNextUpdate(); result.current.fetchCase(); - expect(spyOnGetCase).toHaveBeenCalledTimes(2); + expect(spyOnResolveCase).toHaveBeenCalledTimes(2); }); }); @@ -103,8 +106,8 @@ describe('useGetCase', () => { }); it('unhappy path', async () => { - const spyOnGetCase = jest.spyOn(api, 'getCase'); - spyOnGetCase.mockImplementation(() => { + const spyOnResolveCase = jest.spyOn(api, 'resolveCase'); + spyOnResolveCase.mockImplementation(() => { throw new Error('Something went wrong'); }); @@ -117,6 +120,7 @@ describe('useGetCase', () => { expect(result.current).toEqual({ data: null, + resolveOutcome: null, isLoading: false, isError: true, fetchCase: result.current.fetchCase, diff --git a/x-pack/plugins/cases/public/containers/use_get_case.tsx b/x-pack/plugins/cases/public/containers/use_get_case.tsx index b9326ad057c9e..52610981a227c 100644 --- a/x-pack/plugins/cases/public/containers/use_get_case.tsx +++ b/x-pack/plugins/cases/public/containers/use_get_case.tsx @@ -7,20 +7,22 @@ import { useEffect, useReducer, useCallback, useRef } from 'react'; -import { Case } from './types'; +import { Case, ResolvedCase } from './types'; import * as i18n from './translations'; import { useToasts } from '../common/lib/kibana'; -import { getCase, getSubCase } from './api'; +import { resolveCase, getSubCase } from './api'; interface CaseState { data: Case | null; + resolveOutcome: ResolvedCase['outcome'] | null; + resolveAliasId?: string; isLoading: boolean; isError: boolean; } type Action = | { type: 'FETCH_INIT'; payload: { silent: boolean } } - | { type: 'FETCH_SUCCESS'; payload: Case } + | { type: 'FETCH_SUCCESS'; payload: ResolvedCase } | { type: 'FETCH_FAILURE' } | { type: 'UPDATE_CASE'; payload: Case }; @@ -40,7 +42,9 @@ const dataFetchReducer = (state: CaseState, action: Action): CaseState => { ...state, isLoading: false, isError: false, - data: action.payload, + data: action.payload.case, + resolveOutcome: action.payload.outcome, + resolveAliasId: action.payload.aliasTargetId, }; case 'FETCH_FAILURE': return { @@ -72,6 +76,7 @@ export const useGetCase = (caseId: string, subCaseId?: string): UseGetCase => { isLoading: false, isError: false, data: null, + resolveOutcome: null, }); const toasts = useToasts(); const isCancelledRef = useRef(false); @@ -89,9 +94,12 @@ export const useGetCase = (caseId: string, subCaseId?: string): UseGetCase => { abortCtrlRef.current = new AbortController(); dispatch({ type: 'FETCH_INIT', payload: { silent } }); - const response = await (subCaseId - ? getSubCase(caseId, subCaseId, true, abortCtrlRef.current.signal) - : getCase(caseId, true, abortCtrlRef.current.signal)); + const response: ResolvedCase = subCaseId + ? { + case: await getSubCase(caseId, subCaseId, true, abortCtrlRef.current.signal), + outcome: 'exactMatch', // sub-cases are not resolved, forced to exactMatch always + } + : await resolveCase(caseId, true, abortCtrlRef.current.signal); if (!isCancelledRef.current) { dispatch({ type: 'FETCH_SUCCESS', payload: response }); diff --git a/x-pack/plugins/cases/public/containers/use_get_case_user_actions.test.tsx b/x-pack/plugins/cases/public/containers/use_get_case_user_actions.test.tsx index 62b4cf92434cd..e7e46fa46c7cc 100644 --- a/x-pack/plugins/cases/public/containers/use_get_case_user_actions.test.tsx +++ b/x-pack/plugins/cases/public/containers/use_get_case_user_actions.test.tsx @@ -18,7 +18,9 @@ import { basicPushSnake, caseUserActions, elasticUser, + getJiraConnectorWithoutId, getUserAction, + jiraFields, } from './mock'; import * as api from './api'; @@ -299,15 +301,14 @@ describe('useGetCaseUserActions', () => { const pushAction123 = getUserAction(['pushed'], 'push-to-service'); const push456 = { ...basicPushSnake, - connector_id: '456', connector_name: 'other connector name', external_id: 'other_external_id', }; - const pushAction456 = { - ...getUserAction(['pushed'], 'push-to-service'), + const pushAction456 = getUserAction(['pushed'], 'push-to-service', { newValue: JSON.stringify(push456), - }; + newValConnectorId: '456', + }); const userActions = [ ...caseUserActions, @@ -346,15 +347,14 @@ describe('useGetCaseUserActions', () => { const pushAction123 = getUserAction(['pushed'], 'push-to-service'); const push456 = { ...basicPushSnake, - connector_id: '456', connector_name: 'other connector name', external_id: 'other_external_id', }; - const pushAction456 = { - ...getUserAction(['pushed'], 'push-to-service'), + const pushAction456 = getUserAction(['pushed'], 'push-to-service', { newValue: JSON.stringify(push456), - }; + newValConnectorId: '456', + }); const userActions = [ ...caseUserActions, @@ -392,11 +392,7 @@ describe('useGetCaseUserActions', () => { const userActions = [ ...caseUserActions, getUserAction(['pushed'], 'push-to-service'), - { - ...getUserAction(['connector'], 'update'), - oldValue: JSON.stringify({ id: '123', fields: { issueType: '10006', priority: null } }), - newValue: JSON.stringify({ id: '123', fields: { issueType: '10006', priority: 'High' } }), - }, + createUpdateConnectorFields123HighPriorityUserAction(), ]; const result = getPushedInfo(userActions, '123'); @@ -418,11 +414,7 @@ describe('useGetCaseUserActions', () => { const userActions = [ ...caseUserActions, getUserAction(['pushed'], 'push-to-service'), - { - ...getUserAction(['connector'], 'update'), - oldValue: JSON.stringify({ id: '123', fields: { issueType: '10006', priority: null } }), - newValue: JSON.stringify({ id: '456', fields: { issueTypes: ['10'], severity: '6' } }), - }, + createChangeConnector123To456UserAction(), ]; const result = getPushedInfo(userActions, '123'); @@ -444,16 +436,8 @@ describe('useGetCaseUserActions', () => { const userActions = [ ...caseUserActions, getUserAction(['pushed'], 'push-to-service'), - { - ...getUserAction(['connector'], 'update'), - oldValue: JSON.stringify({ id: '123', fields: { issueType: '10006', priority: null } }), - newValue: JSON.stringify({ id: '456', fields: { issueTypes: ['10'], severity: '6' } }), - }, - { - ...getUserAction(['connector'], 'update'), - oldValue: JSON.stringify({ id: '456', fields: { issueTypes: ['10'], severity: '6' } }), - newValue: JSON.stringify({ id: '123', fields: { issueType: '10006', priority: null } }), - }, + createChangeConnector123To456UserAction(), + createChangeConnector456To123UserAction(), ]; const result = getPushedInfo(userActions, '123'); @@ -474,22 +458,10 @@ describe('useGetCaseUserActions', () => { it('Change fields and connector after push - hasDataToPush: true', () => { const userActions = [ ...caseUserActions, - { - ...getUserAction(['connector'], 'update'), - oldValue: JSON.stringify({ id: '123', fields: { issueType: '10006', priority: null } }), - newValue: JSON.stringify({ id: '123', fields: { issueType: '10006', priority: 'High' } }), - }, + createUpdateConnectorFields123HighPriorityUserAction(), getUserAction(['pushed'], 'push-to-service'), - { - ...getUserAction(['connector'], 'update'), - oldValue: JSON.stringify({ id: '123', fields: { issueType: '10006', priority: 'High' } }), - newValue: JSON.stringify({ id: '456', fields: { issueTypes: ['10'], severity: '6' } }), - }, - { - ...getUserAction(['connector'], 'update'), - oldValue: JSON.stringify({ id: '456', fields: { issueTypes: ['10'], severity: '6' } }), - newValue: JSON.stringify({ id: '123', fields: { issueType: '10006', priority: 'Low' } }), - }, + createChangeConnector123HighPriorityTo456UserAction(), + createChangeConnector456To123PriorityLowUserAction(), ]; const result = getPushedInfo(userActions, '123'); @@ -510,22 +482,10 @@ describe('useGetCaseUserActions', () => { it('Change only connector after push - hasDataToPush: false', () => { const userActions = [ ...caseUserActions, - { - ...getUserAction(['connector'], 'update'), - oldValue: JSON.stringify({ id: '123', fields: { issueType: '10006', priority: null } }), - newValue: JSON.stringify({ id: '123', fields: { issueType: '10006', priority: 'High' } }), - }, + createUpdateConnectorFields123HighPriorityUserAction(), getUserAction(['pushed'], 'push-to-service'), - { - ...getUserAction(['connector'], 'update'), - oldValue: JSON.stringify({ id: '123', fields: { issueType: '10006', priority: 'High' } }), - newValue: JSON.stringify({ id: '456', fields: { issueTypes: ['10'], severity: '6' } }), - }, - { - ...getUserAction(['connector'], 'update'), - oldValue: JSON.stringify({ id: '456', fields: { issueTypes: ['10'], severity: '6' } }), - newValue: JSON.stringify({ id: '123', fields: { issueType: '10006', priority: 'High' } }), - }, + createChangeConnector123HighPriorityTo456UserAction(), + createChangeConnector456To123HighPriorityUserAction(), ]; const result = getPushedInfo(userActions, '123'); @@ -547,45 +507,24 @@ describe('useGetCaseUserActions', () => { const pushAction123 = getUserAction(['pushed'], 'push-to-service'); const push456 = { ...basicPushSnake, - connector_id: '456', connector_name: 'other connector name', external_id: 'other_external_id', }; - const pushAction456 = { - ...getUserAction(['pushed'], 'push-to-service'), + const pushAction456 = getUserAction(['pushed'], 'push-to-service', { newValue: JSON.stringify(push456), - }; + newValConnectorId: '456', + }); const userActions = [ ...caseUserActions, - { - ...getUserAction(['connector'], 'update'), - oldValue: JSON.stringify({ id: '123', fields: { issueType: '10006', priority: null } }), - newValue: JSON.stringify({ id: '123', fields: { issueType: '10006', priority: 'High' } }), - }, + createUpdateConnectorFields123HighPriorityUserAction(), pushAction123, - { - ...getUserAction(['connector'], 'update'), - oldValue: JSON.stringify({ id: '123', fields: { issueType: '10006', priority: 'High' } }), - newValue: JSON.stringify({ id: '456', fields: { issueTypes: ['10'], severity: '6' } }), - }, + createChangeConnector123HighPriorityTo456UserAction(), pushAction456, - { - ...getUserAction(['connector'], 'update'), - oldValue: JSON.stringify({ id: '456', fields: { issueTypes: ['10'], severity: '6' } }), - newValue: JSON.stringify({ id: '123', fields: { issueType: '10006', priority: 'Low' } }), - }, - { - ...getUserAction(['connector'], 'update'), - oldValue: JSON.stringify({ id: '123', fields: { issueType: '10006', priority: 'Low' } }), - newValue: JSON.stringify({ id: '456', fields: { issueTypes: ['10'], severity: '6' } }), - }, - { - ...getUserAction(['connector'], 'update'), - oldValue: JSON.stringify({ id: '456', fields: { issueTypes: ['10'], severity: '6' } }), - newValue: JSON.stringify({ id: '123', fields: { issueType: '10006', priority: 'Low' } }), - }, + createChangeConnector456To123PriorityLowUserAction(), + createChangeConnector123LowPriorityTo456UserAction(), + createChangeConnector456To123PriorityLowUserAction(), ]; const result = getPushedInfo(userActions, '123'); @@ -617,34 +556,22 @@ describe('useGetCaseUserActions', () => { const pushAction123 = getUserAction(['pushed'], 'push-to-service'); const push456 = { ...basicPushSnake, - connector_id: '456', connector_name: 'other connector name', external_id: 'other_external_id', }; - const pushAction456 = { - ...getUserAction(['pushed'], 'push-to-service'), + const pushAction456 = getUserAction(['pushed'], 'push-to-service', { + newValConnectorId: '456', newValue: JSON.stringify(push456), - }; + }); + const userActions = [ ...caseUserActions, - { - ...getUserAction(['connector'], 'update'), - oldValue: JSON.stringify({ id: '123', fields: { issueType: '10006', priority: null } }), - newValue: JSON.stringify({ id: '123', fields: { issueType: '10006', priority: 'High' } }), - }, + createUpdateConnectorFields123HighPriorityUserAction(), pushAction123, - { - ...getUserAction(['connector'], 'update'), - oldValue: JSON.stringify({ id: '123', fields: { issueType: '10006', priority: 'High' } }), - newValue: JSON.stringify({ id: '456', fields: { issueTypes: ['10'], severity: '6' } }), - }, + createChangeConnector123HighPriorityTo456UserAction(), pushAction456, - { - ...getUserAction(['connector'], 'update'), - oldValue: JSON.stringify({ id: '456', fields: { issueTypes: ['10'], severity: '6' } }), - newValue: JSON.stringify({ id: '123', fields: { issueType: '10006', priority: 'High' } }), - }, + createChangeConnector456To123HighPriorityUserAction(), ]; const result = getPushedInfo(userActions, '123'); @@ -675,22 +602,10 @@ describe('useGetCaseUserActions', () => { it('Changing other connectors fields does not count as an update', () => { const userActions = [ ...caseUserActions, - { - ...getUserAction(['connector'], 'update'), - oldValue: JSON.stringify({ id: '123', fields: { issueType: '10006', priority: null } }), - newValue: JSON.stringify({ id: '123', fields: { issueType: '10006', priority: 'High' } }), - }, + createUpdateConnectorFields123HighPriorityUserAction(), getUserAction(['pushed'], 'push-to-service'), - { - ...getUserAction(['connector'], 'update'), - oldValue: JSON.stringify({ id: '123', fields: { issueType: '10006', priority: 'High' } }), - newValue: JSON.stringify({ id: '456', fields: { issueTypes: ['10'], severity: '6' } }), - }, - { - ...getUserAction(['connector'], 'update'), - oldValue: JSON.stringify({ id: '456', fields: { issueTypes: ['10'], severity: '6' } }), - newValue: JSON.stringify({ id: '456', fields: { issueTypes: ['10'], severity: '3' } }), - }, + createChangeConnector123HighPriorityTo456UserAction(), + createUpdateConnectorFields456HighPriorityUserAction(), ]; const result = getPushedInfo(userActions, '123'); @@ -709,3 +624,83 @@ describe('useGetCaseUserActions', () => { }); }); }); + +const jira123HighPriorityFields = { + fields: { ...jiraFields.fields, priority: 'High' }, +}; + +const jira123LowPriorityFields = { + fields: { ...jiraFields.fields, priority: 'Low' }, +}; + +const jira456Fields = { + fields: { issueType: '10', parent: null, priority: null }, +}; + +const jira456HighPriorityFields = { + fields: { ...jira456Fields.fields, priority: 'High' }, +}; + +const createUpdateConnectorFields123HighPriorityUserAction = () => + getUserAction(['connector'], 'update', { + oldValue: getJiraConnectorWithoutId(), + newValue: getJiraConnectorWithoutId(jira123HighPriorityFields), + oldValConnectorId: '123', + newValConnectorId: '123', + }); + +const createUpdateConnectorFields456HighPriorityUserAction = () => + getUserAction(['connector'], 'update', { + oldValue: getJiraConnectorWithoutId(jira456Fields), + newValue: getJiraConnectorWithoutId(jira456HighPriorityFields), + oldValConnectorId: '456', + newValConnectorId: '456', + }); + +const createChangeConnector123HighPriorityTo456UserAction = () => + getUserAction(['connector'], 'update', { + oldValue: getJiraConnectorWithoutId(jira123HighPriorityFields), + oldValConnectorId: '123', + newValue: getJiraConnectorWithoutId(jira456Fields), + newValConnectorId: '456', + }); + +const createChangeConnector123To456UserAction = () => + getUserAction(['connector'], 'update', { + oldValue: getJiraConnectorWithoutId(), + oldValConnectorId: '123', + newValue: getJiraConnectorWithoutId(jira456Fields), + newValConnectorId: '456', + }); + +const createChangeConnector123LowPriorityTo456UserAction = () => + getUserAction(['connector'], 'update', { + oldValue: getJiraConnectorWithoutId(jira123LowPriorityFields), + oldValConnectorId: '123', + newValue: getJiraConnectorWithoutId(jira456Fields), + newValConnectorId: '456', + }); + +const createChangeConnector456To123UserAction = () => + getUserAction(['connector'], 'update', { + oldValue: getJiraConnectorWithoutId(jira456Fields), + oldValConnectorId: '456', + newValue: getJiraConnectorWithoutId(), + newValConnectorId: '123', + }); + +const createChangeConnector456To123HighPriorityUserAction = () => + getUserAction(['connector'], 'update', { + oldValue: getJiraConnectorWithoutId(jira456Fields), + oldValConnectorId: '456', + newValue: getJiraConnectorWithoutId(jira123HighPriorityFields), + newValConnectorId: '123', + }); + +const createChangeConnector456To123PriorityLowUserAction = () => + getUserAction(['connector'], 'update', { + oldValue: getJiraConnectorWithoutId(jira456Fields), + oldValConnectorId: '456', + newValue: getJiraConnectorWithoutId(jira123LowPriorityFields), + newValConnectorId: '123', + }); diff --git a/x-pack/plugins/cases/public/containers/use_get_case_user_actions.tsx b/x-pack/plugins/cases/public/containers/use_get_case_user_actions.tsx index e481519ba19a3..36d600c3f1c9d 100644 --- a/x-pack/plugins/cases/public/containers/use_get_case_user_actions.tsx +++ b/x-pack/plugins/cases/public/containers/use_get_case_user_actions.tsx @@ -18,7 +18,8 @@ import { } from '../../common'; import { getCaseUserActions, getSubCaseUserActions } from './api'; import * as i18n from './translations'; -import { convertToCamelCase, parseString } from './utils'; +import { convertToCamelCase } from './utils'; +import { parseStringAsConnector, parseStringAsExternalService } from '../common/user_actions'; import { useToasts } from '../common/lib/kibana'; export interface CaseService extends CaseExternalService { @@ -58,8 +59,24 @@ export interface UseGetCaseUserActions extends CaseUserActionsState { ) => Promise; } -const getExternalService = (value: string): CaseExternalService | null => - convertToCamelCase(parseString(`${value}`)); +const unknownExternalServiceConnectorId = 'unknown'; + +const getExternalService = ( + connectorId: string | null, + encodedValue: string | null +): CaseExternalService | null => { + const decodedValue = parseStringAsExternalService(connectorId, encodedValue); + + if (decodedValue == null) { + return null; + } + return { + ...convertToCamelCase(decodedValue), + // if in the rare case that the connector id is null we'll set it to unknown if we need to reference it in the UI + // anywhere. The id would only ever be null if a migration failed or some logic error within the backend occurred + connectorId: connectorId ?? unknownExternalServiceConnectorId, + }; +}; const groupConnectorFields = ( userActions: CaseUserActions[] @@ -69,22 +86,26 @@ const groupConnectorFields = ( return acc; } - const oldValue = parseString(`${mua.oldValue}`); - const newValue = parseString(`${mua.newValue}`); + const oldConnector = parseStringAsConnector(mua.oldValConnectorId, mua.oldValue); + const newConnector = parseStringAsConnector(mua.newValConnectorId, mua.newValue); - if (oldValue == null || newValue == null) { + if (!oldConnector || !newConnector) { return acc; } return { ...acc, - [oldValue.id]: [ - ...(acc[oldValue.id] || []), - ...(oldValue.id === newValue.id ? [oldValue.fields, newValue.fields] : [oldValue.fields]), + [oldConnector.id]: [ + ...(acc[oldConnector.id] || []), + ...(oldConnector.id === newConnector.id + ? [oldConnector.fields, newConnector.fields] + : [oldConnector.fields]), ], - [newValue.id]: [ - ...(acc[newValue.id] || []), - ...(oldValue.id === newValue.id ? [oldValue.fields, newValue.fields] : [newValue.fields]), + [newConnector.id]: [ + ...(acc[newConnector.id] || []), + ...(oldConnector.id === newConnector.id + ? [oldConnector.fields, newConnector.fields] + : [newConnector.fields]), ], }; }, {} as Record>); @@ -137,9 +158,7 @@ export const getPushedInfo = ( const hasDataToPushForConnector = (connectorId: string): boolean => { const caseUserActionsReversed = [...caseUserActions].reverse(); const lastPushOfConnectorReversedIndex = caseUserActionsReversed.findIndex( - (mua) => - mua.action === 'push-to-service' && - getExternalService(`${mua.newValue}`)?.connectorId === connectorId + (mua) => mua.action === 'push-to-service' && mua.newValConnectorId === connectorId ); if (lastPushOfConnectorReversedIndex === -1) { @@ -190,7 +209,7 @@ export const getPushedInfo = ( return acc; } - const externalService = getExternalService(`${cua.newValue}`); + const externalService = getExternalService(cua.newValConnectorId, cua.newValue); if (externalService === null) { return acc; } diff --git a/x-pack/plugins/cases/public/containers/utils.ts b/x-pack/plugins/cases/public/containers/utils.ts index de67b1cfbd6fa..458899e5f53c9 100644 --- a/x-pack/plugins/cases/public/containers/utils.ts +++ b/x-pack/plugins/cases/public/containers/utils.ts @@ -30,20 +30,14 @@ import { CaseUserActionsResponseRt, CommentType, CasePatchRequest, + CaseResolveResponse, + CaseResolveResponseRt, } from '../../common'; import { AllCases, Case, UpdateByKey } from './types'; import * as i18n from './translations'; export const getTypedPayload = (a: unknown): T => a as T; -export const parseString = (params: string) => { - try { - return JSON.parse(params); - } catch { - return null; - } -}; - export const convertArrayToCamelCase = (arrayOfSnakes: unknown[]): unknown[] => arrayOfSnakes.reduce((acc: unknown[], value) => { if (isArray(value)) { @@ -88,6 +82,12 @@ export const createToasterPlainError = (message: string) => new ToasterError([me export const decodeCaseResponse = (respCase?: CaseResponse) => pipe(CaseResponseRt.decode(respCase), fold(throwErrors(createToasterPlainError), identity)); +export const decodeCaseResolveResponse = (respCase?: CaseResolveResponse) => + pipe( + CaseResolveResponseRt.decode(respCase), + fold(throwErrors(createToasterPlainError), identity) + ); + export const decodeCasesResponse = (respCase?: CasesResponse) => pipe(CasesResponseRt.decode(respCase), fold(throwErrors(createToasterPlainError), identity)); diff --git a/x-pack/plugins/cases/public/types.ts b/x-pack/plugins/cases/public/types.ts index db2e5d6ab6bff..5b19bcfa8ac46 100644 --- a/x-pack/plugins/cases/public/types.ts +++ b/x-pack/plugins/cases/public/types.ts @@ -16,6 +16,7 @@ import type { } from '../../triggers_actions_ui/public'; import type { DataPublicPluginStart } from '../../../../src/plugins/data/public'; import type { EmbeddableStart } from '../../../../src/plugins/embeddable/public'; +import type { SpacesPluginStart } from '../../spaces/public'; import type { Storage } from '../../../../src/plugins/kibana_utils/public'; import { AllCasesProps } from './components/all_cases'; @@ -36,6 +37,7 @@ export interface StartPlugins { lens: LensPublicStart; storage: Storage; triggersActionsUi: TriggersActionsStart; + spaces?: SpacesPluginStart; } /** diff --git a/x-pack/plugins/cases/server/authorization/__snapshots__/audit_logger.test.ts.snap b/x-pack/plugins/cases/server/authorization/__snapshots__/audit_logger.test.ts.snap index 3ca77944776b3..50c085b7f22a8 100644 --- a/x-pack/plugins/cases/server/authorization/__snapshots__/audit_logger.test.ts.snap +++ b/x-pack/plugins/cases/server/authorization/__snapshots__/audit_logger.test.ts.snap @@ -1596,6 +1596,90 @@ Object { } `; +exports[`audit_logger log function event structure creates the correct audit event for operation: "resolveCase" with an error and entity 1`] = ` +Object { + "error": Object { + "code": "Error", + "message": "an error", + }, + "event": Object { + "action": "case_resolve", + "category": Array [ + "database", + ], + "outcome": "failure", + "type": Array [ + "access", + ], + }, + "kibana": Object { + "saved_object": Object { + "id": "1", + "type": "cases", + }, + }, + "message": "Failed attempt to access cases [id=1] as owner \\"awesome\\"", +} +`; + +exports[`audit_logger log function event structure creates the correct audit event for operation: "resolveCase" with an error but no entity 1`] = ` +Object { + "error": Object { + "code": "Error", + "message": "an error", + }, + "event": Object { + "action": "case_resolve", + "category": Array [ + "database", + ], + "outcome": "failure", + "type": Array [ + "access", + ], + }, + "message": "Failed attempt to access a case as any owners", +} +`; + +exports[`audit_logger log function event structure creates the correct audit event for operation: "resolveCase" without an error but with an entity 1`] = ` +Object { + "event": Object { + "action": "case_resolve", + "category": Array [ + "database", + ], + "outcome": "success", + "type": Array [ + "access", + ], + }, + "kibana": Object { + "saved_object": Object { + "id": "5", + "type": "cases", + }, + }, + "message": "User has accessed cases [id=5] as owner \\"super\\"", +} +`; + +exports[`audit_logger log function event structure creates the correct audit event for operation: "resolveCase" without an error or entity 1`] = ` +Object { + "event": Object { + "action": "case_resolve", + "category": Array [ + "database", + ], + "outcome": "success", + "type": Array [ + "access", + ], + }, + "message": "User has accessed a case as any owners", +} +`; + exports[`audit_logger log function event structure creates the correct audit event for operation: "updateCase" with an error and entity 1`] = ` Object { "error": Object { diff --git a/x-pack/plugins/cases/server/authorization/index.ts b/x-pack/plugins/cases/server/authorization/index.ts index 90b89c7f75766..1a74640515173 100644 --- a/x-pack/plugins/cases/server/authorization/index.ts +++ b/x-pack/plugins/cases/server/authorization/index.ts @@ -152,6 +152,14 @@ export const Operations: Record Promise; */ export enum ReadOperations { GetCase = 'getCase', + ResolveCase = 'resolveCase', FindCases = 'findCases', GetCaseIDsByAlertID = 'getCaseIDsByAlertID', GetCaseStatuses = 'getCaseStatuses', diff --git a/x-pack/plugins/cases/server/client/attachments/add.ts b/x-pack/plugins/cases/server/client/attachments/add.ts index 507405d58cef1..b84a6bd84c43b 100644 --- a/x-pack/plugins/cases/server/client/attachments/add.ts +++ b/x-pack/plugins/cases/server/client/attachments/add.ts @@ -106,7 +106,7 @@ async function getSubCase({ caseId, subCaseId: newSubCase.id, fields: ['status', 'sub_case'], - newValue: JSON.stringify({ status: newSubCase.attributes.status }), + newValue: { status: newSubCase.attributes.status }, owner: newSubCase.attributes.owner, }), ], @@ -220,7 +220,7 @@ const addGeneratedAlerts = async ( subCaseId: updatedCase.subCaseId, commentId: newComment.id, fields: ['comment'], - newValue: JSON.stringify(query), + newValue: query, owner: newComment.attributes.owner, }), ], @@ -408,7 +408,7 @@ export const addComment = async ( subCaseId: updatedCase.subCaseId, commentId: newComment.id, fields: ['comment'], - newValue: JSON.stringify(query), + newValue: query, owner: newComment.attributes.owner, }), ], diff --git a/x-pack/plugins/cases/server/client/attachments/update.ts b/x-pack/plugins/cases/server/client/attachments/update.ts index 9816efd9a8452..b5e9e6c372355 100644 --- a/x-pack/plugins/cases/server/client/attachments/update.ts +++ b/x-pack/plugins/cases/server/client/attachments/update.ts @@ -17,6 +17,7 @@ import { SUB_CASE_SAVED_OBJECT, CaseResponse, CommentPatchRequest, + CommentRequest, } from '../../../common'; import { AttachmentService, CasesService } from '../../services'; import { CasesClientArgs } from '..'; @@ -193,12 +194,12 @@ export async function update( subCaseId: subCaseID, commentId: updatedComment.id, fields: ['comment'], - newValue: JSON.stringify(queryRestAttributes), - oldValue: JSON.stringify( + // casting because typescript is complaining that it's not a Record even though it is + newValue: queryRestAttributes as CommentRequest, + oldValue: // We are interested only in ContextBasicRt attributes // myComment.attribute contains also CommentAttributesBasicRt attributes - pick(Object.keys(queryRestAttributes), myComment.attributes) - ), + pick(Object.keys(queryRestAttributes), myComment.attributes), owner: myComment.attributes.owner, }), ], diff --git a/x-pack/plugins/cases/server/client/cases/client.ts b/x-pack/plugins/cases/server/client/cases/client.ts index 0932308c2e269..fd9bd489f31b2 100644 --- a/x-pack/plugins/cases/server/client/cases/client.ts +++ b/x-pack/plugins/cases/server/client/cases/client.ts @@ -18,6 +18,7 @@ import { CasesClient } from '../client'; import { CasesClientInternal } from '../client_internal'; import { ICasePostRequest, + ICaseResolveResponse, ICaseResponse, ICasesFindRequest, ICasesFindResponse, @@ -31,6 +32,7 @@ import { find } from './find'; import { CasesByAlertIDParams, get, + resolve, getCasesByAlertID, GetParams, getReporters, @@ -57,6 +59,11 @@ export interface CasesSubClient { * Retrieves a single case with the specified ID. */ get(params: GetParams): Promise; + /** + * @experimental + * Retrieves a single case resolving the specified ID. + */ + resolve(params: GetParams): Promise; /** * Pushes a specific case to an external system. */ @@ -99,6 +106,7 @@ export const createCasesSubClient = ( create: (data: CasePostRequest) => create(data, clientArgs), find: (params: CasesFindRequest) => find(params, clientArgs), get: (params: GetParams) => get(params, clientArgs), + resolve: (params: GetParams) => resolve(params, clientArgs), push: (params: PushParams) => push(params, clientArgs, casesClient, casesClientInternal), update: (cases: CasesPatchRequest) => update(cases, clientArgs, casesClientInternal), delete: (ids: string[]) => deleteCases(ids, clientArgs), diff --git a/x-pack/plugins/cases/server/client/cases/create.ts b/x-pack/plugins/cases/server/client/cases/create.ts index 887990fef8938..488bc523f7796 100644 --- a/x-pack/plugins/cases/server/client/cases/create.ts +++ b/x-pack/plugins/cases/server/client/cases/create.ts @@ -106,7 +106,7 @@ export const create = async ( actionBy: { username, full_name, email }, caseId: newCase.id, fields: ['description', 'status', 'tags', 'title', 'connector', 'settings', OWNER_FIELD], - newValue: JSON.stringify(query), + newValue: query, owner: newCase.attributes.owner, }), ], diff --git a/x-pack/plugins/cases/server/client/cases/delete.ts b/x-pack/plugins/cases/server/client/cases/delete.ts index 80a687a0e72f8..4333535f17a24 100644 --- a/x-pack/plugins/cases/server/client/cases/delete.ts +++ b/x-pack/plugins/cases/server/client/cases/delete.ts @@ -168,7 +168,7 @@ export async function deleteCases(ids: string[], clientArgs: CasesClientArgs): P 'settings', OWNER_FIELD, 'comment', - ...(ENABLE_CASE_CONNECTOR ? ['sub_case'] : []), + ...(ENABLE_CASE_CONNECTOR ? ['sub_case' as const] : []), ], owner: caseInfo.attributes.owner, }) diff --git a/x-pack/plugins/cases/server/client/cases/get.ts b/x-pack/plugins/cases/server/client/cases/get.ts index 6b0015d4ffb14..c6ab033c2a848 100644 --- a/x-pack/plugins/cases/server/client/cases/get.ts +++ b/x-pack/plugins/cases/server/client/cases/get.ts @@ -9,10 +9,12 @@ import { pipe } from 'fp-ts/lib/pipeable'; import { fold } from 'fp-ts/lib/Either'; import { identity } from 'fp-ts/lib/function'; -import { SavedObject } from 'kibana/server'; +import { SavedObject, SavedObjectsResolveResponse } from 'kibana/server'; import { CaseResponseRt, CaseResponse, + CaseResolveResponseRt, + CaseResolveResponse, User, UsersRt, AllTagsFindRequest, @@ -230,6 +232,86 @@ export const get = async ( } }; +/** + * Retrieves a case resolving its ID and optionally loading its comments and sub case comments. + * + * @experimental + */ +export const resolve = async ( + { id, includeComments, includeSubCaseComments }: GetParams, + clientArgs: CasesClientArgs +): Promise => { + const { unsecuredSavedObjectsClient, caseService, logger, authorization } = clientArgs; + + try { + if (!ENABLE_CASE_CONNECTOR && includeSubCaseComments) { + throw Boom.badRequest( + 'The `includeSubCaseComments` is not supported when the case connector feature is disabled' + ); + } + + const { + saved_object: savedObject, + ...resolveData + }: SavedObjectsResolveResponse = await caseService.getResolveCase({ + unsecuredSavedObjectsClient, + id, + }); + + await authorization.ensureAuthorized({ + operation: Operations.resolveCase, + entities: [ + { + id: savedObject.id, + owner: savedObject.attributes.owner, + }, + ], + }); + + let subCaseIds: string[] = []; + if (ENABLE_CASE_CONNECTOR) { + const subCasesForCaseId = await caseService.findSubCasesByCaseId({ + unsecuredSavedObjectsClient, + ids: [id], + }); + subCaseIds = subCasesForCaseId.saved_objects.map((so) => so.id); + } + + if (!includeComments) { + return CaseResolveResponseRt.encode({ + ...resolveData, + case: flattenCaseSavedObject({ + savedObject, + subCaseIds, + }), + }); + } + + const theComments = await caseService.getAllCaseComments({ + unsecuredSavedObjectsClient, + id, + options: { + sortField: 'created_at', + sortOrder: 'asc', + }, + includeSubCaseComments: ENABLE_CASE_CONNECTOR && includeSubCaseComments, + }); + + return CaseResolveResponseRt.encode({ + ...resolveData, + case: flattenCaseSavedObject({ + savedObject, + subCaseIds, + comments: theComments.saved_objects, + totalComment: theComments.total, + totalAlerts: countAlertsForID({ comments: theComments, id }), + }), + }); + } catch (error) { + throw createCaseError({ message: `Failed to resolve case id: ${id}: ${error}`, error, logger }); + } +}; + /** * Retrieves the tags from all the cases. */ diff --git a/x-pack/plugins/cases/server/client/cases/mock.ts b/x-pack/plugins/cases/server/client/cases/mock.ts index 313d6cd12a6db..22520cea11014 100644 --- a/x-pack/plugins/cases/server/client/cases/mock.ts +++ b/x-pack/plugins/cases/server/client/cases/mock.ts @@ -231,8 +231,10 @@ export const userActions: CaseUserActionsResponse = [ username: 'elastic', }, new_value: - '{"title":"Case SIR","tags":["sir"],"description":"testing sir","connector":{"id":"456","name":"ServiceNow SN","type":".servicenow-sir","fields":{"category":"Denial of Service","destIp":true,"malwareHash":true,"malwareUrl":true,"priority":"2","sourceIp":true,"subcategory":"45"}},"settings":{"syncAlerts":true}}', + '{"title":"Case SIR","tags":["sir"],"description":"testing sir","connector":{"name":"ServiceNow SN","type":".servicenow-sir","fields":{"category":"Denial of Service","destIp":true,"malwareHash":true,"malwareUrl":true,"priority":"2","sourceIp":true,"subcategory":"45"}},"settings":{"syncAlerts":true}}', + new_val_connector_id: '456', old_value: null, + old_val_connector_id: null, action_id: 'fd830c60-6646-11eb-a291-51bf6b175a53', case_id: 'fcdedd20-6646-11eb-a291-51bf6b175a53', comment_id: null, @@ -248,7 +250,9 @@ export const userActions: CaseUserActionsResponse = [ username: 'elastic', }, new_value: - '{"pushed_at":"2021-02-03T17:41:26.108Z","pushed_by":{"username":"elastic","full_name":"Elastic","email":"elastic@elastic.co"},"connector_id":"456","connector_name":"ServiceNow SN","external_id":"external-id","external_title":"SIR0010037","external_url":"https://dev92273.service-now.com/nav_to.do?uri=sn_si_incident.do?sys_id=external-id"}', + '{"pushed_at":"2021-02-03T17:41:26.108Z","pushed_by":{"username":"elastic","full_name":"Elastic","email":"elastic@elastic.co"},"connector_name":"ServiceNow SN","external_id":"external-id","external_title":"SIR0010037","external_url":"https://dev92273.service-now.com/nav_to.do?uri=sn_si_incident.do?sys_id=external-id"}', + new_val_connector_id: '456', + old_val_connector_id: null, old_value: null, action_id: '0a801750-6647-11eb-a291-51bf6b175a53', case_id: 'fcdedd20-6646-11eb-a291-51bf6b175a53', @@ -265,6 +269,8 @@ export const userActions: CaseUserActionsResponse = [ username: 'elastic', }, new_value: '{"type":"alert","alertId":"alert-id-1","index":".siem-signals-default-000008"}', + new_val_connector_id: null, + old_val_connector_id: null, old_value: null, action_id: '7373eb60-6647-11eb-a291-51bf6b175a53', case_id: 'fcdedd20-6646-11eb-a291-51bf6b175a53', @@ -282,6 +288,8 @@ export const userActions: CaseUserActionsResponse = [ }, new_value: '{"type":"alert","alertId":"alert-id-2","index":".siem-signals-default-000008"}', old_value: null, + new_val_connector_id: null, + old_val_connector_id: null, action_id: '7abc6410-6647-11eb-a291-51bf6b175a53', case_id: 'fcdedd20-6646-11eb-a291-51bf6b175a53', comment_id: 'comment-alert-2', @@ -297,8 +305,10 @@ export const userActions: CaseUserActionsResponse = [ username: 'elastic', }, new_value: - '{"pushed_at":"2021-02-03T17:45:29.400Z","pushed_by":{"username":"elastic","full_name":"Elastic","email":"elastic@elastic.co"},"connector_id":"456","connector_name":"ServiceNow SN","external_id":"external-id","external_title":"SIR0010037","external_url":"https://dev92273.service-now.com/nav_to.do?uri=sn_si_incident.do?sys_id=external-id"}', + '{"pushed_at":"2021-02-03T17:45:29.400Z","pushed_by":{"username":"elastic","full_name":"Elastic","email":"elastic@elastic.co"},"connector_name":"ServiceNow SN","external_id":"external-id","external_title":"SIR0010037","external_url":"https://dev92273.service-now.com/nav_to.do?uri=sn_si_incident.do?sys_id=external-id"}', + new_val_connector_id: '456', old_value: null, + old_val_connector_id: null, action_id: '9b91d8f0-6647-11eb-a291-51bf6b175a53', case_id: 'fcdedd20-6646-11eb-a291-51bf6b175a53', comment_id: null, @@ -315,6 +325,8 @@ export const userActions: CaseUserActionsResponse = [ }, new_value: '{"comment":"a comment!","type":"user"}', old_value: null, + new_val_connector_id: null, + old_val_connector_id: null, action_id: '0818e5e0-6648-11eb-a291-51bf6b175a53', case_id: 'fcdedd20-6646-11eb-a291-51bf6b175a53', comment_id: 'comment-user-1', diff --git a/x-pack/plugins/cases/server/client/cases/push.ts b/x-pack/plugins/cases/server/client/cases/push.ts index 3048cf01bb3ba..1b090a653546d 100644 --- a/x-pack/plugins/cases/server/client/cases/push.ts +++ b/x-pack/plugins/cases/server/client/cases/push.ts @@ -241,7 +241,7 @@ export const push = async ( actionBy: { username, full_name, email }, caseId, fields: ['pushed'], - newValue: JSON.stringify(externalService), + newValue: externalService, owner: myCase.attributes.owner, }), ], diff --git a/x-pack/plugins/cases/server/client/cases/utils.test.ts b/x-pack/plugins/cases/server/client/cases/utils.test.ts index d7c45d3e1e9ae..315e9966d347b 100644 --- a/x-pack/plugins/cases/server/client/cases/utils.test.ts +++ b/x-pack/plugins/cases/server/client/cases/utils.test.ts @@ -799,8 +799,10 @@ describe('utils', () => { username: 'elastic', }, new_value: - // The connector id is 123 - '{"pushed_at":"2021-02-03T17:45:29.400Z","pushed_by":{"username":"elastic","full_name":"Elastic","email":"elastic@elastic.co"},"connector_id":"123","connector_name":"ServiceNow SN","external_id":"external-id","external_title":"SIR0010037","external_url":"https://dev92273.service-now.com/nav_to.do?uri=sn_si_incident.do?sys_id=external-id"}', + // The connector id is 123 + '{"pushed_at":"2021-02-03T17:45:29.400Z","pushed_by":{"username":"elastic","full_name":"Elastic","email":"elastic@elastic.co"},"connector_name":"ServiceNow SN","external_id":"external-id","external_title":"SIR0010037","external_url":"https://dev92273.service-now.com/nav_to.do?uri=sn_si_incident.do?sys_id=external-id"}', + new_val_connector_id: '123', + old_val_connector_id: null, old_value: null, action_id: '9b91d8f0-6647-11eb-a291-51bf6b175a53', case_id: 'fcdedd20-6646-11eb-a291-51bf6b175a53', diff --git a/x-pack/plugins/cases/server/client/cases/utils.ts b/x-pack/plugins/cases/server/client/cases/utils.ts index 359ad4b41ead0..f5cf2fe4b3f51 100644 --- a/x-pack/plugins/cases/server/client/cases/utils.ts +++ b/x-pack/plugins/cases/server/client/cases/utils.ts @@ -20,6 +20,8 @@ import { CommentRequestUserType, CommentRequestAlertType, CommentRequestActionsType, + CaseUserActionResponse, + isPush, } from '../../../common'; import { ActionsClient } from '../../../../actions/server'; import { CasesClientGetAlertsResponse } from '../../client/alerts/types'; @@ -55,22 +57,36 @@ export const getLatestPushInfo = ( userActions: CaseUserActionsResponse ): { index: number; pushedInfo: CaseFullExternalService } | null => { for (const [index, action] of [...userActions].reverse().entries()) { - if (action.action === 'push-to-service' && action.new_value) + if ( + isPush(action.action, action.action_field) && + isValidNewValue(action) && + connectorId === action.new_val_connector_id + ) { try { const pushedInfo = JSON.parse(action.new_value); - if (pushedInfo.connector_id === connectorId) { - // We returned the index of the element in the userActions array. - // As we traverse the userActions in reverse we need to calculate the index of a normal traversal - return { index: userActions.length - index - 1, pushedInfo }; - } + // We returned the index of the element in the userActions array. + // As we traverse the userActions in reverse we need to calculate the index of a normal traversal + return { + index: userActions.length - index - 1, + pushedInfo: { ...pushedInfo, connector_id: connectorId }, + }; } catch (e) { - // Silence JSON parse errors + // ignore parse failures and check the next user action } + } } return null; }; +type NonNullNewValueAction = Omit & { + new_value: string; + new_val_connector_id: string; +}; + +const isValidNewValue = (userAction: CaseUserActionResponse): userAction is NonNullNewValueAction => + userAction.new_val_connector_id != null && userAction.new_value != null; + const getCommentContent = (comment: CommentResponse): string => { if (comment.type === CommentType.user) { return comment.comment; diff --git a/x-pack/plugins/cases/server/client/mocks.ts b/x-pack/plugins/cases/server/client/mocks.ts index 05b7d055656b1..f0ca7ae9eaf71 100644 --- a/x-pack/plugins/cases/server/client/mocks.ts +++ b/x-pack/plugins/cases/server/client/mocks.ts @@ -22,6 +22,7 @@ const createCasesSubClientMock = (): CasesSubClientMock => { return { create: jest.fn(), find: jest.fn(), + resolve: jest.fn(), get: jest.fn(), push: jest.fn(), update: jest.fn(), diff --git a/x-pack/plugins/cases/server/client/typedoc_interfaces.ts b/x-pack/plugins/cases/server/client/typedoc_interfaces.ts index bf444ee9420ed..feeaa6b6dcb58 100644 --- a/x-pack/plugins/cases/server/client/typedoc_interfaces.ts +++ b/x-pack/plugins/cases/server/client/typedoc_interfaces.ts @@ -16,6 +16,7 @@ import { AllCommentsResponse, CasePostRequest, + CaseResolveResponse, CaseResponse, CasesConfigurePatch, CasesConfigureRequest, @@ -40,6 +41,7 @@ export interface ICasePostRequest extends CasePostRequest {} export interface ICasesFindRequest extends CasesFindRequest {} export interface ICasesPatchRequest extends CasesPatchRequest {} export interface ICaseResponse extends CaseResponse {} +export interface ICaseResolveResponse extends CaseResolveResponse {} export interface ICasesResponse extends CasesResponse {} export interface ICasesFindResponse extends CasesFindResponse {} diff --git a/x-pack/plugins/cases/server/client/user_actions/get.test.ts b/x-pack/plugins/cases/server/client/user_actions/get.test.ts new file mode 100644 index 0000000000000..302e069cde4d1 --- /dev/null +++ b/x-pack/plugins/cases/server/client/user_actions/get.test.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 { CaseUserActionResponse, SUB_CASE_SAVED_OBJECT } from '../../../common'; +import { SUB_CASE_REF_NAME } from '../../common'; +import { extractAttributesWithoutSubCases } from './get'; + +describe('get', () => { + describe('extractAttributesWithoutSubCases', () => { + it('returns an empty array when given an empty array', () => { + expect( + extractAttributesWithoutSubCases({ ...getFindResponseFields(), saved_objects: [] }) + ).toEqual([]); + }); + + it('filters out saved objects with a sub case reference', () => { + expect( + extractAttributesWithoutSubCases({ + ...getFindResponseFields(), + saved_objects: [ + { + type: 'a', + references: [{ name: SUB_CASE_REF_NAME, type: SUB_CASE_SAVED_OBJECT, id: '1' }], + id: 'b', + score: 0, + attributes: {} as CaseUserActionResponse, + }, + ], + }) + ).toEqual([]); + }); + + it('filters out saved objects with a sub case reference with other references', () => { + expect( + extractAttributesWithoutSubCases({ + ...getFindResponseFields(), + saved_objects: [ + { + type: 'a', + references: [ + { name: SUB_CASE_REF_NAME, type: SUB_CASE_SAVED_OBJECT, id: '1' }, + { name: 'a', type: 'b', id: '5' }, + ], + id: 'b', + score: 0, + attributes: {} as CaseUserActionResponse, + }, + ], + }) + ).toEqual([]); + }); + + it('keeps saved objects that do not have a sub case reference', () => { + expect( + extractAttributesWithoutSubCases({ + ...getFindResponseFields(), + saved_objects: [ + { + type: 'a', + references: [ + { name: SUB_CASE_REF_NAME, type: 'awesome', id: '1' }, + { name: 'a', type: 'b', id: '5' }, + ], + id: 'b', + score: 0, + attributes: { field: '1' } as unknown as CaseUserActionResponse, + }, + ], + }) + ).toEqual([{ field: '1' }]); + }); + + it('filters multiple saved objects correctly', () => { + expect( + extractAttributesWithoutSubCases({ + ...getFindResponseFields(), + saved_objects: [ + { + type: 'a', + references: [ + { name: SUB_CASE_REF_NAME, type: 'awesome', id: '1' }, + { name: 'a', type: 'b', id: '5' }, + ], + id: 'b', + score: 0, + attributes: { field: '2' } as unknown as CaseUserActionResponse, + }, + { + type: 'a', + references: [{ name: SUB_CASE_REF_NAME, type: SUB_CASE_SAVED_OBJECT, id: '1' }], + id: 'b', + score: 0, + attributes: { field: '1' } as unknown as CaseUserActionResponse, + }, + ], + }) + ).toEqual([{ field: '2' }]); + }); + }); +}); + +const getFindResponseFields = () => ({ page: 1, per_page: 1, total: 0 }); diff --git a/x-pack/plugins/cases/server/client/user_actions/get.ts b/x-pack/plugins/cases/server/client/user_actions/get.ts index 2a6608014c800..660cf1b6a336e 100644 --- a/x-pack/plugins/cases/server/client/user_actions/get.ts +++ b/x-pack/plugins/cases/server/client/user_actions/get.ts @@ -5,14 +5,14 @@ * 2.0. */ +import { SavedObjectReference, SavedObjectsFindResponse } from 'kibana/server'; import { - CASE_COMMENT_SAVED_OBJECT, - CASE_SAVED_OBJECT, CaseUserActionsResponse, CaseUserActionsResponseRt, SUB_CASE_SAVED_OBJECT, + CaseUserActionResponse, } from '../../../common'; -import { createCaseError, checkEnabledCaseConnectorOrThrow } from '../../common'; +import { createCaseError, checkEnabledCaseConnectorOrThrow, SUB_CASE_REF_NAME } from '../../common'; import { CasesClientArgs } from '..'; import { Operations } from '../../authorization'; import { UserActionGet } from './client'; @@ -40,23 +40,12 @@ export const get = async ( operation: Operations.getUserActions, }); - return CaseUserActionsResponseRt.encode( - userActions.saved_objects.reduce((acc, ua) => { - if (subCaseId == null && ua.references.some((uar) => uar.type === SUB_CASE_SAVED_OBJECT)) { - return acc; - } - return [ - ...acc, - { - ...ua.attributes, - action_id: ua.id, - case_id: ua.references.find((r) => r.type === CASE_SAVED_OBJECT)?.id ?? '', - comment_id: ua.references.find((r) => r.type === CASE_COMMENT_SAVED_OBJECT)?.id ?? null, - sub_case_id: ua.references.find((r) => r.type === SUB_CASE_SAVED_OBJECT)?.id ?? '', - }, - ]; - }, []) - ); + const resultsToEncode = + subCaseId == null + ? extractAttributesWithoutSubCases(userActions) + : extractAttributes(userActions); + + return CaseUserActionsResponseRt.encode(resultsToEncode); } catch (error) { throw createCaseError({ message: `Failed to retrieve user actions case id: ${caseId} sub case id: ${subCaseId}: ${error}`, @@ -65,3 +54,21 @@ export const get = async ( }); } }; + +export function extractAttributesWithoutSubCases( + userActions: SavedObjectsFindResponse +): CaseUserActionsResponse { + // exclude user actions relating to sub cases from the results + const hasSubCaseReference = (references: SavedObjectReference[]) => + references.find((ref) => ref.type === SUB_CASE_SAVED_OBJECT && ref.name === SUB_CASE_REF_NAME); + + return userActions.saved_objects + .filter((so) => !hasSubCaseReference(so.references)) + .map((so) => so.attributes); +} + +function extractAttributes( + userActions: SavedObjectsFindResponse +): CaseUserActionsResponse { + return userActions.saved_objects.map((so) => so.attributes); +} diff --git a/x-pack/plugins/cases/server/common/constants.ts b/x-pack/plugins/cases/server/common/constants.ts index 1f6af310d6ece..eba0a64a5c0be 100644 --- a/x-pack/plugins/cases/server/common/constants.ts +++ b/x-pack/plugins/cases/server/common/constants.ts @@ -5,6 +5,8 @@ * 2.0. */ +import { CASE_COMMENT_SAVED_OBJECT, CASE_SAVED_OBJECT, SUB_CASE_SAVED_OBJECT } from '../../common'; + /** * The name of the saved object reference indicating the action connector ID. This is stored in the Saved Object reference * field's name property. @@ -15,3 +17,30 @@ export const CONNECTOR_ID_REFERENCE_NAME = 'connectorId'; * The name of the saved object reference indicating the action connector ID that was used to push a case. */ export const PUSH_CONNECTOR_ID_REFERENCE_NAME = 'pushConnectorId'; + +/** + * The name of the saved object reference indicating the action connector ID that was used for + * adding a connector, or updating the existing connector for a user action's old_value field. + */ +export const USER_ACTION_OLD_ID_REF_NAME = 'oldConnectorId'; + +/** + * The name of the saved object reference indicating the action connector ID that was used for pushing a case, + * for a user action's old_value field. + */ +export const USER_ACTION_OLD_PUSH_ID_REF_NAME = 'oldPushConnectorId'; + +/** + * The name of the saved object reference indicating the caseId reference + */ +export const CASE_REF_NAME = `associated-${CASE_SAVED_OBJECT}`; + +/** + * The name of the saved object reference indicating the commentId reference + */ +export const COMMENT_REF_NAME = `associated-${CASE_COMMENT_SAVED_OBJECT}`; + +/** + * The name of the saved object reference indicating the subCaseId reference + */ +export const SUB_CASE_REF_NAME = `associated-${SUB_CASE_SAVED_OBJECT}`; diff --git a/x-pack/plugins/cases/server/index.ts b/x-pack/plugins/cases/server/index.ts index 5e433b46b80e5..ad76724eb49f7 100644 --- a/x-pack/plugins/cases/server/index.ts +++ b/x-pack/plugins/cases/server/index.ts @@ -15,7 +15,8 @@ export const config: PluginConfigDescriptor = { exposeToBrowser: { markdownPlugins: true, }, - deprecations: ({ renameFromRoot }) => [ + deprecations: ({ deprecate, renameFromRoot }) => [ + deprecate('enabled', '8.0.0'), renameFromRoot('xpack.case.enabled', 'xpack.cases.enabled'), ], }; diff --git a/x-pack/plugins/cases/server/routes/api/cases/get_case.ts b/x-pack/plugins/cases/server/routes/api/cases/get_case.ts index 2313c3cad9007..4d81b6d5e11b3 100644 --- a/x-pack/plugins/cases/server/routes/api/cases/get_case.ts +++ b/x-pack/plugins/cases/server/routes/api/cases/get_case.ts @@ -45,4 +45,38 @@ export function initGetCaseApi({ router, logger }: RouteDeps) { } } ); + + router.get( + { + path: `${CASE_DETAILS_URL}/resolve`, + validate: { + params: schema.object({ + case_id: schema.string(), + }), + query: schema.object({ + includeComments: schema.boolean({ defaultValue: true }), + includeSubCaseComments: schema.maybe(schema.boolean({ defaultValue: false })), + }), + }, + }, + async (context, request, response) => { + try { + const casesClient = await context.cases.getCasesClient(); + const id = request.params.case_id; + + return response.ok({ + body: await casesClient.cases.resolve({ + id, + includeComments: request.query.includeComments, + includeSubCaseComments: request.query.includeSubCaseComments, + }), + }); + } catch (error) { + logger.error( + `Failed to retrieve case in resolve route case id: ${request.params.case_id} \ninclude comments: ${request.query.includeComments} \ninclude sub comments: ${request.query.includeSubCaseComments}: ${error}` + ); + return response.customError(wrapError(error)); + } + } + ); } diff --git a/x-pack/plugins/cases/server/saved_object_types/cases.ts b/x-pack/plugins/cases/server/saved_object_types/cases.ts index a362d77c06626..74c6a053e95c0 100644 --- a/x-pack/plugins/cases/server/saved_object_types/cases.ts +++ b/x-pack/plugins/cases/server/saved_object_types/cases.ts @@ -117,7 +117,7 @@ export const createCaseSavedObjectType = ( type: 'keyword', }, title: { - type: 'keyword', + type: 'text', }, status: { type: 'keyword', diff --git a/x-pack/plugins/cases/server/saved_object_types/comments.ts b/x-pack/plugins/cases/server/saved_object_types/comments.ts index af14123eca580..64e75ad26ae28 100644 --- a/x-pack/plugins/cases/server/saved_object_types/comments.ts +++ b/x-pack/plugins/cases/server/saved_object_types/comments.ts @@ -112,5 +112,6 @@ export const createCaseCommentSavedObjectType = ({ migrations: createCommentsMigrations(migrationDeps), management: { importableAndExportable: true, + visibleInManagement: false, }, }); diff --git a/x-pack/plugins/cases/server/saved_object_types/migrations/cases.test.ts b/x-pack/plugins/cases/server/saved_object_types/migrations/cases.test.ts index bca12a86a544e..9020f65ae352c 100644 --- a/x-pack/plugins/cases/server/saved_object_types/migrations/cases.test.ts +++ b/x-pack/plugins/cases/server/saved_object_types/migrations/cases.test.ts @@ -30,322 +30,324 @@ const create_7_14_0_case = ({ }, }); -describe('7.15.0 connector ID migration', () => { - it('does not create a reference when the connector.id is none', () => { - const caseSavedObject = create_7_14_0_case({ connector: getNoneCaseConnector() }); - - const migratedConnector = caseConnectorIdMigration( - caseSavedObject - ) as SavedObjectSanitizedDoc; - - expect(migratedConnector.references.length).toBe(0); - expect(migratedConnector.attributes.connector).not.toHaveProperty('id'); - expect(migratedConnector.attributes.connector).toMatchInlineSnapshot(` - Object { - "fields": null, - "name": "none", - "type": ".none", - } - `); - }); +describe('case migrations', () => { + describe('7.15.0 connector ID migration', () => { + it('does not create a reference when the connector.id is none', () => { + const caseSavedObject = create_7_14_0_case({ connector: getNoneCaseConnector() }); + + const migratedConnector = caseConnectorIdMigration( + caseSavedObject + ) as SavedObjectSanitizedDoc; + + expect(migratedConnector.references.length).toBe(0); + expect(migratedConnector.attributes.connector).not.toHaveProperty('id'); + expect(migratedConnector.attributes.connector).toMatchInlineSnapshot(` + Object { + "fields": null, + "name": "none", + "type": ".none", + } + `); + }); - it('does not create a reference when the connector is undefined', () => { - const caseSavedObject = create_7_14_0_case(); - - const migratedConnector = caseConnectorIdMigration( - caseSavedObject - ) as SavedObjectSanitizedDoc; - - expect(migratedConnector.references.length).toBe(0); - expect(migratedConnector.attributes.connector).not.toHaveProperty('id'); - expect(migratedConnector.attributes.connector).toMatchInlineSnapshot(` - Object { - "fields": null, - "name": "none", - "type": ".none", - } - `); - }); + it('does not create a reference when the connector is undefined', () => { + const caseSavedObject = create_7_14_0_case(); - it('sets the connector to the default none connector if the connector.id is undefined', () => { - const caseSavedObject = create_7_14_0_case({ - connector: { - fields: null, - name: ConnectorTypes.jira, - type: ConnectorTypes.jira, - } as ESCaseConnectorWithId, - }); + const migratedConnector = caseConnectorIdMigration( + caseSavedObject + ) as SavedObjectSanitizedDoc; - const migratedConnector = caseConnectorIdMigration( - caseSavedObject - ) as SavedObjectSanitizedDoc; - - expect(migratedConnector.references.length).toBe(0); - expect(migratedConnector.attributes.connector).not.toHaveProperty('id'); - expect(migratedConnector.attributes.connector).toMatchInlineSnapshot(` - Object { - "fields": null, - "name": "none", - "type": ".none", - } - `); - }); + expect(migratedConnector.references.length).toBe(0); + expect(migratedConnector.attributes.connector).not.toHaveProperty('id'); + expect(migratedConnector.attributes.connector).toMatchInlineSnapshot(` + Object { + "fields": null, + "name": "none", + "type": ".none", + } + `); + }); - it('does not create a reference when the external_service is null', () => { - const caseSavedObject = create_7_14_0_case({ externalService: null }); + it('sets the connector to the default none connector if the connector.id is undefined', () => { + const caseSavedObject = create_7_14_0_case({ + connector: { + fields: null, + name: ConnectorTypes.jira, + type: ConnectorTypes.jira, + } as ESCaseConnectorWithId, + }); + + const migratedConnector = caseConnectorIdMigration( + caseSavedObject + ) as SavedObjectSanitizedDoc; + + expect(migratedConnector.references.length).toBe(0); + expect(migratedConnector.attributes.connector).not.toHaveProperty('id'); + expect(migratedConnector.attributes.connector).toMatchInlineSnapshot(` + Object { + "fields": null, + "name": "none", + "type": ".none", + } + `); + }); - const migratedConnector = caseConnectorIdMigration( - caseSavedObject - ) as SavedObjectSanitizedDoc; + it('does not create a reference when the external_service is null', () => { + const caseSavedObject = create_7_14_0_case({ externalService: null }); - expect(migratedConnector.references.length).toBe(0); - expect(migratedConnector.attributes.external_service).toBeNull(); - }); + const migratedConnector = caseConnectorIdMigration( + caseSavedObject + ) as SavedObjectSanitizedDoc; - it('does not create a reference when the external_service is undefined and sets external_service to null', () => { - const caseSavedObject = create_7_14_0_case(); + expect(migratedConnector.references.length).toBe(0); + expect(migratedConnector.attributes.external_service).toBeNull(); + }); - const migratedConnector = caseConnectorIdMigration( - caseSavedObject - ) as SavedObjectSanitizedDoc; + it('does not create a reference when the external_service is undefined and sets external_service to null', () => { + const caseSavedObject = create_7_14_0_case(); - expect(migratedConnector.references.length).toBe(0); - expect(migratedConnector.attributes.external_service).toBeNull(); - }); + const migratedConnector = caseConnectorIdMigration( + caseSavedObject + ) as SavedObjectSanitizedDoc; - it('does not create a reference when the external_service.connector_id is none', () => { - const caseSavedObject = create_7_14_0_case({ - externalService: createExternalService({ connector_id: noneConnectorId }), + expect(migratedConnector.references.length).toBe(0); + expect(migratedConnector.attributes.external_service).toBeNull(); }); - const migratedConnector = caseConnectorIdMigration( - caseSavedObject - ) as SavedObjectSanitizedDoc; - - expect(migratedConnector.references.length).toBe(0); - expect(migratedConnector.attributes.external_service).toMatchInlineSnapshot(` - Object { - "connector_name": ".jira", - "external_id": "100", - "external_title": "awesome", - "external_url": "http://www.google.com", - "pushed_at": "2019-11-25T21:54:48.952Z", - "pushed_by": Object { - "email": "testemail@elastic.co", - "full_name": "elastic", - "username": "elastic", - }, - } - `); - }); - - it('preserves the existing references when migrating', () => { - const caseSavedObject = { - ...create_7_14_0_case(), - references: [{ id: '1', name: 'awesome', type: 'hello' }], - }; + it('does not create a reference when the external_service.connector_id is none', () => { + const caseSavedObject = create_7_14_0_case({ + externalService: createExternalService({ connector_id: noneConnectorId }), + }); - const migratedConnector = caseConnectorIdMigration( - caseSavedObject - ) as SavedObjectSanitizedDoc; + const migratedConnector = caseConnectorIdMigration( + caseSavedObject + ) as SavedObjectSanitizedDoc; - expect(migratedConnector.references.length).toBe(1); - expect(migratedConnector.references).toMatchInlineSnapshot(` - Array [ + expect(migratedConnector.references.length).toBe(0); + expect(migratedConnector.attributes.external_service).toMatchInlineSnapshot(` Object { - "id": "1", - "name": "awesome", - "type": "hello", - }, - ] - `); - }); + "connector_name": ".jira", + "external_id": "100", + "external_title": "awesome", + "external_url": "http://www.google.com", + "pushed_at": "2019-11-25T21:54:48.952Z", + "pushed_by": Object { + "email": "testemail@elastic.co", + "full_name": "elastic", + "username": "elastic", + }, + } + `); + }); - it('creates a connector reference and removes the connector.id field', () => { - const caseSavedObject = create_7_14_0_case({ - connector: { - id: '123', - fields: null, - name: 'connector', - type: ConnectorTypes.jira, - }, + it('preserves the existing references when migrating', () => { + const caseSavedObject = { + ...create_7_14_0_case(), + references: [{ id: '1', name: 'awesome', type: 'hello' }], + }; + + const migratedConnector = caseConnectorIdMigration( + caseSavedObject + ) as SavedObjectSanitizedDoc; + + expect(migratedConnector.references.length).toBe(1); + expect(migratedConnector.references).toMatchInlineSnapshot(` + Array [ + Object { + "id": "1", + "name": "awesome", + "type": "hello", + }, + ] + `); }); - const migratedConnector = caseConnectorIdMigration( - caseSavedObject - ) as SavedObjectSanitizedDoc; - - expect(migratedConnector.references.length).toBe(1); - expect(migratedConnector.attributes.connector).not.toHaveProperty('id'); - expect(migratedConnector.attributes.connector).toMatchInlineSnapshot(` - Object { - "fields": null, - "name": "connector", - "type": ".jira", - } - `); - expect(migratedConnector.references).toMatchInlineSnapshot(` - Array [ - Object { - "id": "123", - "name": "connectorId", - "type": "action", + it('creates a connector reference and removes the connector.id field', () => { + const caseSavedObject = create_7_14_0_case({ + connector: { + id: '123', + fields: null, + name: 'connector', + type: ConnectorTypes.jira, }, - ] - `); - }); + }); - it('creates a push connector reference and removes the connector_id field', () => { - const caseSavedObject = create_7_14_0_case({ - externalService: { - connector_id: '100', - connector_name: '.jira', - external_id: '100', - external_title: 'awesome', - external_url: 'http://www.google.com', - pushed_at: '2019-11-25T21:54:48.952Z', - pushed_by: { - full_name: 'elastic', - email: 'testemail@elastic.co', - username: 'elastic', - }, - }, - }); + const migratedConnector = caseConnectorIdMigration( + caseSavedObject + ) as SavedObjectSanitizedDoc; - const migratedConnector = caseConnectorIdMigration( - caseSavedObject - ) as SavedObjectSanitizedDoc; - - expect(migratedConnector.references.length).toBe(1); - expect(migratedConnector.attributes.external_service).not.toHaveProperty('connector_id'); - expect(migratedConnector.attributes.external_service).toMatchInlineSnapshot(` - Object { - "connector_name": ".jira", - "external_id": "100", - "external_title": "awesome", - "external_url": "http://www.google.com", - "pushed_at": "2019-11-25T21:54:48.952Z", - "pushed_by": Object { - "email": "testemail@elastic.co", - "full_name": "elastic", - "username": "elastic", - }, - } - `); - expect(migratedConnector.references).toMatchInlineSnapshot(` - Array [ + expect(migratedConnector.references.length).toBe(1); + expect(migratedConnector.attributes.connector).not.toHaveProperty('id'); + expect(migratedConnector.attributes.connector).toMatchInlineSnapshot(` Object { - "id": "100", - "name": "pushConnectorId", - "type": "action", - }, - ] - `); - }); + "fields": null, + "name": "connector", + "type": ".jira", + } + `); + expect(migratedConnector.references).toMatchInlineSnapshot(` + Array [ + Object { + "id": "123", + "name": "connectorId", + "type": "action", + }, + ] + `); + }); - it('does not create a reference and preserves the existing external_service fields when connector_id is null', () => { - const caseSavedObject = create_7_14_0_case({ - externalService: { - connector_id: null, - connector_name: '.jira', - external_id: '100', - external_title: 'awesome', - external_url: 'http://www.google.com', - pushed_at: '2019-11-25T21:54:48.952Z', - pushed_by: { - full_name: 'elastic', - email: 'testemail@elastic.co', - username: 'elastic', + it('creates a push connector reference and removes the connector_id field', () => { + const caseSavedObject = create_7_14_0_case({ + externalService: { + connector_id: '100', + connector_name: '.jira', + external_id: '100', + external_title: 'awesome', + external_url: 'http://www.google.com', + pushed_at: '2019-11-25T21:54:48.952Z', + pushed_by: { + full_name: 'elastic', + email: 'testemail@elastic.co', + username: 'elastic', + }, }, - }, + }); + + const migratedConnector = caseConnectorIdMigration( + caseSavedObject + ) as SavedObjectSanitizedDoc; + + expect(migratedConnector.references.length).toBe(1); + expect(migratedConnector.attributes.external_service).not.toHaveProperty('connector_id'); + expect(migratedConnector.attributes.external_service).toMatchInlineSnapshot(` + Object { + "connector_name": ".jira", + "external_id": "100", + "external_title": "awesome", + "external_url": "http://www.google.com", + "pushed_at": "2019-11-25T21:54:48.952Z", + "pushed_by": Object { + "email": "testemail@elastic.co", + "full_name": "elastic", + "username": "elastic", + }, + } + `); + expect(migratedConnector.references).toMatchInlineSnapshot(` + Array [ + Object { + "id": "100", + "name": "pushConnectorId", + "type": "action", + }, + ] + `); }); - const migratedConnector = caseConnectorIdMigration( - caseSavedObject - ) as SavedObjectSanitizedDoc; - - expect(migratedConnector.references.length).toBe(0); - expect(migratedConnector.attributes.external_service).not.toHaveProperty('connector_id'); - expect(migratedConnector.attributes.external_service).toMatchInlineSnapshot(` - Object { - "connector_name": ".jira", - "external_id": "100", - "external_title": "awesome", - "external_url": "http://www.google.com", - "pushed_at": "2019-11-25T21:54:48.952Z", - "pushed_by": Object { - "email": "testemail@elastic.co", - "full_name": "elastic", - "username": "elastic", + it('does not create a reference and preserves the existing external_service fields when connector_id is null', () => { + const caseSavedObject = create_7_14_0_case({ + externalService: { + connector_id: null, + connector_name: '.jira', + external_id: '100', + external_title: 'awesome', + external_url: 'http://www.google.com', + pushed_at: '2019-11-25T21:54:48.952Z', + pushed_by: { + full_name: 'elastic', + email: 'testemail@elastic.co', + username: 'elastic', + }, }, - } - `); - }); + }); - it('migrates both connector and external_service when provided', () => { - const caseSavedObject = create_7_14_0_case({ - externalService: { - connector_id: '100', - connector_name: '.jira', - external_id: '100', - external_title: 'awesome', - external_url: 'http://www.google.com', - pushed_at: '2019-11-25T21:54:48.952Z', - pushed_by: { - full_name: 'elastic', - email: 'testemail@elastic.co', - username: 'elastic', - }, - }, - connector: { - id: '123', - fields: null, - name: 'connector', - type: ConnectorTypes.jira, - }, + const migratedConnector = caseConnectorIdMigration( + caseSavedObject + ) as SavedObjectSanitizedDoc; + + expect(migratedConnector.references.length).toBe(0); + expect(migratedConnector.attributes.external_service).not.toHaveProperty('connector_id'); + expect(migratedConnector.attributes.external_service).toMatchInlineSnapshot(` + Object { + "connector_name": ".jira", + "external_id": "100", + "external_title": "awesome", + "external_url": "http://www.google.com", + "pushed_at": "2019-11-25T21:54:48.952Z", + "pushed_by": Object { + "email": "testemail@elastic.co", + "full_name": "elastic", + "username": "elastic", + }, + } + `); }); - const migratedConnector = caseConnectorIdMigration( - caseSavedObject - ) as SavedObjectSanitizedDoc; - - expect(migratedConnector.references.length).toBe(2); - expect(migratedConnector.attributes.external_service).not.toHaveProperty('connector_id'); - expect(migratedConnector.attributes.external_service).toMatchInlineSnapshot(` - Object { - "connector_name": ".jira", - "external_id": "100", - "external_title": "awesome", - "external_url": "http://www.google.com", - "pushed_at": "2019-11-25T21:54:48.952Z", - "pushed_by": Object { - "email": "testemail@elastic.co", - "full_name": "elastic", - "username": "elastic", + it('migrates both connector and external_service when provided', () => { + const caseSavedObject = create_7_14_0_case({ + externalService: { + connector_id: '100', + connector_name: '.jira', + external_id: '100', + external_title: 'awesome', + external_url: 'http://www.google.com', + pushed_at: '2019-11-25T21:54:48.952Z', + pushed_by: { + full_name: 'elastic', + email: 'testemail@elastic.co', + username: 'elastic', + }, }, - } - `); - expect(migratedConnector.attributes.connector).not.toHaveProperty('id'); - expect(migratedConnector.attributes.connector).toMatchInlineSnapshot(` - Object { - "fields": null, - "name": "connector", - "type": ".jira", - } - `); - expect(migratedConnector.references).toMatchInlineSnapshot(` - Array [ - Object { - "id": "123", - "name": "connectorId", - "type": "action", + connector: { + id: '123', + fields: null, + name: 'connector', + type: ConnectorTypes.jira, }, + }); + + const migratedConnector = caseConnectorIdMigration( + caseSavedObject + ) as SavedObjectSanitizedDoc; + + expect(migratedConnector.references.length).toBe(2); + expect(migratedConnector.attributes.external_service).not.toHaveProperty('connector_id'); + expect(migratedConnector.attributes.external_service).toMatchInlineSnapshot(` Object { - "id": "100", - "name": "pushConnectorId", - "type": "action", - }, - ] - `); + "connector_name": ".jira", + "external_id": "100", + "external_title": "awesome", + "external_url": "http://www.google.com", + "pushed_at": "2019-11-25T21:54:48.952Z", + "pushed_by": Object { + "email": "testemail@elastic.co", + "full_name": "elastic", + "username": "elastic", + }, + } + `); + expect(migratedConnector.attributes.connector).not.toHaveProperty('id'); + expect(migratedConnector.attributes.connector).toMatchInlineSnapshot(` + Object { + "fields": null, + "name": "connector", + "type": ".jira", + } + `); + expect(migratedConnector.references).toMatchInlineSnapshot(` + Array [ + Object { + "id": "123", + "name": "connectorId", + "type": "action", + }, + Object { + "id": "100", + "name": "pushConnectorId", + "type": "action", + }, + ] + `); + }); }); }); diff --git a/x-pack/plugins/cases/server/saved_object_types/migrations/cases.ts b/x-pack/plugins/cases/server/saved_object_types/migrations/cases.ts index bffd4171270ef..80f02fa3bf6a6 100644 --- a/x-pack/plugins/cases/server/saved_object_types/migrations/cases.ts +++ b/x-pack/plugins/cases/server/saved_object_types/migrations/cases.ts @@ -14,7 +14,11 @@ import { } from '../../../../../../src/core/server'; import { ESConnectorFields } from '../../services'; import { ConnectorTypes, CaseType } from '../../../common'; -import { transformConnectorIdToReference, transformPushConnectorIdToReference } from './utils'; +import { + transformConnectorIdToReference, + transformPushConnectorIdToReference, +} from '../../services/user_actions/transform'; +import { CONNECTOR_ID_REFERENCE_NAME, PUSH_CONNECTOR_ID_REFERENCE_NAME } from '../../common'; interface UnsanitizedCaseConnector { connector_id: string; @@ -50,11 +54,13 @@ export const caseConnectorIdMigration = ( // removing the id field since it will be stored in the references instead const { connector, external_service, ...restAttributes } = doc.attributes; - const { transformedConnector, references: connectorReferences } = - transformConnectorIdToReference(connector); + const { transformedConnector, references: connectorReferences } = transformConnectorIdToReference( + CONNECTOR_ID_REFERENCE_NAME, + connector + ); const { transformedPushConnector, references: pushConnectorReferences } = - transformPushConnectorIdToReference(external_service); + transformPushConnectorIdToReference(PUSH_CONNECTOR_ID_REFERENCE_NAME, external_service); const { references = [] } = doc; diff --git a/x-pack/plugins/cases/server/saved_object_types/migrations/configuration.test.ts b/x-pack/plugins/cases/server/saved_object_types/migrations/configuration.test.ts index 4467b499817a5..9ae0285598dbf 100644 --- a/x-pack/plugins/cases/server/saved_object_types/migrations/configuration.test.ts +++ b/x-pack/plugins/cases/server/saved_object_types/migrations/configuration.test.ts @@ -40,87 +40,89 @@ const create_7_14_0_configSchema = (connector?: ESCaseConnectorWithId) => ({ }, }); -describe('7.15.0 connector ID migration', () => { - it('does not create a reference when the connector ID is none', () => { - const configureSavedObject = create_7_14_0_configSchema(getNoneCaseConnector()); +describe('configuration migrations', () => { + describe('7.15.0 connector ID migration', () => { + it('does not create a reference when the connector ID is none', () => { + const configureSavedObject = create_7_14_0_configSchema(getNoneCaseConnector()); - const migratedConnector = configureConnectorIdMigration( - configureSavedObject - ) as SavedObjectSanitizedDoc; + const migratedConnector = configureConnectorIdMigration( + configureSavedObject + ) as SavedObjectSanitizedDoc; - expect(migratedConnector.references.length).toBe(0); - expect(migratedConnector.attributes.connector).not.toHaveProperty('id'); - }); + expect(migratedConnector.references.length).toBe(0); + expect(migratedConnector.attributes.connector).not.toHaveProperty('id'); + }); - it('does not create a reference when the connector is undefined and defaults it to the none connector', () => { - const configureSavedObject = create_7_14_0_configSchema(); + it('does not create a reference when the connector is undefined and defaults it to the none connector', () => { + const configureSavedObject = create_7_14_0_configSchema(); - const migratedConnector = configureConnectorIdMigration( - configureSavedObject - ) as SavedObjectSanitizedDoc; + const migratedConnector = configureConnectorIdMigration( + configureSavedObject + ) as SavedObjectSanitizedDoc; - expect(migratedConnector.references.length).toBe(0); - expect(migratedConnector.attributes.connector).toMatchInlineSnapshot(` - Object { - "fields": null, - "name": "none", - "type": ".none", - } - `); - }); - - it('creates a reference using the connector id', () => { - const configureSavedObject = create_7_14_0_configSchema({ - id: '123', - fields: null, - name: 'connector', - type: ConnectorTypes.jira, + expect(migratedConnector.references.length).toBe(0); + expect(migratedConnector.attributes.connector).toMatchInlineSnapshot(` + Object { + "fields": null, + "name": "none", + "type": ".none", + } + `); }); - const migratedConnector = configureConnectorIdMigration( - configureSavedObject - ) as SavedObjectSanitizedDoc; + it('creates a reference using the connector id', () => { + const configureSavedObject = create_7_14_0_configSchema({ + id: '123', + fields: null, + name: 'connector', + type: ConnectorTypes.jira, + }); - expect(migratedConnector.references).toEqual([ - { id: '123', type: ACTION_SAVED_OBJECT_TYPE, name: CONNECTOR_ID_REFERENCE_NAME }, - ]); - expect(migratedConnector.attributes.connector).not.toHaveProperty('id'); - }); + const migratedConnector = configureConnectorIdMigration( + configureSavedObject + ) as SavedObjectSanitizedDoc; - it('returns the other attributes and default connector when the connector is undefined', () => { - const configureSavedObject = create_7_14_0_configSchema(); + expect(migratedConnector.references).toEqual([ + { id: '123', type: ACTION_SAVED_OBJECT_TYPE, name: CONNECTOR_ID_REFERENCE_NAME }, + ]); + expect(migratedConnector.attributes.connector).not.toHaveProperty('id'); + }); - const migratedConnector = configureConnectorIdMigration( - configureSavedObject - ) as SavedObjectSanitizedDoc; + it('returns the other attributes and default connector when the connector is undefined', () => { + const configureSavedObject = create_7_14_0_configSchema(); - expect(migratedConnector).toMatchInlineSnapshot(` - Object { - "attributes": Object { - "closure_type": "close-by-pushing", - "connector": Object { - "fields": null, - "name": "none", - "type": ".none", - }, - "created_at": "2020-04-09T09:43:51.778Z", - "created_by": Object { - "email": "testemail@elastic.co", - "full_name": "elastic", - "username": "elastic", - }, - "owner": "securitySolution", - "updated_at": "2020-04-09T09:43:51.778Z", - "updated_by": Object { - "email": "testemail@elastic.co", - "full_name": "elastic", - "username": "elastic", + const migratedConnector = configureConnectorIdMigration( + configureSavedObject + ) as SavedObjectSanitizedDoc; + + expect(migratedConnector).toMatchInlineSnapshot(` + Object { + "attributes": Object { + "closure_type": "close-by-pushing", + "connector": Object { + "fields": null, + "name": "none", + "type": ".none", + }, + "created_at": "2020-04-09T09:43:51.778Z", + "created_by": Object { + "email": "testemail@elastic.co", + "full_name": "elastic", + "username": "elastic", + }, + "owner": "securitySolution", + "updated_at": "2020-04-09T09:43:51.778Z", + "updated_by": Object { + "email": "testemail@elastic.co", + "full_name": "elastic", + "username": "elastic", + }, }, - }, - "id": "1", - "references": Array [], - "type": "cases-configure", - } - `); + "id": "1", + "references": Array [], + "type": "cases-configure", + } + `); + }); }); }); diff --git a/x-pack/plugins/cases/server/saved_object_types/migrations/configuration.ts b/x-pack/plugins/cases/server/saved_object_types/migrations/configuration.ts index 527d40fca2e35..f9937253e0d2f 100644 --- a/x-pack/plugins/cases/server/saved_object_types/migrations/configuration.ts +++ b/x-pack/plugins/cases/server/saved_object_types/migrations/configuration.ts @@ -13,7 +13,8 @@ import { } from '../../../../../../src/core/server'; import { ConnectorTypes } from '../../../common'; import { addOwnerToSO, SanitizedCaseOwner } from '.'; -import { transformConnectorIdToReference } from './utils'; +import { transformConnectorIdToReference } from '../../services/user_actions/transform'; +import { CONNECTOR_ID_REFERENCE_NAME } from '../../common'; interface UnsanitizedConfigureConnector { connector_id: string; @@ -34,8 +35,10 @@ export const configureConnectorIdMigration = ( ): SavedObjectSanitizedDoc => { // removing the id field since it will be stored in the references instead const { connector, ...restAttributes } = doc.attributes; - const { transformedConnector, references: connectorReferences } = - transformConnectorIdToReference(connector); + const { transformedConnector, references: connectorReferences } = transformConnectorIdToReference( + CONNECTOR_ID_REFERENCE_NAME, + connector + ); const { references = [] } = doc; return { diff --git a/x-pack/plugins/cases/server/saved_object_types/migrations/index.ts b/x-pack/plugins/cases/server/saved_object_types/migrations/index.ts index a445131073d19..a4f50fbfcde5b 100644 --- a/x-pack/plugins/cases/server/saved_object_types/migrations/index.ts +++ b/x-pack/plugins/cases/server/saved_object_types/migrations/index.ts @@ -5,24 +5,17 @@ * 2.0. */ -/* eslint-disable @typescript-eslint/naming-convention */ - import { SavedObjectUnsanitizedDoc, SavedObjectSanitizedDoc, } from '../../../../../../src/core/server'; -import { ConnectorTypes, SECURITY_SOLUTION_OWNER } from '../../../common'; +import { SECURITY_SOLUTION_OWNER } from '../../../common'; export { caseMigrations } from './cases'; export { configureMigrations } from './configuration'; +export { userActionsMigrations } from './user_actions'; export { createCommentsMigrations, CreateCommentsMigrationsDeps } from './comments'; -interface UserActions { - action_field: string[]; - new_value: string; - old_value: string; -} - export interface SanitizedCaseOwner { owner: string; } @@ -38,52 +31,6 @@ export const addOwnerToSO = >( references: doc.references || [], }); -export const userActionsMigrations = { - '7.10.0': (doc: SavedObjectUnsanitizedDoc): SavedObjectSanitizedDoc => { - const { action_field, new_value, old_value, ...restAttributes } = doc.attributes; - - if ( - action_field == null || - !Array.isArray(action_field) || - action_field[0] !== 'connector_id' - ) { - return { ...doc, references: doc.references || [] }; - } - - return { - ...doc, - attributes: { - ...restAttributes, - action_field: ['connector'], - new_value: - new_value != null - ? JSON.stringify({ - id: new_value, - name: 'none', - type: ConnectorTypes.none, - fields: null, - }) - : new_value, - old_value: - old_value != null - ? JSON.stringify({ - id: old_value, - name: 'none', - type: ConnectorTypes.none, - fields: null, - }) - : old_value, - }, - references: doc.references || [], - }; - }, - '7.14.0': ( - doc: SavedObjectUnsanitizedDoc> - ): SavedObjectSanitizedDoc => { - return addOwnerToSO(doc); - }, -}; - export const connectorMappingsMigrations = { '7.14.0': ( doc: SavedObjectUnsanitizedDoc> diff --git a/x-pack/plugins/cases/server/saved_object_types/migrations/user_actions.test.ts b/x-pack/plugins/cases/server/saved_object_types/migrations/user_actions.test.ts new file mode 100644 index 0000000000000..e71c8db0db694 --- /dev/null +++ b/x-pack/plugins/cases/server/saved_object_types/migrations/user_actions.test.ts @@ -0,0 +1,562 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +/* eslint-disable @typescript-eslint/naming-convention */ + +import { SavedObjectMigrationContext, SavedObjectSanitizedDoc } from 'kibana/server'; +import { migrationMocks } from 'src/core/server/mocks'; +import { CaseUserActionAttributes, CASE_USER_ACTION_SAVED_OBJECT } from '../../../common'; +import { + createConnectorObject, + createExternalService, + createJiraConnector, +} from '../../services/test_utils'; +import { userActionsConnectorIdMigration } from './user_actions'; + +const create_7_14_0_userAction = ( + params: { + action?: string; + action_field?: string[]; + new_value?: string | null | object; + old_value?: string | null | object; + } = {} +) => { + const { new_value, old_value, ...restParams } = params; + + return { + type: CASE_USER_ACTION_SAVED_OBJECT, + id: '1', + attributes: { + ...restParams, + new_value: new_value && typeof new_value === 'object' ? JSON.stringify(new_value) : new_value, + old_value: old_value && typeof old_value === 'object' ? JSON.stringify(old_value) : old_value, + }, + }; +}; + +describe('user action migrations', () => { + describe('7.15.0 connector ID migration', () => { + describe('userActionsConnectorIdMigration', () => { + let context: jest.Mocked; + + beforeEach(() => { + context = migrationMocks.createContext(); + }); + + describe('push user action', () => { + it('extracts the external_service connector_id to references for a new pushed user action', () => { + const userAction = create_7_14_0_userAction({ + action: 'push-to-service', + action_field: ['pushed'], + new_value: createExternalService(), + old_value: null, + }); + + const migratedUserAction = userActionsConnectorIdMigration( + userAction, + context + ) as SavedObjectSanitizedDoc; + + const parsedExternalService = JSON.parse(migratedUserAction.attributes.new_value!); + expect(parsedExternalService).not.toHaveProperty('connector_id'); + expect(parsedExternalService).toMatchInlineSnapshot(` + Object { + "connector_name": ".jira", + "external_id": "100", + "external_title": "awesome", + "external_url": "http://www.google.com", + "pushed_at": "2019-11-25T21:54:48.952Z", + "pushed_by": Object { + "email": "testemail@elastic.co", + "full_name": "elastic", + "username": "elastic", + }, + } + `); + + expect(migratedUserAction.references).toMatchInlineSnapshot(` + Array [ + Object { + "id": "100", + "name": "pushConnectorId", + "type": "action", + }, + ] + `); + + expect(migratedUserAction.attributes.old_value).toBeNull(); + }); + + it('extract the external_service connector_id to references for new and old pushed user action', () => { + const userAction = create_7_14_0_userAction({ + action: 'push-to-service', + action_field: ['pushed'], + new_value: createExternalService(), + old_value: createExternalService({ connector_id: '5' }), + }); + + const migratedUserAction = userActionsConnectorIdMigration( + userAction, + context + ) as SavedObjectSanitizedDoc; + + const parsedNewExternalService = JSON.parse(migratedUserAction.attributes.new_value!); + const parsedOldExternalService = JSON.parse(migratedUserAction.attributes.old_value!); + + expect(parsedNewExternalService).not.toHaveProperty('connector_id'); + expect(parsedOldExternalService).not.toHaveProperty('connector_id'); + expect(migratedUserAction.references).toEqual([ + { id: '100', name: 'pushConnectorId', type: 'action' }, + { id: '5', name: 'oldPushConnectorId', type: 'action' }, + ]); + }); + + it('preserves the existing references after extracting the external_service connector_id field', () => { + const userAction = { + ...create_7_14_0_userAction({ + action: 'push-to-service', + action_field: ['pushed'], + new_value: createExternalService(), + old_value: createExternalService({ connector_id: '5' }), + }), + references: [{ id: '500', name: 'someReference', type: 'ref' }], + }; + + const migratedUserAction = userActionsConnectorIdMigration( + userAction, + context + ) as SavedObjectSanitizedDoc; + + const parsedNewExternalService = JSON.parse(migratedUserAction.attributes.new_value!); + const parsedOldExternalService = JSON.parse(migratedUserAction.attributes.old_value!); + + expect(parsedNewExternalService).not.toHaveProperty('connector_id'); + expect(parsedOldExternalService).not.toHaveProperty('connector_id'); + expect(migratedUserAction.references).toEqual([ + { id: '500', name: 'someReference', type: 'ref' }, + { id: '100', name: 'pushConnectorId', type: 'action' }, + { id: '5', name: 'oldPushConnectorId', type: 'action' }, + ]); + }); + + it('leaves the object unmodified when it is not a valid push user action', () => { + const userAction = create_7_14_0_userAction({ + action: 'push-to-service', + action_field: ['invalid field'], + new_value: 'hello', + old_value: null, + }); + + const migratedUserAction = userActionsConnectorIdMigration( + userAction, + context + ) as SavedObjectSanitizedDoc; + + expect(migratedUserAction.attributes.old_value).toBeNull(); + expect(migratedUserAction).toMatchInlineSnapshot(` + Object { + "attributes": Object { + "action": "push-to-service", + "action_field": Array [ + "invalid field", + ], + "new_value": "hello", + "old_value": null, + }, + "id": "1", + "references": Array [], + "type": "cases-user-actions", + } + `); + }); + + it('leaves the object unmodified when it new value is invalid json', () => { + const userAction = create_7_14_0_userAction({ + action: 'push-to-service', + action_field: ['pushed'], + new_value: '{a', + old_value: null, + }); + + const migratedUserAction = userActionsConnectorIdMigration( + userAction, + context + ) as SavedObjectSanitizedDoc; + + expect(migratedUserAction.attributes.old_value).toBeNull(); + expect(migratedUserAction.attributes.new_value).toEqual('{a'); + expect(migratedUserAction).toMatchInlineSnapshot(` + Object { + "attributes": Object { + "action": "push-to-service", + "action_field": Array [ + "pushed", + ], + "new_value": "{a", + "old_value": null, + }, + "id": "1", + "references": Array [], + "type": "cases-user-actions", + } + `); + }); + + it('logs an error new value is invalid json', () => { + const userAction = create_7_14_0_userAction({ + action: 'push-to-service', + action_field: ['pushed'], + new_value: '{a', + old_value: null, + }); + + userActionsConnectorIdMigration(userAction, context); + + expect(context.log.error).toHaveBeenCalled(); + }); + }); + + describe('update connector user action', () => { + it('extracts the connector id to references for a new create connector user action', () => { + const userAction = create_7_14_0_userAction({ + action: 'update', + action_field: ['connector'], + new_value: createJiraConnector(), + old_value: null, + }); + + const migratedUserAction = userActionsConnectorIdMigration( + userAction, + context + ) as SavedObjectSanitizedDoc; + + const parsedConnector = JSON.parse(migratedUserAction.attributes.new_value!); + expect(parsedConnector).not.toHaveProperty('id'); + expect(parsedConnector).toMatchInlineSnapshot(` + Object { + "fields": Object { + "issueType": "bug", + "parent": "2", + "priority": "high", + }, + "name": ".jira", + "type": ".jira", + } + `); + + expect(migratedUserAction.references).toMatchInlineSnapshot(` + Array [ + Object { + "id": "1", + "name": "connectorId", + "type": "action", + }, + ] + `); + + expect(migratedUserAction.attributes.old_value).toBeNull(); + }); + + it('extracts the connector id to references for a new and old create connector user action', () => { + const userAction = create_7_14_0_userAction({ + action: 'update', + action_field: ['connector'], + new_value: createJiraConnector(), + old_value: { ...createJiraConnector(), id: '5' }, + }); + + const migratedUserAction = userActionsConnectorIdMigration( + userAction, + context + ) as SavedObjectSanitizedDoc; + + const parsedNewConnector = JSON.parse(migratedUserAction.attributes.new_value!); + const parsedOldConnector = JSON.parse(migratedUserAction.attributes.new_value!); + + expect(parsedNewConnector).not.toHaveProperty('id'); + expect(parsedOldConnector).not.toHaveProperty('id'); + + expect(migratedUserAction.references).toEqual([ + { id: '1', name: 'connectorId', type: 'action' }, + { id: '5', name: 'oldConnectorId', type: 'action' }, + ]); + }); + + it('preserves the existing references after extracting the connector.id field', () => { + const userAction = { + ...create_7_14_0_userAction({ + action: 'update', + action_field: ['connector'], + new_value: createJiraConnector(), + old_value: { ...createJiraConnector(), id: '5' }, + }), + references: [{ id: '500', name: 'someReference', type: 'ref' }], + }; + + const migratedUserAction = userActionsConnectorIdMigration( + userAction, + context + ) as SavedObjectSanitizedDoc; + + const parsedNewConnectorId = JSON.parse(migratedUserAction.attributes.new_value!); + const parsedOldConnectorId = JSON.parse(migratedUserAction.attributes.old_value!); + + expect(parsedNewConnectorId).not.toHaveProperty('id'); + expect(parsedOldConnectorId).not.toHaveProperty('id'); + expect(migratedUserAction.references).toEqual([ + { id: '500', name: 'someReference', type: 'ref' }, + { id: '1', name: 'connectorId', type: 'action' }, + { id: '5', name: 'oldConnectorId', type: 'action' }, + ]); + }); + + it('leaves the object unmodified when it is not a valid create connector user action', () => { + const userAction = create_7_14_0_userAction({ + action: 'update', + action_field: ['invalid action'], + new_value: 'new json value', + old_value: 'old value', + }); + + const migratedUserAction = userActionsConnectorIdMigration( + userAction, + context + ) as SavedObjectSanitizedDoc; + + expect(migratedUserAction).toMatchInlineSnapshot(` + Object { + "attributes": Object { + "action": "update", + "action_field": Array [ + "invalid action", + ], + "new_value": "new json value", + "old_value": "old value", + }, + "id": "1", + "references": Array [], + "type": "cases-user-actions", + } + `); + }); + + it('leaves the object unmodified when old_value is invalid json', () => { + const userAction = create_7_14_0_userAction({ + action: 'update', + action_field: ['connector'], + new_value: '{}', + old_value: '{b', + }); + + const migratedUserAction = userActionsConnectorIdMigration( + userAction, + context + ) as SavedObjectSanitizedDoc; + + expect(migratedUserAction).toMatchInlineSnapshot(` + Object { + "attributes": Object { + "action": "update", + "action_field": Array [ + "connector", + ], + "new_value": "{}", + "old_value": "{b", + }, + "id": "1", + "references": Array [], + "type": "cases-user-actions", + } + `); + }); + + it('logs an error message when old_value is invalid json', () => { + const userAction = create_7_14_0_userAction({ + action: 'update', + action_field: ['connector'], + new_value: createJiraConnector(), + old_value: '{b', + }); + + userActionsConnectorIdMigration(userAction, context); + + expect(context.log.error).toHaveBeenCalled(); + }); + }); + + describe('create connector user action', () => { + it('extracts the connector id to references for a new create connector user action', () => { + const userAction = create_7_14_0_userAction({ + action: 'create', + action_field: ['connector'], + new_value: createConnectorObject(), + old_value: null, + }); + + const migratedUserAction = userActionsConnectorIdMigration( + userAction, + context + ) as SavedObjectSanitizedDoc; + + const parsedConnector = JSON.parse(migratedUserAction.attributes.new_value!); + expect(parsedConnector.connector).not.toHaveProperty('id'); + expect(parsedConnector).toMatchInlineSnapshot(` + Object { + "connector": Object { + "fields": Object { + "issueType": "bug", + "parent": "2", + "priority": "high", + }, + "name": ".jira", + "type": ".jira", + }, + } + `); + + expect(migratedUserAction.references).toMatchInlineSnapshot(` + Array [ + Object { + "id": "1", + "name": "connectorId", + "type": "action", + }, + ] + `); + + expect(migratedUserAction.attributes.old_value).toBeNull(); + }); + + it('extracts the connector id to references for a new and old create connector user action', () => { + const userAction = create_7_14_0_userAction({ + action: 'create', + action_field: ['connector'], + new_value: createConnectorObject(), + old_value: createConnectorObject({ id: '5' }), + }); + + const migratedUserAction = userActionsConnectorIdMigration( + userAction, + context + ) as SavedObjectSanitizedDoc; + + const parsedNewConnector = JSON.parse(migratedUserAction.attributes.new_value!); + const parsedOldConnector = JSON.parse(migratedUserAction.attributes.new_value!); + + expect(parsedNewConnector.connector).not.toHaveProperty('id'); + expect(parsedOldConnector.connector).not.toHaveProperty('id'); + + expect(migratedUserAction.references).toEqual([ + { id: '1', name: 'connectorId', type: 'action' }, + { id: '5', name: 'oldConnectorId', type: 'action' }, + ]); + }); + + it('preserves the existing references after extracting the connector.id field', () => { + const userAction = { + ...create_7_14_0_userAction({ + action: 'create', + action_field: ['connector'], + new_value: createConnectorObject(), + old_value: createConnectorObject({ id: '5' }), + }), + references: [{ id: '500', name: 'someReference', type: 'ref' }], + }; + + const migratedUserAction = userActionsConnectorIdMigration( + userAction, + context + ) as SavedObjectSanitizedDoc; + + const parsedNewConnectorId = JSON.parse(migratedUserAction.attributes.new_value!); + const parsedOldConnectorId = JSON.parse(migratedUserAction.attributes.old_value!); + + expect(parsedNewConnectorId.connector).not.toHaveProperty('id'); + expect(parsedOldConnectorId.connector).not.toHaveProperty('id'); + expect(migratedUserAction.references).toEqual([ + { id: '500', name: 'someReference', type: 'ref' }, + { id: '1', name: 'connectorId', type: 'action' }, + { id: '5', name: 'oldConnectorId', type: 'action' }, + ]); + }); + + it('leaves the object unmodified when it is not a valid create connector user action', () => { + const userAction = create_7_14_0_userAction({ + action: 'create', + action_field: ['invalid action'], + new_value: 'new json value', + old_value: 'old value', + }); + + const migratedUserAction = userActionsConnectorIdMigration( + userAction, + context + ) as SavedObjectSanitizedDoc; + + expect(migratedUserAction).toMatchInlineSnapshot(` + Object { + "attributes": Object { + "action": "create", + "action_field": Array [ + "invalid action", + ], + "new_value": "new json value", + "old_value": "old value", + }, + "id": "1", + "references": Array [], + "type": "cases-user-actions", + } + `); + }); + + it('leaves the object unmodified when new_value is invalid json', () => { + const userAction = create_7_14_0_userAction({ + action: 'create', + action_field: ['connector'], + new_value: 'new json value', + old_value: 'old value', + }); + + const migratedUserAction = userActionsConnectorIdMigration( + userAction, + context + ) as SavedObjectSanitizedDoc; + + expect(migratedUserAction).toMatchInlineSnapshot(` + Object { + "attributes": Object { + "action": "create", + "action_field": Array [ + "connector", + ], + "new_value": "new json value", + "old_value": "old value", + }, + "id": "1", + "references": Array [], + "type": "cases-user-actions", + } + `); + }); + + it('logs an error message when new_value is invalid json', () => { + const userAction = create_7_14_0_userAction({ + action: 'create', + action_field: ['connector'], + new_value: 'new json value', + old_value: 'old value', + }); + + userActionsConnectorIdMigration(userAction, context); + + expect(context.log.error).toHaveBeenCalled(); + }); + }); + }); + }); +}); diff --git a/x-pack/plugins/cases/server/saved_object_types/migrations/user_actions.ts b/x-pack/plugins/cases/server/saved_object_types/migrations/user_actions.ts new file mode 100644 index 0000000000000..ed6b57ef647f9 --- /dev/null +++ b/x-pack/plugins/cases/server/saved_object_types/migrations/user_actions.ts @@ -0,0 +1,159 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +/* eslint-disable @typescript-eslint/naming-convention */ + +import { addOwnerToSO, SanitizedCaseOwner } from '.'; +import { + SavedObjectUnsanitizedDoc, + SavedObjectSanitizedDoc, + SavedObjectMigrationContext, + LogMeta, +} from '../../../../../../src/core/server'; +import { ConnectorTypes, isCreateConnector, isPush, isUpdateConnector } from '../../../common'; + +import { extractConnectorIdFromJson } from '../../services/user_actions/transform'; +import { UserActionFieldType } from '../../services/user_actions/types'; + +interface UserActions { + action_field: string[]; + new_value: string; + old_value: string; +} + +interface UserActionUnmigratedConnectorDocument { + action?: string; + action_field?: string[]; + new_value?: string | null; + old_value?: string | null; +} + +interface UserActionLogMeta extends LogMeta { + migrations: { userAction: { id: string } }; +} + +export function userActionsConnectorIdMigration( + doc: SavedObjectUnsanitizedDoc, + context: SavedObjectMigrationContext +): SavedObjectSanitizedDoc { + const originalDocWithReferences = { ...doc, references: doc.references ?? [] }; + + if (!isConnectorUserAction(doc.attributes.action, doc.attributes.action_field)) { + return originalDocWithReferences; + } + + try { + return formatDocumentWithConnectorReferences(doc); + } catch (error) { + logError(doc.id, context, error); + + return originalDocWithReferences; + } +} + +function isConnectorUserAction(action?: string, actionFields?: string[]): boolean { + return ( + isCreateConnector(action, actionFields) || + isUpdateConnector(action, actionFields) || + isPush(action, actionFields) + ); +} + +function formatDocumentWithConnectorReferences( + doc: SavedObjectUnsanitizedDoc +): SavedObjectSanitizedDoc { + const { new_value, old_value, action, action_field, ...restAttributes } = doc.attributes; + const { references = [] } = doc; + + const { transformedActionDetails: transformedNewValue, references: newValueConnectorRefs } = + extractConnectorIdFromJson({ + action, + actionFields: action_field, + actionDetails: new_value, + fieldType: UserActionFieldType.New, + }); + + const { transformedActionDetails: transformedOldValue, references: oldValueConnectorRefs } = + extractConnectorIdFromJson({ + action, + actionFields: action_field, + actionDetails: old_value, + fieldType: UserActionFieldType.Old, + }); + + return { + ...doc, + attributes: { + ...restAttributes, + action, + action_field, + new_value: transformedNewValue, + old_value: transformedOldValue, + }, + references: [...references, ...newValueConnectorRefs, ...oldValueConnectorRefs], + }; +} + +function logError(id: string, context: SavedObjectMigrationContext, error: Error) { + context.log.error( + `Failed to migrate user action connector doc id: ${id} version: ${context.migrationVersion} error: ${error.message}`, + { + migrations: { + userAction: { + id, + }, + }, + } + ); +} + +export const userActionsMigrations = { + '7.10.0': (doc: SavedObjectUnsanitizedDoc): SavedObjectSanitizedDoc => { + const { action_field, new_value, old_value, ...restAttributes } = doc.attributes; + + if ( + action_field == null || + !Array.isArray(action_field) || + action_field[0] !== 'connector_id' + ) { + return { ...doc, references: doc.references || [] }; + } + + return { + ...doc, + attributes: { + ...restAttributes, + action_field: ['connector'], + new_value: + new_value != null + ? JSON.stringify({ + id: new_value, + name: 'none', + type: ConnectorTypes.none, + fields: null, + }) + : new_value, + old_value: + old_value != null + ? JSON.stringify({ + id: old_value, + name: 'none', + type: ConnectorTypes.none, + fields: null, + }) + : old_value, + }, + references: doc.references || [], + }; + }, + '7.14.0': ( + doc: SavedObjectUnsanitizedDoc> + ): SavedObjectSanitizedDoc => { + return addOwnerToSO(doc); + }, + '7.16.0': userActionsConnectorIdMigration, +}; diff --git a/x-pack/plugins/cases/server/saved_object_types/migrations/utils.test.ts b/x-pack/plugins/cases/server/saved_object_types/migrations/utils.test.ts deleted file mode 100644 index f591bef6b3236..0000000000000 --- a/x-pack/plugins/cases/server/saved_object_types/migrations/utils.test.ts +++ /dev/null @@ -1,229 +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 { noneConnectorId } from '../../../common'; -import { createExternalService, createJiraConnector } from '../../services/test_utils'; -import { transformConnectorIdToReference, transformPushConnectorIdToReference } from './utils'; - -describe('migration utils', () => { - describe('transformConnectorIdToReference', () => { - it('returns the default none connector when the connector is undefined', () => { - expect(transformConnectorIdToReference().transformedConnector).toMatchInlineSnapshot(` - Object { - "connector": Object { - "fields": null, - "name": "none", - "type": ".none", - }, - } - `); - }); - - it('returns the default none connector when the id is undefined', () => { - expect(transformConnectorIdToReference({ id: undefined }).transformedConnector) - .toMatchInlineSnapshot(` - Object { - "connector": Object { - "fields": null, - "name": "none", - "type": ".none", - }, - } - `); - }); - - it('returns the default none connector when the id is none', () => { - expect(transformConnectorIdToReference({ id: noneConnectorId }).transformedConnector) - .toMatchInlineSnapshot(` - Object { - "connector": Object { - "fields": null, - "name": "none", - "type": ".none", - }, - } - `); - }); - - it('returns the default none connector when the id is none and other fields are defined', () => { - expect( - transformConnectorIdToReference({ ...createJiraConnector(), id: noneConnectorId }) - .transformedConnector - ).toMatchInlineSnapshot(` - Object { - "connector": Object { - "fields": null, - "name": "none", - "type": ".none", - }, - } - `); - }); - - it('returns an empty array of references when the connector is undefined', () => { - expect(transformConnectorIdToReference().references.length).toBe(0); - }); - - it('returns an empty array of references when the id is undefined', () => { - expect(transformConnectorIdToReference({ id: undefined }).references.length).toBe(0); - }); - - it('returns an empty array of references when the id is the none connector', () => { - expect(transformConnectorIdToReference({ id: noneConnectorId }).references.length).toBe(0); - }); - - it('returns an empty array of references when the id is the none connector and other fields are defined', () => { - expect( - transformConnectorIdToReference({ ...createJiraConnector(), id: noneConnectorId }) - .references.length - ).toBe(0); - }); - - it('returns a jira connector', () => { - const transformedFields = transformConnectorIdToReference(createJiraConnector()); - expect(transformedFields.transformedConnector).toMatchInlineSnapshot(` - Object { - "connector": Object { - "fields": Object { - "issueType": "bug", - "parent": "2", - "priority": "high", - }, - "name": ".jira", - "type": ".jira", - }, - } - `); - expect(transformedFields.references).toMatchInlineSnapshot(` - Array [ - Object { - "id": "1", - "name": "connectorId", - "type": "action", - }, - ] - `); - }); - }); - - describe('transformPushConnectorIdToReference', () => { - it('sets external_service to null when it is undefined', () => { - expect(transformPushConnectorIdToReference().transformedPushConnector).toMatchInlineSnapshot(` - Object { - "external_service": null, - } - `); - }); - - it('sets external_service to null when it is null', () => { - expect(transformPushConnectorIdToReference(null).transformedPushConnector) - .toMatchInlineSnapshot(` - Object { - "external_service": null, - } - `); - }); - - it('returns an object when external_service is defined but connector_id is undefined', () => { - expect( - transformPushConnectorIdToReference({ connector_id: undefined }).transformedPushConnector - ).toMatchInlineSnapshot(` - Object { - "external_service": Object {}, - } - `); - }); - - it('returns an object when external_service is defined but connector_id is null', () => { - expect(transformPushConnectorIdToReference({ connector_id: null }).transformedPushConnector) - .toMatchInlineSnapshot(` - Object { - "external_service": Object {}, - } - `); - }); - - it('returns an object when external_service is defined but connector_id is none', () => { - const otherFields = { otherField: 'hi' }; - - expect( - transformPushConnectorIdToReference({ ...otherFields, connector_id: noneConnectorId }) - .transformedPushConnector - ).toMatchInlineSnapshot(` - Object { - "external_service": Object { - "otherField": "hi", - }, - } - `); - }); - - it('returns an empty array of references when the external_service is undefined', () => { - expect(transformPushConnectorIdToReference().references.length).toBe(0); - }); - - it('returns an empty array of references when the external_service is null', () => { - expect(transformPushConnectorIdToReference(null).references.length).toBe(0); - }); - - it('returns an empty array of references when the connector_id is undefined', () => { - expect( - transformPushConnectorIdToReference({ connector_id: undefined }).references.length - ).toBe(0); - }); - - it('returns an empty array of references when the connector_id is null', () => { - expect( - transformPushConnectorIdToReference({ connector_id: undefined }).references.length - ).toBe(0); - }); - - it('returns an empty array of references when the connector_id is the none connector', () => { - expect( - transformPushConnectorIdToReference({ connector_id: noneConnectorId }).references.length - ).toBe(0); - }); - - it('returns an empty array of references when the connector_id is the none connector and other fields are defined', () => { - expect( - transformPushConnectorIdToReference({ - ...createExternalService(), - connector_id: noneConnectorId, - }).references.length - ).toBe(0); - }); - - it('returns the external_service connector', () => { - const transformedFields = transformPushConnectorIdToReference(createExternalService()); - expect(transformedFields.transformedPushConnector).toMatchInlineSnapshot(` - Object { - "external_service": Object { - "connector_name": ".jira", - "external_id": "100", - "external_title": "awesome", - "external_url": "http://www.google.com", - "pushed_at": "2019-11-25T21:54:48.952Z", - "pushed_by": Object { - "email": "testemail@elastic.co", - "full_name": "elastic", - "username": "elastic", - }, - }, - } - `); - expect(transformedFields.references).toMatchInlineSnapshot(` - Array [ - Object { - "id": "100", - "name": "pushConnectorId", - "type": "action", - }, - ] - `); - }); - }); -}); diff --git a/x-pack/plugins/cases/server/saved_object_types/migrations/utils.ts b/x-pack/plugins/cases/server/saved_object_types/migrations/utils.ts deleted file mode 100644 index 0100a04cde679..0000000000000 --- a/x-pack/plugins/cases/server/saved_object_types/migrations/utils.ts +++ /dev/null @@ -1,73 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -/* eslint-disable @typescript-eslint/naming-convention */ - -import { noneConnectorId } from '../../../common'; -import { SavedObjectReference } from '../../../../../../src/core/server'; -import { ACTION_SAVED_OBJECT_TYPE } from '../../../../actions/server'; -import { - getNoneCaseConnector, - CONNECTOR_ID_REFERENCE_NAME, - PUSH_CONNECTOR_ID_REFERENCE_NAME, -} from '../../common'; - -export const transformConnectorIdToReference = (connector?: { - id?: string; -}): { transformedConnector: Record; references: SavedObjectReference[] } => { - const { id: connectorId, ...restConnector } = connector ?? {}; - - const references = createConnectorReference( - connectorId, - ACTION_SAVED_OBJECT_TYPE, - CONNECTOR_ID_REFERENCE_NAME - ); - - const { id: ignoreNoneId, ...restNoneConnector } = getNoneCaseConnector(); - const connectorFieldsToReturn = - connector && references.length > 0 ? restConnector : restNoneConnector; - - return { - transformedConnector: { - connector: connectorFieldsToReturn, - }, - references, - }; -}; - -const createConnectorReference = ( - id: string | null | undefined, - type: string, - name: string -): SavedObjectReference[] => { - return id && id !== noneConnectorId - ? [ - { - id, - type, - name, - }, - ] - : []; -}; - -export const transformPushConnectorIdToReference = ( - external_service?: { connector_id?: string | null } | null -): { transformedPushConnector: Record; references: SavedObjectReference[] } => { - const { connector_id: pushConnectorId, ...restExternalService } = external_service ?? {}; - - const references = createConnectorReference( - pushConnectorId, - ACTION_SAVED_OBJECT_TYPE, - PUSH_CONNECTOR_ID_REFERENCE_NAME - ); - - return { - transformedPushConnector: { external_service: external_service ? restExternalService : null }, - references, - }; -}; diff --git a/x-pack/plugins/cases/server/saved_object_types/user_actions.ts b/x-pack/plugins/cases/server/saved_object_types/user_actions.ts index 883105982bcb3..7ef7c639ed9db 100644 --- a/x-pack/plugins/cases/server/saved_object_types/user_actions.ts +++ b/x-pack/plugins/cases/server/saved_object_types/user_actions.ts @@ -51,5 +51,6 @@ export const caseUserActionSavedObjectType: SavedObjectsType = { migrations: userActionsMigrations, management: { importableAndExportable: true, + visibleInManagement: false, }, }; diff --git a/x-pack/plugins/cases/server/services/cases/index.test.ts b/x-pack/plugins/cases/server/services/cases/index.test.ts index 18f4ff867cfa9..8c71abe5bff4f 100644 --- a/x-pack/plugins/cases/server/services/cases/index.test.ts +++ b/x-pack/plugins/cases/server/services/cases/index.test.ts @@ -40,6 +40,7 @@ import { createSavedObjectReferences, createCaseSavedObjectResponse, basicCaseFields, + createSOFindResponse, } from '../test_utils'; import { ESCaseAttributes } from './types'; @@ -87,13 +88,6 @@ const createFindSO = ( score: 0, }); -const createSOFindResponse = (savedObjects: Array>) => ({ - saved_objects: savedObjects, - total: savedObjects.length, - per_page: savedObjects.length, - page: 1, -}); - const createCaseUpdateParams = ( connector?: CaseConnector, externalService?: CaseFullExternalService diff --git a/x-pack/plugins/cases/server/services/cases/index.ts b/x-pack/plugins/cases/server/services/cases/index.ts index 72c2033f83535..3c76be6d6dd93 100644 --- a/x-pack/plugins/cases/server/services/cases/index.ts +++ b/x-pack/plugins/cases/server/services/cases/index.ts @@ -16,6 +16,7 @@ import { SavedObjectsFindResult, SavedObjectsBulkUpdateResponse, SavedObjectsUpdateResponse, + SavedObjectsResolveResponse, } from 'kibana/server'; import type { estypes } from '@elastic/elasticsearch'; @@ -738,6 +739,27 @@ export class CasesService { throw error; } } + + public async getResolveCase({ + unsecuredSavedObjectsClient, + id: caseId, + }: GetCaseArgs): Promise> { + try { + this.log.debug(`Attempting to resolve case ${caseId}`); + const resolveCaseResult = await unsecuredSavedObjectsClient.resolve( + CASE_SAVED_OBJECT, + caseId + ); + return { + ...resolveCaseResult, + saved_object: transformSavedObjectToExternalModel(resolveCaseResult.saved_object), + }; + } catch (error) { + this.log.error(`Error on resolve case ${caseId}: ${error}`); + throw error; + } + } + public async getSubCase({ unsecuredSavedObjectsClient, id, diff --git a/x-pack/plugins/cases/server/services/mocks.ts b/x-pack/plugins/cases/server/services/mocks.ts index a29d227cfbb0f..1ea9f481d302f 100644 --- a/x-pack/plugins/cases/server/services/mocks.ts +++ b/x-pack/plugins/cases/server/services/mocks.ts @@ -36,6 +36,7 @@ export const createCaseServiceMock = (): CaseServiceMock => { getCases: jest.fn(), getCaseIdsByAlertId: jest.fn(), getMostRecentSubCase: jest.fn(), + getResolveCase: jest.fn(), getSubCase: jest.fn(), getSubCases: jest.fn(), getTags: jest.fn(), diff --git a/x-pack/plugins/cases/server/services/test_utils.ts b/x-pack/plugins/cases/server/services/test_utils.ts index b712ea07f9c71..07743eda61212 100644 --- a/x-pack/plugins/cases/server/services/test_utils.ts +++ b/x-pack/plugins/cases/server/services/test_utils.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { SavedObject, SavedObjectReference } from 'kibana/server'; +import { SavedObject, SavedObjectReference, SavedObjectsFindResult } from 'kibana/server'; import { ESConnectorFields } from '.'; import { CONNECTOR_ID_REFERENCE_NAME, PUSH_CONNECTOR_ID_REFERENCE_NAME } from '../common'; import { @@ -54,7 +54,7 @@ export const createESJiraConnector = ( { key: 'parent', value: '2' }, ], type: ConnectorTypes.jira, - ...(overrides && { ...overrides }), + ...overrides, }; }; @@ -94,7 +94,7 @@ export const createExternalService = ( email: 'testemail@elastic.co', username: 'elastic', }, - ...(overrides && { ...overrides }), + ...overrides, }); export const basicCaseFields = { @@ -198,3 +198,14 @@ export const createSavedObjectReferences = ({ ] : []), ]; + +export const createConnectorObject = (overrides?: Partial) => ({ + connector: { ...createJiraConnector(), ...overrides }, +}); + +export const createSOFindResponse = (savedObjects: Array>) => ({ + saved_objects: savedObjects, + total: savedObjects.length, + per_page: savedObjects.length, + page: 1, +}); diff --git a/x-pack/plugins/cases/server/services/user_actions/helpers.test.ts b/x-pack/plugins/cases/server/services/user_actions/helpers.test.ts new file mode 100644 index 0000000000000..7bcbaf58d0f6e --- /dev/null +++ b/x-pack/plugins/cases/server/services/user_actions/helpers.test.ts @@ -0,0 +1,332 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { UserActionField } from '../../../common'; +import { createConnectorObject, createExternalService, createJiraConnector } from '../test_utils'; +import { buildCaseUserActionItem } from './helpers'; + +const defaultFields = () => ({ + actionAt: 'now', + actionBy: { + email: 'a', + full_name: 'j', + username: '1', + }, + caseId: '300', + owner: 'securitySolution', +}); + +describe('user action helpers', () => { + describe('buildCaseUserActionItem', () => { + describe('push user action', () => { + it('extracts the external_service connector_id to references for a new pushed user action', () => { + const userAction = buildCaseUserActionItem({ + ...defaultFields(), + action: 'push-to-service', + fields: ['pushed'], + newValue: createExternalService(), + }); + + const parsedExternalService = JSON.parse(userAction.attributes.new_value!); + expect(parsedExternalService).not.toHaveProperty('connector_id'); + expect(parsedExternalService).toMatchInlineSnapshot(` + Object { + "connector_name": ".jira", + "external_id": "100", + "external_title": "awesome", + "external_url": "http://www.google.com", + "pushed_at": "2019-11-25T21:54:48.952Z", + "pushed_by": Object { + "email": "testemail@elastic.co", + "full_name": "elastic", + "username": "elastic", + }, + } + `); + + expect(userAction.references).toMatchInlineSnapshot(` + Array [ + Object { + "id": "300", + "name": "associated-cases", + "type": "cases", + }, + Object { + "id": "100", + "name": "pushConnectorId", + "type": "action", + }, + ] + `); + + expect(userAction.attributes.old_value).toBeNull(); + }); + + it('extract the external_service connector_id to references for new and old pushed user action', () => { + const userAction = buildCaseUserActionItem({ + ...defaultFields(), + action: 'push-to-service', + fields: ['pushed'], + newValue: createExternalService(), + oldValue: createExternalService({ connector_id: '5' }), + }); + + const parsedNewExternalService = JSON.parse(userAction.attributes.new_value!); + const parsedOldExternalService = JSON.parse(userAction.attributes.old_value!); + + expect(parsedNewExternalService).not.toHaveProperty('connector_id'); + expect(parsedOldExternalService).not.toHaveProperty('connector_id'); + expect(userAction.references).toEqual([ + { id: '300', name: 'associated-cases', type: 'cases' }, + { id: '100', name: 'pushConnectorId', type: 'action' }, + { id: '5', name: 'oldPushConnectorId', type: 'action' }, + ]); + }); + + it('leaves the object unmodified when it is not a valid push user action', () => { + const userAction = buildCaseUserActionItem({ + ...defaultFields(), + action: 'push-to-service', + fields: ['invalid field'] as unknown as UserActionField, + newValue: 'hello' as unknown as Record, + }); + + expect(userAction.attributes.old_value).toBeNull(); + expect(userAction).toMatchInlineSnapshot(` + Object { + "attributes": Object { + "action": "push-to-service", + "action_at": "now", + "action_by": Object { + "email": "a", + "full_name": "j", + "username": "1", + }, + "action_field": Array [ + "invalid field", + ], + "new_value": "hello", + "old_value": null, + "owner": "securitySolution", + }, + "references": Array [ + Object { + "id": "300", + "name": "associated-cases", + "type": "cases", + }, + ], + } + `); + }); + }); + + describe('update connector user action', () => { + it('extracts the connector id to references for a new create connector user action', () => { + const userAction = buildCaseUserActionItem({ + ...defaultFields(), + action: 'update', + fields: ['connector'], + newValue: createJiraConnector(), + }); + + const parsedConnector = JSON.parse(userAction.attributes.new_value!); + expect(parsedConnector).not.toHaveProperty('id'); + expect(parsedConnector).toMatchInlineSnapshot(` + Object { + "fields": Object { + "issueType": "bug", + "parent": "2", + "priority": "high", + }, + "name": ".jira", + "type": ".jira", + } + `); + + expect(userAction.references).toMatchInlineSnapshot(` + Array [ + Object { + "id": "300", + "name": "associated-cases", + "type": "cases", + }, + Object { + "id": "1", + "name": "connectorId", + "type": "action", + }, + ] + `); + + expect(userAction.attributes.old_value).toBeNull(); + }); + + it('extracts the connector id to references for a new and old create connector user action', () => { + const userAction = buildCaseUserActionItem({ + ...defaultFields(), + action: 'update', + fields: ['connector'], + newValue: createJiraConnector(), + oldValue: { ...createJiraConnector(), id: '5' }, + }); + + const parsedNewConnector = JSON.parse(userAction.attributes.new_value!); + const parsedOldConnector = JSON.parse(userAction.attributes.new_value!); + + expect(parsedNewConnector).not.toHaveProperty('id'); + expect(parsedOldConnector).not.toHaveProperty('id'); + + expect(userAction.references).toEqual([ + { id: '300', name: 'associated-cases', type: 'cases' }, + { id: '1', name: 'connectorId', type: 'action' }, + { id: '5', name: 'oldConnectorId', type: 'action' }, + ]); + }); + + it('leaves the object unmodified when it is not a valid create connector user action', () => { + const userAction = buildCaseUserActionItem({ + ...defaultFields(), + action: 'update', + fields: ['invalid field'] as unknown as UserActionField, + newValue: 'hello' as unknown as Record, + oldValue: 'old value' as unknown as Record, + }); + + expect(userAction).toMatchInlineSnapshot(` + Object { + "attributes": Object { + "action": "update", + "action_at": "now", + "action_by": Object { + "email": "a", + "full_name": "j", + "username": "1", + }, + "action_field": Array [ + "invalid field", + ], + "new_value": "hello", + "old_value": "old value", + "owner": "securitySolution", + }, + "references": Array [ + Object { + "id": "300", + "name": "associated-cases", + "type": "cases", + }, + ], + } + `); + }); + }); + + describe('create connector user action', () => { + it('extracts the connector id to references for a new create connector user action', () => { + const userAction = buildCaseUserActionItem({ + ...defaultFields(), + action: 'create', + fields: ['connector'], + newValue: createConnectorObject(), + }); + + const parsedConnector = JSON.parse(userAction.attributes.new_value!); + expect(parsedConnector.connector).not.toHaveProperty('id'); + expect(parsedConnector).toMatchInlineSnapshot(` + Object { + "connector": Object { + "fields": Object { + "issueType": "bug", + "parent": "2", + "priority": "high", + }, + "name": ".jira", + "type": ".jira", + }, + } + `); + + expect(userAction.references).toMatchInlineSnapshot(` + Array [ + Object { + "id": "300", + "name": "associated-cases", + "type": "cases", + }, + Object { + "id": "1", + "name": "connectorId", + "type": "action", + }, + ] + `); + + expect(userAction.attributes.old_value).toBeNull(); + }); + + it('extracts the connector id to references for a new and old create connector user action', () => { + const userAction = buildCaseUserActionItem({ + ...defaultFields(), + action: 'create', + fields: ['connector'], + newValue: createConnectorObject(), + oldValue: createConnectorObject({ id: '5' }), + }); + + const parsedNewConnector = JSON.parse(userAction.attributes.new_value!); + const parsedOldConnector = JSON.parse(userAction.attributes.new_value!); + + expect(parsedNewConnector.connector).not.toHaveProperty('id'); + expect(parsedOldConnector.connector).not.toHaveProperty('id'); + + expect(userAction.references).toEqual([ + { id: '300', name: 'associated-cases', type: 'cases' }, + { id: '1', name: 'connectorId', type: 'action' }, + { id: '5', name: 'oldConnectorId', type: 'action' }, + ]); + }); + + it('leaves the object unmodified when it is not a valid create connector user action', () => { + const userAction = buildCaseUserActionItem({ + ...defaultFields(), + action: 'create', + fields: ['invalid action'] as unknown as UserActionField, + newValue: 'new json value' as unknown as Record, + oldValue: 'old value' as unknown as Record, + }); + + expect(userAction).toMatchInlineSnapshot(` + Object { + "attributes": Object { + "action": "create", + "action_at": "now", + "action_by": Object { + "email": "a", + "full_name": "j", + "username": "1", + }, + "action_field": Array [ + "invalid action", + ], + "new_value": "new json value", + "old_value": "old value", + "owner": "securitySolution", + }, + "references": Array [ + Object { + "id": "300", + "name": "associated-cases", + "type": "cases", + }, + ], + } + `); + }); + }); + }); +}); diff --git a/x-pack/plugins/cases/server/services/user_actions/helpers.ts b/x-pack/plugins/cases/server/services/user_actions/helpers.ts index 223e731aa8d9b..e91b69f0995bd 100644 --- a/x-pack/plugins/cases/server/services/user_actions/helpers.ts +++ b/x-pack/plugins/cases/server/services/user_actions/helpers.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { SavedObject, SavedObjectsUpdateResponse } from 'kibana/server'; +import { SavedObject, SavedObjectReference, SavedObjectsUpdateResponse } from 'kibana/server'; import { get, isPlainObject, isString } from 'lodash'; import deepEqual from 'fast-deep-equal'; @@ -23,8 +23,68 @@ import { } from '../../../common'; import { isTwoArraysDifference } from '../../client/utils'; import { UserActionItem } from '.'; +import { extractConnectorId } from './transform'; +import { UserActionFieldType } from './types'; +import { CASE_REF_NAME, COMMENT_REF_NAME, SUB_CASE_REF_NAME } from '../../common'; -export const transformNewUserAction = ({ +interface BuildCaseUserActionParams { + action: UserAction; + actionAt: string; + actionBy: User; + caseId: string; + owner: string; + fields: UserActionField; + newValue?: Record | string | null; + oldValue?: Record | string | null; + subCaseId?: string; +} + +export const buildCaseUserActionItem = ({ + action, + actionAt, + actionBy, + caseId, + fields, + newValue, + oldValue, + subCaseId, + owner, +}: BuildCaseUserActionParams): UserActionItem => { + const { transformedActionDetails: transformedNewValue, references: newValueReferences } = + extractConnectorId({ + action, + actionFields: fields, + actionDetails: newValue, + fieldType: UserActionFieldType.New, + }); + + const { transformedActionDetails: transformedOldValue, references: oldValueReferences } = + extractConnectorId({ + action, + actionFields: fields, + actionDetails: oldValue, + fieldType: UserActionFieldType.Old, + }); + + return { + attributes: transformNewUserAction({ + actionField: fields, + action, + actionAt, + owner, + ...actionBy, + newValue: transformedNewValue, + oldValue: transformedOldValue, + }), + references: [ + ...createCaseReferences(caseId, subCaseId), + ...newValueReferences, + ...oldValueReferences, + ], + }; +}; + +const transformNewUserAction = ({ actionField, action, actionAt, @@ -55,103 +115,43 @@ export const transformNewUserAction = ({ owner, }); -interface BuildCaseUserAction { - action: UserAction; - actionAt: string; - actionBy: User; - caseId: string; - owner: string; - fields: UserActionField | unknown[]; - newValue?: string | unknown; - oldValue?: string | unknown; - subCaseId?: string; -} +const createCaseReferences = (caseId: string, subCaseId?: string): SavedObjectReference[] => [ + { + type: CASE_SAVED_OBJECT, + name: CASE_REF_NAME, + id: caseId, + }, + ...(subCaseId + ? [ + { + type: SUB_CASE_SAVED_OBJECT, + name: SUB_CASE_REF_NAME, + id: subCaseId, + }, + ] + : []), +]; -interface BuildCommentUserActionItem extends BuildCaseUserAction { +interface BuildCommentUserActionItem extends BuildCaseUserActionParams { commentId: string; } -export const buildCommentUserActionItem = ({ - action, - actionAt, - actionBy, - caseId, - commentId, - fields, - newValue, - oldValue, - subCaseId, - owner, -}: BuildCommentUserActionItem): UserActionItem => ({ - attributes: transformNewUserAction({ - actionField: fields as UserActionField, - action, - actionAt, - owner, - ...actionBy, - newValue: newValue as string, - oldValue: oldValue as string, - }), - references: [ - { - type: CASE_SAVED_OBJECT, - name: `associated-${CASE_SAVED_OBJECT}`, - id: caseId, - }, - { - type: CASE_COMMENT_SAVED_OBJECT, - name: `associated-${CASE_COMMENT_SAVED_OBJECT}`, - id: commentId, - }, - ...(subCaseId - ? [ - { - type: SUB_CASE_SAVED_OBJECT, - id: subCaseId, - name: `associated-${SUB_CASE_SAVED_OBJECT}`, - }, - ] - : []), - ], -}); +export const buildCommentUserActionItem = (params: BuildCommentUserActionItem): UserActionItem => { + const { commentId } = params; + const { attributes, references } = buildCaseUserActionItem(params); -export const buildCaseUserActionItem = ({ - action, - actionAt, - actionBy, - caseId, - fields, - newValue, - oldValue, - subCaseId, - owner, -}: BuildCaseUserAction): UserActionItem => ({ - attributes: transformNewUserAction({ - actionField: fields as UserActionField, - action, - actionAt, - owner, - ...actionBy, - newValue: newValue as string, - oldValue: oldValue as string, - }), - references: [ - { - type: CASE_SAVED_OBJECT, - name: `associated-${CASE_SAVED_OBJECT}`, - id: caseId, - }, - ...(subCaseId - ? [ - { - type: SUB_CASE_SAVED_OBJECT, - name: `associated-${SUB_CASE_SAVED_OBJECT}`, - id: subCaseId, - }, - ] - : []), - ], -}); + return { + attributes, + references: [ + ...references, + { + type: CASE_COMMENT_SAVED_OBJECT, + name: COMMENT_REF_NAME, + id: commentId, + }, + ], + }; +}; const userActionFieldsAllowed: UserActionField = [ 'comment', @@ -278,8 +278,8 @@ const buildGenericCaseUserActions = ({ caseId, subCaseId, fields: [field], - newValue: JSON.stringify(updatedValue), - oldValue: JSON.stringify(origValue), + newValue: updatedValue, + oldValue: origValue, owner: originalItem.attributes.owner, }), ]; diff --git a/x-pack/plugins/cases/server/services/user_actions/index.test.ts b/x-pack/plugins/cases/server/services/user_actions/index.test.ts new file mode 100644 index 0000000000000..c4a350f4ac015 --- /dev/null +++ b/x-pack/plugins/cases/server/services/user_actions/index.test.ts @@ -0,0 +1,557 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { SavedObject, SavedObjectsFindResult } from 'kibana/server'; +import { transformFindResponseToExternalModel, UserActionItem } from '.'; +import { + CaseUserActionAttributes, + CASE_USER_ACTION_SAVED_OBJECT, + UserAction, + UserActionField, +} from '../../../common'; + +import { + createConnectorObject, + createExternalService, + createJiraConnector, + createSOFindResponse, +} from '../test_utils'; +import { buildCaseUserActionItem, buildCommentUserActionItem } from './helpers'; + +const createConnectorUserAction = ( + subCaseId?: string, + overrides?: Partial +): SavedObject => { + return { + ...createUserActionSO({ + action: 'create', + fields: ['connector'], + newValue: createConnectorObject(), + subCaseId, + }), + ...(overrides && { ...overrides }), + }; +}; + +const updateConnectorUserAction = ({ + subCaseId, + overrides, + oldValue, +}: { + subCaseId?: string; + overrides?: Partial; + oldValue?: string | null | Record; +} = {}): SavedObject => { + return { + ...createUserActionSO({ + action: 'update', + fields: ['connector'], + newValue: createJiraConnector(), + oldValue, + subCaseId, + }), + ...(overrides && { ...overrides }), + }; +}; + +const pushConnectorUserAction = ({ + subCaseId, + overrides, + oldValue, +}: { + subCaseId?: string; + overrides?: Partial; + oldValue?: string | null | Record; +} = {}): SavedObject => { + return { + ...createUserActionSO({ + action: 'push-to-service', + fields: ['pushed'], + newValue: createExternalService(), + oldValue, + subCaseId, + }), + ...(overrides && { ...overrides }), + }; +}; + +const createUserActionFindSO = ( + userAction: SavedObject +): SavedObjectsFindResult => ({ + ...userAction, + score: 0, +}); + +const createUserActionSO = ({ + action, + fields, + subCaseId, + newValue, + oldValue, + attributesOverrides, + commentId, +}: { + action: UserAction; + fields: UserActionField; + subCaseId?: string; + newValue?: string | null | Record; + oldValue?: string | null | Record; + attributesOverrides?: Partial; + commentId?: string; +}): SavedObject => { + const defaultParams = { + action, + actionAt: 'abc', + actionBy: { + email: 'a', + username: 'b', + full_name: 'abc', + }, + caseId: '1', + subCaseId, + fields, + newValue, + oldValue, + owner: 'securitySolution', + }; + + let userAction: UserActionItem; + + if (commentId) { + userAction = buildCommentUserActionItem({ + commentId, + ...defaultParams, + }); + } else { + userAction = buildCaseUserActionItem(defaultParams); + } + + return { + type: CASE_USER_ACTION_SAVED_OBJECT, + id: '100', + attributes: { + ...userAction.attributes, + ...(attributesOverrides && { ...attributesOverrides }), + }, + references: userAction.references, + }; +}; + +describe('CaseUserActionService', () => { + describe('transformFindResponseToExternalModel', () => { + it('does not populate the ids when the response is an empty array', () => { + expect(transformFindResponseToExternalModel(createSOFindResponse([]))).toMatchInlineSnapshot(` + Object { + "page": 1, + "per_page": 0, + "saved_objects": Array [], + "total": 0, + } + `); + }); + + it('preserves the saved object fields and attributes when inject the ids', () => { + const transformed = transformFindResponseToExternalModel( + createSOFindResponse([createUserActionFindSO(createConnectorUserAction())]) + ); + + expect(transformed).toMatchInlineSnapshot(` + Object { + "page": 1, + "per_page": 1, + "saved_objects": Array [ + Object { + "attributes": Object { + "action": "create", + "action_at": "abc", + "action_by": Object { + "email": "a", + "full_name": "abc", + "username": "b", + }, + "action_field": Array [ + "connector", + ], + "action_id": "100", + "case_id": "1", + "comment_id": null, + "new_val_connector_id": "1", + "new_value": "{\\"connector\\":{\\"name\\":\\".jira\\",\\"type\\":\\".jira\\",\\"fields\\":{\\"issueType\\":\\"bug\\",\\"priority\\":\\"high\\",\\"parent\\":\\"2\\"}}}", + "old_val_connector_id": null, + "old_value": null, + "owner": "securitySolution", + "sub_case_id": "", + }, + "id": "100", + "references": Array [ + Object { + "id": "1", + "name": "associated-cases", + "type": "cases", + }, + Object { + "id": "1", + "name": "connectorId", + "type": "action", + }, + ], + "score": 0, + "type": "cases-user-actions", + }, + ], + "total": 1, + } + `); + }); + + it('populates the new_val_connector_id for multiple user actions', () => { + const transformed = transformFindResponseToExternalModel( + createSOFindResponse([ + createUserActionFindSO(createConnectorUserAction()), + createUserActionFindSO(createConnectorUserAction()), + ]) + ); + + expect(transformed.saved_objects[0].attributes.new_val_connector_id).toEqual('1'); + expect(transformed.saved_objects[1].attributes.new_val_connector_id).toEqual('1'); + }); + + it('populates the old_val_connector_id for multiple user actions', () => { + const transformed = transformFindResponseToExternalModel( + createSOFindResponse([ + createUserActionFindSO( + createUserActionSO({ + action: 'create', + fields: ['connector'], + oldValue: createConnectorObject(), + }) + ), + createUserActionFindSO( + createUserActionSO({ + action: 'create', + fields: ['connector'], + oldValue: createConnectorObject({ id: '10' }), + }) + ), + ]) + ); + + expect(transformed.saved_objects[0].attributes.old_val_connector_id).toEqual('1'); + expect(transformed.saved_objects[1].attributes.old_val_connector_id).toEqual('10'); + }); + + describe('reference ids', () => { + it('sets case_id to an empty string when it cannot find the reference', () => { + const userAction = { + ...createConnectorUserAction(), + references: [], + }; + const transformed = transformFindResponseToExternalModel( + createSOFindResponse([createUserActionFindSO(userAction)]) + ); + + expect(transformed.saved_objects[0].attributes.case_id).toEqual(''); + }); + + it('sets comment_id to null when it cannot find the reference', () => { + const userAction = { + ...createUserActionSO({ action: 'create', fields: ['connector'], commentId: '5' }), + references: [], + }; + const transformed = transformFindResponseToExternalModel( + createSOFindResponse([createUserActionFindSO(userAction)]) + ); + + expect(transformed.saved_objects[0].attributes.comment_id).toBeNull(); + }); + + it('sets sub_case_id to an empty string when it cannot find the reference', () => { + const userAction = { + ...createUserActionSO({ action: 'create', fields: ['connector'], subCaseId: '5' }), + references: [], + }; + const transformed = transformFindResponseToExternalModel( + createSOFindResponse([createUserActionFindSO(userAction)]) + ); + + expect(transformed.saved_objects[0].attributes.comment_id).toBeNull(); + }); + + it('sets case_id correctly when it finds the reference', () => { + const userAction = createConnectorUserAction(); + + const transformed = transformFindResponseToExternalModel( + createSOFindResponse([createUserActionFindSO(userAction)]) + ); + + expect(transformed.saved_objects[0].attributes.case_id).toEqual('1'); + }); + + it('sets comment_id correctly when it finds the reference', () => { + const userAction = createUserActionSO({ + action: 'create', + fields: ['connector'], + commentId: '5', + }); + + const transformed = transformFindResponseToExternalModel( + createSOFindResponse([createUserActionFindSO(userAction)]) + ); + + expect(transformed.saved_objects[0].attributes.comment_id).toEqual('5'); + }); + + it('sets sub_case_id correctly when it finds the reference', () => { + const userAction = { + ...createUserActionSO({ action: 'create', fields: ['connector'], subCaseId: '5' }), + }; + + const transformed = transformFindResponseToExternalModel( + createSOFindResponse([createUserActionFindSO(userAction)]) + ); + + expect(transformed.saved_objects[0].attributes.sub_case_id).toEqual('5'); + }); + + it('sets action_id correctly to the saved object id', () => { + const userAction = { + ...createUserActionSO({ action: 'create', fields: ['connector'], subCaseId: '5' }), + }; + + const transformed = transformFindResponseToExternalModel( + createSOFindResponse([createUserActionFindSO(userAction)]) + ); + + expect(transformed.saved_objects[0].attributes.action_id).toEqual('100'); + }); + }); + + describe('create connector', () => { + it('does not populate the new_val_connector_id when it cannot find the reference', () => { + const userAction = { ...createConnectorUserAction(), references: [] }; + const transformed = transformFindResponseToExternalModel( + createSOFindResponse([createUserActionFindSO(userAction)]) + ); + + expect(transformed.saved_objects[0].attributes.new_val_connector_id).toBeNull(); + }); + + it('does not populate the old_val_connector_id when it cannot find the reference', () => { + const userAction = { ...createConnectorUserAction(), references: [] }; + const transformed = transformFindResponseToExternalModel( + createSOFindResponse([createUserActionFindSO(userAction)]) + ); + + expect(transformed.saved_objects[0].attributes.old_val_connector_id).toBeNull(); + }); + + it('does not populate the new_val_connector_id when the reference exists but the action and fields are invalid', () => { + const validUserAction = createConnectorUserAction(); + const invalidUserAction = { + ...validUserAction, + attributes: { ...validUserAction.attributes, action: 'invalid' }, + }; + const transformed = transformFindResponseToExternalModel( + createSOFindResponse([ + createUserActionFindSO(invalidUserAction as SavedObject), + ]) + ); + + expect(transformed.saved_objects[0].attributes.new_val_connector_id).toBeNull(); + }); + + it('does not populate the old_val_connector_id when the reference exists but the action and fields are invalid', () => { + const validUserAction = createUserActionSO({ + action: 'create', + fields: ['connector'], + oldValue: createConnectorObject(), + }); + + const invalidUserAction = { + ...validUserAction, + attributes: { ...validUserAction.attributes, action: 'invalid' }, + }; + const transformed = transformFindResponseToExternalModel( + createSOFindResponse([ + createUserActionFindSO(invalidUserAction as SavedObject), + ]) + ); + + expect(transformed.saved_objects[0].attributes.old_val_connector_id).toBeNull(); + }); + + it('populates the new_val_connector_id', () => { + const userAction = createConnectorUserAction(); + const transformed = transformFindResponseToExternalModel( + createSOFindResponse([createUserActionFindSO(userAction)]) + ); + + expect(transformed.saved_objects[0].attributes.new_val_connector_id).toEqual('1'); + }); + + it('populates the old_val_connector_id', () => { + const userAction = createUserActionSO({ + action: 'create', + fields: ['connector'], + oldValue: createConnectorObject(), + }); + + const transformed = transformFindResponseToExternalModel( + createSOFindResponse([createUserActionFindSO(userAction)]) + ); + + expect(transformed.saved_objects[0].attributes.old_val_connector_id).toEqual('1'); + }); + }); + + describe('update connector', () => { + it('does not populate the new_val_connector_id when it cannot find the reference', () => { + const userAction = { ...updateConnectorUserAction(), references: [] }; + const transformed = transformFindResponseToExternalModel( + createSOFindResponse([createUserActionFindSO(userAction)]) + ); + + expect(transformed.saved_objects[0].attributes.new_val_connector_id).toBeNull(); + }); + + it('does not populate the old_val_connector_id when it cannot find the reference', () => { + const userAction = { + ...updateConnectorUserAction({ oldValue: createJiraConnector() }), + references: [], + }; + const transformed = transformFindResponseToExternalModel( + createSOFindResponse([createUserActionFindSO(userAction)]) + ); + + expect(transformed.saved_objects[0].attributes.old_val_connector_id).toBeNull(); + }); + + it('does not populate the new_val_connector_id when the reference exists but the action and fields are invalid', () => { + const validUserAction = updateConnectorUserAction(); + const invalidUserAction = { + ...validUserAction, + attributes: { ...validUserAction.attributes, action: 'invalid' }, + }; + const transformed = transformFindResponseToExternalModel( + createSOFindResponse([ + createUserActionFindSO(invalidUserAction as SavedObject), + ]) + ); + + expect(transformed.saved_objects[0].attributes.new_val_connector_id).toBeNull(); + }); + + it('does not populate the old_val_connector_id when the reference exists but the action and fields are invalid', () => { + const validUserAction = updateConnectorUserAction({ oldValue: createJiraConnector() }); + + const invalidUserAction = { + ...validUserAction, + attributes: { ...validUserAction.attributes, action: 'invalid' }, + }; + const transformed = transformFindResponseToExternalModel( + createSOFindResponse([ + createUserActionFindSO(invalidUserAction as SavedObject), + ]) + ); + + expect(transformed.saved_objects[0].attributes.old_val_connector_id).toBeNull(); + }); + + it('populates the new_val_connector_id', () => { + const userAction = updateConnectorUserAction(); + const transformed = transformFindResponseToExternalModel( + createSOFindResponse([createUserActionFindSO(userAction)]) + ); + + expect(transformed.saved_objects[0].attributes.new_val_connector_id).toEqual('1'); + }); + + it('populates the old_val_connector_id', () => { + const userAction = updateConnectorUserAction({ oldValue: createJiraConnector() }); + + const transformed = transformFindResponseToExternalModel( + createSOFindResponse([createUserActionFindSO(userAction)]) + ); + + expect(transformed.saved_objects[0].attributes.old_val_connector_id).toEqual('1'); + }); + }); + + describe('push connector', () => { + it('does not populate the new_val_connector_id when it cannot find the reference', () => { + const userAction = { ...pushConnectorUserAction(), references: [] }; + const transformed = transformFindResponseToExternalModel( + createSOFindResponse([createUserActionFindSO(userAction)]) + ); + + expect(transformed.saved_objects[0].attributes.new_val_connector_id).toBeNull(); + }); + + it('does not populate the old_val_connector_id when it cannot find the reference', () => { + const userAction = { + ...pushConnectorUserAction({ oldValue: createExternalService() }), + references: [], + }; + const transformed = transformFindResponseToExternalModel( + createSOFindResponse([createUserActionFindSO(userAction)]) + ); + + expect(transformed.saved_objects[0].attributes.old_val_connector_id).toBeNull(); + }); + + it('does not populate the new_val_connector_id when the reference exists but the action and fields are invalid', () => { + const validUserAction = pushConnectorUserAction(); + const invalidUserAction = { + ...validUserAction, + attributes: { ...validUserAction.attributes, action: 'invalid' }, + }; + const transformed = transformFindResponseToExternalModel( + createSOFindResponse([ + createUserActionFindSO(invalidUserAction as SavedObject), + ]) + ); + + expect(transformed.saved_objects[0].attributes.new_val_connector_id).toBeNull(); + }); + + it('does not populate the old_val_connector_id when the reference exists but the action and fields are invalid', () => { + const validUserAction = pushConnectorUserAction({ oldValue: createExternalService() }); + + const invalidUserAction = { + ...validUserAction, + attributes: { ...validUserAction.attributes, action: 'invalid' }, + }; + const transformed = transformFindResponseToExternalModel( + createSOFindResponse([ + createUserActionFindSO(invalidUserAction as SavedObject), + ]) + ); + + expect(transformed.saved_objects[0].attributes.old_val_connector_id).toBeNull(); + }); + + it('populates the new_val_connector_id', () => { + const userAction = pushConnectorUserAction(); + const transformed = transformFindResponseToExternalModel( + createSOFindResponse([createUserActionFindSO(userAction)]) + ); + + expect(transformed.saved_objects[0].attributes.new_val_connector_id).toEqual('100'); + }); + + it('populates the old_val_connector_id', () => { + const userAction = pushConnectorUserAction({ oldValue: createExternalService() }); + + const transformed = transformFindResponseToExternalModel( + createSOFindResponse([createUserActionFindSO(userAction)]) + ); + + expect(transformed.saved_objects[0].attributes.old_val_connector_id).toEqual('100'); + }); + }); + }); +}); diff --git a/x-pack/plugins/cases/server/services/user_actions/index.ts b/x-pack/plugins/cases/server/services/user_actions/index.ts index b702448165554..4f158862e3d63 100644 --- a/x-pack/plugins/cases/server/services/user_actions/index.ts +++ b/x-pack/plugins/cases/server/services/user_actions/index.ts @@ -5,7 +5,12 @@ * 2.0. */ -import { Logger, SavedObjectReference } from 'kibana/server'; +import { + Logger, + SavedObjectReference, + SavedObjectsFindResponse, + SavedObjectsFindResult, +} from 'kibana/server'; import { CASE_SAVED_OBJECT, @@ -13,8 +18,17 @@ import { CaseUserActionAttributes, MAX_DOCS_PER_PAGE, SUB_CASE_SAVED_OBJECT, + CaseUserActionResponse, + CASE_COMMENT_SAVED_OBJECT, + isCreateConnector, + isPush, + isUpdateConnector, } from '../../../common'; import { ClientArgs } from '..'; +import { UserActionFieldType } from './types'; +import { CASE_REF_NAME, COMMENT_REF_NAME, SUB_CASE_REF_NAME } from '../../common'; +import { ConnectorIdReferenceName, PushConnectorIdReferenceName } from './transform'; +import { findConnectorIdReference } from '../transform'; interface GetCaseUserActionArgs extends ClientArgs { caseId: string; @@ -33,12 +47,16 @@ interface PostCaseUserActionArgs extends ClientArgs { export class CaseUserActionService { constructor(private readonly log: Logger) {} - public async getAll({ unsecuredSavedObjectsClient, caseId, subCaseId }: GetCaseUserActionArgs) { + public async getAll({ + unsecuredSavedObjectsClient, + caseId, + subCaseId, + }: GetCaseUserActionArgs): Promise> { try { const id = subCaseId ?? caseId; const type = subCaseId ? SUB_CASE_SAVED_OBJECT : CASE_SAVED_OBJECT; - return await unsecuredSavedObjectsClient.find({ + const userActions = await unsecuredSavedObjectsClient.find({ type: CASE_USER_ACTION_SAVED_OBJECT, hasReference: { type, id }, page: 1, @@ -46,17 +64,22 @@ export class CaseUserActionService { sortField: 'action_at', sortOrder: 'asc', }); + + return transformFindResponseToExternalModel(userActions); } catch (error) { this.log.error(`Error on GET case user action case id: ${caseId}: ${error}`); throw error; } } - public async bulkCreate({ unsecuredSavedObjectsClient, actions }: PostCaseUserActionArgs) { + public async bulkCreate({ + unsecuredSavedObjectsClient, + actions, + }: PostCaseUserActionArgs): Promise { try { this.log.debug(`Attempting to POST a new case user action`); - return await unsecuredSavedObjectsClient.bulkCreate( + await unsecuredSavedObjectsClient.bulkCreate( actions.map((action) => ({ type: CASE_USER_ACTION_SAVED_OBJECT, ...action })) ); } catch (error) { @@ -65,3 +88,71 @@ export class CaseUserActionService { } } } + +export function transformFindResponseToExternalModel( + userActions: SavedObjectsFindResponse +): SavedObjectsFindResponse { + return { + ...userActions, + saved_objects: userActions.saved_objects.map((so) => ({ + ...so, + ...transformToExternalModel(so), + })), + }; +} + +function transformToExternalModel( + userAction: SavedObjectsFindResult +): SavedObjectsFindResult { + const { references } = userAction; + + const newValueConnectorId = getConnectorIdFromReferences(UserActionFieldType.New, userAction); + const oldValueConnectorId = getConnectorIdFromReferences(UserActionFieldType.Old, userAction); + + const caseId = findReferenceId(CASE_REF_NAME, CASE_SAVED_OBJECT, references) ?? ''; + const commentId = + findReferenceId(COMMENT_REF_NAME, CASE_COMMENT_SAVED_OBJECT, references) ?? null; + const subCaseId = findReferenceId(SUB_CASE_REF_NAME, SUB_CASE_SAVED_OBJECT, references) ?? ''; + + return { + ...userAction, + attributes: { + ...userAction.attributes, + action_id: userAction.id, + case_id: caseId, + comment_id: commentId, + sub_case_id: subCaseId, + new_val_connector_id: newValueConnectorId, + old_val_connector_id: oldValueConnectorId, + }, + }; +} + +function getConnectorIdFromReferences( + fieldType: UserActionFieldType, + userAction: SavedObjectsFindResult +): string | null { + const { + // eslint-disable-next-line @typescript-eslint/naming-convention + attributes: { action, action_field }, + references, + } = userAction; + + if (isCreateConnector(action, action_field) || isUpdateConnector(action, action_field)) { + return findConnectorIdReference(ConnectorIdReferenceName[fieldType], references)?.id ?? null; + } else if (isPush(action, action_field)) { + return ( + findConnectorIdReference(PushConnectorIdReferenceName[fieldType], references)?.id ?? null + ); + } + + return null; +} + +function findReferenceId( + name: string, + type: string, + references: SavedObjectReference[] +): string | undefined { + return references.find((ref) => ref.name === name && ref.type === type)?.id; +} diff --git a/x-pack/plugins/cases/server/services/user_actions/transform.test.ts b/x-pack/plugins/cases/server/services/user_actions/transform.test.ts new file mode 100644 index 0000000000000..2d28770617094 --- /dev/null +++ b/x-pack/plugins/cases/server/services/user_actions/transform.test.ts @@ -0,0 +1,1246 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { noneConnectorId } from '../../../common'; +import { + CONNECTOR_ID_REFERENCE_NAME, + getNoneCaseConnector, + PUSH_CONNECTOR_ID_REFERENCE_NAME, + USER_ACTION_OLD_ID_REF_NAME, + USER_ACTION_OLD_PUSH_ID_REF_NAME, +} from '../../common'; +import { createConnectorObject, createExternalService, createJiraConnector } from '../test_utils'; +import { + extractConnectorIdHelper, + extractConnectorIdFromJson, + extractConnectorId, + transformConnectorIdToReference, + transformPushConnectorIdToReference, +} from './transform'; +import { UserActionFieldType } from './types'; + +describe('user action transform utils', () => { + describe('transformConnectorIdToReference', () => { + it('returns the default none connector when the connector is undefined', () => { + expect(transformConnectorIdToReference(CONNECTOR_ID_REFERENCE_NAME).transformedConnector) + .toMatchInlineSnapshot(` + Object { + "connector": Object { + "fields": null, + "name": "none", + "type": ".none", + }, + } + `); + }); + + it('returns the default none connector when the id is undefined', () => { + expect( + transformConnectorIdToReference(CONNECTOR_ID_REFERENCE_NAME, { id: undefined }) + .transformedConnector + ).toMatchInlineSnapshot(` + Object { + "connector": Object { + "fields": null, + "name": "none", + "type": ".none", + }, + } + `); + }); + + it('returns the default none connector when the id is none', () => { + expect( + transformConnectorIdToReference(CONNECTOR_ID_REFERENCE_NAME, { id: noneConnectorId }) + .transformedConnector + ).toMatchInlineSnapshot(` + Object { + "connector": Object { + "fields": null, + "name": "none", + "type": ".none", + }, + } + `); + }); + + it('returns the default none connector when the id is none and other fields are defined', () => { + expect( + transformConnectorIdToReference(CONNECTOR_ID_REFERENCE_NAME, { + ...createJiraConnector(), + id: noneConnectorId, + }).transformedConnector + ).toMatchInlineSnapshot(` + Object { + "connector": Object { + "fields": null, + "name": "none", + "type": ".none", + }, + } + `); + }); + + it('returns an empty array of references when the connector is undefined', () => { + expect(transformConnectorIdToReference(CONNECTOR_ID_REFERENCE_NAME).references.length).toBe( + 0 + ); + }); + + it('returns an empty array of references when the id is undefined', () => { + expect( + transformConnectorIdToReference(CONNECTOR_ID_REFERENCE_NAME, { id: undefined }).references + .length + ).toBe(0); + }); + + it('returns an empty array of references when the id is the none connector', () => { + expect( + transformConnectorIdToReference(CONNECTOR_ID_REFERENCE_NAME, { id: noneConnectorId }) + .references.length + ).toBe(0); + }); + + it('returns an empty array of references when the id is the none connector and other fields are defined', () => { + expect( + transformConnectorIdToReference(CONNECTOR_ID_REFERENCE_NAME, { + ...createJiraConnector(), + id: noneConnectorId, + }).references.length + ).toBe(0); + }); + + it('returns a jira connector', () => { + const transformedFields = transformConnectorIdToReference( + CONNECTOR_ID_REFERENCE_NAME, + createJiraConnector() + ); + expect(transformedFields.transformedConnector).toMatchInlineSnapshot(` + Object { + "connector": Object { + "fields": Object { + "issueType": "bug", + "parent": "2", + "priority": "high", + }, + "name": ".jira", + "type": ".jira", + }, + } + `); + expect(transformedFields.references).toMatchInlineSnapshot(` + Array [ + Object { + "id": "1", + "name": "connectorId", + "type": "action", + }, + ] + `); + }); + + it('returns a jira connector with the user action reference name', () => { + const transformedFields = transformConnectorIdToReference( + USER_ACTION_OLD_ID_REF_NAME, + createJiraConnector() + ); + expect(transformedFields.transformedConnector).toMatchInlineSnapshot(` + Object { + "connector": Object { + "fields": Object { + "issueType": "bug", + "parent": "2", + "priority": "high", + }, + "name": ".jira", + "type": ".jira", + }, + } + `); + expect(transformedFields.references).toMatchInlineSnapshot(` + Array [ + Object { + "id": "1", + "name": "oldConnectorId", + "type": "action", + }, + ] + `); + }); + }); + + describe('transformPushConnectorIdToReference', () => { + it('sets external_service to null when it is undefined', () => { + expect( + transformPushConnectorIdToReference(PUSH_CONNECTOR_ID_REFERENCE_NAME) + .transformedPushConnector + ).toMatchInlineSnapshot(` + Object { + "external_service": null, + } + `); + }); + + it('sets external_service to null when it is null', () => { + expect( + transformPushConnectorIdToReference(PUSH_CONNECTOR_ID_REFERENCE_NAME, null) + .transformedPushConnector + ).toMatchInlineSnapshot(` + Object { + "external_service": null, + } + `); + }); + + it('returns an object when external_service is defined but connector_id is undefined', () => { + expect( + transformPushConnectorIdToReference(PUSH_CONNECTOR_ID_REFERENCE_NAME, { + connector_id: undefined, + }).transformedPushConnector + ).toMatchInlineSnapshot(` + Object { + "external_service": Object {}, + } + `); + }); + + it('returns an object when external_service is defined but connector_id is null', () => { + expect( + transformPushConnectorIdToReference(PUSH_CONNECTOR_ID_REFERENCE_NAME, { + connector_id: null, + }).transformedPushConnector + ).toMatchInlineSnapshot(` + Object { + "external_service": Object {}, + } + `); + }); + + it('returns an object when external_service is defined but connector_id is none', () => { + const otherFields = { otherField: 'hi' }; + + expect( + transformPushConnectorIdToReference(PUSH_CONNECTOR_ID_REFERENCE_NAME, { + ...otherFields, + connector_id: noneConnectorId, + }).transformedPushConnector + ).toMatchInlineSnapshot(` + Object { + "external_service": Object { + "otherField": "hi", + }, + } + `); + }); + + it('returns an empty array of references when the external_service is undefined', () => { + expect( + transformPushConnectorIdToReference(PUSH_CONNECTOR_ID_REFERENCE_NAME).references.length + ).toBe(0); + }); + + it('returns an empty array of references when the external_service is null', () => { + expect( + transformPushConnectorIdToReference(PUSH_CONNECTOR_ID_REFERENCE_NAME, null).references + .length + ).toBe(0); + }); + + it('returns an empty array of references when the connector_id is undefined', () => { + expect( + transformPushConnectorIdToReference(PUSH_CONNECTOR_ID_REFERENCE_NAME, { + connector_id: undefined, + }).references.length + ).toBe(0); + }); + + it('returns an empty array of references when the connector_id is null', () => { + expect( + transformPushConnectorIdToReference(PUSH_CONNECTOR_ID_REFERENCE_NAME, { + connector_id: undefined, + }).references.length + ).toBe(0); + }); + + it('returns an empty array of references when the connector_id is the none connector', () => { + expect( + transformPushConnectorIdToReference(PUSH_CONNECTOR_ID_REFERENCE_NAME, { + connector_id: noneConnectorId, + }).references.length + ).toBe(0); + }); + + it('returns an empty array of references when the connector_id is the none connector and other fields are defined', () => { + expect( + transformPushConnectorIdToReference(PUSH_CONNECTOR_ID_REFERENCE_NAME, { + ...createExternalService(), + connector_id: noneConnectorId, + }).references.length + ).toBe(0); + }); + + it('returns the external_service connector', () => { + const transformedFields = transformPushConnectorIdToReference( + PUSH_CONNECTOR_ID_REFERENCE_NAME, + createExternalService() + ); + expect(transformedFields.transformedPushConnector).toMatchInlineSnapshot(` + Object { + "external_service": Object { + "connector_name": ".jira", + "external_id": "100", + "external_title": "awesome", + "external_url": "http://www.google.com", + "pushed_at": "2019-11-25T21:54:48.952Z", + "pushed_by": Object { + "email": "testemail@elastic.co", + "full_name": "elastic", + "username": "elastic", + }, + }, + } + `); + expect(transformedFields.references).toMatchInlineSnapshot(` + Array [ + Object { + "id": "100", + "name": "pushConnectorId", + "type": "action", + }, + ] + `); + }); + + it('returns the external_service connector with a user actions reference name', () => { + const transformedFields = transformPushConnectorIdToReference( + USER_ACTION_OLD_PUSH_ID_REF_NAME, + createExternalService() + ); + + expect(transformedFields.references).toMatchInlineSnapshot(` + Array [ + Object { + "id": "100", + "name": "oldPushConnectorId", + "type": "action", + }, + ] + `); + }); + }); + + describe('extractConnectorIdHelper', () => { + it('throws an error when action details has a circular reference', () => { + const circularRef = { prop: {} }; + circularRef.prop = circularRef; + + expect(() => { + extractConnectorIdHelper({ + action: 'a', + actionFields: [], + actionDetails: circularRef, + fieldType: UserActionFieldType.New, + }); + }).toThrow(); + }); + + describe('create action', () => { + it('returns no references and untransformed json when actionDetails is not a valid connector', () => { + expect( + extractConnectorIdHelper({ + action: 'create', + actionFields: ['connector'], + actionDetails: { a: 'hello' }, + fieldType: UserActionFieldType.New, + }) + ).toMatchInlineSnapshot(` + Object { + "references": Array [], + "transformedActionDetails": "{\\"a\\":\\"hello\\"}", + } + `); + }); + + it('returns no references and untransformed json when the action is create and action fields does not contain connector', () => { + expect( + extractConnectorIdHelper({ + action: 'create', + actionFields: ['', 'something', 'onnector'], + actionDetails: { a: 'hello' }, + fieldType: UserActionFieldType.New, + }) + ).toMatchInlineSnapshot(` + Object { + "references": Array [], + "transformedActionDetails": "{\\"a\\":\\"hello\\"}", + } + `); + }); + + it('returns the stringified json without the id', () => { + const jiraConnector = createConnectorObject(); + + const { transformedActionDetails } = extractConnectorIdHelper({ + action: 'create', + actionFields: ['connector'], + actionDetails: jiraConnector, + fieldType: UserActionFieldType.New, + }); + + expect(JSON.parse(transformedActionDetails)).toMatchInlineSnapshot(` + Object { + "connector": Object { + "fields": Object { + "issueType": "bug", + "parent": "2", + "priority": "high", + }, + "name": ".jira", + "type": ".jira", + }, + } + `); + }); + + it('removes the connector.id when the connector is none', () => { + const connector = { connector: getNoneCaseConnector() }; + + const { transformedActionDetails } = extractConnectorIdHelper({ + action: 'create', + actionFields: ['connector'], + actionDetails: connector, + fieldType: UserActionFieldType.New, + })!; + + const parsedJson = JSON.parse(transformedActionDetails); + + expect(parsedJson.connector).not.toHaveProperty('id'); + expect(parsedJson).toMatchInlineSnapshot(` + Object { + "connector": Object { + "fields": null, + "name": "none", + "type": ".none", + }, + } + `); + }); + + it('does not return a reference when the connector is none', () => { + const connector = { connector: getNoneCaseConnector() }; + + const { references } = extractConnectorIdHelper({ + action: 'create', + actionFields: ['connector'], + actionDetails: connector, + fieldType: UserActionFieldType.New, + })!; + + expect(references).toEqual([]); + }); + + it('returns a reference to the connector.id', () => { + const connector = createConnectorObject(); + + const { references } = extractConnectorIdHelper({ + action: 'create', + actionFields: ['connector'], + actionDetails: connector, + fieldType: UserActionFieldType.New, + })!; + + expect(references).toMatchInlineSnapshot(` + Array [ + Object { + "id": "1", + "name": "connectorId", + "type": "action", + }, + ] + `); + }); + + it('returns an old reference name to the connector.id', () => { + const connector = createConnectorObject(); + + const { references } = extractConnectorIdHelper({ + action: 'create', + actionFields: ['connector'], + actionDetails: connector, + fieldType: UserActionFieldType.Old, + })!; + + expect(references).toMatchInlineSnapshot(` + Array [ + Object { + "id": "1", + "name": "oldConnectorId", + "type": "action", + }, + ] + `); + }); + + it('returns the transformed connector and the description', () => { + const details = { ...createConnectorObject(), description: 'a description' }; + + const { transformedActionDetails } = extractConnectorIdHelper({ + action: 'create', + actionFields: ['connector'], + actionDetails: details, + fieldType: UserActionFieldType.Old, + })!; + + const parsedJson = JSON.parse(transformedActionDetails); + + expect(parsedJson).toMatchInlineSnapshot(` + Object { + "connector": Object { + "fields": Object { + "issueType": "bug", + "parent": "2", + "priority": "high", + }, + "name": ".jira", + "type": ".jira", + }, + "description": "a description", + } + `); + }); + }); + + describe('update action', () => { + it('returns no references and untransformed json when actionDetails is not a valid connector', () => { + expect( + extractConnectorIdHelper({ + action: 'update', + actionFields: ['connector'], + actionDetails: { a: 'hello' }, + fieldType: UserActionFieldType.New, + }) + ).toMatchInlineSnapshot(` + Object { + "references": Array [], + "transformedActionDetails": "{\\"a\\":\\"hello\\"}", + } + `); + }); + + it('returns no references and untransformed json when the action is update and action fields does not contain connector', () => { + expect( + extractConnectorIdHelper({ + action: 'update', + actionFields: ['', 'something', 'onnector'], + actionDetails: 5, + fieldType: UserActionFieldType.New, + }) + ).toMatchInlineSnapshot(` + Object { + "references": Array [], + "transformedActionDetails": "5", + } + `); + }); + + it('returns the stringified json without the id', () => { + const jiraConnector = createJiraConnector(); + + const { transformedActionDetails } = extractConnectorIdHelper({ + action: 'update', + actionFields: ['connector'], + actionDetails: jiraConnector, + fieldType: UserActionFieldType.New, + }); + + const transformedConnetor = JSON.parse(transformedActionDetails!); + expect(transformedConnetor).not.toHaveProperty('id'); + expect(transformedConnetor).toMatchInlineSnapshot(` + Object { + "fields": Object { + "issueType": "bug", + "parent": "2", + "priority": "high", + }, + "name": ".jira", + "type": ".jira", + } + `); + }); + + it('returns the stringified json without the id when the connector is none', () => { + const connector = getNoneCaseConnector(); + + const { transformedActionDetails } = extractConnectorIdHelper({ + action: 'update', + actionFields: ['connector'], + actionDetails: connector, + fieldType: UserActionFieldType.New, + }); + + const transformedConnetor = JSON.parse(transformedActionDetails); + expect(transformedConnetor).not.toHaveProperty('id'); + expect(transformedConnetor).toMatchInlineSnapshot(` + Object { + "fields": null, + "name": "none", + "type": ".none", + } + `); + }); + + it('returns a reference to the connector.id', () => { + const jiraConnector = createJiraConnector(); + + const { references } = extractConnectorIdHelper({ + action: 'update', + actionFields: ['connector'], + actionDetails: jiraConnector, + fieldType: UserActionFieldType.New, + })!; + + expect(references).toMatchInlineSnapshot(` + Array [ + Object { + "id": "1", + "name": "connectorId", + "type": "action", + }, + ] + `); + }); + + it('does not return a reference when the connector is none', () => { + const connector = getNoneCaseConnector(); + + const { references } = extractConnectorIdHelper({ + action: 'update', + actionFields: ['connector'], + actionDetails: connector, + fieldType: UserActionFieldType.New, + })!; + + expect(references).toEqual([]); + }); + + it('returns an old reference name to the connector.id', () => { + const jiraConnector = createJiraConnector(); + + const { references } = extractConnectorIdHelper({ + action: 'update', + actionFields: ['connector'], + actionDetails: jiraConnector, + fieldType: UserActionFieldType.Old, + })!; + + expect(references).toMatchInlineSnapshot(` + Array [ + Object { + "id": "1", + "name": "oldConnectorId", + "type": "action", + }, + ] + `); + }); + }); + + describe('push action', () => { + it('returns no references and untransformed json when actionDetails is not a valid external_service', () => { + expect( + extractConnectorIdHelper({ + action: 'push-to-service', + actionFields: ['pushed'], + actionDetails: { a: 'hello' }, + fieldType: UserActionFieldType.New, + }) + ).toMatchInlineSnapshot(` + Object { + "references": Array [], + "transformedActionDetails": "{\\"a\\":\\"hello\\"}", + } + `); + }); + + it('returns no references and untransformed json when the action is push-to-service and action fields does not contain pushed', () => { + expect( + extractConnectorIdHelper({ + action: 'push-to-service', + actionFields: ['', 'something', 'ushed'], + actionDetails: { a: 'hello' }, + fieldType: UserActionFieldType.New, + }) + ).toMatchInlineSnapshot(` + Object { + "references": Array [], + "transformedActionDetails": "{\\"a\\":\\"hello\\"}", + } + `); + }); + + it('returns the stringified json without the connector_id', () => { + const externalService = createExternalService(); + + const { transformedActionDetails } = extractConnectorIdHelper({ + action: 'push-to-service', + actionFields: ['pushed'], + actionDetails: externalService, + fieldType: UserActionFieldType.New, + }); + + const transformedExternalService = JSON.parse(transformedActionDetails); + expect(transformedExternalService).not.toHaveProperty('connector_id'); + expect(transformedExternalService).toMatchInlineSnapshot(` + Object { + "connector_name": ".jira", + "external_id": "100", + "external_title": "awesome", + "external_url": "http://www.google.com", + "pushed_at": "2019-11-25T21:54:48.952Z", + "pushed_by": Object { + "email": "testemail@elastic.co", + "full_name": "elastic", + "username": "elastic", + }, + } + `); + }); + + it('returns a reference to the connector_id', () => { + const externalService = createExternalService(); + + const { references } = extractConnectorIdHelper({ + action: 'push-to-service', + actionFields: ['pushed'], + actionDetails: externalService, + fieldType: UserActionFieldType.New, + })!; + + expect(references).toMatchInlineSnapshot(` + Array [ + Object { + "id": "100", + "name": "pushConnectorId", + "type": "action", + }, + ] + `); + }); + + it('returns an old reference name to the connector_id', () => { + const externalService = createExternalService(); + + const { references } = extractConnectorIdHelper({ + action: 'push-to-service', + actionFields: ['pushed'], + actionDetails: externalService, + fieldType: UserActionFieldType.Old, + })!; + + expect(references).toMatchInlineSnapshot(` + Array [ + Object { + "id": "100", + "name": "oldPushConnectorId", + "type": "action", + }, + ] + `); + }); + }); + }); + + describe('extractConnectorId', () => { + it('returns null when the action details has a circular reference', () => { + const circularRef = { prop: {} }; + circularRef.prop = circularRef; + + const { transformedActionDetails, references } = extractConnectorId({ + action: 'a', + actionFields: ['a'], + actionDetails: circularRef, + fieldType: UserActionFieldType.New, + }); + + expect(transformedActionDetails).toBeNull(); + expect(references).toEqual([]); + }); + + describe('fails to extract the id', () => { + it('returns a null transformed action details when it is initially null', () => { + const { transformedActionDetails, references } = extractConnectorId({ + action: 'a', + actionFields: ['a'], + actionDetails: null, + fieldType: UserActionFieldType.New, + }); + + expect(transformedActionDetails).toBeNull(); + expect(references).toEqual([]); + }); + + it('returns an undefined transformed action details when it is initially undefined', () => { + const { transformedActionDetails, references } = extractConnectorId({ + action: 'a', + actionFields: ['a'], + actionDetails: undefined, + fieldType: UserActionFieldType.New, + }); + + expect(transformedActionDetails).toBeUndefined(); + expect(references).toEqual([]); + }); + + it('returns a json encoded string and empty references when the action is not a valid connector', () => { + const { transformedActionDetails, references } = extractConnectorId({ + action: 'a', + actionFields: ['a'], + actionDetails: { a: 'hello' }, + fieldType: UserActionFieldType.New, + }); + + expect(JSON.parse(transformedActionDetails!)).toEqual({ a: 'hello' }); + expect(references).toEqual([]); + }); + + it('returns a json encoded string and empty references when the action details is an invalid object', () => { + const { transformedActionDetails, references } = extractConnectorId({ + action: 'a', + actionFields: ['a'], + actionDetails: 5 as unknown as Record, + fieldType: UserActionFieldType.New, + }); + + expect(transformedActionDetails!).toEqual('5'); + expect(references).toEqual([]); + }); + }); + + describe('create', () => { + it('extracts the connector.id from a new create jira connector to the references', () => { + const { transformedActionDetails, references } = extractConnectorId({ + action: 'create', + actionFields: ['connector'], + actionDetails: createConnectorObject(), + fieldType: UserActionFieldType.New, + }); + + const parsedJson = JSON.parse(transformedActionDetails!); + + expect(parsedJson).not.toHaveProperty('id'); + expect(parsedJson).toMatchInlineSnapshot(` + Object { + "connector": Object { + "fields": Object { + "issueType": "bug", + "parent": "2", + "priority": "high", + }, + "name": ".jira", + "type": ".jira", + }, + } + `); + expect(references).toMatchInlineSnapshot(` + Array [ + Object { + "id": "1", + "name": "connectorId", + "type": "action", + }, + ] + `); + }); + + it('extracts the connector.id from an old create jira connector to the references', () => { + const { transformedActionDetails, references } = extractConnectorId({ + action: 'create', + actionFields: ['connector'], + actionDetails: createConnectorObject(), + fieldType: UserActionFieldType.Old, + }); + + const parsedJson = JSON.parse(transformedActionDetails!); + + expect(parsedJson).not.toHaveProperty('id'); + expect(parsedJson).toMatchInlineSnapshot(` + Object { + "connector": Object { + "fields": Object { + "issueType": "bug", + "parent": "2", + "priority": "high", + }, + "name": ".jira", + "type": ".jira", + }, + } + `); + expect(references).toMatchInlineSnapshot(` + Array [ + Object { + "id": "1", + "name": "oldConnectorId", + "type": "action", + }, + ] + `); + }); + }); + + describe('update', () => { + it('extracts the connector.id from a new create jira connector to the references', () => { + const { transformedActionDetails, references } = extractConnectorId({ + action: 'update', + actionFields: ['connector'], + actionDetails: createJiraConnector(), + fieldType: UserActionFieldType.New, + }); + + const parsedJson = JSON.parse(transformedActionDetails!); + + expect(parsedJson).not.toHaveProperty('id'); + expect(parsedJson).toMatchInlineSnapshot(` + Object { + "fields": Object { + "issueType": "bug", + "parent": "2", + "priority": "high", + }, + "name": ".jira", + "type": ".jira", + } + `); + expect(references).toMatchInlineSnapshot(` + Array [ + Object { + "id": "1", + "name": "connectorId", + "type": "action", + }, + ] + `); + }); + + it('extracts the connector.id from an old create jira connector to the references', () => { + const { transformedActionDetails, references } = extractConnectorId({ + action: 'update', + actionFields: ['connector'], + actionDetails: createJiraConnector(), + fieldType: UserActionFieldType.Old, + }); + + const parsedJson = JSON.parse(transformedActionDetails!); + + expect(parsedJson).not.toHaveProperty('id'); + expect(parsedJson).toMatchInlineSnapshot(` + Object { + "fields": Object { + "issueType": "bug", + "parent": "2", + "priority": "high", + }, + "name": ".jira", + "type": ".jira", + } + `); + expect(references).toMatchInlineSnapshot(` + Array [ + Object { + "id": "1", + "name": "oldConnectorId", + "type": "action", + }, + ] + `); + }); + }); + + describe('push action', () => { + it('returns the stringified json without the connector_id', () => { + const externalService = createExternalService(); + + const { transformedActionDetails } = extractConnectorId({ + action: 'push-to-service', + actionFields: ['pushed'], + actionDetails: externalService, + fieldType: UserActionFieldType.New, + }); + + const transformedExternalService = JSON.parse(transformedActionDetails!); + expect(transformedExternalService).not.toHaveProperty('connector_id'); + expect(transformedExternalService).toMatchInlineSnapshot(` + Object { + "connector_name": ".jira", + "external_id": "100", + "external_title": "awesome", + "external_url": "http://www.google.com", + "pushed_at": "2019-11-25T21:54:48.952Z", + "pushed_by": Object { + "email": "testemail@elastic.co", + "full_name": "elastic", + "username": "elastic", + }, + } + `); + }); + + it('returns a reference to the connector_id', () => { + const externalService = createExternalService(); + + const { references } = extractConnectorId({ + action: 'push-to-service', + actionFields: ['pushed'], + actionDetails: externalService, + fieldType: UserActionFieldType.New, + })!; + + expect(references).toMatchInlineSnapshot(` + Array [ + Object { + "id": "100", + "name": "pushConnectorId", + "type": "action", + }, + ] + `); + }); + + it('returns a reference to the old action details connector_id', () => { + const externalService = createExternalService(); + + const { references } = extractConnectorId({ + action: 'push-to-service', + actionFields: ['pushed'], + actionDetails: externalService, + fieldType: UserActionFieldType.Old, + })!; + + expect(references).toMatchInlineSnapshot(` + Array [ + Object { + "id": "100", + "name": "oldPushConnectorId", + "type": "action", + }, + ] + `); + }); + }); + }); + + describe('extractConnectorIdFromJson', () => { + describe('fails to extract the id', () => { + it('returns no references and null transformed json when action is undefined', () => { + expect( + extractConnectorIdFromJson({ + actionFields: [], + actionDetails: undefined, + fieldType: UserActionFieldType.New, + }) + ).toEqual({ + transformedActionDetails: undefined, + references: [], + }); + }); + + it('returns no references and undefined transformed json when actionFields is undefined', () => { + expect( + extractConnectorIdFromJson({ action: 'a', fieldType: UserActionFieldType.New }) + ).toEqual({ + transformedActionDetails: undefined, + references: [], + }); + }); + + it('returns no references and undefined transformed json when actionDetails is undefined', () => { + expect( + extractConnectorIdFromJson({ + action: 'a', + actionFields: [], + fieldType: UserActionFieldType.New, + }) + ).toEqual({ + transformedActionDetails: undefined, + references: [], + }); + }); + + it('returns no references and undefined transformed json when actionDetails is null', () => { + expect( + extractConnectorIdFromJson({ + action: 'a', + actionFields: [], + actionDetails: null, + fieldType: UserActionFieldType.New, + }) + ).toEqual({ + transformedActionDetails: null, + references: [], + }); + }); + + it('throws an error when actionDetails is invalid json', () => { + expect(() => + extractConnectorIdFromJson({ + action: 'a', + actionFields: [], + actionDetails: '{a', + fieldType: UserActionFieldType.New, + }) + ).toThrow(); + }); + }); + + describe('create action', () => { + it('returns the stringified json without the id', () => { + const jiraConnector = createConnectorObject(); + + const { transformedActionDetails } = extractConnectorIdFromJson({ + action: 'create', + actionFields: ['connector'], + actionDetails: JSON.stringify(jiraConnector), + fieldType: UserActionFieldType.New, + }); + + expect(JSON.parse(transformedActionDetails!)).toMatchInlineSnapshot(` + Object { + "connector": Object { + "fields": Object { + "issueType": "bug", + "parent": "2", + "priority": "high", + }, + "name": ".jira", + "type": ".jira", + }, + } + `); + }); + + it('returns a reference to the connector.id', () => { + const jiraConnector = createConnectorObject(); + + const { references } = extractConnectorIdFromJson({ + action: 'create', + actionFields: ['connector'], + actionDetails: JSON.stringify(jiraConnector), + fieldType: UserActionFieldType.New, + })!; + + expect(references).toMatchInlineSnapshot(` + Array [ + Object { + "id": "1", + "name": "connectorId", + "type": "action", + }, + ] + `); + }); + }); + + describe('update action', () => { + it('returns the stringified json without the id', () => { + const jiraConnector = createJiraConnector(); + + const { transformedActionDetails } = extractConnectorIdFromJson({ + action: 'update', + actionFields: ['connector'], + actionDetails: JSON.stringify(jiraConnector), + fieldType: UserActionFieldType.New, + }); + + const transformedConnetor = JSON.parse(transformedActionDetails!); + expect(transformedConnetor).not.toHaveProperty('id'); + expect(transformedConnetor).toMatchInlineSnapshot(` + Object { + "fields": Object { + "issueType": "bug", + "parent": "2", + "priority": "high", + }, + "name": ".jira", + "type": ".jira", + } + `); + }); + + it('returns a reference to the connector.id', () => { + const jiraConnector = createJiraConnector(); + + const { references } = extractConnectorIdFromJson({ + action: 'update', + actionFields: ['connector'], + actionDetails: JSON.stringify(jiraConnector), + fieldType: UserActionFieldType.New, + })!; + + expect(references).toMatchInlineSnapshot(` + Array [ + Object { + "id": "1", + "name": "connectorId", + "type": "action", + }, + ] + `); + }); + }); + + describe('push action', () => { + it('returns the stringified json without the connector_id', () => { + const externalService = createExternalService(); + + const { transformedActionDetails } = extractConnectorIdFromJson({ + action: 'push-to-service', + actionFields: ['pushed'], + actionDetails: JSON.stringify(externalService), + fieldType: UserActionFieldType.New, + }); + + const transformedExternalService = JSON.parse(transformedActionDetails!); + expect(transformedExternalService).not.toHaveProperty('connector_id'); + expect(transformedExternalService).toMatchInlineSnapshot(` + Object { + "connector_name": ".jira", + "external_id": "100", + "external_title": "awesome", + "external_url": "http://www.google.com", + "pushed_at": "2019-11-25T21:54:48.952Z", + "pushed_by": Object { + "email": "testemail@elastic.co", + "full_name": "elastic", + "username": "elastic", + }, + } + `); + }); + + it('returns a reference to the connector_id', () => { + const externalService = createExternalService(); + + const { references } = extractConnectorIdFromJson({ + action: 'push-to-service', + actionFields: ['pushed'], + actionDetails: JSON.stringify(externalService), + fieldType: UserActionFieldType.New, + })!; + + expect(references).toMatchInlineSnapshot(` + Array [ + Object { + "id": "100", + "name": "pushConnectorId", + "type": "action", + }, + ] + `); + }); + }); + }); +}); diff --git a/x-pack/plugins/cases/server/services/user_actions/transform.ts b/x-pack/plugins/cases/server/services/user_actions/transform.ts new file mode 100644 index 0000000000000..93595374208a3 --- /dev/null +++ b/x-pack/plugins/cases/server/services/user_actions/transform.ts @@ -0,0 +1,320 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +/* eslint-disable @typescript-eslint/naming-convention */ + +import * as rt from 'io-ts'; +import { isString } from 'lodash'; + +import { SavedObjectReference } from '../../../../../../src/core/server'; +import { + CaseAttributes, + CaseConnector, + CaseConnectorRt, + CaseExternalServiceBasicRt, + isCreateConnector, + isPush, + isUpdateConnector, + noneConnectorId, +} from '../../../common'; +import { + CONNECTOR_ID_REFERENCE_NAME, + getNoneCaseConnector, + PUSH_CONNECTOR_ID_REFERENCE_NAME, + USER_ACTION_OLD_ID_REF_NAME, + USER_ACTION_OLD_PUSH_ID_REF_NAME, +} from '../../common'; +import { ACTION_SAVED_OBJECT_TYPE } from '../../../../actions/server'; +import { UserActionFieldType } from './types'; + +/** + * Extracts the connector id from a json encoded string and formats it as a saved object reference. This will remove + * the field it extracted the connector id from. + */ +export function extractConnectorIdFromJson({ + action, + actionFields, + actionDetails, + fieldType, +}: { + action?: string; + actionFields?: string[]; + actionDetails?: string | null; + fieldType: UserActionFieldType; +}): { transformedActionDetails?: string | null; references: SavedObjectReference[] } { + if (!action || !actionFields || !actionDetails) { + return { transformedActionDetails: actionDetails, references: [] }; + } + + const decodedJson = JSON.parse(actionDetails); + + return extractConnectorIdHelper({ + action, + actionFields, + actionDetails: decodedJson, + fieldType, + }); +} + +/** + * Extracts the connector id from an unencoded object and formats it as a saved object reference. + * This will remove the field it extracted the connector id from. + */ +export function extractConnectorId({ + action, + actionFields, + actionDetails, + fieldType, +}: { + action: string; + actionFields: string[]; + actionDetails?: Record | string | null; + fieldType: UserActionFieldType; +}): { + transformedActionDetails?: string | null; + references: SavedObjectReference[]; +} { + if (!actionDetails || isString(actionDetails)) { + // the action was null, undefined, or a regular string so just return it unmodified and not encoded + return { transformedActionDetails: actionDetails, references: [] }; + } + + try { + return extractConnectorIdHelper({ + action, + actionFields, + actionDetails, + fieldType, + }); + } catch (error) { + return { transformedActionDetails: encodeActionDetails(actionDetails), references: [] }; + } +} + +function encodeActionDetails(actionDetails: Record): string | null { + try { + return JSON.stringify(actionDetails); + } catch (error) { + return null; + } +} + +/** + * Internal helper function for extracting the connector id. This is only exported for usage in unit tests. + * This function handles encoding the transformed fields as a json string + */ +export function extractConnectorIdHelper({ + action, + actionFields, + actionDetails, + fieldType, +}: { + action: string; + actionFields: string[]; + actionDetails: unknown; + fieldType: UserActionFieldType; +}): { transformedActionDetails: string; references: SavedObjectReference[] } { + let transformedActionDetails: unknown = actionDetails; + let referencesToReturn: SavedObjectReference[] = []; + + try { + if (isCreateCaseConnector(action, actionFields, actionDetails)) { + const { transformedActionDetails: transformedConnectorPortion, references } = + transformConnectorFromCreateAndUpdateAction(actionDetails.connector, fieldType); + + // the above call only transforms the connector portion of the action details so let's add back + // the rest of the details and we'll overwrite the connector portion when the transformed one + transformedActionDetails = { + ...actionDetails, + ...transformedConnectorPortion, + }; + referencesToReturn = references; + } else if (isUpdateCaseConnector(action, actionFields, actionDetails)) { + const { + transformedActionDetails: { connector: transformedConnector }, + references, + } = transformConnectorFromCreateAndUpdateAction(actionDetails, fieldType); + + transformedActionDetails = transformedConnector; + referencesToReturn = references; + } else if (isPushConnector(action, actionFields, actionDetails)) { + ({ transformedActionDetails, references: referencesToReturn } = + transformConnectorFromPushAction(actionDetails, fieldType)); + } + } catch (error) { + // ignore any errors, we'll just return whatever was passed in for action details in that case + } + + return { + transformedActionDetails: JSON.stringify(transformedActionDetails), + references: referencesToReturn, + }; +} + +function isCreateCaseConnector( + action: string, + actionFields: string[], + actionDetails: unknown +): actionDetails is { connector: CaseConnector } { + try { + const unsafeCase = actionDetails as CaseAttributes; + + return ( + isCreateConnector(action, actionFields) && + unsafeCase.connector !== undefined && + CaseConnectorRt.is(unsafeCase.connector) + ); + } catch { + return false; + } +} + +export const ConnectorIdReferenceName: Record = { + [UserActionFieldType.New]: CONNECTOR_ID_REFERENCE_NAME, + [UserActionFieldType.Old]: USER_ACTION_OLD_ID_REF_NAME, +}; + +function transformConnectorFromCreateAndUpdateAction( + connector: CaseConnector, + fieldType: UserActionFieldType +): { + transformedActionDetails: { connector: unknown }; + references: SavedObjectReference[]; +} { + const { transformedConnector, references } = transformConnectorIdToReference( + ConnectorIdReferenceName[fieldType], + connector + ); + + return { + transformedActionDetails: transformedConnector, + references, + }; +} + +type ConnectorIdRefNameType = + | typeof CONNECTOR_ID_REFERENCE_NAME + | typeof USER_ACTION_OLD_ID_REF_NAME; + +export const transformConnectorIdToReference = ( + referenceName: ConnectorIdRefNameType, + connector?: { + id?: string; + } +): { + transformedConnector: { connector: unknown }; + references: SavedObjectReference[]; +} => { + const { id: connectorId, ...restConnector } = connector ?? {}; + + const references = createConnectorReference(connectorId, ACTION_SAVED_OBJECT_TYPE, referenceName); + + const { id: ignoreNoneId, ...restNoneConnector } = getNoneCaseConnector(); + const connectorFieldsToReturn = + connector && isConnectorIdValid(connectorId) ? restConnector : restNoneConnector; + + return { + transformedConnector: { + connector: connectorFieldsToReturn, + }, + references, + }; +}; + +const createConnectorReference = ( + id: string | null | undefined, + type: string, + name: string +): SavedObjectReference[] => { + return isConnectorIdValid(id) + ? [ + { + id, + type, + name, + }, + ] + : []; +}; + +const isConnectorIdValid = (id: string | null | undefined): id is string => + id != null && id !== noneConnectorId; + +function isUpdateCaseConnector( + action: string, + actionFields: string[], + actionDetails: unknown +): actionDetails is CaseConnector { + try { + return isUpdateConnector(action, actionFields) && CaseConnectorRt.is(actionDetails); + } catch { + return false; + } +} + +type CaseExternalService = rt.TypeOf; + +function isPushConnector( + action: string, + actionFields: string[], + actionDetails: unknown +): actionDetails is CaseExternalService { + try { + return isPush(action, actionFields) && CaseExternalServiceBasicRt.is(actionDetails); + } catch { + return false; + } +} + +export const PushConnectorIdReferenceName: Record = + { + [UserActionFieldType.New]: PUSH_CONNECTOR_ID_REFERENCE_NAME, + [UserActionFieldType.Old]: USER_ACTION_OLD_PUSH_ID_REF_NAME, + }; + +function transformConnectorFromPushAction( + externalService: CaseExternalService, + fieldType: UserActionFieldType +): { + transformedActionDetails: {} | null; + references: SavedObjectReference[]; +} { + const { transformedPushConnector, references } = transformPushConnectorIdToReference( + PushConnectorIdReferenceName[fieldType], + externalService + ); + + return { + transformedActionDetails: transformedPushConnector.external_service, + references, + }; +} + +type PushConnectorIdRefNameType = + | typeof PUSH_CONNECTOR_ID_REFERENCE_NAME + | typeof USER_ACTION_OLD_PUSH_ID_REF_NAME; + +export const transformPushConnectorIdToReference = ( + referenceName: PushConnectorIdRefNameType, + external_service?: { connector_id?: string | null } | null +): { + transformedPushConnector: { external_service: {} | null }; + references: SavedObjectReference[]; +} => { + const { connector_id: pushConnectorId, ...restExternalService } = external_service ?? {}; + + const references = createConnectorReference( + pushConnectorId, + ACTION_SAVED_OBJECT_TYPE, + referenceName + ); + + return { + transformedPushConnector: { external_service: external_service ? restExternalService : null }, + references, + }; +}; diff --git a/x-pack/plugins/cases/server/services/user_actions/types.ts b/x-pack/plugins/cases/server/services/user_actions/types.ts new file mode 100644 index 0000000000000..3c67535255ecc --- /dev/null +++ b/x-pack/plugins/cases/server/services/user_actions/types.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. + */ + +/** + * Indicates whether which user action field is being parsed, the new_value or the old_value. + */ +export enum UserActionFieldType { + New = 'New', + Old = 'Old', +} diff --git a/x-pack/plugins/cloud/server/config.ts b/x-pack/plugins/cloud/server/config.ts index 4b83071bf473a..2cc413178c3ae 100644 --- a/x-pack/plugins/cloud/server/config.ts +++ b/x-pack/plugins/cloud/server/config.ts @@ -52,5 +52,6 @@ export const config: PluginConfigDescriptor = { organization_url: true, full_story: true, }, + deprecations: ({ deprecate }) => [deprecate('enabled', '8.0.0')], schema: configSchema, }; diff --git a/x-pack/plugins/cross_cluster_replication/server/index.ts b/x-pack/plugins/cross_cluster_replication/server/index.ts index b1803950614d8..a6a3ec0fe5753 100644 --- a/x-pack/plugins/cross_cluster_replication/server/index.ts +++ b/x-pack/plugins/cross_cluster_replication/server/index.ts @@ -17,4 +17,5 @@ export const config: PluginConfigDescriptor = { exposeToBrowser: { ui: true, }, + deprecations: ({ deprecate }) => [deprecate('enabled', '8.0.0')], }; diff --git a/x-pack/plugins/data_visualizer/public/application/common/components/stats_table/components/field_data_expanded_row/choropleth_map.tsx b/x-pack/plugins/data_visualizer/public/application/common/components/stats_table/components/field_data_expanded_row/choropleth_map.tsx index c2e1e0b7dffa9..318ff655abb21 100644 --- a/x-pack/plugins/data_visualizer/public/application/common/components/stats_table/components/field_data_expanded_row/choropleth_map.tsx +++ b/x-pack/plugins/data_visualizer/public/application/common/components/stats_table/components/field_data_expanded_row/choropleth_map.tsx @@ -11,6 +11,7 @@ import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import { FIELD_ORIGIN, + LAYER_TYPE, SOURCE_TYPES, STYLE_TYPE, COLOR_MAP_TYPE, @@ -85,7 +86,7 @@ export const getChoroplethTopValuesLayer = ( }, isTimeAware: true, }, - type: 'VECTOR', + type: LAYER_TYPE.VECTOR, }; }; diff --git a/x-pack/plugins/encrypted_saved_objects/server/index.ts b/x-pack/plugins/encrypted_saved_objects/server/index.ts index 2706da22d108b..b765f1fcaf6fa 100644 --- a/x-pack/plugins/encrypted_saved_objects/server/index.ts +++ b/x-pack/plugins/encrypted_saved_objects/server/index.ts @@ -5,7 +5,7 @@ * 2.0. */ -import type { PluginInitializerContext } from 'src/core/server'; +import type { PluginConfigDescriptor, PluginInitializerContext } from 'src/core/server'; import { ConfigSchema } from './config'; import { EncryptedSavedObjectsPlugin } from './plugin'; @@ -15,6 +15,9 @@ export { EncryptedSavedObjectsPluginSetup, EncryptedSavedObjectsPluginStart } fr export { EncryptedSavedObjectsClient } from './saved_objects'; export type { IsMigrationNeededPredicate } from './create_migration'; -export const config = { schema: ConfigSchema }; +export const config: PluginConfigDescriptor = { + schema: ConfigSchema, + deprecations: ({ deprecate }) => [deprecate('enabled', '8.0.0')], +}; export const plugin = (initializerContext: PluginInitializerContext) => new EncryptedSavedObjectsPlugin(initializerContext); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/components/curations_table.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/components/curations_table.test.tsx new file mode 100644 index 0000000000000..0b647bffb3e26 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/components/curations_table.test.tsx @@ -0,0 +1,143 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { mockKibanaValues, setMockActions, setMockValues } from '../../../../__mocks__/kea_logic'; +import '../../../../__mocks__/react_router'; +import '../../../__mocks__/engine_logic.mock'; + +import React from 'react'; + +import { shallow, ReactWrapper } from 'enzyme'; + +import { EuiBasicTable } from '@elastic/eui'; + +import { mountWithIntl } from '../../../../test_helpers'; + +import { CurationsTable } from './curations_table'; + +describe('CurationsTable', () => { + const { navigateToUrl } = mockKibanaValues; + + const values = { + dataLoading: false, + curations: [ + { + id: 'cur-id-1', + last_updated: 'January 1, 1970 at 12:00PM', + queries: ['hiking'], + }, + { + id: 'cur-id-2', + last_updated: 'January 2, 1970 at 12:00PM', + queries: ['mountains', 'valleys'], + }, + ], + meta: { + page: { + current: 1, + size: 10, + total_results: 2, + }, + }, + }; + + const actions = { + deleteCuration: jest.fn(), + onPaginate: jest.fn(), + }; + + beforeEach(() => { + jest.clearAllMocks(); + setMockValues(values); + setMockActions(actions); + }); + + it('passes loading prop based on dataLoading', () => { + setMockValues({ ...values, dataLoading: true }); + const wrapper = shallow(); + + expect(wrapper.find(EuiBasicTable).prop('loading')).toEqual(true); + }); + + describe('populated table render', () => { + let wrapper: ReactWrapper; + + beforeAll(() => { + wrapper = mountWithIntl(); + }); + + it('renders queries and last updated columns', () => { + const tableContent = wrapper.find(EuiBasicTable).text(); + + expect(tableContent).toContain('Queries'); + expect(tableContent).toContain('hiking'); + expect(tableContent).toContain('mountains, valleys'); + + expect(tableContent).toContain('Last updated'); + expect(tableContent).toContain('Jan 1, 1970 12:00 PM'); + expect(tableContent).toContain('Jan 2, 1970 12:00 PM'); + }); + + it('renders queries with curation links', () => { + expect( + wrapper.find('EuiLinkTo[data-test-subj="CurationsTableQueriesLink"]').first().prop('to') + ).toEqual('/engines/some-engine/curations/cur-id-1'); + + expect( + wrapper.find('EuiLinkTo[data-test-subj="CurationsTableQueriesLink"]').last().prop('to') + ).toEqual('/engines/some-engine/curations/cur-id-2'); + }); + + describe('action column', () => { + it('edit action navigates to curation link', () => { + wrapper.find('[data-test-subj="CurationsTableEditButton"]').first().simulate('click'); + expect(navigateToUrl).toHaveBeenCalledWith('/engines/some-engine/curations/cur-id-1'); + + wrapper.find('[data-test-subj="CurationsTableEditButton"]').last().simulate('click'); + expect(navigateToUrl).toHaveBeenCalledWith('/engines/some-engine/curations/cur-id-2'); + }); + + it('delete action calls deleteCuration', () => { + wrapper.find('[data-test-subj="CurationsTableDeleteButton"]').first().simulate('click'); + expect(actions.deleteCuration).toHaveBeenCalledWith('cur-id-1'); + + wrapper.find('[data-test-subj="CurationsTableDeleteButton"]').last().simulate('click'); + expect(actions.deleteCuration).toHaveBeenCalledWith('cur-id-2'); + }); + }); + }); + + describe('pagination', () => { + it('passes pagination props from meta.page', () => { + setMockValues({ + ...values, + meta: { + page: { + current: 5, + size: 10, + total_results: 50, + }, + }, + }); + const wrapper = shallow(); + + expect(wrapper.find(EuiBasicTable).prop('pagination')).toEqual({ + pageIndex: 4, + pageSize: 10, + totalItemCount: 50, + hidePerPageOptions: true, + }); + }); + + it('calls onPaginate on pagination change', () => { + const wrapper = shallow(); + wrapper.find(EuiBasicTable).simulate('change', { page: { index: 0 } }); + + expect(actions.onPaginate).toHaveBeenCalledWith(1); + }); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/components/curations_table.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/components/curations_table.tsx new file mode 100644 index 0000000000000..ad508bea1dbc7 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/components/curations_table.tsx @@ -0,0 +1,117 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; + +import { useValues, useActions } from 'kea'; + +import { EuiBasicTable, EuiBasicTableColumn } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; + +import { EDIT_BUTTON_LABEL, DELETE_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 { ENGINE_CURATION_PATH } from '../../../routes'; +import { FormattedDateTime } from '../../../utils/formatted_date_time'; +import { generateEnginePath } from '../../engine'; + +import { CurationsLogic } from '../curations_logic'; +import { Curation } from '../types'; +import { convertToDate } from '../utils'; + +export const CurationsTable: React.FC = () => { + const { dataLoading, curations, meta } = useValues(CurationsLogic); + const { onPaginate, deleteCuration } = useActions(CurationsLogic); + + const columns: Array> = [ + { + field: 'queries', + name: i18n.translate( + 'xpack.enterpriseSearch.appSearch.engine.curations.table.column.queries', + { defaultMessage: 'Queries' } + ), + render: (queries: Curation['queries'], curation: Curation) => ( + + {queries.join(', ')} + + ), + width: '40%', + truncateText: true, + mobileOptions: { + header: true, + // Note: the below props are valid props per https://elastic.github.io/eui/#/tabular-content/tables (Responsive tables), but EUI's types have a bug reporting it as an error + // @ts-ignore + enlarge: true, + width: '100%', + truncateText: false, + }, + }, + { + field: 'last_updated', + name: i18n.translate( + 'xpack.enterpriseSearch.appSearch.engine.curations.table.column.lastUpdated', + { defaultMessage: 'Last updated' } + ), + width: '30%', + dataType: 'string', + render: (dateString: string) => , + }, + { + width: '120px', + actions: [ + { + name: EDIT_BUTTON_LABEL, + description: i18n.translate( + 'xpack.enterpriseSearch.appSearch.engine.curations.table.editTooltip', + { defaultMessage: 'Edit curation' } + ), + type: 'icon', + icon: 'pencil', + color: 'primary', + onClick: (curation: Curation) => { + const { navigateToUrl } = KibanaLogic.values; + const url = generateEnginePath(ENGINE_CURATION_PATH, { curationId: curation.id }); + navigateToUrl(url); + }, + 'data-test-subj': 'CurationsTableEditButton', + }, + { + name: DELETE_BUTTON_LABEL, + description: i18n.translate( + 'xpack.enterpriseSearch.appSearch.engine.curations.table.deleteTooltip', + { defaultMessage: 'Delete curation' } + ), + type: 'icon', + icon: 'trash', + color: 'danger', + onClick: (curation: Curation) => deleteCuration(curation.id), + 'data-test-subj': 'CurationsTableDeleteButton', + }, + ], + }, + ]; + + return ( + + ); +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/components/index.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/components/index.ts index 8c9e58e6ba0f4..473162dcbd91e 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/components/index.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/components/index.ts @@ -5,4 +5,5 @@ * 2.0. */ +export { CurationsTable } from './curations_table'; export { EmptyState } from './empty_state'; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curations_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curations_logic.test.ts index f4296d4ff2089..0d02fbe413870 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curations_logic.test.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curations_logic.test.ts @@ -50,6 +50,7 @@ describe('CurationsLogic', () => { dataLoading: true, curations: [], meta: DEFAULT_META, + selectedPageTab: 'overview', }; beforeEach(() => { @@ -89,6 +90,19 @@ describe('CurationsLogic', () => { }); }); }); + + describe('onSelectPageTab', () => { + it('should set the selected page tab', () => { + mount(); + + CurationsLogic.actions.onSelectPageTab('settings'); + + expect(CurationsLogic.values).toEqual({ + ...DEFAULT_VALUES, + selectedPageTab: 'settings', + }); + }); + }); }); describe('listeners', () => { diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curations_logic.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curations_logic.ts index a98f83396b3b8..76adfa4b6ed4d 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curations_logic.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curations_logic.ts @@ -23,10 +23,13 @@ import { EngineLogic, generateEnginePath } from '../engine'; import { DELETE_MESSAGE, SUCCESS_MESSAGE } from './constants'; import { Curation, CurationsAPIResponse } from './types'; +type CurationsPageTabs = 'overview' | 'settings'; + interface CurationsValues { dataLoading: boolean; curations: Curation[]; meta: Meta; + selectedPageTab: CurationsPageTabs; } interface CurationsActions { @@ -35,6 +38,7 @@ interface CurationsActions { loadCurations(): void; deleteCuration(id: string): string; createCuration(queries: Curation['queries']): Curation['queries']; + onSelectPageTab(pageTab: CurationsPageTabs): { pageTab: CurationsPageTabs }; } export const CurationsLogic = kea>({ @@ -45,8 +49,15 @@ export const CurationsLogic = kea id, createCuration: (queries) => queries, + onSelectPageTab: (pageTab) => ({ pageTab }), }), reducers: () => ({ + selectedPageTab: [ + 'overview', + { + onSelectPageTab: (_, { pageTab }) => pageTab, + }, + ], dataLoading: [ true, { diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/views/curations.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/views/curations.test.tsx index 85827d5374179..effd03e2e7437 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/views/curations.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/views/curations.test.tsx @@ -5,23 +5,23 @@ * 2.0. */ -import { mockKibanaValues, setMockActions, setMockValues } from '../../../../__mocks__/kea_logic'; +import { setMockActions, setMockValues } from '../../../../__mocks__/kea_logic'; import '../../../../__mocks__/react_router'; import '../../../__mocks__/engine_logic.mock'; import React from 'react'; -import { shallow, ReactWrapper } from 'enzyme'; +import { shallow } from 'enzyme'; -import { EuiBasicTable } from '@elastic/eui'; +import { EuiTab } from '@elastic/eui'; -import { mountWithIntl, getPageTitle } from '../../../../test_helpers'; +import { mountWithIntl, getPageHeaderTabs, getPageTitle } from '../../../../test_helpers'; -import { Curations, CurationsTable } from './curations'; +import { Curations } from './curations'; +import { CurationsOverview } from './curations_overview'; +import { CurationsSettings } from './curations_settings'; describe('Curations', () => { - const { navigateToUrl } = mockKibanaValues; - const values = { dataLoading: false, curations: [ @@ -43,12 +43,13 @@ describe('Curations', () => { total_results: 2, }, }, + selectedPageTab: 'overview', }; const actions = { loadCurations: jest.fn(), - deleteCuration: jest.fn(), onPaginate: jest.fn(), + onSelectPageTab: jest.fn(), }; beforeEach(() => { @@ -57,11 +58,38 @@ describe('Curations', () => { setMockActions(actions); }); - it('renders', () => { + it('renders with a set of tabs in the page header', () => { const wrapper = shallow(); expect(getPageTitle(wrapper)).toEqual('Curated results'); - expect(wrapper.find(CurationsTable)).toHaveLength(1); + + const tabs = getPageHeaderTabs(wrapper).find(EuiTab); + + tabs.at(0).simulate('click'); + expect(actions.onSelectPageTab).toHaveBeenNthCalledWith(1, 'overview'); + + tabs.at(1).simulate('click'); + expect(actions.onSelectPageTab).toHaveBeenNthCalledWith(2, 'settings'); + }); + + it('renders an overview view', () => { + setMockValues({ ...values, selectedPageTab: 'overview' }); + const wrapper = shallow(); + const tabs = getPageHeaderTabs(wrapper).find(EuiTab); + + expect(tabs.at(0).prop('isSelected')).toEqual(true); + + expect(wrapper.find(CurationsOverview)).toHaveLength(1); + }); + + it('renders a settings view', () => { + setMockValues({ ...values, selectedPageTab: 'settings' }); + const wrapper = shallow(); + const tabs = getPageHeaderTabs(wrapper).find(EuiTab); + + expect(tabs.at(1).prop('isSelected')).toEqual(true); + + expect(wrapper.find(CurationsSettings)).toHaveLength(1); }); describe('loading state', () => { @@ -86,91 +114,4 @@ describe('Curations', () => { expect(actions.loadCurations).toHaveBeenCalledTimes(1); }); - - describe('CurationsTable', () => { - it('passes loading prop based on dataLoading', () => { - setMockValues({ ...values, dataLoading: true }); - const wrapper = shallow(); - - expect(wrapper.find(EuiBasicTable).prop('loading')).toEqual(true); - }); - - describe('populated table render', () => { - let wrapper: ReactWrapper; - - beforeAll(() => { - wrapper = mountWithIntl(); - }); - - it('renders queries and last updated columns', () => { - const tableContent = wrapper.find(EuiBasicTable).text(); - - expect(tableContent).toContain('Queries'); - expect(tableContent).toContain('hiking'); - expect(tableContent).toContain('mountains, valleys'); - - expect(tableContent).toContain('Last updated'); - expect(tableContent).toContain('Jan 1, 1970 12:00 PM'); - expect(tableContent).toContain('Jan 2, 1970 12:00 PM'); - }); - - it('renders queries with curation links', () => { - expect( - wrapper.find('EuiLinkTo[data-test-subj="CurationsTableQueriesLink"]').first().prop('to') - ).toEqual('/engines/some-engine/curations/cur-id-1'); - - expect( - wrapper.find('EuiLinkTo[data-test-subj="CurationsTableQueriesLink"]').last().prop('to') - ).toEqual('/engines/some-engine/curations/cur-id-2'); - }); - - describe('action column', () => { - it('edit action navigates to curation link', () => { - wrapper.find('[data-test-subj="CurationsTableEditButton"]').first().simulate('click'); - expect(navigateToUrl).toHaveBeenCalledWith('/engines/some-engine/curations/cur-id-1'); - - wrapper.find('[data-test-subj="CurationsTableEditButton"]').last().simulate('click'); - expect(navigateToUrl).toHaveBeenCalledWith('/engines/some-engine/curations/cur-id-2'); - }); - - it('delete action calls deleteCuration', () => { - wrapper.find('[data-test-subj="CurationsTableDeleteButton"]').first().simulate('click'); - expect(actions.deleteCuration).toHaveBeenCalledWith('cur-id-1'); - - wrapper.find('[data-test-subj="CurationsTableDeleteButton"]').last().simulate('click'); - expect(actions.deleteCuration).toHaveBeenCalledWith('cur-id-2'); - }); - }); - }); - - describe('pagination', () => { - it('passes pagination props from meta.page', () => { - setMockValues({ - ...values, - meta: { - page: { - current: 5, - size: 10, - total_results: 50, - }, - }, - }); - const wrapper = shallow(); - - expect(wrapper.find(EuiBasicTable).prop('pagination')).toEqual({ - pageIndex: 4, - pageSize: 10, - totalItemCount: 50, - hidePerPageOptions: true, - }); - }); - - it('calls onPaginate on pagination change', () => { - const wrapper = shallow(); - wrapper.find(EuiBasicTable).simulate('change', { page: { index: 0 } }); - - expect(actions.onPaginate).toHaveBeenCalledWith(1); - }); - }); - }); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/views/curations.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/views/curations.tsx index 12497ab52baf6..9584b21424fe3 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/views/curations.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/views/curations.tsx @@ -9,28 +9,47 @@ import React, { useEffect } from 'react'; import { useValues, useActions } from 'kea'; -import { EuiBasicTable, EuiBasicTableColumn, EuiPanel } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { EDIT_BUTTON_LABEL, DELETE_BUTTON_LABEL } from '../../../../shared/constants'; -import { KibanaLogic } from '../../../../shared/kibana'; -import { EuiButtonTo, EuiLinkTo } from '../../../../shared/react_router_helpers'; -import { convertMetaToPagination, handlePageChange } from '../../../../shared/table_pagination'; +import { EuiButtonTo } from '../../../../shared/react_router_helpers'; -import { ENGINE_CURATIONS_NEW_PATH, ENGINE_CURATION_PATH } from '../../../routes'; -import { FormattedDateTime } from '../../../utils/formatted_date_time'; +import { ENGINE_CURATIONS_NEW_PATH } from '../../../routes'; import { generateEnginePath } from '../../engine'; import { AppSearchPageTemplate } from '../../layout'; -import { EmptyState } from '../components'; import { CURATIONS_OVERVIEW_TITLE, CREATE_NEW_CURATION_TITLE } from '../constants'; import { CurationsLogic } from '../curations_logic'; -import { Curation } from '../types'; -import { getCurationsBreadcrumbs, convertToDate } from '../utils'; +import { getCurationsBreadcrumbs } from '../utils'; + +import { CurationsOverview } from './curations_overview'; +import { CurationsSettings } from './curations_settings'; export const Curations: React.FC = () => { - const { dataLoading, curations, meta } = useValues(CurationsLogic); - const { loadCurations } = useActions(CurationsLogic); + const { dataLoading, curations, meta, selectedPageTab } = useValues(CurationsLogic); + const { loadCurations, onSelectPageTab } = useActions(CurationsLogic); + + const pageTabs = [ + { + label: i18n.translate( + 'xpack.enterpriseSearch.appSearch.engine.curations.overviewPageTabLabel', + { + defaultMessage: 'Overview', + } + ), + isSelected: selectedPageTab === 'overview', + onClick: () => onSelectPageTab('overview'), + }, + { + label: i18n.translate( + 'xpack.enterpriseSearch.appSearch.engine.curations.settingsPageTabLabel', + { + defaultMessage: 'Settings', + } + ), + isSelected: selectedPageTab === 'settings', + onClick: () => onSelectPageTab('settings'), + }, + ]; useEffect(() => { loadCurations(); @@ -50,105 +69,12 @@ export const Curations: React.FC = () => { {CREATE_NEW_CURATION_TITLE} , ], + tabs: pageTabs, }} isLoading={dataLoading && !curations.length} - isEmptyState={!curations.length} - emptyState={} > - - - + {selectedPageTab === 'overview' && } + {selectedPageTab === 'settings' && } ); }; - -export const CurationsTable: React.FC = () => { - const { dataLoading, curations, meta } = useValues(CurationsLogic); - const { onPaginate, deleteCuration } = useActions(CurationsLogic); - - const columns: Array> = [ - { - field: 'queries', - name: i18n.translate( - 'xpack.enterpriseSearch.appSearch.engine.curations.table.column.queries', - { defaultMessage: 'Queries' } - ), - render: (queries: Curation['queries'], curation: Curation) => ( - - {queries.join(', ')} - - ), - width: '40%', - truncateText: true, - mobileOptions: { - header: true, - // Note: the below props are valid props per https://elastic.github.io/eui/#/tabular-content/tables (Responsive tables), but EUI's types have a bug reporting it as an error - // @ts-ignore - enlarge: true, - width: '100%', - truncateText: false, - }, - }, - { - field: 'last_updated', - name: i18n.translate( - 'xpack.enterpriseSearch.appSearch.engine.curations.table.column.lastUpdated', - { defaultMessage: 'Last updated' } - ), - width: '30%', - dataType: 'string', - render: (dateString: string) => , - }, - { - width: '120px', - actions: [ - { - name: EDIT_BUTTON_LABEL, - description: i18n.translate( - 'xpack.enterpriseSearch.appSearch.engine.curations.table.editTooltip', - { defaultMessage: 'Edit curation' } - ), - type: 'icon', - icon: 'pencil', - color: 'primary', - onClick: (curation: Curation) => { - const { navigateToUrl } = KibanaLogic.values; - const url = generateEnginePath(ENGINE_CURATION_PATH, { curationId: curation.id }); - navigateToUrl(url); - }, - 'data-test-subj': 'CurationsTableEditButton', - }, - { - name: DELETE_BUTTON_LABEL, - description: i18n.translate( - 'xpack.enterpriseSearch.appSearch.engine.curations.table.deleteTooltip', - { defaultMessage: 'Delete curation' } - ), - type: 'icon', - icon: 'trash', - color: 'danger', - onClick: (curation: Curation) => deleteCuration(curation.id), - 'data-test-subj': 'CurationsTableDeleteButton', - }, - ], - }, - ]; - - return ( - - ); -}; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/views/curations_overview.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/views/curations_overview.test.tsx new file mode 100644 index 0000000000000..a034c3b2a0689 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/views/curations_overview.test.tsx @@ -0,0 +1,47 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { setMockValues } from '../../../../__mocks__/kea_logic'; +import '../../../../__mocks__/react_router'; +import '../../../__mocks__/engine_logic.mock'; + +import React from 'react'; + +import { shallow } from 'enzyme'; + +import { CurationsTable, EmptyState } from '../components'; + +import { CurationsOverview } from './curations_overview'; + +describe('CurationsOverview', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('renders an empty message when there are no curations', () => { + setMockValues({ curations: [] }); + const wrapper = shallow(); + + expect(wrapper.is(EmptyState)).toBe(true); + }); + + it('renders a curations table when there are curations present', () => { + setMockValues({ + curations: [ + { + id: 'cur-id-1', + }, + { + id: 'cur-id-2', + }, + ], + }); + const wrapper = shallow(); + + expect(wrapper.find(CurationsTable)).toHaveLength(1); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/views/curations_overview.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/views/curations_overview.tsx new file mode 100644 index 0000000000000..ec67d06da4769 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/views/curations_overview.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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; + +import { useValues } from 'kea'; + +import { EuiPanel } from '@elastic/eui'; + +import { CurationsTable, EmptyState } from '../components'; +import { CurationsLogic } from '../curations_logic'; + +export const CurationsOverview: React.FC = () => { + const { curations } = useValues(CurationsLogic); + + return curations.length ? ( + + + + ) : ( + + ); +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/views/curations_settings.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/views/curations_settings.test.tsx new file mode 100644 index 0000000000000..855570829cce4 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/views/curations_settings.test.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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import '../../../../__mocks__/react_router'; +import '../../../__mocks__/engine_logic.mock'; + +import React from 'react'; + +import { shallow } from 'enzyme'; + +import { CurationsSettings } from './curations_settings'; + +describe('CurationsSettings', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('renders empty', () => { + const wrapper = shallow(); + + expect(wrapper.isEmptyRender()).toBe(true); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/views/curations_settings.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/views/curations_settings.tsx new file mode 100644 index 0000000000000..4bff7f3b2ef5e --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/views/curations_settings.tsx @@ -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. + */ + +import React from 'react'; + +export const CurationsSettings: React.FC = () => { + return null; +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/result_settings_logic.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/result_settings_logic.ts index 49a09b8a7a3f5..5ff153c3beb64 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/result_settings_logic.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/result_settings_logic.ts @@ -321,26 +321,25 @@ export const ResultSettingsLogic = kea {
  • diff --git a/x-pack/plugins/enterprise_search/public/applications/test_helpers/get_page_header.tsx b/x-pack/plugins/enterprise_search/public/applications/test_helpers/get_page_header.tsx index a251188b5cd90..94440ad4e61a7 100644 --- a/x-pack/plugins/enterprise_search/public/applications/test_helpers/get_page_header.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/test_helpers/get_page_header.tsx @@ -9,7 +9,7 @@ import React, { Fragment } from 'react'; import { shallow, ShallowWrapper } from 'enzyme'; -import { EuiPageHeaderProps } from '@elastic/eui'; +import { EuiPageHeaderProps, EuiTab } from '@elastic/eui'; /* * Given an AppSearchPageTemplate or WorkplaceSearchPageTemplate, these @@ -35,13 +35,30 @@ export const getPageHeaderActions = (wrapper: ShallowWrapper) => { return shallow(
    - {actions.map((action: React.ReactNode, i) => ( + {actions.map((action, i) => ( {action} ))}
    ); }; +export const getPageHeaderTabs = (wrapper: ShallowWrapper) => { + // The tabs prop of EuiPageHeader takes an `Array` + // instead of an array of EuiTab jsx components + // These are then rendered inside of EuiPageHeader as EuiTabs + // See https://elastic.github.io/eui/#/layout/page-header#tabs-in-the-page-header + + const tabs = getPageHeader(wrapper).tabs || []; + + return shallow( +
    + {tabs.map((tabProps, i) => ( + + ))} +
    + ); +}; + export const getPageHeaderChildren = (wrapper: ShallowWrapper) => { const children = getPageHeader(wrapper).children || null; 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 7903b4a31c8a9..35836d5526615 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 @@ -16,6 +16,7 @@ export { getPageDescription, getPageHeaderActions, getPageHeaderChildren, + getPageHeaderTabs, } from './get_page_header'; // Misc diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/__mocks__/content_sources.mock.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/__mocks__/content_sources.mock.ts index 5f515fc99769c..28e796c256396 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/__mocks__/content_sources.mock.ts +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/__mocks__/content_sources.mock.ts @@ -25,6 +25,7 @@ export const contentSources = [ allowsReauth: true, boost: 1, activities: [], + isOauth1: false, }, { id: '124', @@ -40,6 +41,7 @@ export const contentSources = [ allowsReauth: true, boost: 0.5, activities: [], + isOauth1: true, }, ]; @@ -303,6 +305,7 @@ export const sourceConfigData = { privateSourcesEnabled: false, categories: ['wiki', 'atlassian', 'intranet'], configuredFields: { + isOauth1: false, clientId: 'CyztADsSECRETCSAUCEh1a', clientSecret: 'GSjJxqSECRETCSAUCEksHk', baseUrl: 'https://mine.atlassian.net', diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/source_config_fields/source_config_fields.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/source_config_fields/source_config_fields.test.tsx index 9af91107d7304..9aa0286b2bef0 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/source_config_fields/source_config_fields.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/source_config_fields/source_config_fields.test.tsx @@ -25,6 +25,7 @@ describe('SourceConfigFields', () => { it('renders with all items, hiding API Keys', () => { const wrapper = shallow( { it('shows API keys', () => { const wrapper = shallow( - + ); expect(wrapper.find(ApiKey)).toHaveLength(2); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/source_config_fields/source_config_fields.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/source_config_fields/source_config_fields.tsx index 236d475b8f687..e33e7817b5209 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/source_config_fields/source_config_fields.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/source_config_fields/source_config_fields.tsx @@ -20,6 +20,7 @@ import { ApiKey } from '../api_key'; import { CredentialItem } from '../credential_item'; interface SourceConfigFieldsProps { + isOauth1?: boolean; clientId?: string; clientSecret?: string; publicKey?: string; @@ -28,14 +29,13 @@ interface SourceConfigFieldsProps { } export const SourceConfigFields: React.FC = ({ + isOauth1, clientId, clientSecret, publicKey, consumerKey, baseUrl, }) => { - const showApiKey = (publicKey || consumerKey) && !clientId; - const credentialItem = (label: string, item?: string) => item && ; @@ -58,10 +58,10 @@ export const SourceConfigFields: React.FC = ({ return ( <> - {showApiKey && keyElement} - {credentialItem(CLIENT_ID_LABEL, clientId)} + {isOauth1 && keyElement} + {!isOauth1 && credentialItem(CLIENT_ID_LABEL, clientId)} - {credentialItem(CLIENT_SECRET_LABEL, clientSecret)} + {!isOauth1 && credentialItem(CLIENT_SECRET_LABEL, clientSecret)} {credentialItem(BASE_URL_LABEL, baseUrl)} diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/types.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/types.ts index d8fcb414cff75..c524bd4f7617a 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/types.ts +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/types.ts @@ -108,6 +108,7 @@ export interface ContentSourceDetails extends ContentSource { allowsReauth: boolean; boost: number; activities: SourceActivity[]; + isOauth1: boolean; } interface DescriptionList { diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/source_settings.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/source_settings.tsx index ea12dabb86001..36a6374dddcd6 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/source_settings.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/source_settings.tsx @@ -44,7 +44,6 @@ import { SOURCE_SETTINGS_DESCRIPTION, SOURCE_NAME_LABEL, SOURCE_CONFIG_TITLE, - SOURCE_CONFIG_DESCRIPTION, SOURCE_CONFIG_LINK, SOURCE_REMOVE_TITLE, SOURCE_REMOVE_DESCRIPTION, @@ -78,6 +77,7 @@ export const SourceSettings: React.FC = () => { custom: isCustom, isIndexedSource, areThumbnailsConfigEnabled, + isOauth1, indexing: { enabled, features: { @@ -99,10 +99,9 @@ export const SourceSettings: React.FC = () => { getSourceConfigData(serviceType); }, []); - const { - configuration: { isPublicKey }, - editPath, - } = staticSourceData.find((source) => source.serviceType === serviceType) as SourceDataItem; + const { editPath } = staticSourceData.find( + (source) => source.serviceType === serviceType + ) as SourceDataItem; const [inputValue, setValue] = useState(name); const [confirmModalVisible, setModalVisibility] = useState(false); @@ -206,12 +205,13 @@ export const SourceSettings: React.FC = () => { {showConfig && ( - + diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/constants.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/constants.ts index 001f06261de4b..b90d6abe38702 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/constants.ts +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/constants.ts @@ -306,13 +306,6 @@ export const SOURCE_CONFIG_TITLE = i18n.translate( } ); -export const SOURCE_CONFIG_DESCRIPTION = i18n.translate( - 'xpack.enterpriseSearch.workplaceSearch.sources.config.description', - { - defaultMessage: 'Edit content source connector settings to change.', - } -); - export const SYNC_MANAGEMENT_TITLE = i18n.translate( 'xpack.enterpriseSearch.workplaceSearch.contentSources.syncManagementTitle', { @@ -358,7 +351,7 @@ export const SYNC_MANAGEMENT_CONTENT_EXTRACTION_LABEL = i18n.translate( export const SOURCE_CONFIG_LINK = i18n.translate( 'xpack.enterpriseSearch.workplaceSearch.sources.config.link', { - defaultMessage: 'Edit content source connector settings', + defaultMessage: 'Edit connector settings', } ); diff --git a/x-pack/plugins/enterprise_search/server/index.ts b/x-pack/plugins/enterprise_search/server/index.ts index ecd068c8bdbd9..dae584a883bd7 100644 --- a/x-pack/plugins/enterprise_search/server/index.ts +++ b/x-pack/plugins/enterprise_search/server/index.ts @@ -37,4 +37,5 @@ export const config: PluginConfigDescriptor = { exposeToBrowser: { host: true, }, + deprecations: ({ deprecate }) => [deprecate('enabled', '8.0.0')], }; diff --git a/x-pack/plugins/fleet/common/constants/output.ts b/x-pack/plugins/fleet/common/constants/output.ts index 80c7e56dbb52f..9a236001aca25 100644 --- a/x-pack/plugins/fleet/common/constants/output.ts +++ b/x-pack/plugins/fleet/common/constants/output.ts @@ -13,8 +13,10 @@ export const outputType = { Elasticsearch: 'elasticsearch', } as const; +export const DEFAULT_OUTPUT_ID = 'default'; + export const DEFAULT_OUTPUT: NewOutput = { - name: 'default', + name: DEFAULT_OUTPUT_ID, is_default: true, type: outputType.Elasticsearch, hosts: [''], diff --git a/x-pack/plugins/fleet/common/constants/package_policy.ts b/x-pack/plugins/fleet/common/constants/package_policy.ts index f1d6f00d05773..42bbe5bb5b79a 100644 --- a/x-pack/plugins/fleet/common/constants/package_policy.ts +++ b/x-pack/plugins/fleet/common/constants/package_policy.ts @@ -6,3 +6,5 @@ */ export const PACKAGE_POLICY_SAVED_OBJECT_TYPE = 'ingest-package-policies'; + +export const PACKAGE_POLICY_DEFAULT_INDEX_PRIVILEGES = ['auto_configure', 'create_doc']; diff --git a/x-pack/plugins/fleet/common/services/package_policies_to_agent_inputs.ts b/x-pack/plugins/fleet/common/services/package_policies_to_agent_inputs.ts index f262521461b98..119bb04af5ca8 100644 --- a/x-pack/plugins/fleet/common/services/package_policies_to_agent_inputs.ts +++ b/x-pack/plugins/fleet/common/services/package_policies_to_agent_inputs.ts @@ -11,7 +11,8 @@ import type { PackagePolicy, FullAgentPolicyInput, FullAgentPolicyInputStream } import { DEFAULT_OUTPUT } from '../constants'; export const storedPackagePoliciesToAgentInputs = ( - packagePolicies: PackagePolicy[] + packagePolicies: PackagePolicy[], + outputId: string = DEFAULT_OUTPUT.name ): FullAgentPolicyInput[] => { const fullInputs: FullAgentPolicyInput[] = []; @@ -32,7 +33,7 @@ export const storedPackagePoliciesToAgentInputs = ( data_stream: { namespace: packagePolicy.namespace || 'default', }, - use_output: DEFAULT_OUTPUT.name, + use_output: outputId, ...(input.compiled_input || {}), ...(input.streams.length ? { diff --git a/x-pack/plugins/fleet/common/types/index.ts b/x-pack/plugins/fleet/common/types/index.ts index 0deda3bf32657..bd970fc2cd83e 100644 --- a/x-pack/plugins/fleet/common/types/index.ts +++ b/x-pack/plugins/fleet/common/types/index.ts @@ -8,7 +8,11 @@ export * from './models'; export * from './rest_spec'; -import type { PreconfiguredAgentPolicy, PreconfiguredPackage } from './models/preconfiguration'; +import type { + PreconfiguredAgentPolicy, + PreconfiguredPackage, + PreconfiguredOutput, +} from './models/preconfiguration'; export interface FleetConfigType { enabled: boolean; @@ -26,6 +30,7 @@ export interface FleetConfigType { }; agentPolicies?: PreconfiguredAgentPolicy[]; packages?: PreconfiguredPackage[]; + outputs?: PreconfiguredOutput[]; agentIdVerificationEnabled?: boolean; } diff --git a/x-pack/plugins/fleet/common/types/models/agent_policy.ts b/x-pack/plugins/fleet/common/types/models/agent_policy.ts index f64467ca674fb..3f9e43e72c51d 100644 --- a/x-pack/plugins/fleet/common/types/models/agent_policy.ts +++ b/x-pack/plugins/fleet/common/types/models/agent_policy.ts @@ -23,6 +23,8 @@ export interface NewAgentPolicy { monitoring_enabled?: MonitoringType; unenroll_timeout?: number; is_preconfigured?: boolean; + data_output_id?: string; + monitoring_output_id?: string; } export interface AgentPolicy extends NewAgentPolicy { @@ -71,12 +73,14 @@ export interface FullAgentPolicyOutputPermissions { }; } +export type FullAgentPolicyOutput = Pick & { + [key: string]: any; +}; + export interface FullAgentPolicy { id: string; outputs: { - [key: string]: Pick & { - [key: string]: any; - }; + [key: string]: FullAgentPolicyOutput; }; output_permissions?: { [output: string]: FullAgentPolicyOutputPermissions; diff --git a/x-pack/plugins/fleet/common/types/models/epm.ts b/x-pack/plugins/fleet/common/types/models/epm.ts index bbb571f963dc9..66852bc965b07 100644 --- a/x-pack/plugins/fleet/common/types/models/epm.ts +++ b/x-pack/plugins/fleet/common/types/models/epm.ts @@ -281,7 +281,6 @@ export enum RegistryDataStreamKeys { ingest_pipeline = 'ingest_pipeline', elasticsearch = 'elasticsearch', dataset_is_prefix = 'dataset_is_prefix', - permissions = 'permissions', } export interface RegistryDataStream { @@ -297,15 +296,15 @@ export interface RegistryDataStream { [RegistryDataStreamKeys.ingest_pipeline]?: string; [RegistryDataStreamKeys.elasticsearch]?: RegistryElasticsearch; [RegistryDataStreamKeys.dataset_is_prefix]?: boolean; - [RegistryDataStreamKeys.permissions]?: RegistryDataStreamPermissions; } export interface RegistryElasticsearch { + privileges?: RegistryDataStreamPrivileges; 'index_template.settings'?: estypes.IndicesIndexSettings; 'index_template.mappings'?: estypes.MappingTypeMapping; } -export interface RegistryDataStreamPermissions { +export interface RegistryDataStreamPrivileges { cluster?: string[]; indices?: string[]; } diff --git a/x-pack/plugins/fleet/common/types/models/output.ts b/x-pack/plugins/fleet/common/types/models/output.ts index c1dc2a4b4e058..4f70460e89ff8 100644 --- a/x-pack/plugins/fleet/common/types/models/output.ts +++ b/x-pack/plugins/fleet/common/types/models/output.ts @@ -17,11 +17,13 @@ export interface NewOutput { hosts?: string[]; ca_sha256?: string; api_key?: string; - config?: Record; config_yaml?: string; + is_preconfigured?: boolean; } -export type OutputSOAttributes = NewOutput; +export type OutputSOAttributes = NewOutput & { + output_id?: string; +}; export type Output = NewOutput & { id: string; diff --git a/x-pack/plugins/fleet/common/types/models/package_policy.ts b/x-pack/plugins/fleet/common/types/models/package_policy.ts index ab977c5d67d0d..aca537ae31b52 100644 --- a/x-pack/plugins/fleet/common/types/models/package_policy.ts +++ b/x-pack/plugins/fleet/common/types/models/package_policy.ts @@ -25,6 +25,11 @@ export interface NewPackagePolicyInputStream { data_stream: { dataset: string; type: string; + elasticsearch?: { + privileges?: { + indices?: string[]; + }; + }; }; vars?: PackagePolicyConfigRecord; config?: PackagePolicyConfigRecord; diff --git a/x-pack/plugins/fleet/common/types/models/preconfiguration.ts b/x-pack/plugins/fleet/common/types/models/preconfiguration.ts index 6087c910510cc..17f9b946885b1 100644 --- a/x-pack/plugins/fleet/common/types/models/preconfiguration.ts +++ b/x-pack/plugins/fleet/common/types/models/preconfiguration.ts @@ -11,6 +11,7 @@ import type { NewPackagePolicyInput, } from './package_policy'; import type { NewAgentPolicy } from './agent_policy'; +import type { Output } from './output'; export type InputsOverride = Partial & { vars?: Array; @@ -29,3 +30,7 @@ export interface PreconfiguredAgentPolicy extends Omit; + +export interface PreconfiguredOutput extends Omit { + config?: Record; +} diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/details_page/components/package_policies/package_policies_table.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/details_page/components/package_policies/package_policies_table.tsx index d3a6bb7561d39..9f125533f36c2 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/details_page/components/package_policies/package_policies_table.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/details_page/components/package_policies/package_policies_table.tsx @@ -102,6 +102,7 @@ export const PackagePoliciesTable: React.FunctionComponent = ({ { field: 'name', sortable: true, + truncateText: true, name: i18n.translate('xpack.fleet.policyDetails.packagePoliciesTable.nameColumnTitle', { defaultMessage: 'Name', }), diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/edit_package_policy_page/index.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/edit_package_policy_page/index.tsx index d887076568a68..35092cb67f7ef 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/edit_package_policy_page/index.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/edit_package_policy_page/index.tsx @@ -726,7 +726,7 @@ const UpgradeStatusCallout: React.FunctionComponent<{ > 0) setDatasetValues(values.sort()); } catch (e) { setDatasetValues([AGENT_DATASET]); } diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_details_page/components/agent_logs/filter_log_level.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_details_page/components/agent_logs/filter_log_level.tsx index b423f3a8a57b3..805d2fab45240 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_details_page/components/agent_logs/filter_log_level.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_details_page/components/agent_logs/filter_log_level.tsx @@ -6,7 +6,7 @@ */ import React, { memo, useState, useEffect, useCallback } from 'react'; -import { EuiPopover, EuiFilterButton, EuiFilterSelectItem } from '@elastic/eui'; +import { EuiPopover, EuiFilterButton, EuiFilterSelectItem, EuiIcon, EuiSpacer } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { useStartServices } from '../../../../../hooks'; @@ -57,6 +57,29 @@ export const LogLevelFilter: React.FunctionComponent<{ fetchValues(); }, [data.autocomplete]); + const noLogsFound = ( +
    +
    + + +

    + {i18n.translate('xpack.fleet.agentLogs.logLevelEmpty', { + defaultMessage: 'No Logs Found', + })} +

    +
    +
    + ); + const filterSelect = levelValues.map((level) => ( + onToggleLevel(level)} + > + {level} + + )); + return ( - {levelValues.map((level) => ( - onToggleLevel(level)} - > - {level} - - ))} + {levelValues.length === 0 ? noLogsFound : filterSelect} ); }); diff --git a/x-pack/plugins/fleet/public/applications/integrations/hooks/use_local_search.tsx b/x-pack/plugins/fleet/public/applications/integrations/hooks/use_local_search.tsx index fc2966697418a..06c4bac5c7e8e 100644 --- a/x-pack/plugins/fleet/public/applications/integrations/hooks/use_local_search.tsx +++ b/x-pack/plugins/fleet/public/applications/integrations/hooks/use_local_search.tsx @@ -11,7 +11,7 @@ import { useEffect, useRef } from 'react'; import type { PackageList } from '../../../types'; export const searchIdField = 'id'; -export const fieldsToSearch = ['name', 'title']; +export const fieldsToSearch = ['name', 'title', 'description']; export function useLocalSearch(packageList: PackageList) { const localSearchRef = useRef(null); diff --git a/x-pack/plugins/fleet/public/applications/integrations/hooks/use_package_install.tsx b/x-pack/plugins/fleet/public/applications/integrations/hooks/use_package_install.tsx index edbe06f33b18e..a59d4422348fb 100644 --- a/x-pack/plugins/fleet/public/applications/integrations/hooks/use_package_install.tsx +++ b/x-pack/plugins/fleet/public/applications/integrations/hooks/use_package_install.tsx @@ -96,6 +96,7 @@ function usePackageInstall({ notifications }: { notifications: NotificationsStar }); history.push(settingsPath); } + notifications.toasts.addSuccess({ title: toMountPoint( { + return ( +
    + +
    + ); +}; + +AssetsFacetGroup.args = args; diff --git a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/components/package_card.stories.tsx b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/components/package_card.stories.tsx new file mode 100644 index 0000000000000..e8814b8b8c877 --- /dev/null +++ b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/components/package_card.stories.tsx @@ -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 React from 'react'; + +import type { SavedObject } from 'src/core/public'; + +import type { Installation } from '../../../../../../common'; + +import type { PackageCardProps } from './package_card'; +import { PackageCard } from './package_card'; + +export default { + title: 'Sections/EPM/Package Card', + description: 'A card representing a package available in Fleet', +}; + +type Args = Omit & { width: number }; + +const args: Args = { + width: 250, + title: 'Title', + description: 'Description', + name: 'beats', + release: 'ga', + id: 'id', + version: '1.0.0', + download: '/', + path: 'path', +}; + +const argTypes = { + release: { + control: { + type: 'radio', + options: ['ga', 'beta', 'experimental'], + }, + }, +}; + +export const NotInstalled = ({ width, ...props }: Args) => ( +
    + +
    +); + +export const Installed = ({ width, ...props }: Args) => { + const savedObject: SavedObject = { + id: props.id, + type: props.type || '', + attributes: { + name: props.name, + version: props.version, + install_version: props.version, + es_index_patterns: {}, + installed_kibana: [], + installed_es: [], + install_status: 'installed', + install_source: 'registry', + install_started_at: '2020-01-01T00:00:00.000Z', + }, + references: [], + }; + + return ( +
    + +
    + ); +}; + +NotInstalled.args = args; +NotInstalled.argTypes = argTypes; +Installed.args = args; +Installed.argTypes = argTypes; diff --git a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/components/package_card.tsx b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/components/package_card.tsx index c12e67fdb5718..c2d6d0f1e028b 100644 --- a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/components/package_card.tsx +++ b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/components/package_card.tsx @@ -15,7 +15,7 @@ import { PackageIcon } from '../../../components'; import { RELEASE_BADGE_LABEL, RELEASE_BADGE_DESCRIPTION } from './release_badge'; -type PackageCardProps = PackageListItem; +export type PackageCardProps = PackageListItem; // adding the `href` causes EuiCard to use a `a` instead of a `button` // `a` tags use `euiLinkColor` which results in blueish Badge text diff --git a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/components/package_list_grid.stories.tsx b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/components/package_list_grid.stories.tsx new file mode 100644 index 0000000000000..d84e286b6f560 --- /dev/null +++ b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/components/package_list_grid.stories.tsx @@ -0,0 +1,138 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; + +import { action } from '@storybook/addon-actions'; + +import type { SavedObject } from 'src/core/public'; + +import type { Installation } from '../../../../../../common'; + +import type { ListProps } from './package_list_grid'; +import { PackageListGrid } from './package_list_grid'; + +export default { + component: PackageListGrid, + title: 'Sections/EPM/Package List Grid', +}; + +type Args = Pick; + +const args: Args = { + title: 'Installed integrations', + isLoading: false, + showMissingIntegrationMessage: false, +}; + +const savedObject: SavedObject = { + id: 'id', + type: 'integration', + attributes: { + name: 'savedObject', + version: '1.2.3', + install_version: '1.2.3', + es_index_patterns: {}, + installed_kibana: [], + installed_es: [], + install_status: 'installed', + install_source: 'registry', + install_started_at: '2020-01-01T00:00:00.000Z', + }, + references: [], +}; + +export const EmptyList = (props: Args) => ( + +); + +export const List = (props: Args) => ( + +); + +EmptyList.args = args; +List.args = args; 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 6bbd479c5c2ba..db63c5c7dd832 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 @@ -28,7 +28,7 @@ import { useLocalSearch, searchIdField } from '../../../hooks'; import { PackageCard } from './package_card'; -interface ListProps { +export interface ListProps { isLoading?: boolean; controls?: ReactNode; title: string; diff --git a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/components/requirements.stories.tsx b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/components/requirements.stories.tsx new file mode 100644 index 0000000000000..205d739d48696 --- /dev/null +++ b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/components/requirements.stories.tsx @@ -0,0 +1,39 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; + +import { Requirements as Component } from './requirements'; + +export default { + component: Component, + title: 'Sections/EPM/Requirements', +}; + +interface Args { + width: number; +} + +const args: Args = { + width: 250, +}; + +export const Requirements = ({ width }: Args) => { + return ( +
    + +
    + ); +}; + +Requirements.args = args; diff --git a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/index.test.tsx b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/index.test.tsx index 31a3e2164a247..d70b6c68016be 100644 --- a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/index.test.tsx +++ b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/index.test.tsx @@ -241,38 +241,6 @@ describe('when on integration detail', () => { 'http://localhost/mock/app/integrations/edit-integration/e8a37031-2907-44f6-89d2-98bd493f60dc' ); }); - - it('should NOT show link for agent count if it is zero', async () => { - await mockedApi.waitForApi(); - const firstRowAgentCount = renderResult.getAllByTestId('rowAgentCount')[0]; - expect(firstRowAgentCount.textContent).toEqual('0'); - expect(firstRowAgentCount.tagName).not.toEqual('A'); - }); - - it('should show add agent button if agent count is zero', async () => { - await mockedApi.waitForApi(); - const firstRowAgentCount = renderResult.getAllByTestId('rowAgentCount')[0]; - expect(firstRowAgentCount.textContent).toEqual('0'); - - const addAgentButton = renderResult.getAllByTestId('addAgentButton')[0]; - expect(addAgentButton).not.toBeNull(); - }); - - it('should show link for agent count if greater than zero', async () => { - await mockedApi.waitForApi(); - const secondRowAgentCount = renderResult.getAllByTestId('rowAgentCount')[1]; - expect(secondRowAgentCount.textContent).toEqual('100'); - expect(secondRowAgentCount.tagName).toEqual('A'); - }); - - it('should NOT show add agent button if agent count is greater than zero', async () => { - await mockedApi.waitForApi(); - const secondRowAgentCount = renderResult.getAllByTestId('rowAgentCount')[1]; - expect(secondRowAgentCount.textContent).toEqual('100'); - - const addAgentButton = renderResult.getAllByTestId('addAgentButton')[1]; - expect(addAgentButton).toBeUndefined(); - }); }); }); 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 fadcbdd70c28d..82436eb4d3f51 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 @@ -305,7 +305,7 @@ export function Detail() { {packageInfo.version} {updateAvailable ? ( - + ) : null} diff --git a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/policies/components/package_policy_agents_cell.test.tsx b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/policies/components/package_policy_agents_cell.test.tsx new file mode 100644 index 0000000000000..8872c61299093 --- /dev/null +++ b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/policies/components/package_policy_agents_cell.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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; + +import { act } from '@testing-library/react'; + +import { createIntegrationsTestRendererMock } from '../../../../../../../../mock'; + +import { PackagePolicyAgentsCell } from './package_policy_agents_cell'; + +function renderCell({ agentCount = 0, agentPolicyId = '123', onAddAgent = () => {} }) { + const renderer = createIntegrationsTestRendererMock(); + + return renderer.render( + + ); +} + +describe('PackagePolicyAgentsCell', () => { + test('it should display add agent if count is 0', async () => { + const utils = renderCell({ agentCount: 0 }); + await act(async () => { + expect(utils.queryByText('Add agent')).toBeInTheDocument(); + }); + }); + + test('it should display only count if count > 0', async () => { + const utils = renderCell({ agentCount: 9999 }); + await act(async () => { + expect(utils.queryByText('Add agent')).not.toBeInTheDocument(); + expect(utils.queryByText('9999')).toBeInTheDocument(); + }); + }); +}); diff --git a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/policies/components/package_policy_agents_cell.tsx b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/policies/components/package_policy_agents_cell.tsx new file mode 100644 index 0000000000000..37543e7e5ae1b --- /dev/null +++ b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/policies/components/package_policy_agents_cell.tsx @@ -0,0 +1,42 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; + +import { EuiButton } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; + +import { LinkedAgentCount } from '../../../../../../components'; + +export const PackagePolicyAgentsCell = ({ + agentPolicyId, + agentCount = 0, + onAddAgent, +}: { + agentPolicyId: string; + agentCount?: number; + onAddAgent: () => void; +}) => { + if (agentCount > 0) { + return ( + + ); + } + + return ( + + + + ); +}; diff --git a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/policies/package_policies.tsx b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/policies/package_policies.tsx index 92b4012011fc8..42eb68099970a 100644 --- a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/policies/package_policies.tsx +++ b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/policies/package_policies.tsx @@ -13,19 +13,16 @@ import type { EuiTableFieldDataColumnType, } from '@elastic/eui'; import { - EuiButtonIcon, EuiBasicTable, EuiLink, EuiFlexGroup, EuiFlexItem, - EuiToolTip, EuiText, EuiButton, EuiSpacer, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { FormattedRelative, FormattedMessage } from '@kbn/i18n/react'; -import styled from 'styled-components'; import { InstallStatus } from '../../../../../types'; import type { GetAgentPoliciesResponseItem, InMemoryPackagePolicy } from '../../../../../types'; @@ -41,10 +38,10 @@ import { PACKAGE_POLICY_SAVED_OBJECT_TYPE } from '../../../../../constants'; import { AgentEnrollmentFlyout, AgentPolicySummaryLine, - LinkedAgentCount, PackagePolicyActionsMenu, } from '../../../../../components'; +import { PackagePolicyAgentsCell } from './components/package_policy_agents_cell'; import { usePackagePoliciesWithAgentPolicy } from './use_package_policies_with_agent_policy'; import { Persona } from './persona'; @@ -58,10 +55,6 @@ interface InMemoryPackagePolicyAndAgentPolicy { agentPolicy: GetAgentPoliciesResponseItem; } -const AddAgentButton = styled(EuiButtonIcon)` - margin-left: ${(props) => props.theme.eui.euiSizeS}; -`; - const IntegrationDetailsLink = memo<{ packagePolicy: InMemoryPackagePolicyAndAgentPolicy['packagePolicy']; }>(({ packagePolicy }) => { @@ -266,51 +259,6 @@ export const PackagePoliciesPage = ({ name, version }: PackagePoliciesPanelProps return ; }, }, - { - field: '', - name: i18n.translate('xpack.fleet.epm.packageDetails.integrationList.agentCount', { - defaultMessage: 'Agents', - }), - truncateText: true, - align: 'left', - width: '8ch', - render({ packagePolicy, agentPolicy }: InMemoryPackagePolicyAndAgentPolicy) { - const count = agentPolicy?.agents ?? 0; - - return ( - <> - - {count === 0 && ( - - setFlyoutOpenForPolicyId(agentPolicy.id)} - data-test-subj="addAgentButton" - aria-label={i18n.translate( - 'xpack.fleet.epm.packageDetails.integrationList.addAgent', - { - defaultMessage: 'Add Agent', - } - )} - /> - - )} - - ); - }, - }, { field: 'packagePolicy.updated_by', name: i18n.translate('xpack.fleet.epm.packageDetails.integrationList.updatedBy', { @@ -335,6 +283,21 @@ export const PackagePoliciesPage = ({ name, version }: PackagePoliciesPanelProps ); }, }, + { + field: '', + name: i18n.translate('xpack.fleet.epm.packageDetails.integrationList.agentCount', { + defaultMessage: 'Agents', + }), + render({ agentPolicy }: InMemoryPackagePolicyAndAgentPolicy) { + return ( + setFlyoutOpenForPolicyId(agentPolicy.id)} + /> + ); + }, + }, { field: '', name: i18n.translate('xpack.fleet.epm.packageDetails.integrationList.actions', { diff --git a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/settings/install_button.tsx b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/settings/install_button.tsx new file mode 100644 index 0000000000000..f2813058afe5a --- /dev/null +++ b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/settings/install_button.tsx @@ -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 { EuiButton } from '@elastic/eui'; +import React, { Fragment, useCallback, useState } from 'react'; +import { FormattedMessage } from '@kbn/i18n/react'; + +import type { PackageInfo, UpgradePackagePolicyDryRunResponse } from '../../../../../types'; +import { InstallStatus } from '../../../../../types'; +import { + useCapabilities, + useGetPackageInstallStatus, + useInstallPackage, +} from '../../../../../hooks'; + +import { ConfirmPackageInstall } from './confirm_package_install'; + +type InstallationButtonProps = Pick & { + disabled?: boolean; + dryRunData?: UpgradePackagePolicyDryRunResponse | null; + isUpgradingPackagePolicies?: boolean; + latestVersion?: string; + numOfAssets: number; + packagePolicyIds?: string[]; + setIsUpgradingPackagePolicies?: React.Dispatch>; +}; +export function InstallButton(props: InstallationButtonProps) { + const { name, numOfAssets, title, version } = props; + const hasWriteCapabilites = useCapabilities().write; + const installPackage = useInstallPackage(); + const getPackageInstallStatus = useGetPackageInstallStatus(); + const { status: installationStatus } = getPackageInstallStatus(name); + + const isInstalling = installationStatus === InstallStatus.installing; + const [isInstallModalVisible, setIsInstallModalVisible] = useState(false); + + const toggleInstallModal = useCallback(() => { + setIsInstallModalVisible(!isInstallModalVisible); + }, [isInstallModalVisible]); + + const handleClickInstall = useCallback(() => { + installPackage({ name, version, title }); + toggleInstallModal(); + }, [installPackage, name, title, toggleInstallModal, version]); + + const installModal = ( + + ); + + return hasWriteCapabilites ? ( + + + {isInstalling ? ( + + ) : ( + + )} + + + {isInstallModalVisible && installModal} + + ) : null; +} diff --git a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/settings/installation_button.tsx b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/settings/installation_button.tsx deleted file mode 100644 index eab28a051f061..0000000000000 --- a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/settings/installation_button.tsx +++ /dev/null @@ -1,161 +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 } from '@elastic/eui'; -import React, { Fragment, useCallback, useMemo, useState } from 'react'; -import { FormattedMessage } from '@kbn/i18n/react'; - -import type { PackageInfo } from '../../../../../types'; -import { InstallStatus } from '../../../../../types'; -import { - useCapabilities, - useUninstallPackage, - useGetPackageInstallStatus, - useInstallPackage, -} from '../../../../../hooks'; - -import { ConfirmPackageUninstall } from './confirm_package_uninstall'; -import { ConfirmPackageInstall } from './confirm_package_install'; - -type InstallationButtonProps = Pick & { - disabled?: boolean; - isUpdate?: boolean; - latestVersion?: string; -}; -export function InstallationButton(props: InstallationButtonProps) { - const { assets, name, title, version, disabled = true, isUpdate = false, latestVersion } = props; - const hasWriteCapabilites = useCapabilities().write; - const installPackage = useInstallPackage(); - const uninstallPackage = useUninstallPackage(); - const getPackageInstallStatus = useGetPackageInstallStatus(); - const { status: installationStatus } = getPackageInstallStatus(name); - - const isInstalling = installationStatus === InstallStatus.installing; - const isRemoving = installationStatus === InstallStatus.uninstalling; - const isInstalled = installationStatus === InstallStatus.installed; - const showUninstallButton = isInstalled || isRemoving; - const [isModalVisible, setModalVisible] = useState(false); - const toggleModal = useCallback(() => { - setModalVisible(!isModalVisible); - }, [isModalVisible]); - - const handleClickInstall = useCallback(() => { - installPackage({ name, version, title }); - toggleModal(); - }, [installPackage, name, title, toggleModal, version]); - - const handleClickUpdate = useCallback(() => { - installPackage({ name, version, title, fromUpdate: true }); - }, [installPackage, name, title, version]); - - const handleClickUninstall = useCallback(() => { - uninstallPackage({ name, version, title, redirectToVersion: latestVersion ?? version }); - toggleModal(); - }, [uninstallPackage, name, title, toggleModal, version, latestVersion]); - - // counts the number of assets in the package - const numOfAssets = useMemo( - () => - Object.entries(assets).reduce( - (acc, [serviceName, serviceNameValue]) => - acc + - Object.entries(serviceNameValue).reduce( - (acc2, [assetName, assetNameValue]) => acc2 + assetNameValue.length, - 0 - ), - 0 - ), - [assets] - ); - - const installButton = ( - - {isInstalling ? ( - - ) : ( - - )} - - ); - - const updateButton = ( - - - - ); - - const uninstallButton = ( - - {isRemoving ? ( - - ) : ( - - )} - - ); - - const uninstallModal = ( - - ); - - const installModal = ( - - ); - - return hasWriteCapabilites ? ( - - {isUpdate ? updateButton : showUninstallButton ? uninstallButton : installButton} - {isModalVisible && (isInstalled ? uninstallModal : installModal)} - - ) : null; -} diff --git a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/settings/settings.tsx b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/settings/settings.tsx index 98cc172197d44..07c95e0d77ec7 100644 --- a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/settings/settings.tsx +++ b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/settings/settings.tsx @@ -5,30 +5,42 @@ * 2.0. */ -import React, { memo } from 'react'; +import React, { memo, useEffect, useMemo, useState } from 'react'; import styled from 'styled-components'; import { FormattedMessage } from '@kbn/i18n/react'; import semverLt from 'semver/functions/lt'; -import { EuiTitle, EuiFlexGroup, EuiFlexItem, EuiText, EuiSpacer, EuiLink } from '@elastic/eui'; +import { + EuiCallOut, + EuiTitle, + EuiFlexGroup, + EuiFlexItem, + EuiText, + EuiSpacer, + EuiLink, +} from '@elastic/eui'; -import type { PackageInfo } from '../../../../../types'; +import { i18n } from '@kbn/i18n'; + +import type { PackageInfo, UpgradePackagePolicyDryRunResponse } from '../../../../../types'; import { InstallStatus } from '../../../../../types'; -import { useGetPackagePolicies, useGetPackageInstallStatus, useLink } from '../../../../../hooks'; +import { + useGetPackagePolicies, + useGetPackageInstallStatus, + useLink, + sendUpgradePackagePolicyDryRun, +} from '../../../../../hooks'; import { PACKAGE_POLICY_SAVED_OBJECT_TYPE } from '../../../../../constants'; -import { UpdateIcon } from '../components'; -import { InstallationButton } from './installation_button'; +import { InstallButton } from './install_button'; +import { UpdateButton } from './update_button'; +import { UninstallButton } from './uninstall_button'; const SettingsTitleCell = styled.td` padding-right: ${(props) => props.theme.eui.spacerSizes.xl}; padding-bottom: ${(props) => props.theme.eui.spacerSizes.m}; `; -const UpdatesAvailableMsgContainer = styled.span` - padding-left: ${(props) => props.theme.eui.spacerSizes.s}; -`; - const NoteLabel = () => ( ( /> ); -const UpdatesAvailableMsg = () => ( - - +const UpdatesAvailableMsg = ({ latestVersion }: { latestVersion: string }) => ( + - + ); const LatestVersionLink = ({ name, version }: { name: string; version: string }) => { @@ -68,14 +86,35 @@ interface Props { export const SettingsPage: React.FC = memo(({ packageInfo }: Props) => { const { name, title, removable, latestVersion, version } = packageInfo; + const [dryRunData, setDryRunData] = useState(); + const [isUpgradingPackagePolicies, setIsUpgradingPackagePolicies] = useState(false); const getPackageInstallStatus = useGetPackageInstallStatus(); const { data: packagePoliciesData } = useGetPackagePolicies({ - perPage: 0, + perPage: 1000, page: 1, kuery: `${PACKAGE_POLICY_SAVED_OBJECT_TYPE}.package.name:${name}`, }); + const { status: installationStatus, version: installedVersion } = getPackageInstallStatus(name); const packageHasUsages = !!packagePoliciesData?.total; + + const packagePolicyIds = useMemo( + () => packagePoliciesData?.items.map(({ id }) => id), + [packagePoliciesData] + ); + + useEffect(() => { + const fetchDryRunData = async () => { + if (packagePolicyIds && packagePolicyIds.length) { + const { data } = await sendUpgradePackagePolicyDryRun(packagePolicyIds, latestVersion); + + setDryRunData(data); + } + }; + + fetchDryRunData(); + }, [latestVersion, packagePolicyIds]); + const updateAvailable = installedVersion && semverLt(installedVersion, latestVersion) ? true : false; @@ -88,6 +127,20 @@ export const SettingsPage: React.FC = memo(({ packageInfo }: Props) => { const isUpdating = installationStatus === InstallStatus.installing && installedVersion; + const numOfAssets = useMemo( + () => + Object.entries(packageInfo.assets).reduce( + (acc, [serviceName, serviceNameValue]) => + acc + + Object.entries(serviceNameValue).reduce( + (acc2, [assetName, assetNameValue]) => acc2 + assetNameValue.length, + 0 + ), + 0 + ), + [packageInfo.assets] + ); + return ( @@ -129,7 +182,6 @@ export const SettingsPage: React.FC = memo(({ packageInfo }: Props) => { {installedVersion} - {updateAvailable && } @@ -147,15 +199,21 @@ export const SettingsPage: React.FC = memo(({ packageInfo }: Props) => { - {updateAvailable && ( -

    - -

    + {(updateAvailable || isUpgradingPackagePolicies) && ( + <> + + +

    + +

    + )}
    )} @@ -189,8 +247,9 @@ export const SettingsPage: React.FC = memo(({ packageInfo }: Props) => {

    -

    @@ -220,8 +279,9 @@ export const SettingsPage: React.FC = memo(({ packageInfo }: Props) => {

    - diff --git a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/settings/uninstall_button.tsx b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/settings/uninstall_button.tsx new file mode 100644 index 0000000000000..00b6ccc2f7912 --- /dev/null +++ b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/settings/uninstall_button.tsx @@ -0,0 +1,92 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { EuiButton } from '@elastic/eui'; +import React, { useCallback, useState } from 'react'; +import { FormattedMessage } from '@kbn/i18n/react'; + +import { InstallStatus } from '../../../../../types'; +import type { PackageInfo } from '../../../../../types'; + +import { + useCapabilities, + useGetPackageInstallStatus, + useUninstallPackage, +} from '../../../../../hooks'; + +import { ConfirmPackageUninstall } from './confirm_package_uninstall'; + +interface UninstallButtonProps extends Pick { + disabled?: boolean; + latestVersion?: string; + numOfAssets: number; +} + +export const UninstallButton: React.FunctionComponent = ({ + disabled = false, + latestVersion, + name, + numOfAssets, + title, + version, +}) => { + const hasWriteCapabilites = useCapabilities().write; + const uninstallPackage = useUninstallPackage(); + const getPackageInstallStatus = useGetPackageInstallStatus(); + const { status: installationStatus } = getPackageInstallStatus(name); + const isRemoving = installationStatus === InstallStatus.uninstalling; + + const [isUninstallModalVisible, setIsUninstallModalVisible] = useState(false); + + const handleClickUninstall = useCallback(() => { + uninstallPackage({ name, version, title, redirectToVersion: latestVersion ?? version }); + setIsUninstallModalVisible(false); + }, [uninstallPackage, name, title, version, latestVersion]); + + const uninstallModal = ( + setIsUninstallModalVisible(false)} + onConfirm={handleClickUninstall} + /> + ); + + return hasWriteCapabilites ? ( + <> + setIsUninstallModalVisible(true)} + color="danger" + disabled={disabled || isRemoving ? true : false} + > + {isRemoving ? ( + + ) : ( + + )} + + {isUninstallModalVisible && uninstallModal} + + ) : null; +}; diff --git a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/settings/update_button.tsx b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/settings/update_button.tsx new file mode 100644 index 0000000000000..8cdb3ece30621 --- /dev/null +++ b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/settings/update_button.tsx @@ -0,0 +1,310 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { useCallback, useEffect, useMemo, useState } from 'react'; +import { useHistory } from 'react-router-dom'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { + EuiFlexGroup, + EuiFlexItem, + EuiButton, + EuiCheckbox, + EuiCallOut, + EuiConfirmModal, + EuiSpacer, +} from '@elastic/eui'; +import { sumBy } from 'lodash'; + +import type { + GetAgentPoliciesResponse, + PackageInfo, + UpgradePackagePolicyDryRunResponse, +} from '../../../../../types'; +import { InstallStatus } from '../../../../../types'; +import { AGENT_POLICY_SAVED_OBJECT_TYPE } from '../../../../../constants'; +import { + sendGetAgentPolicies, + useInstallPackage, + useGetPackageInstallStatus, + sendUpgradePackagePolicy, + useStartServices, + useCapabilities, + useLink, +} from '../../../../../hooks'; +import { toMountPoint } from '../../../../../../../../../../../src/plugins/kibana_react/public'; + +interface UpdateButtonProps extends Pick { + dryRunData?: UpgradePackagePolicyDryRunResponse | null; + packagePolicyIds?: string[]; + isUpgradingPackagePolicies?: boolean; + setIsUpgradingPackagePolicies?: React.Dispatch>; +} + +/* + + Updating an integration to a new version entails a bit of logic. We allow the user to choose whether they'd like to + simultaneously upgrade any package policies that include the current version of the integration. For example, if + a user is running four agent policies that include the `nginx-0.2.4` package and they update to `nginx-0.7.0`, they + can elect to also deploy the new integration version to any agent running one of those four agent policies. + + If the user does not elect to upgrade their running policies, we simply install the latest version of the package and + navigate to the new version's settings page, e.g. `/detail/nginx-0.7.0/settings`. + + If the user _does_ elect to upgrade their running policies, we display a confirmation modal. In this modal, we'll report the + number of agents and policies that will be affected by the upgrade, and if there are any conflicts. In the case of a conflict + between versions, an upgrade for a given package policy will be skipped and the user will need to manually recreate their policy + to resolve any breaking changes between versions. Once the user confirms, we first install the latest version of the integration, + then we make a call to the "upgrade policies" API endpoint with a list of all package policy ID's that include the current version + of the integration. This API endpoint will complete the upgrade process in bulk for each package policy provided. Upon completion, + we navigate to the new version's settings page, as above. + +*/ + +export const UpdateButton: React.FunctionComponent = ({ + dryRunData, + isUpgradingPackagePolicies = false, + name, + packagePolicyIds = [], + setIsUpgradingPackagePolicies = () => {}, + title, + version, +}) => { + const history = useHistory(); + const { getPath } = useLink(); + + const { notifications } = useStartServices(); + const hasWriteCapabilites = useCapabilities().write; + + const installPackage = useInstallPackage(); + const getPackageInstallStatus = useGetPackageInstallStatus(); + const { status: installationStatus } = getPackageInstallStatus(name); + const isInstalling = installationStatus === InstallStatus.installing; + + const [isUpdateModalVisible, setIsUpdateModalVisible] = useState(false); + const [upgradePackagePolicies, setUpgradePackagePolicies] = useState(true); + const [agentPolicyData, setAgentPolicyData] = useState(); + + useEffect(() => { + const fetchAgentPolicyData = async () => { + if (packagePolicyIds && packagePolicyIds.length > 0) { + const { data } = await sendGetAgentPolicies({ + perPage: 1000, + page: 1, + // Fetch all agent policies that include one of the eligible package policies + kuery: `${AGENT_POLICY_SAVED_OBJECT_TYPE}.package_policies:${packagePolicyIds + .map((id) => `"${id}"`) + .join(' or ')}`, + }); + + setAgentPolicyData(data); + } + }; + + fetchAgentPolicyData(); + }, [packagePolicyIds]); + + const packagePolicyCount = useMemo(() => packagePolicyIds.length, [packagePolicyIds]); + const agentCount = useMemo( + () => sumBy(agentPolicyData?.items, ({ agents }) => agents ?? 0), + [agentPolicyData] + ); + const conflictCount = useMemo( + () => dryRunData?.filter((item) => item.hasErrors).length, + [dryRunData] + ); + + const handleUpgradePackagePoliciesChange = useCallback(() => { + setUpgradePackagePolicies((prev) => !prev); + }, []); + + const navigateToNewSettingsPage = useCallback(() => { + const settingsPath = getPath('integration_details_settings', { + pkgkey: `${name}-${version}`, + }); + history.push(settingsPath); + }, [history, getPath, name, version]); + + const handleClickUpdate = useCallback(async () => { + await installPackage({ name, version, title, fromUpdate: true }); + }, [installPackage, name, title, version]); + + const handleClickUpgradePolicies = useCallback(async () => { + if (isUpgradingPackagePolicies) { + return; + } + + setIsUpgradingPackagePolicies(true); + + await installPackage({ name, version, title }); + + await sendUpgradePackagePolicy( + // Only upgrade policies that don't have conflicts + packagePolicyIds.filter( + (id) => !dryRunData?.find((dryRunRecord) => dryRunRecord.diff?.[0].id === id)?.hasErrors + ) + ); + + setIsUpgradingPackagePolicies(false); + setIsUpdateModalVisible(false); + + notifications.toasts.addSuccess({ + title: toMountPoint( + + ), + text: toMountPoint( + + ), + }); + + navigateToNewSettingsPage(); + }, [ + dryRunData, + installPackage, + isUpgradingPackagePolicies, + name, + navigateToNewSettingsPage, + notifications.toasts, + packagePolicyIds, + setIsUpgradingPackagePolicies, + title, + version, + ]); + + const updateModal = ( + { + setIsUpdateModalVisible(false); + }} + cancelButtonText={i18n.translate( + 'xpack.fleet.integrations.settings.confirmUpdateModal.cancel', + { defaultMessage: 'Cancel' } + )} + onConfirm={handleClickUpgradePolicies} + confirmButtonText={i18n.translate( + 'xpack.fleet.integrations.settings.confirmUpdateModal.confirm', + { defaultMessage: 'Upgrade {packageName} and policies', values: { packageName: title } } + )} + title={i18n.translate('xpack.fleet.integrations.settings.confirmUpdateModal.updateTitle', { + defaultMessage: 'Upgrade {packageName} and policies', + values: { packageName: title }, + })} + > + <> + {conflictCount && conflictCount > 0 ? ( + <> + + + + {' '} + + + + + + ) : null} + + + + ), + agentCountText: ( + + + + ), + }} + /> + + + ); + + return hasWriteCapabilites ? ( + <> + + + setIsUpdateModalVisible(true) : handleClickUpdate + } + > + + + + {packagePolicyCount > 0 && ( + + + + )} + + + {isUpdateModalVisible && updateModal} + + ) : null; +}; diff --git a/x-pack/plugins/fleet/public/components/agent_policy_package_badges.tsx b/x-pack/plugins/fleet/public/components/agent_policy_package_badges.tsx index 02518945cf7a5..9db4f77a02c37 100644 --- a/x-pack/plugins/fleet/public/components/agent_policy_package_badges.tsx +++ b/x-pack/plugins/fleet/public/components/agent_policy_package_badges.tsx @@ -84,33 +84,42 @@ export const AgentPolicyPackageBadges: React.FunctionComponent = ({ )} - {packages.map((pkg, idx) => { - return ( - - - - - // this collides with some EuiText (+img) CSS from the EuiIcon component - // which makes the button large, wide, and poorly layed out - // override those styles until the bug is fixed or we find a better approach - { margin: 'unset', width: '16px' } - } - /> - - {pkg.title} - - - ); - })} + + {packages.map((pkg, idx) => { + return ( + + + + + + // this collides with some EuiText (+img) CSS from the EuiIcon component + // which makes the button large, wide, and poorly layed out + // override those styles until the bug is fixed or we find a better approach + { margin: 'unset', width: '16px' } + } + /> + + {pkg.title} + + + + ); + })} + {showFleetServerWarning && ( <> diff --git a/x-pack/plugins/fleet/public/components/package_policy_actions_menu.tsx b/x-pack/plugins/fleet/public/components/package_policy_actions_menu.tsx index eefa3c870f283..ee7d5f97fcbac 100644 --- a/x-pack/plugins/fleet/public/components/package_policy_actions_menu.tsx +++ b/x-pack/plugins/fleet/public/components/package_policy_actions_menu.tsx @@ -69,7 +69,7 @@ export const PackagePolicyActionsMenu: React.FunctionComponent<{ > , ] @@ -117,7 +117,10 @@ export const PackagePolicyActionsMenu: React.FunctionComponent<{ disabled={!hasWriteCapabilities} icon="trash" onClick={() => { - deletePackagePoliciesPrompt([packagePolicy.id], refreshAgentPolicy); + deletePackagePoliciesPrompt([packagePolicy.id], () => { + setIsActionsMenuOpen(false); + refreshAgentPolicy(); + }); }} > }); }; +export const sendGetAgentPolicies = (query?: GetAgentPoliciesRequest['query']) => { + return sendRequest({ + path: agentPolicyRouteService.getListPath(), + method: 'get', + query, + }); +}; + export const useGetOneAgentPolicy = (agentPolicyId: string | undefined) => { return useConditionalRequest({ path: agentPolicyId ? agentPolicyRouteService.getInfoPath(agentPolicyId) : undefined, diff --git a/x-pack/plugins/fleet/public/hooks/use_request/package_policy.ts b/x-pack/plugins/fleet/public/hooks/use_request/package_policy.ts index d39b15a3b3bfa..f8d14647439b2 100644 --- a/x-pack/plugins/fleet/public/hooks/use_request/package_policy.ts +++ b/x-pack/plugins/fleet/public/hooks/use_request/package_policy.ts @@ -66,14 +66,23 @@ export const sendGetOnePackagePolicy = (packagePolicyId: string) => { }); }; -export function sendUpgradePackagePolicyDryRun(packagePolicyIds: string[]) { +export function sendUpgradePackagePolicyDryRun( + packagePolicyIds: string[], + packageVersion?: string +) { + const body: { packagePolicyIds: string[]; dryRun: boolean; packageVersion?: string } = { + packagePolicyIds, + dryRun: true, + }; + + if (packageVersion) { + body.packageVersion = packageVersion; + } + return sendRequest({ path: packagePolicyRouteService.getUpgradePath(), method: 'post', - body: JSON.stringify({ - packagePolicyIds, - dryRun: true, - }), + body: JSON.stringify(body), }); } diff --git a/x-pack/plugins/fleet/server/constants/index.ts b/x-pack/plugins/fleet/server/constants/index.ts index 28f3ea96f732e..0dcd5e7f47800 100644 --- a/x-pack/plugins/fleet/server/constants/index.ts +++ b/x-pack/plugins/fleet/server/constants/index.ts @@ -49,6 +49,7 @@ export { DEFAULT_FLEET_SERVER_AGENT_POLICY, DEFAULT_OUTPUT, DEFAULT_PACKAGES, + PACKAGE_POLICY_DEFAULT_INDEX_PRIVILEGES, // Fleet Server index FLEET_SERVER_SERVERS_INDEX, ENROLLMENT_API_KEYS_INDEX, diff --git a/x-pack/plugins/fleet/server/errors/utils.ts b/x-pack/plugins/fleet/server/errors/utils.ts index 2eae04e05bd6b..d58f82b94fcd7 100644 --- a/x-pack/plugins/fleet/server/errors/utils.ts +++ b/x-pack/plugins/fleet/server/errors/utils.ts @@ -11,6 +11,6 @@ export function isESClientError(error: unknown): error is ResponseError { return error instanceof ResponseError; } -export const isElasticsearchVersionConflictError = (error: Error): boolean => { +export function isElasticsearchVersionConflictError(error: Error): boolean { return isESClientError(error) && error.meta.statusCode === 409; -}; +} diff --git a/x-pack/plugins/fleet/server/index.ts b/x-pack/plugins/fleet/server/index.ts index 21cdf659f2f5a..accd5e040f4f0 100644 --- a/x-pack/plugins/fleet/server/index.ts +++ b/x-pack/plugins/fleet/server/index.ts @@ -9,7 +9,11 @@ import { schema } from '@kbn/config-schema'; import type { TypeOf } from '@kbn/config-schema'; import type { PluginConfigDescriptor, PluginInitializerContext } from 'src/core/server'; -import { PreconfiguredPackagesSchema, PreconfiguredAgentPoliciesSchema } from './types'; +import { + PreconfiguredPackagesSchema, + PreconfiguredAgentPoliciesSchema, + PreconfiguredOutputsSchema, +} from './types'; import { FleetPlugin } from './plugin'; @@ -38,7 +42,8 @@ export const config: PluginConfigDescriptor = { epm: true, agents: true, }, - deprecations: ({ renameFromRoot, unused, unusedFromRoot }) => [ + deprecations: ({ deprecate, renameFromRoot, unused, unusedFromRoot }) => [ + deprecate('enabled', '8.0.0'), // Fleet plugin was named ingestManager before renameFromRoot('xpack.ingestManager.enabled', 'xpack.fleet.enabled'), renameFromRoot('xpack.ingestManager.registryUrl', 'xpack.fleet.registryUrl'), @@ -113,6 +118,7 @@ export const config: PluginConfigDescriptor = { }), packages: PreconfiguredPackagesSchema, agentPolicies: PreconfiguredAgentPoliciesSchema, + outputs: PreconfiguredOutputsSchema, agentIdVerificationEnabled: schema.boolean({ defaultValue: true }), }), }; 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 a7cf606e92c0b..b3197d918d231 100644 --- a/x-pack/plugins/fleet/server/routes/agent_policy/handlers.ts +++ b/x-pack/plugins/fleet/server/routes/agent_policy/handlers.ts @@ -136,7 +136,7 @@ export const createAgentPolicyHandler: RequestHandler< }); } - await agentPolicyService.createFleetPolicyChangeAction(soClient, agentPolicy.id); + await agentPolicyService.createFleetServerPolicy(soClient, agentPolicy.id); const body: CreateAgentPolicyResponse = { item: agentPolicy, diff --git a/x-pack/plugins/fleet/server/routes/setup/handlers.test.ts b/x-pack/plugins/fleet/server/routes/setup/handlers.test.ts index 0e22f544ddfa3..bd82989a9e828 100644 --- a/x-pack/plugins/fleet/server/routes/setup/handlers.test.ts +++ b/x-pack/plugins/fleet/server/routes/setup/handlers.test.ts @@ -11,17 +11,17 @@ import type { PostFleetSetupResponse } from '../../../common'; import { RegistryError } from '../../errors'; import { createAppContextStartContractMock, xpackMocks } from '../../mocks'; import { appContextService } from '../../services/app_context'; -import { setupIngestManager } from '../../services/setup'; +import { setupFleet } from '../../services/setup'; import { fleetSetupHandler } from './handlers'; jest.mock('../../services/setup', () => { return { - setupIngestManager: jest.fn(), + setupFleet: jest.fn(), }; }); -const mockSetupIngestManager = setupIngestManager as jest.MockedFunction; +const mockSetupFleet = setupFleet as jest.MockedFunction; describe('FleetSetupHandler', () => { let context: ReturnType; @@ -45,7 +45,7 @@ describe('FleetSetupHandler', () => { }); it('POST /setup succeeds w/200 and body of resolved value', async () => { - mockSetupIngestManager.mockImplementation(() => + mockSetupFleet.mockImplementation(() => Promise.resolve({ isInitialized: true, nonFatalErrors: [], @@ -59,9 +59,7 @@ describe('FleetSetupHandler', () => { }); it('POST /setup fails w/500 on custom error', async () => { - mockSetupIngestManager.mockImplementation(() => - Promise.reject(new Error('SO method mocked to throw')) - ); + mockSetupFleet.mockImplementation(() => Promise.reject(new Error('SO method mocked to throw'))); await fleetSetupHandler(context, request, response); expect(response.customError).toHaveBeenCalledTimes(1); @@ -74,7 +72,7 @@ describe('FleetSetupHandler', () => { }); it('POST /setup fails w/502 on RegistryError', async () => { - mockSetupIngestManager.mockImplementation(() => + mockSetupFleet.mockImplementation(() => Promise.reject(new RegistryError('Registry method mocked to throw')) ); diff --git a/x-pack/plugins/fleet/server/routes/setup/handlers.ts b/x-pack/plugins/fleet/server/routes/setup/handlers.ts index fe1e30f9f05d6..6311b9d970d35 100644 --- a/x-pack/plugins/fleet/server/routes/setup/handlers.ts +++ b/x-pack/plugins/fleet/server/routes/setup/handlers.ts @@ -9,7 +9,7 @@ import type { RequestHandler } from 'src/core/server'; import { appContextService } from '../../services'; import type { GetFleetStatusResponse, PostFleetSetupResponse } from '../../../common'; -import { setupIngestManager } from '../../services/setup'; +import { setupFleet } from '../../services/setup'; import { hasFleetServers } from '../../services/fleet_server'; import { defaultIngestErrorHandler } from '../../errors'; @@ -46,7 +46,7 @@ export const fleetSetupHandler: RequestHandler = async (context, request, respon try { const soClient = context.core.savedObjects.client; const esClient = context.core.elasticsearch.client.asCurrentUser; - const setupStatus = await setupIngestManager(soClient, esClient); + const setupStatus = await setupFleet(soClient, esClient); const body: PostFleetSetupResponse = { ...setupStatus, nonFatalErrors: setupStatus.nonFatalErrors.map((e) => { diff --git a/x-pack/plugins/fleet/server/saved_objects/index.ts b/x-pack/plugins/fleet/server/saved_objects/index.ts index 449a1984aa53b..83188e0047044 100644 --- a/x-pack/plugins/fleet/server/saved_objects/index.ts +++ b/x-pack/plugins/fleet/server/saved_objects/index.ts @@ -156,6 +156,8 @@ const getSavedObjectTypes = ( revision: { type: 'integer' }, monitoring_enabled: { type: 'keyword', index: false }, is_preconfigured: { type: 'keyword' }, + data_output_id: { type: 'keyword' }, + monitoring_output_id: { type: 'keyword' }, }, }, migrations: { @@ -196,6 +198,7 @@ const getSavedObjectTypes = ( }, mappings: { properties: { + output_id: { type: 'keyword', index: false }, name: { type: 'keyword' }, type: { type: 'keyword' }, is_default: { type: 'boolean' }, @@ -203,6 +206,7 @@ const getSavedObjectTypes = ( ca_sha256: { type: 'keyword', index: false }, config: { type: 'flattened' }, config_yaml: { type: 'text' }, + is_preconfigured: { type: 'boolean', index: false }, }, }, migrations: { @@ -251,6 +255,11 @@ const getSavedObjectTypes = ( properties: { dataset: { type: 'keyword' }, type: { type: 'keyword' }, + elasticsearch: { + properties: { + privileges: { type: 'flattened' }, + }, + }, }, }, vars: { type: 'flattened' }, diff --git a/x-pack/plugins/fleet/server/services/agent_policies/__snapshots__/full_agent_policy.test.ts.snap b/x-pack/plugins/fleet/server/services/agent_policies/__snapshots__/full_agent_policy.test.ts.snap new file mode 100644 index 0000000000000..970bccbafa634 --- /dev/null +++ b/x-pack/plugins/fleet/server/services/agent_policies/__snapshots__/full_agent_policy.test.ts.snap @@ -0,0 +1,292 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`getFullAgentPolicy should support a different data output 1`] = ` +Object { + "agent": Object { + "monitoring": Object { + "enabled": true, + "logs": false, + "metrics": true, + "namespace": "default", + "use_output": "default", + }, + }, + "fleet": Object { + "hosts": Array [ + "http://fleetserver:8220", + ], + }, + "id": "agent-policy", + "inputs": Array [], + "output_permissions": Object { + "data-output-id": Object { + "_elastic_agent_checks": Object { + "cluster": Array [ + "monitor", + ], + }, + "_fallback": Object { + "cluster": Array [ + "monitor", + ], + "indices": Array [ + Object { + "names": Array [ + "logs-*", + "metrics-*", + "traces-*", + "synthetics-*", + ".logs-endpoint.diagnostic.collection-*", + ], + "privileges": Array [ + "auto_configure", + "create_doc", + ], + }, + ], + }, + }, + "default": Object { + "_elastic_agent_checks": Object { + "cluster": Array [ + "monitor", + ], + "indices": Array [ + Object { + "names": Array [ + "metrics-elastic_agent-default", + "metrics-elastic_agent.elastic_agent-default", + "metrics-elastic_agent.apm_server-default", + "metrics-elastic_agent.filebeat-default", + "metrics-elastic_agent.fleet_server-default", + "metrics-elastic_agent.metricbeat-default", + "metrics-elastic_agent.osquerybeat-default", + "metrics-elastic_agent.packetbeat-default", + "metrics-elastic_agent.endpoint_security-default", + "metrics-elastic_agent.auditbeat-default", + "metrics-elastic_agent.heartbeat-default", + ], + "privileges": Array [ + "auto_configure", + "create_doc", + ], + }, + ], + }, + }, + }, + "outputs": Object { + "data-output-id": Object { + "api_key": undefined, + "ca_sha256": undefined, + "hosts": Array [ + "http://es-data.co:9201", + ], + "type": "elasticsearch", + }, + "default": Object { + "api_key": undefined, + "ca_sha256": undefined, + "hosts": Array [ + "http://127.0.0.1:9201", + ], + "type": "elasticsearch", + }, + }, + "revision": 1, +} +`; + +exports[`getFullAgentPolicy should support a different monitoring output 1`] = ` +Object { + "agent": Object { + "monitoring": Object { + "enabled": true, + "logs": false, + "metrics": true, + "namespace": "default", + "use_output": "monitoring-output-id", + }, + }, + "fleet": Object { + "hosts": Array [ + "http://fleetserver:8220", + ], + }, + "id": "agent-policy", + "inputs": Array [], + "output_permissions": Object { + "default": Object { + "_elastic_agent_checks": Object { + "cluster": Array [ + "monitor", + ], + }, + "_fallback": Object { + "cluster": Array [ + "monitor", + ], + "indices": Array [ + Object { + "names": Array [ + "logs-*", + "metrics-*", + "traces-*", + "synthetics-*", + ".logs-endpoint.diagnostic.collection-*", + ], + "privileges": Array [ + "auto_configure", + "create_doc", + ], + }, + ], + }, + }, + "monitoring-output-id": Object { + "_elastic_agent_checks": Object { + "cluster": Array [ + "monitor", + ], + "indices": Array [ + Object { + "names": Array [ + "metrics-elastic_agent-default", + "metrics-elastic_agent.elastic_agent-default", + "metrics-elastic_agent.apm_server-default", + "metrics-elastic_agent.filebeat-default", + "metrics-elastic_agent.fleet_server-default", + "metrics-elastic_agent.metricbeat-default", + "metrics-elastic_agent.osquerybeat-default", + "metrics-elastic_agent.packetbeat-default", + "metrics-elastic_agent.endpoint_security-default", + "metrics-elastic_agent.auditbeat-default", + "metrics-elastic_agent.heartbeat-default", + ], + "privileges": Array [ + "auto_configure", + "create_doc", + ], + }, + ], + }, + }, + }, + "outputs": Object { + "default": Object { + "api_key": undefined, + "ca_sha256": undefined, + "hosts": Array [ + "http://127.0.0.1:9201", + ], + "type": "elasticsearch", + }, + "monitoring-output-id": Object { + "api_key": undefined, + "ca_sha256": undefined, + "hosts": Array [ + "http://es-monitoring.co:9201", + ], + "type": "elasticsearch", + }, + }, + "revision": 1, +} +`; + +exports[`getFullAgentPolicy should support both different outputs for data and monitoring 1`] = ` +Object { + "agent": Object { + "monitoring": Object { + "enabled": true, + "logs": false, + "metrics": true, + "namespace": "default", + "use_output": "monitoring-output-id", + }, + }, + "fleet": Object { + "hosts": Array [ + "http://fleetserver:8220", + ], + }, + "id": "agent-policy", + "inputs": Array [], + "output_permissions": Object { + "data-output-id": Object { + "_elastic_agent_checks": Object { + "cluster": Array [ + "monitor", + ], + }, + "_fallback": Object { + "cluster": Array [ + "monitor", + ], + "indices": Array [ + Object { + "names": Array [ + "logs-*", + "metrics-*", + "traces-*", + "synthetics-*", + ".logs-endpoint.diagnostic.collection-*", + ], + "privileges": Array [ + "auto_configure", + "create_doc", + ], + }, + ], + }, + }, + "monitoring-output-id": Object { + "_elastic_agent_checks": Object { + "cluster": Array [ + "monitor", + ], + "indices": Array [ + Object { + "names": Array [ + "metrics-elastic_agent-default", + "metrics-elastic_agent.elastic_agent-default", + "metrics-elastic_agent.apm_server-default", + "metrics-elastic_agent.filebeat-default", + "metrics-elastic_agent.fleet_server-default", + "metrics-elastic_agent.metricbeat-default", + "metrics-elastic_agent.osquerybeat-default", + "metrics-elastic_agent.packetbeat-default", + "metrics-elastic_agent.endpoint_security-default", + "metrics-elastic_agent.auditbeat-default", + "metrics-elastic_agent.heartbeat-default", + ], + "privileges": Array [ + "auto_configure", + "create_doc", + ], + }, + ], + }, + }, + }, + "outputs": Object { + "data-output-id": Object { + "api_key": undefined, + "ca_sha256": undefined, + "hosts": Array [ + "http://es-data.co:9201", + ], + "type": "elasticsearch", + }, + "monitoring-output-id": Object { + "api_key": undefined, + "ca_sha256": undefined, + "hosts": Array [ + "http://es-monitoring.co:9201", + ], + "type": "elasticsearch", + }, + }, + "revision": 1, +} +`; 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 new file mode 100644 index 0000000000000..8df1234982ee6 --- /dev/null +++ b/x-pack/plugins/fleet/server/services/agent_policies/full_agent_policy.test.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 { savedObjectsClientMock } from 'src/core/server/mocks'; + +import type { AgentPolicy, Output } from '../../types'; + +import { agentPolicyService } from '../agent_policy'; +import { agentPolicyUpdateEventHandler } from '../agent_policy_update'; + +import { getFullAgentPolicy } from './full_agent_policy'; + +const mockedAgentPolicyService = agentPolicyService as jest.Mocked; + +function mockAgentPolicy(data: Partial) { + mockedAgentPolicyService.get.mockResolvedValue({ + id: 'agent-policy', + status: 'active', + package_policies: [], + is_managed: false, + namespace: 'default', + revision: 1, + name: 'Policy', + updated_at: '2020-01-01', + updated_by: 'qwerty', + ...data, + }); +} + +jest.mock('../settings', () => { + return { + getSettings: () => { + return { + id: '93f74c0-e876-11ea-b7d3-8b2acec6f75c', + fleet_server_hosts: ['http://fleetserver:8220'], + }; + }, + }; +}); + +jest.mock('../agent_policy'); + +jest.mock('../output', () => { + return { + outputService: { + getDefaultOutputId: () => 'test-id', + get: (soClient: any, id: string): Output => { + switch (id) { + case 'data-output-id': + return { + id: 'data-output-id', + is_default: false, + name: 'Data output', + // @ts-ignore + type: 'elasticsearch', + hosts: ['http://es-data.co:9201'], + }; + case 'monitoring-output-id': + return { + id: 'monitoring-output-id', + is_default: false, + name: 'Monitoring output', + // @ts-ignore + type: 'elasticsearch', + hosts: ['http://es-monitoring.co:9201'], + }; + default: + return { + id: 'test-id', + is_default: true, + name: 'default', + // @ts-ignore + type: 'elasticsearch', + hosts: ['http://127.0.0.1:9201'], + }; + } + }, + }, + }; +}); + +jest.mock('../agent_policy_update'); +jest.mock('../agents'); +jest.mock('../package_policy'); + +function getAgentPolicyUpdateMock() { + return agentPolicyUpdateEventHandler as unknown as jest.Mock< + typeof agentPolicyUpdateEventHandler + >; +} + +describe('getFullAgentPolicy', () => { + beforeEach(() => { + getAgentPolicyUpdateMock().mockClear(); + mockedAgentPolicyService.get.mockReset(); + }); + + it('should return a policy without monitoring if monitoring is not enabled', async () => { + mockAgentPolicy({ + revision: 1, + }); + const agentPolicy = await getFullAgentPolicy(savedObjectsClientMock.create(), 'agent-policy'); + + expect(agentPolicy).toMatchObject({ + id: 'agent-policy', + outputs: { + default: { + type: 'elasticsearch', + hosts: ['http://127.0.0.1:9201'], + ca_sha256: undefined, + api_key: undefined, + }, + }, + inputs: [], + revision: 1, + fleet: { + hosts: ['http://fleetserver:8220'], + }, + agent: { + monitoring: { + enabled: false, + logs: false, + metrics: false, + }, + }, + }); + }); + + it('should return a policy with monitoring if monitoring is enabled for logs', async () => { + mockAgentPolicy({ + namespace: 'default', + revision: 1, + monitoring_enabled: ['logs'], + }); + const agentPolicy = await getFullAgentPolicy(savedObjectsClientMock.create(), 'agent-policy'); + + expect(agentPolicy).toMatchObject({ + id: 'agent-policy', + outputs: { + default: { + type: 'elasticsearch', + hosts: ['http://127.0.0.1:9201'], + ca_sha256: undefined, + api_key: undefined, + }, + }, + inputs: [], + revision: 1, + fleet: { + hosts: ['http://fleetserver:8220'], + }, + agent: { + monitoring: { + namespace: 'default', + use_output: 'default', + enabled: true, + logs: true, + metrics: false, + }, + }, + }); + }); + + it('should return a policy with monitoring if monitoring is enabled for metrics', async () => { + mockAgentPolicy({ + namespace: 'default', + revision: 1, + monitoring_enabled: ['metrics'], + }); + const agentPolicy = await getFullAgentPolicy(savedObjectsClientMock.create(), 'agent-policy'); + + expect(agentPolicy).toMatchObject({ + id: 'agent-policy', + outputs: { + default: { + type: 'elasticsearch', + hosts: ['http://127.0.0.1:9201'], + ca_sha256: undefined, + api_key: undefined, + }, + }, + inputs: [], + revision: 1, + fleet: { + hosts: ['http://fleetserver:8220'], + }, + agent: { + monitoring: { + namespace: 'default', + use_output: 'default', + enabled: true, + logs: false, + metrics: true, + }, + }, + }); + }); + + it('should support a different monitoring output', async () => { + mockAgentPolicy({ + namespace: 'default', + revision: 1, + monitoring_enabled: ['metrics'], + monitoring_output_id: 'monitoring-output-id', + }); + const agentPolicy = await getFullAgentPolicy(savedObjectsClientMock.create(), 'agent-policy'); + + expect(agentPolicy).toMatchSnapshot(); + }); + + it('should support a different data output', async () => { + mockAgentPolicy({ + namespace: 'default', + revision: 1, + monitoring_enabled: ['metrics'], + data_output_id: 'data-output-id', + }); + const agentPolicy = await getFullAgentPolicy(savedObjectsClientMock.create(), 'agent-policy'); + + expect(agentPolicy).toMatchSnapshot(); + }); + + it('should support both different outputs for data and monitoring ', async () => { + mockAgentPolicy({ + namespace: 'default', + revision: 1, + monitoring_enabled: ['metrics'], + data_output_id: 'data-output-id', + monitoring_output_id: 'monitoring-output-id', + }); + const agentPolicy = await getFullAgentPolicy(savedObjectsClientMock.create(), 'agent-policy'); + + expect(agentPolicy).toMatchSnapshot(); + }); + + it('should use "default" as the default policy id', async () => { + mockAgentPolicy({ + id: 'policy', + status: 'active', + package_policies: [], + is_managed: false, + namespace: 'default', + revision: 1, + data_output_id: 'test-id', + monitoring_output_id: 'test-id', + }); + + const agentPolicy = await getFullAgentPolicy(savedObjectsClientMock.create(), 'agent-policy'); + + expect(agentPolicy?.outputs.default).toBeDefined(); + }); +}); 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 new file mode 100644 index 0000000000000..4e8b3a2c1952e --- /dev/null +++ b/x-pack/plugins/fleet/server/services/agent_policies/full_agent_policy.ts @@ -0,0 +1,229 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license 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 } from 'kibana/server'; +import { safeLoad } from 'js-yaml'; + +import type { + FullAgentPolicy, + PackagePolicy, + Settings, + Output, + FullAgentPolicyOutput, +} from '../../types'; +import { agentPolicyService } from '../agent_policy'; +import { outputService } from '../output'; +import { + storedPackagePoliciesToAgentPermissions, + DEFAULT_PERMISSIONS, +} from '../package_policies_to_agent_permissions'; +import { storedPackagePoliciesToAgentInputs, dataTypes, outputType } from '../../../common'; +import type { FullAgentPolicyOutputPermissions } from '../../../common'; +import { getSettings } from '../settings'; +import { PACKAGE_POLICY_DEFAULT_INDEX_PRIVILEGES, DEFAULT_OUTPUT } from '../../constants'; + +const MONITORING_DATASETS = [ + 'elastic_agent', + 'elastic_agent.elastic_agent', + 'elastic_agent.apm_server', + 'elastic_agent.filebeat', + 'elastic_agent.fleet_server', + 'elastic_agent.metricbeat', + 'elastic_agent.osquerybeat', + 'elastic_agent.packetbeat', + 'elastic_agent.endpoint_security', + 'elastic_agent.auditbeat', + 'elastic_agent.heartbeat', +]; + +export async function getFullAgentPolicy( + soClient: SavedObjectsClientContract, + id: string, + options?: { standalone: boolean } +): Promise { + let agentPolicy; + const standalone = options?.standalone; + + try { + agentPolicy = await agentPolicyService.get(soClient, id); + } catch (err) { + if (!err.isBoom || err.output.statusCode !== 404) { + throw err; + } + } + + if (!agentPolicy) { + return null; + } + + const defaultOutputId = await outputService.getDefaultOutputId(soClient); + if (!defaultOutputId) { + throw new Error('Default output is not setup'); + } + + const dataOutputId = agentPolicy.data_output_id || defaultOutputId; + const monitoringOutputId = agentPolicy.monitoring_output_id || defaultOutputId; + + const outputs = await Promise.all( + Array.from(new Set([dataOutputId, monitoringOutputId])).map((outputId) => + outputService.get(soClient, outputId) + ) + ); + + const dataOutput = outputs.find((output) => output.id === dataOutputId); + if (!dataOutput) { + throw new Error(`Data output not found ${dataOutputId}`); + } + const monitoringOutput = outputs.find((output) => output.id === monitoringOutputId); + if (!monitoringOutput) { + throw new Error(`Monitoring output not found ${monitoringOutputId}`); + } + + const fullAgentPolicy: FullAgentPolicy = { + id: agentPolicy.id, + outputs: { + ...outputs.reduce((acc, output) => { + acc[getOutputIdForAgentPolicy(output)] = transformOutputToFullPolicyOutput(output); + + return acc; + }, {}), + }, + inputs: storedPackagePoliciesToAgentInputs( + agentPolicy.package_policies as PackagePolicy[], + getOutputIdForAgentPolicy(dataOutput) + ), + revision: agentPolicy.revision, + ...(agentPolicy.monitoring_enabled && agentPolicy.monitoring_enabled.length > 0 + ? { + agent: { + monitoring: { + namespace: agentPolicy.namespace, + use_output: getOutputIdForAgentPolicy(monitoringOutput), + enabled: true, + logs: agentPolicy.monitoring_enabled.includes(dataTypes.Logs), + metrics: agentPolicy.monitoring_enabled.includes(dataTypes.Metrics), + }, + }, + } + : { + agent: { + monitoring: { enabled: false, logs: false, metrics: false }, + }, + }), + }; + + const dataPermissions = (await storedPackagePoliciesToAgentPermissions( + soClient, + agentPolicy.package_policies + )) || { _fallback: DEFAULT_PERMISSIONS }; + + dataPermissions._elastic_agent_checks = { + cluster: DEFAULT_PERMISSIONS.cluster, + }; + + // TODO: fetch this from the elastic agent package https://github.com/elastic/kibana/issues/107738 + const monitoringNamespace = fullAgentPolicy.agent?.monitoring.namespace; + const monitoringPermissions: FullAgentPolicyOutputPermissions = + monitoringOutputId === dataOutputId + ? dataPermissions + : { + _elastic_agent_checks: { + cluster: DEFAULT_PERMISSIONS.cluster, + }, + }; + if ( + fullAgentPolicy.agent?.monitoring.enabled && + monitoringNamespace && + monitoringOutput && + monitoringOutput.type === outputType.Elasticsearch + ) { + let names: string[] = []; + if (fullAgentPolicy.agent.monitoring.logs) { + names = names.concat( + MONITORING_DATASETS.map((dataset) => `logs-${dataset}-${monitoringNamespace}`) + ); + } + if (fullAgentPolicy.agent.monitoring.metrics) { + names = names.concat( + MONITORING_DATASETS.map((dataset) => `metrics-${dataset}-${monitoringNamespace}`) + ); + } + + monitoringPermissions._elastic_agent_checks.indices = [ + { + names, + privileges: PACKAGE_POLICY_DEFAULT_INDEX_PRIVILEGES, + }, + ]; + } + + // Only add permissions if output.type is "elasticsearch" + fullAgentPolicy.output_permissions = Object.keys(fullAgentPolicy.outputs).reduce< + NonNullable + >((outputPermissions, outputId) => { + const output = fullAgentPolicy.outputs[outputId]; + if (output && output.type === outputType.Elasticsearch) { + outputPermissions[outputId] = + outputId === getOutputIdForAgentPolicy(dataOutput) + ? dataPermissions + : monitoringPermissions; + } + return outputPermissions; + }, {}); + + // only add settings if not in standalone + if (!standalone) { + let settings: Settings; + try { + settings = await getSettings(soClient); + } catch (error) { + throw new Error('Default settings is not setup'); + } + if (settings.fleet_server_hosts && settings.fleet_server_hosts.length) { + fullAgentPolicy.fleet = { + hosts: settings.fleet_server_hosts, + }; + } + } + return fullAgentPolicy; +} + +function transformOutputToFullPolicyOutput( + output: Output, + standalone = false +): FullAgentPolicyOutput { + // eslint-disable-next-line @typescript-eslint/naming-convention + const { config_yaml, type, hosts, ca_sha256, api_key } = output; + const configJs = config_yaml ? safeLoad(config_yaml) : {}; + const newOutput: FullAgentPolicyOutput = { + type, + hosts, + ca_sha256, + api_key, + ...configJs, + }; + + if (standalone) { + delete newOutput.api_key; + newOutput.username = 'ES_USERNAME'; + newOutput.password = 'ES_PASSWORD'; + } + + return newOutput; +} + +/** + * Get id used in full agent policy (sent to the agents) + * we use "default" for the default policy to avoid breaking changes + */ +function getOutputIdForAgentPolicy(output: Output) { + if (output.is_default) { + return DEFAULT_OUTPUT.name; + } + + return output.id; +} diff --git a/x-pack/plugins/uptime/public/components/overview/empty_state/index.ts b/x-pack/plugins/fleet/server/services/agent_policies/index.ts similarity index 70% rename from x-pack/plugins/uptime/public/components/overview/empty_state/index.ts rename to x-pack/plugins/fleet/server/services/agent_policies/index.ts index 5ffcc15ed404b..b793ed26a08b5 100644 --- a/x-pack/plugins/uptime/public/components/overview/empty_state/index.ts +++ b/x-pack/plugins/fleet/server/services/agent_policies/index.ts @@ -5,5 +5,4 @@ * 2.0. */ -export { EmptyStateComponent } from './empty_state'; -export { EmptyState } from './empty_state_container'; +export { getFullAgentPolicy } from './full_agent_policy'; 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 59e0f6fd7840e..5617f8ef7bd7c 100644 --- a/x-pack/plugins/fleet/server/services/agent_policy.test.ts +++ b/x-pack/plugins/fleet/server/services/agent_policy.test.ts @@ -7,13 +7,16 @@ import { elasticsearchServiceMock, savedObjectsClientMock } from 'src/core/server/mocks'; -import type { AgentPolicy, NewAgentPolicy, Output } from '../types'; +import type { AgentPolicy, FullAgentPolicy, NewAgentPolicy } from '../types'; import { agentPolicyService } from './agent_policy'; import { agentPolicyUpdateEventHandler } from './agent_policy_update'; import { getAgentsByKuery } from './agents'; import { packagePolicyService } from './package_policy'; +import { appContextService } from './app_context'; +import { outputService } from './output'; +import { getFullAgentPolicy } from './agent_policies'; function getSavedObjectMock(agentPolicyAttributes: any) { const mock = savedObjectsClientMock.create(); @@ -47,27 +50,18 @@ function getSavedObjectMock(agentPolicyAttributes: any) { return mock; } -jest.mock('./output', () => { - return { - outputService: { - getDefaultOutputId: () => 'test-id', - get: (): Output => { - return { - id: 'test-id', - is_default: true, - name: 'default', - // @ts-ignore - type: 'elasticsearch', - hosts: ['http://127.0.0.1:9201'], - }; - }, - }, - }; -}); - +jest.mock('./output'); jest.mock('./agent_policy_update'); jest.mock('./agents'); jest.mock('./package_policy'); +jest.mock('./app_context'); +jest.mock('./agent_policies/full_agent_policy'); + +const mockedAppContextService = appContextService as jest.Mocked; +const mockedOutputService = outputService as jest.Mocked; +const mockedGetFullAgentPolicy = getFullAgentPolicy as jest.Mock< + ReturnType +>; function getAgentPolicyUpdateMock() { return agentPolicyUpdateEventHandler as unknown as jest.Mock< @@ -186,106 +180,17 @@ describe('agent policy', () => { }); }); - describe('getFullAgentPolicy', () => { - it('should return a policy without monitoring if monitoring is not enabled', async () => { + describe('bumpAllAgentPoliciesForOutput', () => { + it('should call agentPolicyUpdateEventHandler with updated event once', async () => { const soClient = getSavedObjectMock({ revision: 1, + monitoring_enabled: ['metrics'], }); - const agentPolicy = await agentPolicyService.getFullAgentPolicy(soClient, 'agent-policy'); - - expect(agentPolicy).toMatchObject({ - id: 'agent-policy', - outputs: { - default: { - type: 'elasticsearch', - hosts: ['http://127.0.0.1:9201'], - ca_sha256: undefined, - api_key: undefined, - }, - }, - inputs: [], - revision: 1, - fleet: { - hosts: ['http://fleetserver:8220'], - }, - agent: { - monitoring: { - enabled: false, - logs: false, - metrics: false, - }, - }, - }); - }); + const esClient = elasticsearchServiceMock.createClusterClient().asInternalUser; - it('should return a policy with monitoring if monitoring is enabled for logs', async () => { - const soClient = getSavedObjectMock({ - namespace: 'default', - revision: 1, - monitoring_enabled: ['logs'], - }); - const agentPolicy = await agentPolicyService.getFullAgentPolicy(soClient, 'agent-policy'); - - expect(agentPolicy).toMatchObject({ - id: 'agent-policy', - outputs: { - default: { - type: 'elasticsearch', - hosts: ['http://127.0.0.1:9201'], - ca_sha256: undefined, - api_key: undefined, - }, - }, - inputs: [], - revision: 1, - fleet: { - hosts: ['http://fleetserver:8220'], - }, - agent: { - monitoring: { - namespace: 'default', - use_output: 'default', - enabled: true, - logs: true, - metrics: false, - }, - }, - }); - }); + await agentPolicyService.bumpAllAgentPoliciesForOutput(soClient, esClient, 'output-id-123'); - it('should return a policy with monitoring if monitoring is enabled for metrics', async () => { - const soClient = getSavedObjectMock({ - namespace: 'default', - revision: 1, - monitoring_enabled: ['metrics'], - }); - const agentPolicy = await agentPolicyService.getFullAgentPolicy(soClient, 'agent-policy'); - - expect(agentPolicy).toMatchObject({ - id: 'agent-policy', - outputs: { - default: { - type: 'elasticsearch', - hosts: ['http://127.0.0.1:9201'], - ca_sha256: undefined, - api_key: undefined, - }, - }, - inputs: [], - revision: 1, - fleet: { - hosts: ['http://fleetserver:8220'], - }, - agent: { - monitoring: { - namespace: 'default', - use_output: 'default', - enabled: true, - logs: false, - metrics: true, - }, - }, - }); + expect(agentPolicyUpdateEventHandler).toHaveBeenCalledTimes(1); }); }); @@ -321,4 +226,64 @@ describe('agent policy', () => { expect(calledWith[2]).toHaveProperty('is_managed', true); }); }); + + describe('createFleetServerPolicy', () => { + beforeEach(() => { + mockedGetFullAgentPolicy.mockReset(); + }); + it('should not create a .fleet-policy document if we cannot get the full policy', async () => { + const soClient = savedObjectsClientMock.create(); + const esClient = elasticsearchServiceMock.createClusterClient().asInternalUser; + mockedAppContextService.getInternalUserESClient.mockReturnValue(esClient); + mockedOutputService.getDefaultOutputId.mockResolvedValue('default-output'); + mockedGetFullAgentPolicy.mockResolvedValue(null); + + soClient.get.mockResolvedValue({ + attributes: {}, + id: 'policy123', + type: 'mocked', + references: [], + }); + await agentPolicyService.createFleetServerPolicy(soClient, 'policy123'); + + expect(esClient.create).not.toBeCalled(); + }); + + it('should create a .fleet-policy document if we can get the full policy', async () => { + const soClient = savedObjectsClientMock.create(); + const esClient = elasticsearchServiceMock.createClusterClient().asInternalUser; + mockedAppContextService.getInternalUserESClient.mockReturnValue(esClient); + mockedOutputService.getDefaultOutputId.mockResolvedValue('default-output'); + mockedGetFullAgentPolicy.mockResolvedValue({ + id: 'policy123', + revision: 1, + inputs: [ + { + id: 'input-123', + }, + ], + } as FullAgentPolicy); + + soClient.get.mockResolvedValue({ + attributes: {}, + id: 'policy123', + type: 'mocked', + references: [], + }); + await agentPolicyService.createFleetServerPolicy(soClient, 'policy123'); + + expect(esClient.create).toBeCalledWith( + expect.objectContaining({ + index: '.fleet-policies', + body: expect.objectContaining({ + '@timestamp': expect.anything(), + data: { id: 'policy123', inputs: [{ id: 'input-123' }], revision: 1 }, + default_fleet_server: false, + policy_id: 'policy123', + revision_idx: 1, + }), + }) + ); + }); + }); }); diff --git a/x-pack/plugins/fleet/server/services/agent_policy.ts b/x-pack/plugins/fleet/server/services/agent_policy.ts index 9fe275bb9a3c9..6ebe890aeaef2 100644 --- a/x-pack/plugins/fleet/server/services/agent_policy.ts +++ b/x-pack/plugins/fleet/server/services/agent_policy.ts @@ -6,7 +6,6 @@ */ import { uniq, omit } from 'lodash'; -import { safeLoad } from 'js-yaml'; import uuid from 'uuid/v4'; import type { ElasticsearchClient, @@ -32,52 +31,27 @@ import type { ListWithKuery, NewPackagePolicy, } from '../types'; -import { - agentPolicyStatuses, - storedPackagePoliciesToAgentInputs, - dataTypes, - packageToPackagePolicy, - AGENT_POLICY_INDEX, -} from '../../common'; +import { agentPolicyStatuses, packageToPackagePolicy, AGENT_POLICY_INDEX } from '../../common'; import type { DeleteAgentPolicyResponse, - Settings, FleetServerPolicy, Installation, Output, DeletePackagePoliciesResponse, } from '../../common'; import { AgentPolicyNameExistsError, HostedAgentPolicyRestrictionRelatedError } from '../errors'; -import { - storedPackagePoliciesToAgentPermissions, - DEFAULT_PERMISSIONS, -} from '../services/package_policies_to_agent_permissions'; import { getPackageInfo } from './epm/packages'; import { getAgentsByKuery } from './agents'; import { packagePolicyService } from './package_policy'; import { outputService } from './output'; import { agentPolicyUpdateEventHandler } from './agent_policy_update'; -import { getSettings } from './settings'; import { normalizeKuery, escapeSearchQueryPhrase } from './saved_object'; import { appContextService } from './app_context'; +import { getFullAgentPolicy } from './agent_policies'; const SAVED_OBJECT_TYPE = AGENT_POLICY_SAVED_OBJECT_TYPE; -const MONITORING_DATASETS = [ - 'elastic_agent', - 'elastic_agent.elastic_agent', - 'elastic_agent.apm_server', - 'elastic_agent.filebeat', - 'elastic_agent.fleet_server', - 'elastic_agent.metricbeat', - 'elastic_agent.osquerybeat', - 'elastic_agent.packetbeat', - 'elastic_agent.endpoint_security', - 'elastic_agent.auditbeat', - 'elastic_agent.heartbeat', -]; - class AgentPolicyService { private triggerAgentPolicyUpdatedEvent = async ( soClient: SavedObjectsClientContract, @@ -455,7 +429,7 @@ class AgentPolicyService { throw new Error('Copied agent policy not found'); } - await this.createFleetPolicyChangeAction(soClient, newAgentPolicy.id); + await this.createFleetServerPolicy(soClient, newAgentPolicy.id); return updatedAgentPolicy; } @@ -471,6 +445,38 @@ class AgentPolicyService { return res; } + public async bumpAllAgentPoliciesForOutput( + soClient: SavedObjectsClientContract, + esClient: ElasticsearchClient, + outputId: string, + options?: { user?: AuthenticatedUser } + ): Promise> { + const currentPolicies = await soClient.find({ + type: SAVED_OBJECT_TYPE, + fields: ['revision', 'data_output_id', 'monitoring_output_id'], + searchFields: ['data_output_id', 'monitoring_output_id'], + search: escapeSearchQueryPhrase(outputId), + }); + const bumpedPolicies = currentPolicies.saved_objects.map((policy) => { + policy.attributes = { + ...policy.attributes, + revision: policy.attributes.revision + 1, + updated_at: new Date().toISOString(), + updated_by: options?.user ? options.user.username : 'system', + }; + return policy; + }); + const res = await soClient.bulkUpdate(bumpedPolicies); + + await Promise.all( + currentPolicies.saved_objects.map((policy) => + this.triggerAgentPolicyUpdatedEvent(soClient, esClient, 'updated', policy.id) + ) + ); + + return res; + } + public async bumpAllAgentPolicies( soClient: SavedObjectsClientContract, esClient: ElasticsearchClient, @@ -649,10 +655,11 @@ class AgentPolicyService { }; } - public async createFleetPolicyChangeAction( + public async createFleetServerPolicy( soClient: SavedObjectsClientContract, agentPolicyId: string ) { + // Use internal ES client so we have permissions to write to .fleet* indices const esClient = appContextService.getInternalUserESClient(); const defaultOutputId = await outputService.getDefaultOutputId(soClient); @@ -660,14 +667,6 @@ class AgentPolicyService { return; } - await this.createFleetPolicyChangeFleetServer(soClient, esClient, agentPolicyId); - } - - public async createFleetPolicyChangeFleetServer( - soClient: SavedObjectsClientContract, - esClient: ElasticsearchClient, - agentPolicyId: string - ) { const policy = await agentPolicyService.get(soClient, agentPolicyId); const fullPolicy = await agentPolicyService.getFullAgentPolicy(soClient, agentPolicyId); if (!policy || !fullPolicy || !fullPolicy.revision) { @@ -723,139 +722,7 @@ class AgentPolicyService { id: string, options?: { standalone: boolean } ): Promise { - let agentPolicy; - const standalone = options?.standalone; - - try { - agentPolicy = await this.get(soClient, id); - } catch (err) { - if (!err.isBoom || err.output.statusCode !== 404) { - throw err; - } - } - - if (!agentPolicy) { - return null; - } - - const defaultOutputId = await outputService.getDefaultOutputId(soClient); - if (!defaultOutputId) { - throw new Error('Default output is not setup'); - } - const defaultOutput = await outputService.get(soClient, defaultOutputId); - - const fullAgentPolicy: FullAgentPolicy = { - id: agentPolicy.id, - outputs: { - // TEMPORARY as we only support a default output - ...[defaultOutput].reduce( - // eslint-disable-next-line @typescript-eslint/naming-convention - (outputs, { config_yaml, name, type, hosts, ca_sha256, api_key }) => { - const configJs = config_yaml ? safeLoad(config_yaml) : {}; - outputs[name] = { - type, - hosts, - ca_sha256, - api_key, - ...configJs, - }; - - if (options?.standalone) { - delete outputs[name].api_key; - outputs[name].username = 'ES_USERNAME'; - outputs[name].password = 'ES_PASSWORD'; - } - - return outputs; - }, - {} - ), - }, - inputs: storedPackagePoliciesToAgentInputs(agentPolicy.package_policies as PackagePolicy[]), - revision: agentPolicy.revision, - ...(agentPolicy.monitoring_enabled && agentPolicy.monitoring_enabled.length > 0 - ? { - agent: { - monitoring: { - namespace: agentPolicy.namespace, - use_output: defaultOutput.name, - enabled: true, - logs: agentPolicy.monitoring_enabled.includes(dataTypes.Logs), - metrics: agentPolicy.monitoring_enabled.includes(dataTypes.Metrics), - }, - }, - } - : { - agent: { - monitoring: { enabled: false, logs: false, metrics: false }, - }, - }), - }; - - const permissions = (await storedPackagePoliciesToAgentPermissions( - soClient, - agentPolicy.package_policies - )) || { _fallback: DEFAULT_PERMISSIONS }; - - permissions._elastic_agent_checks = { - cluster: DEFAULT_PERMISSIONS.cluster, - }; - - // TODO: fetch this from the elastic agent package - const monitoringOutput = fullAgentPolicy.agent?.monitoring.use_output; - const monitoringNamespace = fullAgentPolicy.agent?.monitoring.namespace; - if ( - fullAgentPolicy.agent?.monitoring.enabled && - monitoringNamespace && - monitoringOutput && - fullAgentPolicy.outputs[monitoringOutput]?.type === 'elasticsearch' - ) { - let names: string[] = []; - if (fullAgentPolicy.agent.monitoring.logs) { - names = names.concat( - MONITORING_DATASETS.map((dataset) => `logs-${dataset}-${monitoringNamespace}`) - ); - } - if (fullAgentPolicy.agent.monitoring.metrics) { - names = names.concat( - MONITORING_DATASETS.map((dataset) => `metrics-${dataset}-${monitoringNamespace}`) - ); - } - - permissions._elastic_agent_checks.indices = [ - { - names, - privileges: ['auto_configure', 'create_doc'], - }, - ]; - } - - // Only add permissions if output.type is "elasticsearch" - fullAgentPolicy.output_permissions = Object.keys(fullAgentPolicy.outputs).reduce< - NonNullable - >((outputPermissions, outputName) => { - const output = fullAgentPolicy.outputs[outputName]; - if (output && output.type === 'elasticsearch') { - outputPermissions[outputName] = permissions; - } - return outputPermissions; - }, {}); - - // only add settings if not in standalone - if (!standalone) { - let settings: Settings; - try { - settings = await getSettings(soClient); - } catch (error) { - throw new Error('Default settings is not setup'); - } - if (settings.fleet_server_hosts && settings.fleet_server_hosts.length) { - fullAgentPolicy.fleet = { - hosts: settings.fleet_server_hosts, - }; - } - } - return fullAgentPolicy; + return getFullAgentPolicy(soClient, id, options); } } diff --git a/x-pack/plugins/fleet/server/services/agent_policy_update.ts b/x-pack/plugins/fleet/server/services/agent_policy_update.ts index 9703467d84c18..51bf068b8b111 100644 --- a/x-pack/plugins/fleet/server/services/agent_policy_update.ts +++ b/x-pack/plugins/fleet/server/services/agent_policy_update.ts @@ -43,11 +43,11 @@ export async function agentPolicyUpdateEventHandler( name: 'Default', agentPolicyId, }); - await agentPolicyService.createFleetPolicyChangeAction(internalSoClient, agentPolicyId); + await agentPolicyService.createFleetServerPolicy(internalSoClient, agentPolicyId); } if (action === 'updated') { - await agentPolicyService.createFleetPolicyChangeAction(internalSoClient, agentPolicyId); + await agentPolicyService.createFleetServerPolicy(internalSoClient, agentPolicyId); } if (action === 'deleted') { diff --git a/x-pack/plugins/fleet/server/services/agents/setup.ts b/x-pack/plugins/fleet/server/services/agents/setup.ts index 81ae6b177783d..2b680dee1146e 100644 --- a/x-pack/plugins/fleet/server/services/agents/setup.ts +++ b/x-pack/plugins/fleet/server/services/agents/setup.ts @@ -11,12 +11,9 @@ import { SO_SEARCH_LIMIT } from '../../constants'; import { agentPolicyService } from '../agent_policy'; /** - * During the migration from 7.9 to 7.10 we introduce a new agent action POLICY_CHANGE per policy - * this function ensure that action exist for each policy - * - * @param soClient + * Ensure a .fleet-policy document exist for each agent policy so Fleet server can retrieve it */ -export async function ensureAgentActionPolicyChangeExists( +export async function ensureFleetServerAgentPoliciesExists( soClient: SavedObjectsClientContract, esClient: ElasticsearchClient ) { @@ -32,7 +29,7 @@ export async function ensureAgentActionPolicyChangeExists( )); if (!policyChangeActionExist) { - return agentPolicyService.createFleetPolicyChangeAction(soClient, agentPolicy.id); + return agentPolicyService.createFleetServerPolicy(soClient, agentPolicy.id); } }) ); diff --git a/x-pack/plugins/fleet/server/services/epm/packages/cleanup.test.ts b/x-pack/plugins/fleet/server/services/epm/packages/cleanup.test.ts new file mode 100644 index 0000000000000..482e42a46060e --- /dev/null +++ b/x-pack/plugins/fleet/server/services/epm/packages/cleanup.test.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 type { SavedObjectsClientContract } from 'kibana/server'; +import { savedObjectsClientMock } from 'src/core/server/mocks'; + +import type { PackagePolicyServiceInterface } from '../../package_policy'; +import * as storage from '../archive/storage'; +import { packagePolicyService } from '../../package_policy'; + +import { removeOldAssets } from './cleanup'; + +jest.mock('../..', () => ({ + appContextService: { + getLogger: () => ({ + info: jest.fn(), + }), + }, +})); + +jest.mock('../../package_policy'); + +describe(' Cleanup old assets', () => { + let soClient: jest.Mocked; + const packagePolicyServiceMock = + packagePolicyService as jest.Mocked; + let removeArchiveEntriesMock: jest.MockedFunction; + + function mockFindVersions(versions: string[]) { + soClient.find.mockResolvedValue({ + page: 0, + per_page: 0, + total: 0, + saved_objects: [], + aggregations: { + versions: { + buckets: versions.map((v) => ({ key: '0.3.3' })), + }, + }, + }); + } + + beforeEach(() => { + soClient = savedObjectsClientMock.create(); + packagePolicyServiceMock.list.mockClear(); + removeArchiveEntriesMock = jest.spyOn(storage, 'removeArchiveEntries') as any; + removeArchiveEntriesMock.mockClear(); + }); + it('should remove old assets from 2 versions if none of the policies are using it', async () => { + mockFindVersions(['0.3.3', '0.3.4']); + packagePolicyServiceMock.list.mockResolvedValue({ total: 0, items: [], page: 0, perPage: 0 }); + soClient.createPointInTimeFinder = jest.fn().mockResolvedValue({ + close: jest.fn(), + find: function* asyncGenerator() { + yield { saved_objects: [{ id: '1' }, { id: '2' }] }; + }, + }); + + await removeOldAssets({ soClient, pkgName: 'apache', currentVersion: '1.0.0' }); + + expect(removeArchiveEntriesMock).toHaveBeenCalledWith({ + savedObjectsClient: soClient, + refs: [ + { id: '1', type: 'epm-packages-assets' }, + { id: '2', type: 'epm-packages-assets' }, + ], + }); + expect(removeArchiveEntriesMock).toHaveBeenCalledTimes(2); + }); + + it('should not remove old assets if used by policies', async () => { + mockFindVersions(['0.3.3']); + packagePolicyServiceMock.list.mockResolvedValue({ total: 1, items: [], page: 0, perPage: 0 }); + + await removeOldAssets({ soClient, pkgName: 'apache', currentVersion: '1.0.0' }); + + expect(removeArchiveEntriesMock).not.toHaveBeenCalled(); + }); + + it('should remove old assets from all pages', async () => { + mockFindVersions(['0.3.3']); + packagePolicyServiceMock.list.mockResolvedValue({ total: 0, items: [], page: 0, perPage: 0 }); + soClient.createPointInTimeFinder = jest.fn().mockResolvedValue({ + close: jest.fn(), + find: function* asyncGenerator() { + yield { saved_objects: [{ id: '1' }, { id: '2' }] }; + yield { saved_objects: [{ id: '3' }] }; + }, + }); + + await removeOldAssets({ soClient, pkgName: 'apache', currentVersion: '1.0.0' }); + + expect(removeArchiveEntriesMock).toHaveBeenCalledWith({ + savedObjectsClient: soClient, + refs: [ + { id: '1', type: 'epm-packages-assets' }, + { id: '2', type: 'epm-packages-assets' }, + ], + }); + expect(removeArchiveEntriesMock).toHaveBeenCalledWith({ + savedObjectsClient: soClient, + refs: [{ id: '3', type: 'epm-packages-assets' }], + }); + expect(removeArchiveEntriesMock).toHaveBeenCalledTimes(2); + }); +}); diff --git a/x-pack/plugins/fleet/server/services/epm/packages/cleanup.ts b/x-pack/plugins/fleet/server/services/epm/packages/cleanup.ts new file mode 100644 index 0000000000000..d70beb53eddab --- /dev/null +++ b/x-pack/plugins/fleet/server/services/epm/packages/cleanup.ts @@ -0,0 +1,80 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license 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 } from 'src/core/server'; + +import { removeArchiveEntries } from '../archive/storage'; + +import { ASSETS_SAVED_OBJECT_TYPE, PACKAGE_POLICY_SAVED_OBJECT_TYPE } from '../../../../common'; +import type { PackageAssetReference } from '../../../../common'; +import { packagePolicyService } from '../../package_policy'; +import { appContextService } from '../..'; + +export async function removeOldAssets(options: { + soClient: SavedObjectsClientContract; + pkgName: string; + currentVersion: string; +}) { + const { soClient, pkgName, currentVersion } = options; + + // find all assets of older versions + const aggs = { + versions: { terms: { field: `${ASSETS_SAVED_OBJECT_TYPE}.attributes.package_version` } }, + }; + const oldVersionsAgg = await soClient.find({ + type: ASSETS_SAVED_OBJECT_TYPE, + filter: `${ASSETS_SAVED_OBJECT_TYPE}.attributes.package_name:${pkgName} AND ${ASSETS_SAVED_OBJECT_TYPE}.attributes.package_version<${currentVersion}`, + aggs, + page: 0, + perPage: 0, + }); + + const oldVersions = oldVersionsAgg.aggregations.versions.buckets.map( + (obj: { key: string }) => obj.key + ); + + for (const oldVersion of oldVersions) { + await removeAssetsFromVersion(soClient, pkgName, oldVersion); + } +} + +async function removeAssetsFromVersion( + soClient: SavedObjectsClientContract, + pkgName: string, + oldVersion: string +) { + // check if any policies are using this package version + const { total } = await packagePolicyService.list(soClient, { + kuery: `${PACKAGE_POLICY_SAVED_OBJECT_TYPE}.package.name:${pkgName} AND ${PACKAGE_POLICY_SAVED_OBJECT_TYPE}.package.version:${oldVersion}`, + page: 0, + perPage: 0, + }); + // don't delete if still being used + if (total > 0) { + appContextService + .getLogger() + .info(`Package "${pkgName}-${oldVersion}" still being used by policies`); + return; + } + + // check if old version has assets + const finder = await soClient.createPointInTimeFinder({ + type: ASSETS_SAVED_OBJECT_TYPE, + filter: `${ASSETS_SAVED_OBJECT_TYPE}.attributes.package_name:${pkgName} AND ${ASSETS_SAVED_OBJECT_TYPE}.attributes.package_version:${oldVersion}`, + perPage: 1000, + fields: ['id'], + }); + + for await (const assets of finder.find()) { + const refs = assets.saved_objects.map( + (obj) => ({ id: obj.id, type: ASSETS_SAVED_OBJECT_TYPE } as PackageAssetReference) + ); + + await removeArchiveEntries({ savedObjectsClient: soClient, refs }); + } + await finder.close(); +} 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 2568f40594f10..bd1968f03c263 100644 --- a/x-pack/plugins/fleet/server/services/epm/packages/install.ts +++ b/x-pack/plugins/fleet/server/services/epm/packages/install.ts @@ -38,6 +38,7 @@ import { isUnremovablePackage, getInstallation, getInstallationObject } from './ import { removeInstallation } from './remove'; import { getPackageSavedObjects } from './get'; import { _installPackage } from './_install_package'; +import { removeOldAssets } from './cleanup'; export async function isPackageInstalled(options: { savedObjectsClient: SavedObjectsClientContract; @@ -267,7 +268,12 @@ async function installPackageFromRegistry({ installType, installSource: 'registry', }) - .then((assets) => { + .then(async (assets) => { + await removeOldAssets({ + soClient: savedObjectsClient, + pkgName: packageInfo.name, + currentVersion: packageInfo.version, + }); return { assets, status: 'installed', installType }; }) .catch(async (err: Error) => { diff --git a/x-pack/plugins/fleet/server/services/epm/registry/requests.test.ts b/x-pack/plugins/fleet/server/services/epm/registry/requests.test.ts index 01fd4ad143d18..61f6cc164eb30 100644 --- a/x-pack/plugins/fleet/server/services/epm/registry/requests.test.ts +++ b/x-pack/plugins/fleet/server/services/epm/registry/requests.test.ts @@ -15,7 +15,7 @@ const { Response, FetchError } = jest.requireActual('node-fetch'); const fetchMock = require('node-fetch') as jest.Mock; jest.setTimeout(120 * 1000); -describe('setupIngestManager', () => { +describe('Registry request', () => { beforeEach(async () => {}); afterEach(async () => { diff --git a/x-pack/plugins/fleet/server/services/fleet_server/saved_object_migrations.ts b/x-pack/plugins/fleet/server/services/fleet_server/saved_object_migrations.ts index 379bc8fa39bff..bbaf9c9479eb4 100644 --- a/x-pack/plugins/fleet/server/services/fleet_server/saved_object_migrations.ts +++ b/x-pack/plugins/fleet/server/services/fleet_server/saved_object_migrations.ts @@ -190,11 +190,7 @@ async function migrateAgentPolicies() { // @ts-expect-error value is number | TotalHits if (res.body.hits.total.value === 0) { - return agentPolicyService.createFleetPolicyChangeFleetServer( - soClient, - esClient, - agentPolicy.id - ); + return agentPolicyService.createFleetServerPolicy(soClient, agentPolicy.id); } }) ); diff --git a/x-pack/plugins/fleet/server/services/output.test.ts b/x-pack/plugins/fleet/server/services/output.test.ts index 26e3955607ada..8103794fb0805 100644 --- a/x-pack/plugins/fleet/server/services/output.test.ts +++ b/x-pack/plugins/fleet/server/services/output.test.ts @@ -5,8 +5,10 @@ * 2.0. */ -import { outputService } from './output'; +import { savedObjectsClientMock } from '../../../../../src/core/server/mocks'; +import type { OutputSOAttributes } from '../types'; +import { outputService, outputIdToUuid } from './output'; import { appContextService } from './app_context'; jest.mock('./app_context'); @@ -34,7 +36,97 @@ const CONFIG_WITHOUT_ES_HOSTS = { }, }; +function getMockedSoClient() { + 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', + }, + }; + } + default: + throw new Error('not found'); + } + }); + + return soClient; +} + 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, + name: 'Test', + type: 'elasticsearch', + }, + { id: 'output-test' } + ); + + expect(soClient.create).toBeCalled(); + + // ID should always be the same for a predefined id + expect(soClient.create.mock.calls[0][2]?.id).toEqual(outputIdToUuid('output-test')); + expect((soClient.create.mock.calls[0][1] as OutputSOAttributes).output_id).toEqual( + 'output-test' + ); + }); + }); + + describe('get', () => { + it('work with a predefined id', async () => { + const soClient = getMockedSoClient(); + const output = await outputService.get(soClient, 'output-test'); + + expect(soClient.get).toHaveBeenCalledWith('ingest-outputs', outputIdToUuid('output-test')); + + expect(output.id).toEqual('output-test'); + }); + }); + + describe('getDefaultOutputId', () => { + 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 defaultId = await outputService.getDefaultOutputId(soClient); + + expect(soClient.find).toHaveBeenCalled(); + + expect(defaultId).toEqual('output-test'); + }); + }); + describe('getDefaultESHosts', () => { afterEach(() => { mockedAppContextService.getConfig.mockReset(); diff --git a/x-pack/plugins/fleet/server/services/output.ts b/x-pack/plugins/fleet/server/services/output.ts index 8c6bc7eca0401..5a7ba1e2c1223 100644 --- a/x-pack/plugins/fleet/server/services/output.ts +++ b/x-pack/plugins/fleet/server/services/output.ts @@ -5,7 +5,8 @@ * 2.0. */ -import type { SavedObjectsClientContract } from 'src/core/server'; +import type { SavedObject, SavedObjectsClientContract } from 'src/core/server'; +import uuid from 'uuid/v5'; import type { NewOutput, Output, OutputSOAttributes } from '../types'; import { DEFAULT_OUTPUT, OUTPUT_SAVED_OBJECT_TYPE } from '../constants'; @@ -17,8 +18,33 @@ const SAVED_OBJECT_TYPE = OUTPUT_SAVED_OBJECT_TYPE; const DEFAULT_ES_HOSTS = ['http://localhost:9200']; +// differentiate +function isUUID(val: string) { + return ( + typeof val === 'string' && + val.match(/[0-9a-fA-F]{8}\-[0-9a-fA-F]{4}\-[0-9a-fA-F]{4}\-[0-9a-fA-F]{4}\-[0-9a-fA-F]{12}/) + ); +} + +export function outputIdToUuid(id: string) { + if (isUUID(id)) { + return id; + } + + // UUID v5 need a namespace (uuid.DNS), changing this params will result in loosing the ability to generate predicable uuid + return uuid(id, uuid.DNS); +} + +function outputSavedObjectToOutput(so: SavedObject) { + const { output_id: outputId, ...atributes } = so.attributes; + return { + id: outputId ?? so.id, + ...atributes, + }; +} + class OutputService { - public async getDefaultOutput(soClient: SavedObjectsClientContract) { + private async _getDefaultOutputsSO(soClient: SavedObjectsClientContract) { return await soClient.find({ type: OUTPUT_SAVED_OBJECT_TYPE, searchFields: ['is_default'], @@ -27,7 +53,7 @@ class OutputService { } public async ensureDefaultOutput(soClient: SavedObjectsClientContract) { - const outputs = await this.getDefaultOutput(soClient); + const outputs = await this._getDefaultOutputsSO(soClient); if (!outputs.saved_objects.length) { const newDefaultOutput = { @@ -39,10 +65,7 @@ class OutputService { return await this.create(soClient, newDefaultOutput); } - return { - id: outputs.saved_objects[0].id, - ...outputs.saved_objects[0].attributes, - }; + return outputSavedObjectToOutput(outputs.saved_objects[0]); } public getDefaultESHosts(): string[] { @@ -60,49 +83,84 @@ class OutputService { } public async getDefaultOutputId(soClient: SavedObjectsClientContract) { - const outputs = await this.getDefaultOutput(soClient); + const outputs = await this._getDefaultOutputsSO(soClient); if (!outputs.saved_objects.length) { return null; } - return outputs.saved_objects[0].id; + return outputSavedObjectToOutput(outputs.saved_objects[0]).id; } public async create( soClient: SavedObjectsClientContract, output: NewOutput, - options?: { id?: string } + options?: { id?: string; overwrite?: boolean } ): Promise { - const data = { ...output }; + 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})`); + } + } if (data.hosts) { data.hosts = data.hosts.map(normalizeHostsForAgents); } - const newSo = await soClient.create( - SAVED_OBJECT_TYPE, - data as Output, - options - ); + if (options?.id) { + data.output_id = options?.id; + } + + const newSo = await soClient.create(SAVED_OBJECT_TYPE, data, { + ...options, + id: options?.id ? outputIdToUuid(options.id) : undefined, + }); return { - id: newSo.id, + id: options?.id ?? newSo.id, ...newSo.attributes, }; } + public async bulkGet( + soClient: SavedObjectsClientContract, + ids: string[], + { ignoreNotFound = false } = { ignoreNotFound: true } + ) { + const res = await soClient.bulkGet( + ids.map((id) => ({ id: outputIdToUuid(id), type: SAVED_OBJECT_TYPE })) + ); + + return res.saved_objects + .map((so) => { + if (so.error) { + if (!ignoreNotFound || so.error.statusCode !== 404) { + throw so.error; + } + return undefined; + } + + return outputSavedObjectToOutput(so); + }) + .filter((output): output is Output => typeof output !== 'undefined'); + } + public async get(soClient: SavedObjectsClientContract, id: string): Promise { - const outputSO = await soClient.get(SAVED_OBJECT_TYPE, id); + const outputSO = await soClient.get(SAVED_OBJECT_TYPE, outputIdToUuid(id)); if (outputSO.error) { throw new Error(outputSO.error.message); } - return { - id: outputSO.id, - ...outputSO.attributes, - }; + return outputSavedObjectToOutput(outputSO); + } + + public async delete(soClient: SavedObjectsClientContract, id: string) { + return soClient.delete(SAVED_OBJECT_TYPE, outputIdToUuid(id)); } public async update(soClient: SavedObjectsClientContract, id: string, data: Partial) { @@ -111,8 +169,11 @@ class OutputService { if (updateData.hosts) { updateData.hosts = updateData.hosts.map(normalizeHostsForAgents); } - - const outputSO = await soClient.update(SAVED_OBJECT_TYPE, id, updateData); + const outputSO = await soClient.update( + SAVED_OBJECT_TYPE, + outputIdToUuid(id), + updateData + ); if (outputSO.error) { throw new Error(outputSO.error.message); @@ -127,12 +188,7 @@ class OutputService { }); return { - items: outputs.saved_objects.map((outputSO) => { - return { - id: outputSO.id, - ...outputSO.attributes, - }; - }), + items: outputs.saved_objects.map(outputSavedObjectToOutput), total: outputs.total, page: 1, perPage: 1000, diff --git a/x-pack/plugins/fleet/server/services/package_policies_to_agent_permissions.test.ts b/x-pack/plugins/fleet/server/services/package_policies_to_agent_permissions.test.ts index a84118cdf1bfa..9f8ac01afe6c9 100644 --- a/x-pack/plugins/fleet/server/services/package_policies_to_agent_permissions.test.ts +++ b/x-pack/plugins/fleet/server/services/package_policies_to_agent_permissions.test.ts @@ -13,7 +13,7 @@ import type { PackagePolicy, RegistryDataStream } from '../types'; import { getPackageInfo } from './epm/packages'; import { - getDataStreamPermissions, + getDataStreamPrivileges, storedPackagePoliciesToAgentPermissions, } from './package_policies_to_agent_permissions'; @@ -380,12 +380,12 @@ describe('storedPackagePoliciesToAgentPermissions()', () => { }); }); -describe('getDataStreamPermissions()', () => { - it('returns defaults for a datastream with no permissions', () => { +describe('getDataStreamPrivileges()', () => { + it('returns defaults for a datastream with no privileges', () => { const dataStream = { type: 'logs', dataset: 'test' } as RegistryDataStream; - const permissions = getDataStreamPermissions(dataStream); + const privileges = getDataStreamPrivileges(dataStream); - expect(permissions).toMatchObject({ + expect(privileges).toMatchObject({ names: ['logs-test-*'], privileges: ['auto_configure', 'create_doc'], }); @@ -393,9 +393,9 @@ describe('getDataStreamPermissions()', () => { it('adds the namespace to the index name', () => { const dataStream = { type: 'logs', dataset: 'test' } as RegistryDataStream; - const permissions = getDataStreamPermissions(dataStream, 'namespace'); + const privileges = getDataStreamPrivileges(dataStream, 'namespace'); - expect(permissions).toMatchObject({ + expect(privileges).toMatchObject({ names: ['logs-test-namespace'], privileges: ['auto_configure', 'create_doc'], }); @@ -407,9 +407,9 @@ describe('getDataStreamPermissions()', () => { dataset: 'test', dataset_is_prefix: true, } as RegistryDataStream; - const permissions = getDataStreamPermissions(dataStream, 'namespace'); + const privileges = getDataStreamPrivileges(dataStream, 'namespace'); - expect(permissions).toMatchObject({ + expect(privileges).toMatchObject({ names: ['logs-test.*-namespace'], privileges: ['auto_configure', 'create_doc'], }); @@ -421,25 +421,27 @@ describe('getDataStreamPermissions()', () => { dataset: 'test', hidden: true, } as RegistryDataStream; - const permissions = getDataStreamPermissions(dataStream, 'namespace'); + const privileges = getDataStreamPrivileges(dataStream, 'namespace'); - expect(permissions).toMatchObject({ + expect(privileges).toMatchObject({ names: ['.logs-test-namespace'], privileges: ['auto_configure', 'create_doc'], }); }); - it('uses custom permissions if they are present in the datastream', () => { + it('uses custom privileges if they are present in the datastream', () => { const dataStream = { type: 'logs', dataset: 'test', - permissions: { indices: ['read', 'write'] }, + elasticsearch: { + privileges: { indices: ['read', 'monitor'] }, + }, } as RegistryDataStream; - const permissions = getDataStreamPermissions(dataStream, 'namespace'); + const privileges = getDataStreamPrivileges(dataStream, 'namespace'); - expect(permissions).toMatchObject({ + expect(privileges).toMatchObject({ names: ['logs-test-namespace'], - privileges: ['read', 'write'], + privileges: ['read', 'monitor'], }); }); }); diff --git a/x-pack/plugins/fleet/server/services/package_policies_to_agent_permissions.ts b/x-pack/plugins/fleet/server/services/package_policies_to_agent_permissions.ts index 07ad892adc653..22dcb8ac7b4cb 100644 --- a/x-pack/plugins/fleet/server/services/package_policies_to_agent_permissions.ts +++ b/x-pack/plugins/fleet/server/services/package_policies_to_agent_permissions.ts @@ -6,9 +6,9 @@ */ import type { SavedObjectsClientContract } from 'kibana/server'; -import type { FullAgentPolicyOutputPermissions, RegistryDataStreamPermissions } from '../../common'; +import type { FullAgentPolicyOutputPermissions, RegistryDataStreamPrivileges } from '../../common'; +import { PACKAGE_POLICY_DEFAULT_INDEX_PRIVILEGES } from '../constants'; import { getPackageInfo } from '../../server/services/epm/packages'; - import type { PackagePolicy } from '../types'; export const DEFAULT_PERMISSIONS = { @@ -22,7 +22,7 @@ export const DEFAULT_PERMISSIONS = { 'synthetics-*', '.logs-endpoint.diagnostic.collection-*', ], - privileges: ['auto_configure', 'create_doc'], + privileges: PACKAGE_POLICY_DEFAULT_INDEX_PRIVILEGES, }, ], }; @@ -104,12 +104,16 @@ export async function storedPackagePoliciesToAgentPermissions( return; } - const ds = { + const ds: DataStreamMeta = { type: stream.data_stream.type, dataset: stream.compiled_stream?.data_stream?.dataset ?? stream.data_stream.dataset, }; + if (stream.data_stream.elasticsearch) { + ds.elasticsearch = stream.data_stream.elasticsearch; + } + dataStreams_.push(ds); }); @@ -121,7 +125,7 @@ export async function storedPackagePoliciesToAgentPermissions( packagePolicy.name, { indices: dataStreamsForPermissions.map((ds) => - getDataStreamPermissions(ds, packagePolicy.namespace) + getDataStreamPrivileges(ds, packagePolicy.namespace) ), }, ]; @@ -136,10 +140,12 @@ interface DataStreamMeta { dataset: string; dataset_is_prefix?: boolean; hidden?: boolean; - permissions?: RegistryDataStreamPermissions; + elasticsearch?: { + privileges?: RegistryDataStreamPrivileges; + }; } -export function getDataStreamPermissions(dataStream: DataStreamMeta, namespace: string = '*') { +export function getDataStreamPrivileges(dataStream: DataStreamMeta, namespace: string = '*') { let index = `${dataStream.type}-${dataStream.dataset}`; if (dataStream.dataset_is_prefix) { @@ -152,8 +158,12 @@ export function getDataStreamPermissions(dataStream: DataStreamMeta, namespace: index += `-${namespace}`; + const privileges = dataStream?.elasticsearch?.privileges?.indices?.length + ? dataStream.elasticsearch.privileges.indices + : PACKAGE_POLICY_DEFAULT_INDEX_PRIVILEGES; + return { names: [index], - privileges: dataStream.permissions?.indices || ['auto_configure', 'create_doc'], + privileges, }; } 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 aa70fc155fe74..fe5a3030bd95a 100644 --- a/x-pack/plugins/fleet/server/services/package_policy.test.ts +++ b/x-pack/plugins/fleet/server/services/package_policy.test.ts @@ -14,19 +14,25 @@ import { import type { SavedObjectsClient, SavedObjectsUpdateResponse } from 'src/core/server'; import type { KibanaRequest } from 'kibana/server'; -import type { PackageInfo, PackagePolicySOAttributes, AgentPolicySOAttributes } from '../types'; +import type { + PackageInfo, + PackagePolicySOAttributes, + AgentPolicySOAttributes, + PostPackagePolicyDeleteCallback, + RegistryDataStream, + PackagePolicyInputStream, +} from '../types'; import { createPackagePolicyMock } from '../../common/mocks'; + import type { PutPackagePolicyUpdateCallback, PostPackagePolicyCreateCallback } from '..'; import { createAppContextStartContractMock, xpackMocks } from '../mocks'; -import type { PostPackagePolicyDeleteCallback } from '../types'; - import type { DeletePackagePoliciesResponse } from '../../common'; import { IngestManagerError } from '../errors'; -import { packagePolicyService } from './package_policy'; +import { packagePolicyService, _applyIndexPrivileges } from './package_policy'; import { appContextService } from './app_context'; async function mockedGetAssetsData(_a: any, _b: any, dataset: string) { @@ -1069,3 +1075,119 @@ describe('Package policy service', () => { }); }); }); + +describe('_applyIndexPrivileges()', () => { + function createPackageStream(indexPrivileges?: string[]): RegistryDataStream { + const stream: RegistryDataStream = { + type: '', + dataset: '', + title: '', + release: '', + package: '', + path: '', + }; + + if (indexPrivileges) { + stream.elasticsearch = { + privileges: { + indices: indexPrivileges, + }, + }; + } + + return stream; + } + + function createInputStream( + opts: Partial = {} + ): PackagePolicyInputStream { + return { + id: '', + enabled: true, + data_stream: { + dataset: '', + type: '', + }, + ...opts, + }; + } + + beforeAll(async () => { + appContextService.start(createAppContextStartContractMock()); + }); + + it('should do nothing if packageStream has no privileges', () => { + const packageStream = createPackageStream(); + const inputStream = createInputStream(); + + const streamOut = _applyIndexPrivileges(packageStream, inputStream); + expect(streamOut).toEqual(inputStream); + }); + + it('should not apply privileges if all privileges are forbidden', () => { + const forbiddenPrivileges = ['write', 'delete', 'delete_index', 'all']; + const packageStream = createPackageStream(forbiddenPrivileges); + const inputStream = createInputStream(); + + const streamOut = _applyIndexPrivileges(packageStream, inputStream); + expect(streamOut).toEqual(inputStream); + }); + + it('should not apply privileges if all privileges are unrecognized', () => { + const unrecognizedPrivileges = ['idnotexist', 'invalidperm']; + const packageStream = createPackageStream(unrecognizedPrivileges); + const inputStream = createInputStream(); + + const streamOut = _applyIndexPrivileges(packageStream, inputStream); + expect(streamOut).toEqual(inputStream); + }); + + it('should apply privileges if all privileges are valid', () => { + const validPrivileges = [ + 'auto_configure', + 'create_doc', + 'maintenance', + 'monitor', + 'read', + 'read_cross_cluster', + ]; + + const packageStream = createPackageStream(validPrivileges); + const inputStream = createInputStream(); + const expectedStream = { + ...inputStream, + data_stream: { + ...inputStream.data_stream, + elasticsearch: { + privileges: { + indices: validPrivileges, + }, + }, + }, + }; + + const streamOut = _applyIndexPrivileges(packageStream, inputStream); + expect(streamOut).toEqual(expectedStream); + }); + + it('should only apply valid privileges when there is a mix of valid and invalid', () => { + const mixedPrivileges = ['auto_configure', 'read_cross_cluster', 'idontexist', 'delete']; + + const packageStream = createPackageStream(mixedPrivileges); + const inputStream = createInputStream(); + const expectedStream = { + ...inputStream, + data_stream: { + ...inputStream.data_stream, + elasticsearch: { + privileges: { + indices: ['auto_configure', 'read_cross_cluster'], + }, + }, + }, + }; + + const streamOut = _applyIndexPrivileges(packageStream, inputStream); + expect(streamOut).toEqual(expectedStream); + }); +}); diff --git a/x-pack/plugins/fleet/server/services/package_policy.ts b/x-pack/plugins/fleet/server/services/package_policy.ts index 598dd16b2928e..d84d50e00b8a9 100644 --- a/x-pack/plugins/fleet/server/services/package_policy.ts +++ b/x-pack/plugins/fleet/server/services/package_policy.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { omit } from 'lodash'; +import { omit, partition } from 'lodash'; import { i18n } from '@kbn/i18n'; import semverLte from 'semver/functions/lte'; import { getFlattenedObject } from '@kbn/std'; @@ -38,6 +38,7 @@ import type { ListWithKuery, ListResult, UpgradePackagePolicyDryRunResponseItem, + RegistryDataStream, } from '../../common'; import { PACKAGE_POLICY_SAVED_OBJECT_TYPE } from '../constants'; import { @@ -64,6 +65,7 @@ import { getAssetsData } from './epm/packages/assets'; import { compileTemplate } from './epm/agent/agent'; import { normalizeKuery } from './saved_object'; import { appContextService } from '.'; +import { removeOldAssets } from './epm/packages/cleanup'; export type InputsOverride = Partial & { vars?: Array; @@ -71,6 +73,15 @@ export type InputsOverride = Partial & { const SAVED_OBJECT_TYPE = PACKAGE_POLICY_SAVED_OBJECT_TYPE; +export const DATA_STREAM_ALLOWED_INDEX_PRIVILEGES = new Set([ + 'auto_configure', + 'create_doc', + 'maintenance', + 'monitor', + 'read', + 'read_cross_cluster', +]); + class PackagePolicyService { public async create( soClient: SavedObjectsClientContract, @@ -565,6 +576,11 @@ class PackagePolicyService { name: packagePolicy.name, success: true, }); + await removeOldAssets({ + soClient, + pkgName: packageInfo.name, + currentVersion: packageInfo.version, + }); } catch (error) { result.push({ id, @@ -794,13 +810,51 @@ async function _compilePackageStreams( return await Promise.all(streamsPromises); } +// temporary export to enable testing pending refactor https://github.com/elastic/kibana/issues/112386 +export function _applyIndexPrivileges( + packageDataStream: RegistryDataStream, + stream: PackagePolicyInputStream +): PackagePolicyInputStream { + const streamOut = { ...stream }; + + const indexPrivileges = packageDataStream?.elasticsearch?.privileges?.indices; + + if (!indexPrivileges?.length) { + return streamOut; + } + + const [valid, invalid] = partition(indexPrivileges, (permission) => + DATA_STREAM_ALLOWED_INDEX_PRIVILEGES.has(permission) + ); + + if (invalid.length) { + appContextService + .getLogger() + .warn( + `Ignoring invalid or forbidden index privilege(s) in "${stream.id}" data stream: ${invalid}` + ); + } + + if (valid.length) { + stream.data_stream.elasticsearch = { + privileges: { + indices: valid, + }, + }; + } + + return streamOut; +} + async function _compilePackageStream( registryPkgInfo: RegistryPackage, pkgInfo: PackageInfo, vars: PackagePolicy['vars'], input: PackagePolicyInput, - stream: PackagePolicyInputStream + streamIn: PackagePolicyInputStream ) { + let stream = streamIn; + if (!stream.enabled) { return { ...stream, compiled_stream: undefined }; } @@ -820,6 +874,8 @@ async function _compilePackageStream( ); } + stream = _applyIndexPrivileges(packageDataStream, streamIn); + const streamFromPkg = (packageDataStream.streams || []).find( (pkgStream) => pkgStream.input === input.type ); diff --git a/x-pack/plugins/fleet/server/services/preconfiguration.test.ts b/x-pack/plugins/fleet/server/services/preconfiguration.test.ts index 86fdd2f0aa800..43887bc2787f4 100644 --- a/x-pack/plugins/fleet/server/services/preconfiguration.test.ts +++ b/x-pack/plugins/fleet/server/services/preconfiguration.test.ts @@ -9,7 +9,7 @@ import { elasticsearchServiceMock, savedObjectsClientMock } from 'src/core/serve import { SavedObjectsErrorHelpers } from '../../../../../src/core/server'; -import type { PreconfiguredAgentPolicy } from '../../common/types'; +import type { PreconfiguredAgentPolicy, PreconfiguredOutput } from '../../common/types'; import type { AgentPolicy, NewPackagePolicy, Output } from '../types'; import { AGENT_POLICY_SAVED_OBJECT_TYPE } from '../constants'; @@ -19,9 +19,15 @@ import * as agentPolicy from './agent_policy'; import { ensurePreconfiguredPackagesAndPolicies, comparePreconfiguredPolicyToCurrent, + ensurePreconfiguredOutputs, + cleanPreconfiguredOutputs, } from './preconfiguration'; +import { outputService } from './output'; jest.mock('./agent_policy_update'); +jest.mock('./output'); + +const mockedOutputService = outputService as jest.Mocked; const mockInstalledPackages = new Map(); const mockConfiguredPolicies = new Map(); @@ -156,12 +162,17 @@ jest.mock('./app_context', () => ({ })); const spyAgentPolicyServiceUpdate = jest.spyOn(agentPolicy.agentPolicyService, 'update'); +const spyAgentPolicyServicBumpAllAgentPoliciesForOutput = jest.spyOn( + agentPolicy.agentPolicyService, + 'bumpAllAgentPoliciesForOutput' +); describe('policy preconfiguration', () => { beforeEach(() => { mockInstalledPackages.clear(); mockConfiguredPolicies.clear(); spyAgentPolicyServiceUpdate.mockClear(); + spyAgentPolicyServicBumpAllAgentPoliciesForOutput.mockClear(); }); it('should perform a no-op when passed no policies or packages', async () => { @@ -480,3 +491,168 @@ describe('comparePreconfiguredPolicyToCurrent', () => { expect(hasChanged).toBe(false); }); }); + +describe('output preconfiguration', () => { + beforeEach(() => { + mockedOutputService.create.mockReset(); + mockedOutputService.update.mockReset(); + mockedOutputService.getDefaultESHosts.mockReturnValue(['http://default-es:9200']); + mockedOutputService.bulkGet.mockImplementation(async (soClient, id): Promise => { + return [ + { + id: 'existing-output-1', + is_default: false, + name: 'Output 1', + // @ts-ignore + type: 'elasticsearch', + hosts: ['http://es.co:80'], + is_preconfigured: true, + }, + ]; + }); + }); + + it('should create preconfigured output that does not exists', async () => { + const soClient = savedObjectsClientMock.create(); + const esClient = elasticsearchServiceMock.createClusterClient().asInternalUser; + await ensurePreconfiguredOutputs(soClient, esClient, [ + { + id: 'non-existing-output-1', + name: 'Output 1', + type: 'elasticsearch', + is_default: false, + hosts: ['http://test.fr'], + }, + ]); + + 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; + await ensurePreconfiguredOutputs(soClient, esClient, [ + { + id: 'non-existing-output-1', + name: 'Output 1', + type: 'elasticsearch', + is_default: false, + }, + ]); + + expect(mockedOutputService.create).toBeCalled(); + expect(mockedOutputService.create.mock.calls[0][1].hosts).toEqual(['http://default-es:9200']); + }); + + it('should update output if preconfigured 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 }); + await ensurePreconfiguredOutputs(soClient, esClient, [ + { + id: 'existing-output-1', + is_default: false, + name: 'Output 1', + type: 'elasticsearch', + hosts: ['http://newhostichanged.co:9201'], // field that changed + }, + ]); + + expect(mockedOutputService.create).not.toBeCalled(); + expect(mockedOutputService.update).toBeCalled(); + expect(spyAgentPolicyServicBumpAllAgentPoliciesForOutput).toBeCalled(); + }); + + const SCENARIOS: Array<{ name: string; data: PreconfiguredOutput }> = [ + { + name: 'no changes', + data: { + id: 'existing-output-1', + is_default: false, + name: 'Output 1', + type: 'elasticsearch', + hosts: ['http://es.co:80'], + }, + }, + { + name: 'hosts without port', + data: { + id: 'existing-output-1', + is_default: false, + name: 'Output 1', + type: 'elasticsearch', + hosts: ['http://es.co'], + }, + }, + ]; + SCENARIOS.forEach((scenario) => { + const { data, name } = scenario; + it(`should do nothing if preconfigured output exists and did not changed (${name})`, async () => { + const soClient = savedObjectsClientMock.create(); + const esClient = elasticsearchServiceMock.createClusterClient().asInternalUser; + await ensurePreconfiguredOutputs(soClient, esClient, [data]); + + expect(mockedOutputService.create).not.toBeCalled(); + expect(mockedOutputService.update).not.toBeCalled(); + }); + }); + + it('should not delete non deleted preconfigured output', async () => { + const soClient = savedObjectsClientMock.create(); + mockedOutputService.list.mockResolvedValue({ + items: [ + { id: 'output1', is_preconfigured: true } as Output, + { id: 'output2', is_preconfigured: true } as Output, + ], + page: 1, + perPage: 10000, + total: 1, + }); + await cleanPreconfiguredOutputs(soClient, [ + { + id: 'output1', + is_default: false, + name: 'Output 1', + type: 'elasticsearch', + hosts: ['http://es.co:9201'], + }, + { + id: 'output2', + is_default: false, + name: 'Output 2', + type: 'elasticsearch', + hosts: ['http://es.co:9201'], + }, + ]); + + expect(mockedOutputService.delete).not.toBeCalled(); + }); + + it('should delete deleted preconfigured output', async () => { + const soClient = savedObjectsClientMock.create(); + mockedOutputService.list.mockResolvedValue({ + items: [ + { id: 'output1', is_preconfigured: true } as Output, + { id: 'output2', is_preconfigured: true } as Output, + ], + page: 1, + perPage: 10000, + total: 1, + }); + await cleanPreconfiguredOutputs(soClient, [ + { + id: 'output1', + is_default: false, + name: 'Output 1', + type: 'elasticsearch', + hosts: ['http://es.co:9201'], + }, + ]); + + expect(mockedOutputService.delete).toBeCalled(); + expect(mockedOutputService.delete).toBeCalledTimes(1); + expect(mockedOutputService.delete.mock.calls[0][1]).toEqual('output2'); + }); +}); diff --git a/x-pack/plugins/fleet/server/services/preconfiguration.ts b/x-pack/plugins/fleet/server/services/preconfiguration.ts index 37ed98a6f4aa0..30c5c27c68916 100644 --- a/x-pack/plugins/fleet/server/services/preconfiguration.ts +++ b/x-pack/plugins/fleet/server/services/preconfiguration.ts @@ -8,6 +8,7 @@ import type { ElasticsearchClient, SavedObjectsClientContract } from 'src/core/server'; import { i18n } from '@kbn/i18n'; import { groupBy, omit, pick, isEqual } from 'lodash'; +import { safeDump } from 'js-yaml'; import type { NewPackagePolicy, @@ -17,16 +18,15 @@ import type { PreconfiguredAgentPolicy, PreconfiguredPackage, PreconfigurationError, + PreconfiguredOutput, } from '../../common'; -import { AGENT_POLICY_SAVED_OBJECT_TYPE } from '../../common'; - +import { AGENT_POLICY_SAVED_OBJECT_TYPE, normalizeHostsForAgents } from '../../common'; import { PRECONFIGURATION_DELETION_RECORD_SAVED_OBJECT_TYPE, PRECONFIGURATION_LATEST_KEYWORD, } from '../constants'; import { escapeSearchQueryPhrase } from './saved_object'; - import { pkgToPkgKey } from './epm/registry'; import { getInstallation, getPackageInfo } from './epm/packages'; import { ensurePackagesCompletedInstall } from './epm/packages/install'; @@ -35,6 +35,7 @@ import { agentPolicyService, addPackageToAgentPolicy } from './agent_policy'; import type { InputsOverride } from './package_policy'; import { overridePackageInputs } from './package_policy'; import { appContextService } from './app_context'; +import { outputService } from './output'; interface PreconfigurationResult { policies: Array<{ id: string; updated_at: string }>; @@ -42,6 +43,89 @@ interface PreconfigurationResult { nonFatalErrors: PreconfigurationError[]; } +function isPreconfiguredOutputDifferentFromCurrent( + existingOutput: Output, + preconfiguredOutput: Partial +): boolean { + return ( + existingOutput.is_default !== preconfiguredOutput.is_default || + existingOutput.name !== preconfiguredOutput.name || + existingOutput.type !== preconfiguredOutput.type || + (preconfiguredOutput.hosts && + !isEqual( + existingOutput.hosts?.map(normalizeHostsForAgents), + preconfiguredOutput.hosts.map(normalizeHostsForAgents) + )) || + existingOutput.ca_sha256 !== preconfiguredOutput.ca_sha256 || + existingOutput.config_yaml !== preconfiguredOutput.config_yaml + ); +} + +export async function ensurePreconfiguredOutputs( + soClient: SavedObjectsClientContract, + esClient: ElasticsearchClient, + outputs: PreconfiguredOutput[] +) { + if (outputs.length === 0) { + return; + } + + const existingOutputs = await outputService.bulkGet( + soClient, + outputs.map(({ id }) => id), + { ignoreNotFound: true } + ); + + await Promise.all( + outputs.map(async (output) => { + const existingOutput = existingOutputs.find((o) => o.id === output.id); + + const { id, config, ...outputData } = output; + + const configYaml = config ? safeDump(config) : undefined; + + const data = { + ...outputData, + config_yaml: configYaml, + is_preconfigured: true, + }; + + if (!data.hosts || data.hosts.length === 0) { + data.hosts = outputService.getDefaultESHosts(); + } + + if (!existingOutput) { + await outputService.create(soClient, data, { id, overwrite: true }); + } else if (isPreconfiguredOutputDifferentFromCurrent(existingOutput, data)) { + await outputService.update(soClient, id, data); + // Bump revision of all policies using that output + if (outputData.is_default) { + await agentPolicyService.bumpAllAgentPolicies(soClient, esClient); + } else { + await agentPolicyService.bumpAllAgentPoliciesForOutput(soClient, esClient, id); + } + } + }) + ); +} + +export async function cleanPreconfiguredOutputs( + soClient: SavedObjectsClientContract, + outputs: PreconfiguredOutput[] +) { + const existingPreconfiguredOutput = (await outputService.list(soClient)).items.filter( + (o) => o.is_preconfigured === true + ); + const logger = appContextService.getLogger(); + + 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); + } + } +} + export async function ensurePreconfiguredPackagesAndPolicies( soClient: SavedObjectsClientContract, esClient: ElasticsearchClient, @@ -224,7 +308,7 @@ export async function ensurePreconfiguredPackagesAndPolicies( } // Add the is_managed flag after configuring package policies to avoid errors if (shouldAddIsManagedFlag) { - agentPolicyService.update(soClient, esClient, policy!.id, { is_managed: true }); + await agentPolicyService.update(soClient, esClient, policy!.id, { is_managed: true }); } } } diff --git a/x-pack/plugins/fleet/server/services/setup.test.ts b/x-pack/plugins/fleet/server/services/setup.test.ts index 212b0fabd26fb..e6b76694a9fca 100644 --- a/x-pack/plugins/fleet/server/services/setup.test.ts +++ b/x-pack/plugins/fleet/server/services/setup.test.ts @@ -8,7 +8,7 @@ import { createAppContextStartContractMock, xpackMocks } from '../mocks'; import { appContextService } from './app_context'; -import { setupIngestManager } from './setup'; +import { setupFleet } from './setup'; const mockedMethodThrowsError = () => jest.fn().mockImplementation(() => { @@ -21,7 +21,7 @@ const mockedMethodThrowsCustom = () => throw new CustomTestError('method mocked to throw'); }); -describe('setupIngestManager', () => { +describe('setupFleet', () => { let context: ReturnType; beforeEach(async () => { @@ -44,7 +44,7 @@ describe('setupIngestManager', () => { soClient.update = mockedMethodThrowsError(); const esClient = context.core.elasticsearch.client.asCurrentUser; - const setupPromise = setupIngestManager(soClient, esClient); + const setupPromise = setupFleet(soClient, esClient); await expect(setupPromise).rejects.toThrow('SO method mocked to throw'); await expect(setupPromise).rejects.toThrow(Error); }); @@ -57,7 +57,7 @@ describe('setupIngestManager', () => { soClient.update = mockedMethodThrowsCustom(); const esClient = context.core.elasticsearch.client.asCurrentUser; - const setupPromise = setupIngestManager(soClient, esClient); + const setupPromise = setupFleet(soClient, esClient); await expect(setupPromise).rejects.toThrow('method mocked to throw'); await expect(setupPromise).rejects.toThrow(CustomTestError); }); diff --git a/x-pack/plugins/fleet/server/services/setup.ts b/x-pack/plugins/fleet/server/services/setup.ts index 1f3c3c5082b34..08c580d80c804 100644 --- a/x-pack/plugins/fleet/server/services/setup.ts +++ b/x-pack/plugins/fleet/server/services/setup.ts @@ -15,13 +15,17 @@ import { SO_SEARCH_LIMIT, DEFAULT_PACKAGES } from '../constants'; import { appContextService } from './app_context'; import { agentPolicyService } from './agent_policy'; -import { ensurePreconfiguredPackagesAndPolicies } from './preconfiguration'; +import { + cleanPreconfiguredOutputs, + ensurePreconfiguredOutputs, + ensurePreconfiguredPackagesAndPolicies, +} from './preconfiguration'; import { outputService } from './output'; import { generateEnrollmentAPIKey, hasEnrollementAPIKeysForPolicy } from './api_keys'; import { settingsService } from '.'; import { awaitIfPending } from './setup_utils'; -import { ensureAgentActionPolicyChangeExists } from './agents'; +import { ensureFleetServerAgentPoliciesExists } from './agents'; import { awaitIfFleetServerSetupPending } from './fleet_server'; import { ensureFleetFinalPipelineIsInstalled } from './epm/elasticsearch/ingest_pipeline/install'; import { ensureDefaultComponentTemplate } from './epm/elasticsearch/template/install'; @@ -34,7 +38,7 @@ export interface SetupStatus { nonFatalErrors: Array; } -export async function setupIngestManager( +export async function setupFleet( soClient: SavedObjectsClientContract, esClient: ElasticsearchClient ): Promise { @@ -45,23 +49,27 @@ async function createSetupSideEffects( soClient: SavedObjectsClientContract, esClient: ElasticsearchClient ): Promise { - const [defaultOutput] = await Promise.all([ - outputService.ensureDefaultOutput(soClient), + const { + agentPolicies: policiesOrUndefined, + packages: packagesOrUndefined, + outputs: outputsOrUndefined, + } = appContextService.getConfig() ?? {}; + + const policies = policiesOrUndefined ?? []; + let packages = packagesOrUndefined ?? []; + + await Promise.all([ + ensurePreconfiguredOutputs(soClient, esClient, outputsOrUndefined ?? []), settingsService.settingsSetup(soClient), ]); + const defaultOutput = await outputService.ensureDefaultOutput(soClient); + await awaitIfFleetServerSetupPending(); if (appContextService.getConfig()?.agentIdVerificationEnabled) { await ensureFleetGlobalEsAssets(soClient, esClient); } - const { agentPolicies: policiesOrUndefined, packages: packagesOrUndefined } = - appContextService.getConfig() ?? {}; - - const policies = policiesOrUndefined ?? []; - - let packages = packagesOrUndefined ?? []; - // Ensure that required packages are always installed even if they're left out of the config const preconfiguredPackageNames = new Set(packages.map((pkg) => pkg.name)); @@ -90,8 +98,10 @@ async function createSetupSideEffects( defaultOutput ); + await cleanPreconfiguredOutputs(soClient, outputsOrUndefined ?? []); + await ensureDefaultEnrollmentAPIKeysExists(soClient, esClient); - await ensureAgentActionPolicyChangeExists(soClient, esClient); + await ensureFleetServerAgentPoliciesExists(soClient, esClient); return { isInitialized: true, diff --git a/x-pack/plugins/fleet/server/types/index.tsx b/x-pack/plugins/fleet/server/types/index.tsx index f686b969fd038..63e6c277ed710 100644 --- a/x-pack/plugins/fleet/server/types/index.tsx +++ b/x-pack/plugins/fleet/server/types/index.tsx @@ -27,6 +27,7 @@ export { PackagePolicySOAttributes, FullAgentPolicyInput, FullAgentPolicy, + FullAgentPolicyOutput, AgentPolicy, AgentPolicySOAttributes, NewAgentPolicy, diff --git a/x-pack/plugins/fleet/server/types/models/package_policy.ts b/x-pack/plugins/fleet/server/types/models/package_policy.ts index e69e38c187284..30321bdca3309 100644 --- a/x-pack/plugins/fleet/server/types/models/package_policy.ts +++ b/x-pack/plugins/fleet/server/types/models/package_policy.ts @@ -63,7 +63,19 @@ const PackagePolicyBaseSchema = { id: schema.maybe(schema.string()), // BWC < 7.11 enabled: schema.boolean(), keep_enabled: schema.maybe(schema.boolean()), - data_stream: schema.object({ dataset: schema.string(), type: schema.string() }), + data_stream: schema.object({ + dataset: schema.string(), + type: schema.string(), + elasticsearch: schema.maybe( + schema.object({ + privileges: schema.maybe( + schema.object({ + indices: schema.maybe(schema.arrayOf(schema.string())), + }) + ), + }) + ), + }), vars: schema.maybe(ConfigRecordSchema), config: schema.maybe( schema.recordOf( diff --git a/x-pack/plugins/fleet/server/types/models/preconfiguration.test.ts b/x-pack/plugins/fleet/server/types/models/preconfiguration.test.ts new file mode 100644 index 0000000000000..eb349e0d0f823 --- /dev/null +++ b/x-pack/plugins/fleet/server/types/models/preconfiguration.test.ts @@ -0,0 +1,81 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { PreconfiguredOutputsSchema, PreconfiguredAgentPoliciesSchema } from './preconfiguration'; + +describe('Test preconfiguration schema', () => { + describe('PreconfiguredOutputsSchema', () => { + it('should not allow multiple default output', () => { + expect(() => { + PreconfiguredOutputsSchema.validate([ + { + id: 'output-1', + name: 'Output 1', + type: 'elasticsearch', + is_default: true, + }, + { + id: 'output-2', + name: 'Output 2', + type: 'elasticsearch', + is_default: true, + }, + ]); + }).toThrowError('preconfigured outputs need to have only one default output.'); + }); + it('should not allow multiple output with same ids', () => { + expect(() => { + PreconfiguredOutputsSchema.validate([ + { + id: 'nonuniqueid', + name: 'Output 1', + type: 'elasticsearch', + }, + { + id: 'nonuniqueid', + name: 'Output 2', + type: 'elasticsearch', + }, + ]); + }).toThrowError('preconfigured outputs need to have unique ids.'); + }); + it('should not allow multiple output with same names', () => { + expect(() => { + PreconfiguredOutputsSchema.validate([ + { + id: 'output-1', + name: 'nonuniquename', + type: 'elasticsearch', + }, + { + id: 'output-2', + name: 'nonuniquename', + type: 'elasticsearch', + }, + ]); + }).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 4ea9f086bda68..b65fa122911dc 100644 --- a/x-pack/plugins/fleet/server/types/models/preconfiguration.ts +++ b/x-pack/plugins/fleet/server/types/models/preconfiguration.ts @@ -14,6 +14,8 @@ import { DEFAULT_FLEET_SERVER_AGENT_POLICY, DEFAULT_PACKAGES, } from '../../constants'; +import type { PreconfiguredOutput } from '../../../common'; +import { outputType } from '../../../common'; import { AgentPolicyBaseSchema } from './agent_policy'; import { NamespaceSchema } from './package_policy'; @@ -47,47 +49,94 @@ export const PreconfiguredPackagesSchema = schema.arrayOf( } ); -export const PreconfiguredAgentPoliciesSchema = schema.arrayOf( +function validatePreconfiguredOutputs(outputs: PreconfiguredOutput[]) { + const acc = { names: new Set(), ids: new Set(), is_default: false }; + + for (const output of outputs) { + if (acc.names.has(output.name)) { + return 'preconfigured outputs need to have unique names.'; + } + 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.'; + } + + acc.ids.add(output.id); + acc.names.add(output.name); + acc.is_default = acc.is_default || output.is_default; + } +} + +export const PreconfiguredOutputsSchema = 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()), - package_policies: schema.arrayOf( - 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, - }) - ) - ), - }) - ) - ), - }) - ), + id: schema.string(), + is_default: schema.boolean({ defaultValue: false }), + name: schema.string(), + type: schema.oneOf([schema.literal(outputType.Elasticsearch)]), + hosts: schema.maybe(schema.arrayOf(schema.uri({ scheme: ['http', 'https'] }))), + ca_sha256: schema.maybe(schema.string()), + config: schema.maybe(schema.object({}, { unknowns: 'allow' })), }), + { + defaultValue: [], + validate: validatePreconfiguredOutputs, + } +); + +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({ + 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.'; + } + }, + } + ), { defaultValue: [DEFAULT_AGENT_POLICY, DEFAULT_FLEET_SERVER_AGENT_POLICY], } diff --git a/x-pack/plugins/fleet/storybook/decorator.tsx b/x-pack/plugins/fleet/storybook/decorator.tsx new file mode 100644 index 0000000000000..499b01c2bfba0 --- /dev/null +++ b/x-pack/plugins/fleet/storybook/decorator.tsx @@ -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 React from 'react'; + +import { of } from 'rxjs'; +import type { DecoratorFn } from '@storybook/react'; +import { action } from '@storybook/addon-actions'; +import { createMemoryHistory } from 'history'; + +import { I18nProvider } from '@kbn/i18n/react'; + +import { ScopedHistory } from '../../../../src/core/public'; +import { IntegrationsAppContext } from '../public/applications/integrations/app'; +import type { FleetConfigType, FleetStartServices } from '../public/plugin'; + +// TODO: clintandrewhall - this is not ideal, or complete. The root context of Fleet applications +// requires full start contracts of its dependencies. As a result, we have to mock all of those contracts +// with Storybook equivalents. This is a temporary solution, and should be replaced with a more complete +// mock later, (or, ideally, Fleet starts to use a service abstraction). +// +// Expect this to grow as components that are given Stories need access to mocked services. +export const contextDecorator: DecoratorFn = (story: Function) => { + const basepath = '/'; + const memoryHistory = createMemoryHistory({ initialEntries: [basepath] }); + const history = new ScopedHistory(memoryHistory, basepath); + + const startServices = { + application: { + currentAppId$: of('home'), + navigateToUrl: (url: string) => action(`Navigate to: ${url}`), + getUrlForApp: (url: string) => url, + }, + http: { + basePath: { + prepend: () => basepath, + }, + }, + notifications: {}, + history, + uiSettings: { + get$: (key: string) => { + switch (key) { + case 'theme:darkMode': + return of(false); + default: + return of(); + } + }, + }, + i18n: { + Context: I18nProvider, + }, + } as unknown as FleetStartServices; + + const config = { + enabled: true, + agents: { + enabled: true, + elasticsearch: {}, + }, + } as unknown as FleetConfigType; + + const extensions = {}; + + const kibanaVersion = '1.2.3'; + + return ( + + {story()} + + ); +}; diff --git a/x-pack/plugins/fleet/storybook/main.ts b/x-pack/plugins/fleet/storybook/main.ts new file mode 100644 index 0000000000000..2cf4124c61783 --- /dev/null +++ b/x-pack/plugins/fleet/storybook/main.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 type { Configuration } from 'webpack'; +import { defaultConfig, WebpackConfig } from '@kbn/storybook'; + +module.exports = { + ...defaultConfig, + addons: ['@storybook/addon-essentials'], + babel: () => ({ + presets: [require.resolve('@kbn/babel-preset/webpack_preset')], + }), + webpackFinal: (config: Configuration) => { + return WebpackConfig({ config }); + }, +}; diff --git a/x-pack/plugins/fleet/storybook/manager.ts b/x-pack/plugins/fleet/storybook/manager.ts new file mode 100644 index 0000000000000..471a735ed370f --- /dev/null +++ b/x-pack/plugins/fleet/storybook/manager.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 { addons } from '@storybook/addons'; +import { create } from '@storybook/theming'; +import { PANEL_ID } from '@storybook/addon-actions'; + +addons.setConfig({ + theme: create({ + base: 'light', + brandTitle: 'Kibana Fleet Storybook', + brandUrl: 'https://github.com/elastic/kibana/tree/master/x-pack/plugins/fleet', + }), + showPanel: true.valueOf, + selectedPanel: PANEL_ID, +}); diff --git a/x-pack/plugins/fleet/storybook/preview.tsx b/x-pack/plugins/fleet/storybook/preview.tsx new file mode 100644 index 0000000000000..a50ff2faaff56 --- /dev/null +++ b/x-pack/plugins/fleet/storybook/preview.tsx @@ -0,0 +1,28 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { addDecorator } from '@storybook/react'; +import { Title, Subtitle, Description, Primary, Stories } from '@storybook/addon-docs/blocks'; + +import { contextDecorator } from './decorator'; + +addDecorator(contextDecorator); + +export const parameters = { + docs: { + page: () => ( + <> + + <Subtitle /> + <Description /> + <Primary /> + <Stories /> + </> + ), + }, +}; diff --git a/x-pack/plugins/fleet/tsconfig.json b/x-pack/plugins/fleet/tsconfig.json index 5002bf2893872..a9dd66ce503a9 100644 --- a/x-pack/plugins/fleet/tsconfig.json +++ b/x-pack/plugins/fleet/tsconfig.json @@ -14,6 +14,7 @@ "server/**/*.json", "scripts/**/*", "package.json", + "storybook/**/*", "../../../typings/**/*" ], "references": [ diff --git a/x-pack/plugins/graph/public/components/settings/settings.test.tsx b/x-pack/plugins/graph/public/components/settings/settings.test.tsx index 060b1e93fbdc0..1c3fbb9f54f67 100644 --- a/x-pack/plugins/graph/public/components/settings/settings.test.tsx +++ b/x-pack/plugins/graph/public/components/settings/settings.test.tsx @@ -24,6 +24,18 @@ import { createMockGraphStore } from '../../state_management/mocks'; import { Provider } from 'react-redux'; import { UrlTemplate } from '../../types'; +jest.mock('@elastic/eui', () => { + const original = jest.requireActual('@elastic/eui'); + + return { + ...original, + htmlIdGenerator: (fn: unknown) => { + let counter = 0; + return () => String(counter++); + }, + }; +}); + describe('settings', () => { let store: GraphStore; let dispatchSpy: jest.Mock; diff --git a/x-pack/plugins/graph/public/helpers/use_workspace_loader.test.tsx b/x-pack/plugins/graph/public/helpers/use_workspace_loader.test.tsx index cd0857f82ab6b..c4c40ada490f1 100644 --- a/x-pack/plugins/graph/public/helpers/use_workspace_loader.test.tsx +++ b/x-pack/plugins/graph/public/helpers/use_workspace_loader.test.tsx @@ -4,15 +4,13 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -import React from 'react'; -import { mount } from 'enzyme'; import { useWorkspaceLoader, UseWorkspaceLoaderProps } from './use_workspace_loader'; import { coreMock } from 'src/core/public/mocks'; import { spacesPluginMock } from '../../../spaces/public/mocks'; import { createMockGraphStore } from '../state_management/mocks'; import { Workspace } from '../types'; import { SavedObjectsClientCommon } from 'src/plugins/data/common'; -import { act } from 'react-dom/test-utils'; +import { renderHook, act, RenderHookOptions } from '@testing-library/react-hooks'; jest.mock('react-router-dom', () => { const useLocation = () => ({ @@ -41,20 +39,6 @@ const mockSavedObjectsClient = { find: jest.fn().mockResolvedValue({ title: 'test' }), } as unknown as SavedObjectsClientCommon; -async function setup(props: UseWorkspaceLoaderProps) { - const returnVal = {}; - function TestComponent() { - Object.assign(returnVal, useWorkspaceLoader(props)); - return null; - } - await act(async () => { - const promise = Promise.resolve(); - mount(<TestComponent />); - await act(() => promise); - }); - return returnVal; -} - describe('use_workspace_loader', () => { const defaultProps = { workspaceRef: { current: {} as Workspace }, @@ -62,13 +46,16 @@ describe('use_workspace_loader', () => { savedObjectsClient: mockSavedObjectsClient, coreStart: coreMock.createStart(), spaces: spacesPluginMock.createStartContract(), - }; + } as unknown as UseWorkspaceLoaderProps; it('should not redirect if outcome is exactMatch', async () => { await act(async () => { - await setup(defaultProps as unknown as UseWorkspaceLoaderProps); + renderHook( + () => useWorkspaceLoader(defaultProps), + defaultProps as RenderHookOptions<UseWorkspaceLoaderProps> + ); }); - expect(defaultProps.spaces.ui.redirectLegacyUrl).not.toHaveBeenCalled(); + expect(defaultProps.spaces?.ui.redirectLegacyUrl).not.toHaveBeenCalled(); expect(defaultProps.store.dispatch).toHaveBeenCalled(); }); it('should redirect if outcome is aliasMatch', async () => { @@ -83,11 +70,15 @@ describe('use_workspace_loader', () => { alias_target_id: 'aliasTargetId', }), }, - }; + } as unknown as UseWorkspaceLoaderProps; + await act(async () => { - await setup(props as unknown as UseWorkspaceLoaderProps); + renderHook( + () => useWorkspaceLoader(props), + props as RenderHookOptions<UseWorkspaceLoaderProps> + ); }); - expect(props.spaces.ui.redirectLegacyUrl).toHaveBeenCalledWith( + expect(props.spaces?.ui.redirectLegacyUrl).toHaveBeenCalledWith( '#/workspace/aliasTargetId?query={}', 'Graph' ); diff --git a/x-pack/plugins/graph/public/state_management/mocks.ts b/x-pack/plugins/graph/public/state_management/mocks.ts index 6f7b5dba2e4a1..5003e550f87a1 100644 --- a/x-pack/plugins/graph/public/state_management/mocks.ts +++ b/x-pack/plugins/graph/public/state_management/mocks.ts @@ -42,6 +42,7 @@ export function createMockGraphStore({ }): MockedGraphEnvironment { const workspaceMock = { runLayout: jest.fn(), + simpleSearch: jest.fn(), nodes: [], edges: [], options: {}, @@ -55,7 +56,7 @@ export function createMockGraphStore({ chrome: { setBreadcrumbs: jest.fn(), } as unknown as ChromeStart, - createWorkspace: jest.fn(), + createWorkspace: jest.fn((index, advancedSettings) => workspaceMock), getWorkspace: jest.fn(() => workspaceMock), indexPatternProvider: { get: jest.fn(() => diff --git a/x-pack/plugins/graph/public/state_management/persistence.test.ts b/x-pack/plugins/graph/public/state_management/persistence.test.ts index dc59869fafd4c..2ef68f2198070 100644 --- a/x-pack/plugins/graph/public/state_management/persistence.test.ts +++ b/x-pack/plugins/graph/public/state_management/persistence.test.ts @@ -23,6 +23,18 @@ import { settingsSelector } from './advanced_settings'; import { openSaveModal } from '../services/save_modal'; const waitForPromise = () => new Promise((r) => setTimeout(r)); +// mocking random id generator function +jest.mock('@elastic/eui', () => { + const original = jest.requireActual('@elastic/eui'); + + return { + ...original, + htmlIdGenerator: (fn: unknown) => { + let counter = 0; + return () => counter++; + }, + }; +}); jest.mock('../services/persistence', () => ({ lookupIndexPatternId: jest.fn(() => ({ id: '123', attributes: { title: 'test-pattern' } })), diff --git a/x-pack/plugins/graph/server/index.ts b/x-pack/plugins/graph/server/index.ts index 10ddca631a898..528e122da9a4d 100644 --- a/x-pack/plugins/graph/server/index.ts +++ b/x-pack/plugins/graph/server/index.ts @@ -18,4 +18,5 @@ export const config: PluginConfigDescriptor<ConfigSchema> = { savePolicy: true, }, schema: configSchema, + deprecations: ({ deprecate }) => [deprecate('enabled', '8.0.0')], }; diff --git a/x-pack/plugins/index_lifecycle_management/server/index.ts b/x-pack/plugins/index_lifecycle_management/server/index.ts index e90518dbfa357..1f8b01913fd3e 100644 --- a/x-pack/plugins/index_lifecycle_management/server/index.ts +++ b/x-pack/plugins/index_lifecycle_management/server/index.ts @@ -17,4 +17,5 @@ export const config: PluginConfigDescriptor<IndexLifecycleManagementConfig> = { exposeToBrowser: { ui: true, }, + deprecations: ({ deprecate }) => [deprecate('enabled', '8.0.0')], }; diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/field_parameters/path_parameter.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/field_parameters/path_parameter.tsx index aeb4debddcd65..7307dd0e41eda 100644 --- a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/field_parameters/path_parameter.tsx +++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/field_parameters/path_parameter.tsx @@ -86,7 +86,7 @@ export const PathParameter = ({ field, allFields }: Props) => { 'xpack.idxMgmt.mappingsEditor.aliasType.aliasTargetFieldDescription', { defaultMessage: - 'Select the field you want your alias to point to. You will then be able to use the alias instead of the target field in search requests, and selected other APIs like field capabilities.', + 'Select the field you want your alias to point to. You will then be able to use the alias instead of the target field in search requests and select other APIs like field capabilities.', } )} withToggle={false} diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/lib/utils.test.ts b/x-pack/plugins/index_management/public/application/components/mappings_editor/lib/utils.test.ts index 321500730c82f..162bb59a0528a 100644 --- a/x-pack/plugins/index_management/public/application/components/mappings_editor/lib/utils.test.ts +++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/lib/utils.test.ts @@ -10,7 +10,7 @@ jest.mock('../constants', () => { return { MAIN_DATA_TYPE_DEFINITION: {}, TYPE_DEFINITION }; }); -import { stripUndefinedValues, getTypeLabelFromField } from './utils'; +import { stripUndefinedValues, getTypeLabelFromField, getFieldMeta } from './utils'; describe('utils', () => { describe('stripUndefinedValues()', () => { @@ -77,4 +77,17 @@ describe('utils', () => { ).toBe('Other: hyperdrive'); }); }); + + describe('getFieldMeta', () => { + test('returns "canHaveMultiFields:true" for IP data type', () => { + expect(getFieldMeta({ name: 'ip_field', type: 'ip' })).toEqual({ + canHaveChildFields: false, + canHaveMultiFields: true, + childFieldsName: 'fields', + hasChildFields: false, + hasMultiFields: false, + isExpanded: false, + }); + }); + }); }); diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/lib/utils.ts b/x-pack/plugins/index_management/public/application/components/mappings_editor/lib/utils.ts index dd1a96b3c5b79..c07919dea6b15 100644 --- a/x-pack/plugins/index_management/public/application/components/mappings_editor/lib/utils.ts +++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/lib/utils.ts @@ -40,7 +40,7 @@ import { TreeItem } from '../components/tree'; export const getUniqueId = () => uuid.v4(); export const getChildFieldsName = (dataType: DataType): ChildFieldName | undefined => { - if (dataType === 'text' || dataType === 'keyword') { + if (dataType === 'text' || dataType === 'keyword' || dataType === 'ip') { return 'fields'; } else if (dataType === 'object' || dataType === 'nested') { return 'properties'; diff --git a/x-pack/plugins/index_management/server/index.ts b/x-pack/plugins/index_management/server/index.ts index 507401398a407..14b67e2ffd581 100644 --- a/x-pack/plugins/index_management/server/index.ts +++ b/x-pack/plugins/index_management/server/index.ts @@ -5,15 +5,16 @@ * 2.0. */ -import { PluginInitializerContext } from 'src/core/server'; +import { PluginInitializerContext, PluginConfigDescriptor } from 'src/core/server'; import { IndexMgmtServerPlugin } from './plugin'; import { configSchema } from './config'; export const plugin = (context: PluginInitializerContext) => new IndexMgmtServerPlugin(context); -export const config = { +export const config: PluginConfigDescriptor = { schema: configSchema, + deprecations: ({ deprecate }) => [deprecate('enabled', '8.0.0')], }; /** @public */ diff --git a/x-pack/plugins/infra/server/lib/adapters/framework/adapter_types.ts b/x-pack/plugins/infra/server/lib/adapters/framework/adapter_types.ts index 5cd2ef210086d..ad40a8565f65b 100644 --- a/x-pack/plugins/infra/server/lib/adapters/framework/adapter_types.ts +++ b/x-pack/plugins/infra/server/lib/adapters/framework/adapter_types.ts @@ -15,7 +15,7 @@ import { PluginStart as DataPluginStart, } from '../../../../../../../src/plugins/data/server'; import { HomeServerPluginSetup } from '../../../../../../../src/plugins/home/server'; -import { VisTypeTimeseriesSetup } from '../../../../../../../src/plugins/vis_type_timeseries/server'; +import { VisTypeTimeseriesSetup } from '../../../../../../../src/plugins/vis_types/timeseries/server'; import { PluginSetupContract as FeaturesPluginSetup } from '../../../../../../plugins/features/server'; import { SpacesPluginSetup } from '../../../../../../plugins/spaces/server'; import { PluginSetupContract as AlertingPluginContract } from '../../../../../alerting/server'; diff --git a/x-pack/plugins/infra/server/lib/adapters/framework/kibana_framework_adapter.ts b/x-pack/plugins/infra/server/lib/adapters/framework/kibana_framework_adapter.ts index cce1ab51c484a..25763824a336d 100644 --- a/x-pack/plugins/infra/server/lib/adapters/framework/kibana_framework_adapter.ts +++ b/x-pack/plugins/infra/server/lib/adapters/framework/kibana_framework_adapter.ts @@ -34,7 +34,7 @@ import { RequestHandler } from '../../../../../../../src/core/server'; import { InfraConfig } from '../../../plugin'; import type { InfraPluginRequestHandlerContext } from '../../../types'; import { UI_SETTINGS } from '../../../../../../../src/plugins/data/server'; -import { TimeseriesVisData } from '../../../../../../../src/plugins/vis_type_timeseries/server'; +import { TimeseriesVisData } from '../../../../../../../src/plugins/vis_types/timeseries/server'; import { InfraServerPluginStartDeps } from './adapter_types'; export class KibanaFramework { 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 a9ddcc8d3d4c1..730da9511dc38 100644 --- a/x-pack/plugins/infra/server/lib/adapters/metrics/kibana_metrics_adapter.ts +++ b/x-pack/plugins/infra/server/lib/adapters/metrics/kibana_metrics_adapter.ts @@ -21,7 +21,7 @@ import { import { calculateMetricInterval } from '../../../utils/calculate_metric_interval'; import { CallWithRequestParams, InfraDatabaseSearchResponse } from '../framework'; import type { InfraPluginRequestHandlerContext } from '../../../types'; -import { isVisSeriesData } from '../../../../../../../src/plugins/vis_type_timeseries/server'; +import { isVisSeriesData } from '../../../../../../../src/plugins/vis_types/timeseries/server'; export class KibanaMetricsAdapter implements InfraMetricsAdapter { private framework: KibanaFramework; 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 c7c1eb5454d1d..45eef3cc85a57 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 @@ -64,6 +64,7 @@ export const evaluateAlert = <Params extends EvaluatedAlertParams = EvaluatedAle esClient: ElasticsearchClient, params: Params, config: InfraSource['configuration'], + prevGroups: string[], timeframe?: { start?: number; end: number } ) => { const { criteria, groupBy, filterQuery, shouldDropPartialBuckets } = params; @@ -91,21 +92,53 @@ export const evaluateAlert = <Params extends EvaluatedAlertParams = EvaluatedAle : [false]; }; - return mapValues(currentValues, (points: any[] | typeof NaN | null) => { - if (isTooManyBucketsPreviewException(points)) throw points; - return { - ...criterion, - metric: criterion.metric ?? DOCUMENT_COUNT_I18N, - currentValue: Array.isArray(points) ? last(points)?.value : NaN, - timestamp: Array.isArray(points) ? last(points)?.key : NaN, - shouldFire: pointsEvaluator(points, threshold, comparator), - shouldWarn: pointsEvaluator(points, warningThreshold, warningComparator), - isNoData: Array.isArray(points) - ? points.map((point) => point?.value === null || point === null) - : [points === null], - isError: isNaN(Array.isArray(points) ? last(points)?.value : points), - }; - }); + // If any previous groups are no longer being reported, backfill them with null values + const currentGroups = Object.keys(currentValues); + + const missingGroups = prevGroups.filter((g) => !currentGroups.includes(g)); + if (currentGroups.length === 0 && missingGroups.length === 0) { + missingGroups.push(UNGROUPED_FACTORY_KEY); + } + const backfillTimestamp = + last(last(Object.values(currentValues)))?.key ?? new Date().toISOString(); + const backfilledPrevGroups: Record< + string, + Array<{ key: string; value: number }> + > = missingGroups.reduce( + (result, group) => ({ + ...result, + [group]: [ + { + key: backfillTimestamp, + value: criterion.aggType === Aggregators.COUNT ? 0 : null, + }, + ], + }), + {} + ); + const currentValuesWithBackfilledPrevGroups = { + ...currentValues, + ...backfilledPrevGroups, + }; + + return mapValues( + currentValuesWithBackfilledPrevGroups, + (points: any[] | typeof NaN | null) => { + if (isTooManyBucketsPreviewException(points)) throw points; + return { + ...criterion, + metric: criterion.metric ?? DOCUMENT_COUNT_I18N, + currentValue: Array.isArray(points) ? last(points)?.value : NaN, + timestamp: Array.isArray(points) ? last(points)?.key : NaN, + shouldFire: pointsEvaluator(points, threshold, comparator), + shouldWarn: pointsEvaluator(points, warningThreshold, warningComparator), + isNoData: Array.isArray(points) + ? points.map((point) => point?.value === null || point === null) + : [points === null], + isError: isNaN(Array.isArray(points) ? last(points)?.value : points), + }; + } + ); }) ); }; @@ -119,7 +152,7 @@ const getMetric: ( filterQuery: string | undefined, timeframe?: { start?: number; end: number }, shouldDropPartialBuckets?: boolean -) => Promise<Record<string, number[]>> = async function ( +) => Promise<Record<string, Array<{ key: string; value: number }>>> = async function ( esClient, params, index, diff --git a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.test.ts b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.test.ts index 8eb19ad582057..869d0afd52367 100644 --- a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.test.ts +++ b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.test.ts @@ -37,10 +37,13 @@ let persistAlertInstances = false; // eslint-disable-line prefer-const type TestRuleState = Record<string, unknown> & { aRuleStateKey: string; + groups: string[]; + groupBy?: string | string[]; }; const initialRuleState: TestRuleState = { aRuleStateKey: 'INITIAL_RULE_STATE_VALUE', + groups: [], }; const mockOptions = { @@ -90,6 +93,7 @@ const mockOptions = { describe('The metric threshold alert type', () => { describe('querying the entire infrastructure', () => { + afterAll(() => clearInstances()); const instanceID = '*'; const execute = (comparator: Comparator, threshold: number[], sourceId: string = 'default') => executor({ @@ -157,20 +161,29 @@ describe('The metric threshold alert type', () => { }); describe('querying with a groupBy parameter', () => { - const execute = (comparator: Comparator, threshold: number[], sourceId: string = 'default') => + afterAll(() => clearInstances()); + const execute = ( + comparator: Comparator, + threshold: number[], + groupBy: string[] = ['something'], + metric?: string, + state?: any + ) => executor({ ...mockOptions, services, params: { - groupBy: 'something', + groupBy, criteria: [ { ...baseNonCountCriterion, comparator, threshold, + metric: metric ?? baseNonCountCriterion.metric, }, ], }, + state: state ?? mockOptions.state.wrapped, }); const instanceIdA = 'a'; const instanceIdB = 'b'; @@ -194,9 +207,35 @@ describe('The metric threshold alert type', () => { expect(mostRecentAction(instanceIdA).action.group).toBe('a'); expect(mostRecentAction(instanceIdB).action.group).toBe('b'); }); + test('reports previous groups and the groupBy parameter in its state', async () => { + const stateResult = await execute(Comparator.GT, [0.75]); + expect(stateResult.groups).toEqual(expect.arrayContaining(['a', 'b'])); + expect(stateResult.groupBy).toEqual(['something']); + }); + test('persists previous groups that go missing, until the groupBy param changes', async () => { + const stateResult1 = await execute(Comparator.GT, [0.75], ['something'], 'test.metric.2'); + expect(stateResult1.groups).toEqual(expect.arrayContaining(['a', 'b', 'c'])); + const stateResult2 = await execute( + Comparator.GT, + [0.75], + ['something'], + 'test.metric.1', + stateResult1 + ); + expect(stateResult2.groups).toEqual(expect.arrayContaining(['a', 'b', 'c'])); + const stateResult3 = await execute( + Comparator.GT, + [0.75], + ['something', 'something-else'], + 'test.metric.1', + stateResult2 + ); + expect(stateResult3.groups).toEqual(expect.arrayContaining(['a', 'b'])); + }); }); describe('querying with multiple criteria', () => { + afterAll(() => clearInstances()); const execute = ( comparator: Comparator, thresholdA: number[], @@ -257,6 +296,7 @@ describe('The metric threshold alert type', () => { }); }); describe('querying with the count aggregator', () => { + afterAll(() => clearInstances()); const instanceID = '*'; const execute = (comparator: Comparator, threshold: number[], sourceId: string = 'default') => executor({ @@ -279,8 +319,47 @@ describe('The metric threshold alert type', () => { await execute(Comparator.LT, [0.5]); expect(mostRecentAction(instanceID)).toBe(undefined); }); + describe('with a groupBy parameter', () => { + const executeGroupBy = ( + comparator: Comparator, + threshold: number[], + sourceId: string = 'default', + state?: any + ) => + executor({ + ...mockOptions, + services, + params: { + sourceId, + groupBy: 'something', + criteria: [ + { + ...baseCountCriterion, + comparator, + threshold, + }, + ], + }, + state: state ?? mockOptions.state.wrapped, + }); + const instanceIdA = 'a'; + const instanceIdB = 'b'; + + test('successfully detects and alerts on a document count of 0', async () => { + const resultState = await executeGroupBy(Comparator.LT_OR_EQ, [0]); + expect(mostRecentAction(instanceIdA)).toBe(undefined); + expect(mostRecentAction(instanceIdB)).toBe(undefined); + await executeGroupBy(Comparator.LT_OR_EQ, [0], 'empty-response', resultState); + expect(mostRecentAction(instanceIdA).id).toBe(FIRED_ACTIONS.id); + expect(mostRecentAction(instanceIdB).id).toBe(FIRED_ACTIONS.id); + await executeGroupBy(Comparator.LT_OR_EQ, [0]); + expect(mostRecentAction(instanceIdA)).toBe(undefined); + expect(mostRecentAction(instanceIdB)).toBe(undefined); + }); + }); }); describe('querying with the p99 aggregator', () => { + afterAll(() => clearInstances()); const instanceID = '*'; const execute = (comparator: Comparator, threshold: number[], sourceId: string = 'default') => executor({ @@ -306,6 +385,7 @@ describe('The metric threshold alert type', () => { }); }); describe('querying with the p95 aggregator', () => { + afterAll(() => clearInstances()); const instanceID = '*'; const execute = (comparator: Comparator, threshold: number[], sourceId: string = 'default') => executor({ @@ -332,6 +412,7 @@ describe('The metric threshold alert type', () => { }); }); describe("querying a metric that hasn't reported data", () => { + afterAll(() => clearInstances()); const instanceID = '*'; const execute = (alertOnNoData: boolean, sourceId: string = 'default') => executor({ @@ -360,7 +441,51 @@ describe('The metric threshold alert type', () => { }); }); + describe('querying a groupBy alert that starts reporting no data, and then later reports data', () => { + afterAll(() => clearInstances()); + const instanceID = '*'; + const instanceIdA = 'a'; + const instanceIdB = 'b'; + const execute = (metric: string, state?: any) => + executor({ + ...mockOptions, + services, + params: { + groupBy: 'something', + sourceId: 'default', + criteria: [ + { + ...baseNonCountCriterion, + comparator: Comparator.GT, + threshold: [0], + metric, + }, + ], + alertOnNoData: true, + }, + state: state ?? mockOptions.state.wrapped, + }); + const resultState: any[] = []; + test('first sends a No Data alert with the * group, but then reports groups when data is available', async () => { + resultState.push(await execute('test.metric.3')); + expect(mostRecentAction(instanceID).id).toBe(FIRED_ACTIONS.id); + resultState.push(await execute('test.metric.3', resultState.pop())); + expect(mostRecentAction(instanceID).id).toBe(FIRED_ACTIONS.id); + resultState.push(await execute('test.metric.1', resultState.pop())); + expect(mostRecentAction(instanceID)).toBe(undefined); + expect(mostRecentAction(instanceIdA).id).toBe(FIRED_ACTIONS.id); + expect(mostRecentAction(instanceIdB).id).toBe(FIRED_ACTIONS.id); + }); + test('sends No Data alerts for the previously detected groups when they stop reporting data, but not the * group', async () => { + await execute('test.metric.3', resultState.pop()); + expect(mostRecentAction(instanceID)).toBe(undefined); + expect(mostRecentAction(instanceIdA).id).toBe(FIRED_ACTIONS.id); + expect(mostRecentAction(instanceIdB).id).toBe(FIRED_ACTIONS.id); + }); + }); + describe("querying a rate-aggregated metric that hasn't reported data", () => { + afterAll(() => clearInstances()); const instanceID = '*'; const execute = (sourceId: string = 'default') => executor({ @@ -439,6 +564,7 @@ describe('The metric threshold alert type', () => { */ describe('querying a metric with a percentage metric', () => { + afterAll(() => clearInstances()); const instanceID = '*'; const execute = () => executor({ @@ -497,7 +623,15 @@ const services: AlertServicesMock & services.scopedClusterClient.asCurrentUser.search.mockImplementation((params?: any): any => { const from = params?.body.query.bool.filter[0]?.range['@timestamp'].gte; if (params.index === 'alternatebeat-*') return mocks.changedSourceIdResponse(from); + if (params.index === 'empty-response') return mocks.emptyMetricResponse; const metric = params?.body.query.bool.filter[1]?.exists.field; + if (metric === 'test.metric.3') { + return elasticsearchClientMock.createSuccessTransportRequestPromise( + params?.body.aggs.aggregatedIntervals?.aggregations.aggregatedValueMax + ? mocks.emptyRateResponse + : mocks.emptyMetricResponse + ); + } if (params?.body.aggs.groupings) { if (params?.body.aggs.groupings.composite.after) { return elasticsearchClientMock.createSuccessTransportRequestPromise( @@ -517,12 +651,6 @@ services.scopedClusterClient.asCurrentUser.search.mockImplementation((params?: a return elasticsearchClientMock.createSuccessTransportRequestPromise( mocks.alternateMetricResponse() ); - } else if (metric === 'test.metric.3') { - return elasticsearchClientMock.createSuccessTransportRequestPromise( - params?.body.aggs.aggregatedIntervals.aggregations.aggregatedValueMax - ? mocks.emptyRateResponse - : mocks.emptyMetricResponse - ); } return elasticsearchClientMock.createSuccessTransportRequestPromise(mocks.basicMetricResponse()); }); @@ -534,6 +662,13 @@ services.savedObjectsClient.get.mockImplementation(async (type: string, sourceId type, references: [], }; + if (sourceId === 'empty-response') + return { + id: 'empty', + attributes: { metricAlias: 'empty-response' }, + type, + references: [], + }; return { id: 'default', attributes: { metricAlias: 'metricbeat-*' }, type, references: [] }; }); @@ -561,7 +696,13 @@ services.alertInstanceFactory.mockImplementation((instanceID: string) => { }); function mostRecentAction(id: string) { - return alertInstances.get(id)!.actionQueue.pop(); + const instance = alertInstances.get(id); + if (!instance) return undefined; + return instance.actionQueue.pop(); +} + +function clearInstances() { + alertInstances.clear(); } const baseNonCountCriterion: Pick< diff --git a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.ts b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.ts index 9c99ad6bf49e2..f49b281909f4b 100644 --- a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.ts +++ b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { first, last } from 'lodash'; +import { first, last, isEqual } from 'lodash'; import { i18n } from '@kbn/i18n'; import moment from 'moment'; import { ALERT_REASON } from '@kbn/rule-data-utils'; @@ -24,12 +24,16 @@ import { // buildRecoveredAlertReason, stateToAlertMessage, } from '../common/messages'; +import { UNGROUPED_FACTORY_KEY } from '../common/utils'; import { createFormatter } from '../../../../common/formatters'; import { AlertStates, Comparator } from './types'; import { evaluateAlert, EvaluatedAlertParams } from './lib/evaluate_alert'; export type MetricThresholdAlertTypeParams = Record<string, any>; -export type MetricThresholdAlertTypeState = AlertTypeState; // no specific state used +export type MetricThresholdAlertTypeState = AlertTypeState & { + groups: string[]; + groupBy?: string | string[]; +}; export type MetricThresholdAlertInstanceState = AlertInstanceState; // no specific instace state used export type MetricThresholdAlertInstanceContext = AlertInstanceContext; // no specific instace state used @@ -58,7 +62,7 @@ export const createMetricThresholdExecutor = (libs: InfraBackendLibs) => MetricThresholdAlertInstanceContext, MetricThresholdAllowedActionGroups >(async function (options) { - const { services, params } = options; + const { services, params, state } = options; const { criteria } = params; if (criteria.length === 0) throw new Error('Cannot execute an alert with 0 conditions'); const { alertWithLifecycle, savedObjectsClient } = services; @@ -80,14 +84,28 @@ export const createMetricThresholdExecutor = (libs: InfraBackendLibs) => sourceId || 'default' ); const config = source.configuration; + + const previousGroupBy = state.groupBy; + const prevGroups = isEqual(previousGroupBy, params.groupBy) + ? // Filter out the * key from the previous groups, only include it if it's one of + // the current groups. In case of a groupBy alert that starts out with no data and no + // groups, we don't want to persist the existence of the * alert instance + state.groups?.filter((g) => g !== UNGROUPED_FACTORY_KEY) ?? [] + : []; + const alertResults = await evaluateAlert( services.scopedClusterClient.asCurrentUser, params as EvaluatedAlertParams, - config + config, + prevGroups ); // Because each alert result has the same group definitions, just grab the groups from the first one. - const groups = Object.keys(first(alertResults)!); + const resultGroups = Object.keys(first(alertResults)!); + // Merge the list of currently fetched groups and previous groups, and uniquify them. This is necessary for reporting + // no data results on groups that get removed + const groups = [...new Set([...prevGroups, ...resultGroups])]; + for (const group of groups) { // AND logic; all criteria must be across the threshold const shouldAlertFire = alertResults.every((result) => @@ -169,6 +187,8 @@ export const createMetricThresholdExecutor = (libs: InfraBackendLibs) => }); } } + + return { groups, groupBy: params.groupBy }; }); export const FIRED_ACTIONS = { diff --git a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/test_mocks.ts b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/test_mocks.ts index b1173f2d611c8..db6b771e91784 100644 --- a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/test_mocks.ts +++ b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/test_mocks.ts @@ -199,12 +199,20 @@ export const alternateCompositeResponse = (from: number) => ({ buckets: bucketsA(from), }, }, + { + key: { + groupBy0: 'c', + }, + aggregatedIntervals: { + buckets: bucketsC(from), + }, + }, ], }, }, hits: { total: { - value: 2, + value: 3, }, }, }); diff --git a/x-pack/plugins/infra/server/plugin.ts b/x-pack/plugins/infra/server/plugin.ts index de445affc178e..b77b81cf41ee1 100644 --- a/x-pack/plugins/infra/server/plugin.ts +++ b/x-pack/plugins/infra/server/plugin.ts @@ -9,7 +9,12 @@ import { Server } from '@hapi/hapi'; import { schema, TypeOf } from '@kbn/config-schema'; import { i18n } from '@kbn/i18n'; import { Logger } from '@kbn/logging'; -import { CoreSetup, PluginInitializerContext, Plugin } from 'src/core/server'; +import { + CoreSetup, + PluginInitializerContext, + Plugin, + PluginConfigDescriptor, +} from 'src/core/server'; import { LOGS_FEATURE_ID, METRICS_FEATURE_ID } from '../common/constants'; import { InfraStaticSourceConfiguration } from '../common/source_configuration/source_configuration'; import { inventoryViewSavedObjectType } from '../common/saved_objects/inventory_view'; @@ -36,7 +41,7 @@ import { createGetLogQueryFields } from './services/log_queries/get_log_query_fi import { handleEsError } from '../../../../src/plugins/es_ui_shared/server'; import { RulesService } from './services/rules'; -export const config = { +export const config: PluginConfigDescriptor = { schema: schema.object({ enabled: schema.boolean({ defaultValue: true }), inventory: schema.object({ @@ -63,6 +68,7 @@ export const config = { }) ), }), + deprecations: ({ deprecate }) => [deprecate('enabled', '8.0.0')], }; export type InfraConfig = TypeOf<typeof config.schema>; diff --git a/x-pack/plugins/infra/server/utils/get_all_composite_data.ts b/x-pack/plugins/infra/server/utils/get_all_composite_data.ts index df97c91aacd04..1ab290796e36d 100644 --- a/x-pack/plugins/infra/server/utils/get_all_composite_data.ts +++ b/x-pack/plugins/infra/server/utils/get_all_composite_data.ts @@ -24,7 +24,7 @@ export const getAllCompositeData = async < const { body: response } = await esClientSearch(options); // Nothing available, return the previous buckets. - if (response.hits.total.value === 0) { + if (response.hits?.total.value === 0) { return previousBuckets; } diff --git a/x-pack/plugins/infra/tsconfig.json b/x-pack/plugins/infra/tsconfig.json index a9739bdfdedc7..a2d1d2b63655a 100644 --- a/x-pack/plugins/infra/tsconfig.json +++ b/x-pack/plugins/infra/tsconfig.json @@ -22,7 +22,7 @@ { "path": "../../../src/plugins/kibana_utils/tsconfig.json" }, { "path": "../../../src/plugins/kibana_react/tsconfig.json" }, { "path": "../../../src/plugins/usage_collection/tsconfig.json" }, - { "path": "../../../src/plugins/vis_type_timeseries/tsconfig.json" }, + { "path": "../../../src/plugins/vis_types/timeseries/tsconfig.json" }, { "path": "../data_enhanced/tsconfig.json" }, { "path": "../alerting/tsconfig.json" }, { "path": "../features/tsconfig.json" }, diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/processors/dissect.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/processors/dissect.tsx index 3864581317e38..be55000bf374a 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/processors/dissect.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/processors/dissect.tsx @@ -42,11 +42,7 @@ const getFieldsConfig = (esDocUrl: string): Record<string, FieldConfig> => { defaultMessage="Pattern used to dissect the specified field. The pattern is defined by the parts of the string to discard. Use a {keyModifier} to alter the dissection behavior." values={{ keyModifier: ( - <EuiLink - target="_blank" - external - href={esDocUrl + '/dissect-processor.html#dissect-key-modifiers'} - > + <EuiLink target="_blank" external href={esDocUrl}> {i18n.translate( 'xpack.ingestPipelines.pipelineEditor.dissectForm.patternFieldHelpText.dissectProcessorLink', { @@ -97,7 +93,7 @@ const getFieldsConfig = (esDocUrl: string): Record<string, FieldConfig> => { export const Dissect: FunctionComponent = () => { const { services } = useKibana(); - const fieldsConfig = getFieldsConfig(services.documentation.getEsDocsBasePath()); + const fieldsConfig = getFieldsConfig(services.documentation.getDissectKeyModifiersUrl()); return ( <> diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/processors/enrich.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/processors/enrich.tsx index dfbcfc9566507..1c6292795d587 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/processors/enrich.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/processors/enrich.tsx @@ -139,7 +139,6 @@ const fieldsConfig: FieldsConfig = { export const Enrich: FunctionComponent = () => { const { services } = useKibana(); - const esDocUrl = services.documentation.getEsDocsBasePath(); return ( <> <FieldNameField @@ -161,7 +160,11 @@ export const Enrich: FunctionComponent = () => { defaultMessage="Name of the {enrichPolicyLink}." values={{ enrichPolicyLink: ( - <EuiLink external target="_blank" href={esDocUrl + '/ingest-enriching-data.html'}> + <EuiLink + external + target="_blank" + href={services.documentation.getEnrichDataUrl()} + > {i18n.translate( 'xpack.ingestPipelines.pipelineEditor.enrichForm.policyNameHelpText.enrichPolicyLink', { defaultMessage: 'enrich policy' } @@ -206,11 +209,7 @@ export const Enrich: FunctionComponent = () => { defaultMessage="Operator used to match the geo-shape of incoming documents to enrich documents. Only used for {geoMatchPolicyLink}." values={{ geoMatchPolicyLink: ( - <EuiLink - external - target="_blank" - href={esDocUrl + '/enrich-policy-definition.html'} - > + <EuiLink external target="_blank" href={services.documentation.getGeoMatchUrl()}> {i18n.translate( 'xpack.ingestPipelines.pipelineEditor.enrichForm.shapeRelationFieldHelpText.geoMatchPoliciesLink', { defaultMessage: 'geo-match enrich policies' } diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/processors/inference.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/processors/inference.tsx index 9575e6d690e00..9c3601c368342 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/processors/inference.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/processors/inference.tsx @@ -28,14 +28,12 @@ const { emptyField, isJsonField } = fieldValidators; const INFERENCE_CONFIG_DOCS = { regression: { - path: 'inference-processor.html#inference-processor-regression-opt', linkLabel: i18n.translate( 'xpack.ingestPipelines.pipelineEditor.inferenceForm.inferenceConfigField.regressionLinkLabel', { defaultMessage: 'regression' } ), }, classification: { - path: 'inference-processor.html#inference-processor-classification-opt', linkLabel: i18n.translate( 'xpack.ingestPipelines.pipelineEditor.inferenceForm.inferenceConfigField.classificationLinkLabel', { defaultMessage: 'classification' } @@ -43,27 +41,22 @@ const INFERENCE_CONFIG_DOCS = { }, }; -const getInferenceConfigHelpText = (esDocsBasePath: string): React.ReactNode => { +const getInferenceConfigHelpText = ( + regressionDocsLink: string, + classificationDocsLink: string +): React.ReactNode => { return ( <FormattedMessage id="xpack.ingestPipelines.pipelineEditor.inferenceForm.inferenceConfigurationHelpText" defaultMessage="Contains the inference type and its options. There are two types: {regression} and {classification}." values={{ regression: ( - <EuiLink - external - target="_blank" - href={`${esDocsBasePath}/${INFERENCE_CONFIG_DOCS.regression.path}`} - > + <EuiLink external target="_blank" href={regressionDocsLink}> {INFERENCE_CONFIG_DOCS.regression.linkLabel} </EuiLink> ), classification: ( - <EuiLink - external - target="_blank" - href={`${esDocsBasePath}/${INFERENCE_CONFIG_DOCS.classification.path}`} - > + <EuiLink external target="_blank" href={classificationDocsLink}> {INFERENCE_CONFIG_DOCS.classification.linkLabel} </EuiLink> ), @@ -158,7 +151,8 @@ const fieldsConfig: FieldsConfig = { export const Inference: FunctionComponent = () => { const { services } = useKibana(); - const esDocUrl = services.documentation.getEsDocsBasePath(); + const regressionDocsLink = services.documentation.getRegressionUrl(); + const classificationDocsLink = services.documentation.getClassificationUrl(); return ( <> <UseField config={fieldsConfig.model_id} component={Field} path="fields.model_id" /> @@ -188,7 +182,7 @@ export const Inference: FunctionComponent = () => { <UseField config={{ ...fieldsConfig.inference_config, - helpText: getInferenceConfigHelpText(esDocUrl), + helpText: getInferenceConfigHelpText(regressionDocsLink, classificationDocsLink), }} component={XJsonEditor} componentProps={{ diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/test_pipeline/test_pipeline_tabs/tab_documents/tab_documents.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/test_pipeline/test_pipeline_tabs/tab_documents/tab_documents.tsx index 3d9e60ff9f05e..e6e9968ab4511 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/test_pipeline/test_pipeline_tabs/tab_documents/tab_documents.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/test_pipeline/test_pipeline_tabs/tab_documents/tab_documents.tsx @@ -179,7 +179,7 @@ export const DocumentsTab: FunctionComponent<Props> = ({ values={{ learnMoreLink: ( <EuiLink - href={`${services.documentation.getEsDocsBasePath()}/simulate-pipeline-api.html`} + href={services.documentation.getSimulatePipelineApiUrl()} target="_blank" external > diff --git a/x-pack/plugins/ingest_pipelines/public/application/services/documentation.ts b/x-pack/plugins/ingest_pipelines/public/application/services/documentation.ts index 8aa165cc502a8..801088b868370 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/services/documentation.ts +++ b/x-pack/plugins/ingest_pipelines/public/application/services/documentation.ts @@ -13,16 +13,28 @@ export class DocumentationService { private processorsUrl: string = ''; private handlingFailureUrl: string = ''; private putPipelineApiUrl: string = ''; + private simulatePipelineApiUrl: string = ''; + private enrichDataUrl: string = ''; + private geoMatchUrl: string = ''; + private dissectKeyModifiersUrl: string = ''; + private classificationUrl: string = ''; + private regressionUrl: string = ''; public setup(docLinks: DocLinksStart): void { const { DOC_LINK_VERSION, ELASTIC_WEBSITE_URL, links } = docLinks; const docsBase = `${ELASTIC_WEBSITE_URL}guide/en`; this.esDocBasePath = `${docsBase}/elasticsearch/reference/${DOC_LINK_VERSION}`; - this.ingestNodeUrl = `${links.ingest.pipelines}`; - this.processorsUrl = `${links.ingest.processors}`; - this.handlingFailureUrl = `${links.ingest.pipelineFailure}`; - this.putPipelineApiUrl = `${links.apis.createPipeline}`; + this.ingestNodeUrl = links.ingest.pipelines; + this.processorsUrl = links.ingest.processors; + this.handlingFailureUrl = links.ingest.pipelineFailure; + this.putPipelineApiUrl = links.apis.createPipeline; + this.simulatePipelineApiUrl = links.apis.simulatePipeline; + this.enrichDataUrl = links.ingest.enrich; + this.geoMatchUrl = links.ingest.geoMatch; + this.dissectKeyModifiersUrl = links.ingest.dissectKeyModifiers; + this.classificationUrl = links.ingest.inferenceClassification; + this.regressionUrl = links.ingest.inferenceRegression; } public getEsDocsBasePath() { @@ -44,6 +56,30 @@ export class DocumentationService { public getPutPipelineApiUrl() { return this.putPipelineApiUrl; } + + public getSimulatePipelineApiUrl() { + return this.simulatePipelineApiUrl; + } + + public getEnrichDataUrl() { + return this.enrichDataUrl; + } + + public getGeoMatchUrl() { + return this.geoMatchUrl; + } + + public getDissectKeyModifiersUrl() { + return this.dissectKeyModifiersUrl; + } + + public getClassificationUrl() { + return this.classificationUrl; + } + + public getRegressionUrl() { + return this.regressionUrl; + } } export const documentationService = new DocumentationService(); diff --git a/x-pack/plugins/lens/common/expressions/xy_chart/axis_config.ts b/x-pack/plugins/lens/common/expressions/xy_chart/axis_config.ts index 9a9273e43f6f1..29b0fb1352e5b 100644 --- a/x-pack/plugins/lens/common/expressions/xy_chart/axis_config.ts +++ b/x-pack/plugins/lens/common/expressions/xy_chart/axis_config.ts @@ -27,12 +27,18 @@ interface AxisConfig { hide?: boolean; } -export type YAxisMode = 'auto' | 'left' | 'right'; +export type YAxisMode = 'auto' | 'left' | 'right' | 'bottom'; +export type LineStyle = 'solid' | 'dashed' | 'dotted'; +export type FillStyle = 'none' | 'above' | 'below'; export interface YConfig { forAccessor: string; axisMode?: YAxisMode; color?: string; + icon?: string; + lineWidth?: number; + lineStyle?: LineStyle; + fill?: FillStyle; } export type AxisTitlesVisibilityConfigResult = AxesSettingsConfig & { @@ -161,6 +167,24 @@ export const yAxisConfig: ExpressionFunctionDefinition< types: ['string'], help: 'The color of the series', }, + lineStyle: { + types: ['string'], + options: ['solid', 'dotted', 'dashed'], + help: 'The style of the threshold line', + }, + lineWidth: { + types: ['number'], + help: 'The width of the threshold line', + }, + icon: { + types: ['string'], + help: 'An optional icon used for threshold lines', + }, + fill: { + types: ['string'], + options: ['none', 'above', 'below'], + help: '', + }, }, fn: function fn(input: unknown, args: YConfig) { return { diff --git a/x-pack/plugins/lens/public/app_plugin/app.test.tsx b/x-pack/plugins/lens/public/app_plugin/app.test.tsx index 25a809cb3c05d..275e6519367c7 100644 --- a/x-pack/plugins/lens/public/app_plugin/app.test.tsx +++ b/x-pack/plugins/lens/public/app_plugin/app.test.tsx @@ -604,6 +604,9 @@ describe('Lens App', () => { }); it('handles save failure by showing a warning, but still allows another save', async () => { + const mockedConsoleDir = jest.spyOn(console, 'dir'); // mocked console.dir to avoid messages in the console when running tests + mockedConsoleDir.mockImplementation(() => {}); + const services = makeDefaultServices(sessionIdSubject); services.attributeService.wrapAttributes = jest .fn() @@ -620,6 +623,9 @@ describe('Lens App', () => { }); expect(props.redirectTo).not.toHaveBeenCalled(); expect(getButton(instance).disableButton).toEqual(false); + // eslint-disable-next-line no-console + expect(console.dir).toHaveBeenCalledTimes(1); + mockedConsoleDir.mockRestore(); }); it('saves new doc and redirects to originating app', async () => { diff --git a/x-pack/plugins/lens/public/app_plugin/lens_top_nav.tsx b/x-pack/plugins/lens/public/app_plugin/lens_top_nav.tsx index 332d404c6375f..f0785496dcf32 100644 --- a/x-pack/plugins/lens/public/app_plugin/lens_top_nav.tsx +++ b/x-pack/plugins/lens/public/app_plugin/lens_top_nav.tsx @@ -255,7 +255,7 @@ export const LensTopNavMenu = ({ }, }, actions: { - inspect: lensInspector.inspect, + inspect: () => lensInspector.inspect({ title }), exportToCSV: () => { if (!activeData) { return; @@ -335,7 +335,7 @@ export const LensTopNavMenu = ({ setIsSaveModalVisible, uiSettings, unsavedTitle, - lensInspector.inspect, + lensInspector, ] ); diff --git a/x-pack/plugins/lens/public/assets/chart_bar_threshold.tsx b/x-pack/plugins/lens/public/assets/chart_bar_threshold.tsx new file mode 100644 index 0000000000000..88e0a46b5538c --- /dev/null +++ b/x-pack/plugins/lens/public/assets/chart_bar_threshold.tsx @@ -0,0 +1,40 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { EuiIconProps } from '@elastic/eui'; + +export const LensIconChartBarThreshold = ({ + title, + titleId, + ...props +}: Omit<EuiIconProps, 'type'>) => ( + <svg + viewBox="0 0 16 12" + width={30} + height={22} + fill="none" + xmlns="http://www.w3.org/2000/svg" + aria-labelledby={titleId} + {...props} + > + {title ? <title id={titleId}>{title} : null} + + + + + +); diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/add_layer.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/add_layer.tsx index 0259acc4dcca1..69e4aa629cec6 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/add_layer.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/add_layer.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import React, { useState } from 'react'; +import React, { useState, useMemo } from 'react'; import { EuiToolTip, EuiButton, @@ -38,12 +38,17 @@ export function AddLayerButton({ }: AddLayerButtonProps) { const [showLayersChoice, toggleLayersChoice] = useState(false); - const hasMultipleLayers = Boolean(visualization.appendLayer && visualizationState); - if (!hasMultipleLayers) { + const supportedLayers = useMemo(() => { + if (!visualization.appendLayer || !visualizationState) { + return null; + } + return visualization.getSupportedLayers?.(visualizationState, layersMeta); + }, [visualization, visualizationState, layersMeta]); + + if (supportedLayers == null) { return null; } - const supportedLayers = visualization.getSupportedLayers?.(visualizationState, layersMeta); - if (supportedLayers?.length === 1) { + if (supportedLayers.length === 1) { return ( new Promise((r) => setTimeout(r, time)); + let container: HTMLDivElement | undefined; beforeEach(() => { @@ -137,7 +141,7 @@ describe('ConfigPanel', () => { const updater = () => 'updated'; updateDatasource('mockindexpattern', updater); - await new Promise((r) => setTimeout(r, 0)); + await waitMs(0); expect(lensStore.dispatch).toHaveBeenCalledTimes(1); expect( (lensStore.dispatch as jest.Mock).mock.calls[0][0].payload.updater( @@ -147,7 +151,7 @@ describe('ConfigPanel', () => { updateAll('mockindexpattern', updater, props.visualizationState); // wait for one tick so async updater has a chance to trigger - await new Promise((r) => setTimeout(r, 0)); + await waitMs(0); expect(lensStore.dispatch).toHaveBeenCalledTimes(2); expect( (lensStore.dispatch as jest.Mock).mock.calls[0][0].payload.updater( @@ -293,4 +297,164 @@ describe('ConfigPanel', () => { expect(focusedEl?.children[0].getAttribute('data-test-subj')).toEqual('lns-layerPanel-1'); }); }); + + describe('initial default value', () => { + function prepareAndMountComponent(props: ReturnType) { + (generateId as jest.Mock).mockReturnValue(`newId`); + return mountWithProvider( + , + + { + preloadedState: { + datasourceStates: { + mockindexpattern: { + isLoading: false, + state: 'state', + }, + }, + activeDatasourceId: 'mockindexpattern', + }, + }, + { + attachTo: container, + } + ); + } + function clickToAddLayer(instance: ReactWrapper) { + act(() => { + instance.find('[data-test-subj="lnsLayerAddButton"]').first().simulate('click'); + }); + instance.update(); + act(() => { + instance + .find(`[data-test-subj="lnsLayerAddButton-${layerTypes.THRESHOLD}"]`) + .first() + .simulate('click'); + }); + instance.update(); + + return waitMs(0); + } + + function clickToAddDimension(instance: ReactWrapper) { + act(() => { + instance.find('[data-test-subj="lns-empty-dimension"]').last().simulate('click'); + }); + return waitMs(0); + } + + it('should not add an initial dimension when not specified', async () => { + const props = getDefaultProps(); + props.activeVisualization.getSupportedLayers = jest.fn(() => [ + { type: layerTypes.DATA, label: 'Data Layer' }, + { + type: layerTypes.THRESHOLD, + label: 'Threshold layer', + }, + ]); + mockDatasource.initializeDimension = jest.fn(); + + const { instance, lensStore } = await prepareAndMountComponent(props); + await clickToAddLayer(instance); + + expect(lensStore.dispatch).toHaveBeenCalledTimes(1); + expect(mockDatasource.initializeDimension).not.toHaveBeenCalled(); + }); + + it('should not add an initial dimension when initialDimensions are not available for the given layer type', async () => { + const props = getDefaultProps(); + props.activeVisualization.getSupportedLayers = jest.fn(() => [ + { + type: layerTypes.DATA, + label: 'Data Layer', + initialDimensions: [ + { + groupId: 'testGroup', + columnId: 'myColumn', + dataType: 'number', + label: 'Initial value', + staticValue: 100, + }, + ], + }, + { + type: layerTypes.THRESHOLD, + label: 'Threshold layer', + }, + ]); + mockDatasource.initializeDimension = jest.fn(); + + const { instance, lensStore } = await prepareAndMountComponent(props); + await clickToAddLayer(instance); + + expect(lensStore.dispatch).toHaveBeenCalledTimes(1); + expect(mockDatasource.initializeDimension).not.toHaveBeenCalled(); + }); + + it('should use group initial dimension value when adding a new layer if available', async () => { + const props = getDefaultProps(); + props.activeVisualization.getSupportedLayers = jest.fn(() => [ + { type: layerTypes.DATA, label: 'Data Layer' }, + { + type: layerTypes.THRESHOLD, + label: 'Threshold layer', + initialDimensions: [ + { + groupId: 'testGroup', + columnId: 'myColumn', + dataType: 'number', + label: 'Initial value', + staticValue: 100, + }, + ], + }, + ]); + mockDatasource.initializeDimension = jest.fn(); + + const { instance, lensStore } = await prepareAndMountComponent(props); + await clickToAddLayer(instance); + + expect(lensStore.dispatch).toHaveBeenCalledTimes(1); + expect(mockDatasource.initializeDimension).toHaveBeenCalledWith(undefined, 'newId', { + columnId: 'myColumn', + dataType: 'number', + groupId: 'testGroup', + label: 'Initial value', + staticValue: 100, + }); + }); + + it('should add an initial dimension value when clicking on the empty dimension button', async () => { + const props = getDefaultProps(); + props.activeVisualization.getSupportedLayers = jest.fn(() => [ + { + type: layerTypes.DATA, + label: 'Data Layer', + initialDimensions: [ + { + groupId: 'a', + columnId: 'newId', + dataType: 'number', + label: 'Initial value', + staticValue: 100, + }, + ], + }, + ]); + mockDatasource.initializeDimension = jest.fn(); + + const { instance, lensStore } = await prepareAndMountComponent(props); + + await clickToAddDimension(instance); + expect(lensStore.dispatch).toHaveBeenCalledTimes(1); + + expect(mockDatasource.initializeDimension).toHaveBeenCalledWith('state', 'first', { + groupId: 'a', + columnId: 'newId', + dataType: 'number', + label: 'Initial value', + staticValue: 100, + }); + }); + }); }); diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/config_panel.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/config_panel.tsx index f7fe2beefa963..57e4cf5b8dffd 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/config_panel.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/config_panel.tsx @@ -26,8 +26,9 @@ import { useLensSelector, selectVisualization, VisualizationState, + LensAppState, } from '../../../state_management'; -import { AddLayerButton } from './add_layer'; +import { AddLayerButton, getLayerType } from './add_layer'; export const ConfigPanelWrapper = memo(function ConfigPanelWrapper(props: ConfigPanelWrapperProps) { const visualization = useLensSelector(selectVisualization); @@ -177,6 +178,33 @@ export function LayerPanels( layerIds.length ) === 'clear' } + onEmptyDimensionAdd={(columnId, { groupId }) => { + // avoid state update if the datasource does not support initializeDimension + if ( + activeDatasourceId != null && + datasourceMap[activeDatasourceId]?.initializeDimension + ) { + dispatchLens( + updateState({ + subType: 'LAYER_DEFAULT_DIMENSION', + updater: (state) => + addInitialValueIfAvailable({ + ...props, + state, + activeDatasourceId, + layerId, + layerType: getLayerType( + activeVisualization, + state.visualization.state, + layerId + ), + columnId, + groupId, + }), + }) + ); + } + }} onRemoveLayer={() => { dispatchLens( updateState({ @@ -232,21 +260,92 @@ export function LayerPanels( dispatchLens( updateState({ subType: 'ADD_LAYER', - updater: (state) => - appendLayer({ + updater: (state) => { + const newState = appendLayer({ activeVisualization, generateId: () => id, trackUiEvent, activeDatasource: datasourceMap[activeDatasourceId!], state, layerType, - }), + }); + return addInitialValueIfAvailable({ + ...props, + activeDatasourceId: activeDatasourceId!, + state: newState, + layerId: id, + layerType, + }); + }, }) ); - setNextFocusedLayerId(id); }} /> ); } + +function addInitialValueIfAvailable({ + state, + activeVisualization, + framePublicAPI, + layerType, + activeDatasourceId, + datasourceMap, + layerId, + columnId, + groupId, +}: ConfigPanelWrapperProps & { + state: LensAppState; + activeDatasourceId: string; + activeVisualization: Visualization; + layerId: string; + layerType: string; + columnId?: string; + groupId?: string; +}) { + const layerInfo = activeVisualization + .getSupportedLayers(state.visualization.state, framePublicAPI) + .find(({ type }) => type === layerType); + + const activeDatasource = datasourceMap[activeDatasourceId]; + + if (layerInfo?.initialDimensions && activeDatasource?.initializeDimension) { + const info = groupId + ? layerInfo.initialDimensions.find(({ groupId: id }) => id === groupId) + : // pick the first available one if not passed + layerInfo.initialDimensions[0]; + + if (info) { + return { + ...state, + datasourceStates: { + ...state.datasourceStates, + [activeDatasourceId]: { + ...state.datasourceStates[activeDatasourceId], + state: activeDatasource.initializeDimension( + state.datasourceStates[activeDatasourceId].state, + layerId, + { + ...info, + columnId: columnId || info.columnId, + } + ), + }, + }, + visualization: { + ...state.visualization, + state: activeVisualization.setDimension({ + groupId: info.groupId, + layerId, + columnId: columnId || info.columnId, + prevState: state.visualization.state, + frame: framePublicAPI, + }), + }, + }; + } + } + return state; +} diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.test.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.test.tsx index 13b7b8cfecf56..f777fd0976dfd 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.test.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.test.tsx @@ -83,6 +83,7 @@ describe('LayerPanel', () => { registerNewLayerRef: jest.fn(), isFullscreen: false, toggleFullscreen: jest.fn(), + onEmptyDimensionAdd: jest.fn(), }; } @@ -920,4 +921,33 @@ describe('LayerPanel', () => { expect(updateVisualization).toHaveBeenCalledTimes(1); }); }); + + describe('add a new dimension', () => { + it('should call onEmptyDimensionAdd callback on new dimension creation', async () => { + mockVisualization.getConfiguration.mockReturnValue({ + groups: [ + { + groupLabel: 'A', + groupId: 'a', + accessors: [], + filterOperations: () => true, + supportsMoreColumns: true, + dataTestSubj: 'lnsGroup', + }, + ], + }); + const props = getDefaultProps(); + const { instance } = await mountWithProvider(); + + act(() => { + instance.find('[data-test-subj="lns-empty-dimension"]').first().simulate('click'); + }); + instance.update(); + + expect(props.onEmptyDimensionAdd).toHaveBeenCalledWith( + 'newid', + expect.objectContaining({ groupId: 'a' }) + ); + }); + }); }); 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 520c2bc837c60..8c947d3502f93 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 @@ -57,6 +57,7 @@ export function LayerPanel( onRemoveLayer: () => void; registerNewLayerRef: (layerId: string, instance: HTMLDivElement | null) => void; toggleFullscreen: () => void; + onEmptyDimensionAdd: (columnId: string, group: { groupId: string }) => void; } ) { const [activeDimension, setActiveDimension] = useState( @@ -124,7 +125,11 @@ export function LayerPanel( dateRange, }; - const { groups, supportStaticValue } = useMemo( + const { + groups, + supportStaticValue, + supportFieldFormat = true, + } = useMemo( () => activeVisualization.getConfiguration(layerVisualizationConfigProps), // eslint-disable-next-line react-hooks/exhaustive-deps [ @@ -227,13 +232,25 @@ export function LayerPanel( const isDimensionPanelOpen = Boolean(activeId); const updateDataLayerState = useCallback( - (newState: unknown, { isDimensionComplete = true }: { isDimensionComplete?: boolean } = {}) => { + ( + newState: unknown, + { + isDimensionComplete = true, + // this flag is a hack to force a sync render where it was planned an async/setTimeout state update + // TODO: revisit this once we get rid of updateDatasourceAsync upstream + forceRender = false, + }: { isDimensionComplete?: boolean; forceRender?: boolean } = {} + ) => { if (!activeGroup || !activeId) { return; } if (allAccessors.includes(activeId)) { if (isDimensionComplete) { - updateDatasourceAsync(datasourceId, newState); + if (forceRender) { + updateDatasource(datasourceId, newState); + } else { + updateDatasourceAsync(datasourceId, newState); + } } else { // The datasource can indicate that the previously-valid column is no longer // complete, which clears the visualization. This keeps the flyout open and reuses @@ -263,7 +280,11 @@ export function LayerPanel( ); setActiveDimension({ ...activeDimension, isNew: false }); } else { - updateDatasourceAsync(datasourceId, newState); + if (forceRender) { + updateDatasource(datasourceId, newState); + } else { + updateDatasourceAsync(datasourceId, newState); + } } }, // eslint-disable-next-line react-hooks/exhaustive-deps @@ -295,11 +316,10 @@ export function LayerPanel( hasBorder hasShadow > -

    +
    )} -
    + {groups.map((group, groupIndex) => { const isMissing = !isEmptyLayer && group.required && group.accessors.length === 0; @@ -460,6 +480,8 @@ export function LayerPanel( columnId: accessorConfig.columnId, groupId: group.groupId, filterOperations: group.filterOperations, + invalid: group.invalid, + invalidMessage: group.invalidMessage, }} /> @@ -478,6 +500,7 @@ export function LayerPanel( layerDatasource={layerDatasource} layerDatasourceDropProps={layerDatasourceDropProps} onClick={(id) => { + props.onEmptyDimensionAdd(id, group); setActiveDimension({ activeGroup: group, activeId: id, @@ -538,6 +561,8 @@ export function LayerPanel( toggleFullscreen, isFullscreen, setState: updateDataLayerState, + supportStaticValue: Boolean(supportStaticValue), + supportFieldFormat: Boolean(supportFieldFormat), layerType: activeVisualization.getLayerType(layerId, visualizationState), }} /> diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_settings.test.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_settings.test.tsx new file mode 100644 index 0000000000000..04c430143a3c8 --- /dev/null +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_settings.test.tsx @@ -0,0 +1,71 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { + createMockFramePublicAPI, + createMockVisualization, + mountWithProvider, +} from '../../../mocks'; +import { Visualization } from '../../../types'; +import { LayerSettings } from './layer_settings'; + +describe('LayerSettings', () => { + let mockVisualization: jest.Mocked; + const frame = createMockFramePublicAPI(); + + function getDefaultProps() { + return { + activeVisualization: mockVisualization, + layerConfigProps: { + layerId: 'myLayer', + state: {}, + frame, + dateRange: { fromDate: 'now-7d', toDate: 'now' }, + activeData: frame.activeData, + setState: jest.fn(), + }, + }; + } + + beforeEach(() => { + mockVisualization = { + ...createMockVisualization(), + id: 'testVis', + visualizationTypes: [ + { + icon: 'empty', + id: 'testVis', + label: 'TEST1', + groupLabel: 'testVisGroup', + }, + ], + }; + }); + + it('should render nothing with no custom renderer nor description', async () => { + // @ts-expect-error + mockVisualization.getDescription.mockReturnValue(undefined); + const { instance } = await mountWithProvider(); + expect(instance.html()).toBe(null); + }); + + it('should render a static header if visualization has only a description value', async () => { + mockVisualization.getDescription.mockReturnValue({ + icon: 'myIcon', + label: 'myVisualizationType', + }); + const { instance } = await mountWithProvider(); + expect(instance.find('StaticHeader').first().prop('label')).toBe('myVisualizationType'); + }); + + it('should call the custom renderer if available', async () => { + mockVisualization.renderLayerHeader = jest.fn(); + await mountWithProvider(); + expect(mockVisualization.renderLayerHeader).toHaveBeenCalled(); + }); +}); diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_settings.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_settings.tsx index 467b1ecfe1b5b..fc88ff2af8bbe 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_settings.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_settings.tsx @@ -6,44 +6,23 @@ */ import React from 'react'; -import { EuiFlexGroup, EuiFlexItem, EuiIcon, EuiTitle } from '@elastic/eui'; import { NativeRenderer } from '../../../native_renderer'; import { Visualization, VisualizationLayerWidgetProps } from '../../../types'; +import { StaticHeader } from '../../../shared_components'; export function LayerSettings({ - layerId, activeVisualization, layerConfigProps, }: { - layerId: string; activeVisualization: Visualization; layerConfigProps: VisualizationLayerWidgetProps; }) { - const description = activeVisualization.getDescription(layerConfigProps.state); - if (!activeVisualization.renderLayerHeader) { + const description = activeVisualization.getDescription(layerConfigProps.state); if (!description) { return null; } - return ( - - {description.icon && ( - - {' '} - - )} - - -
    {description.label}
    -
    -
    -
    - ); + return ; } return ( diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/suggestion_helpers.test.ts b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/suggestion_helpers.test.ts index 632989057b488..90fa2ab080dd2 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/suggestion_helpers.test.ts +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/suggestion_helpers.test.ts @@ -45,21 +45,22 @@ describe('suggestion helpers', () => { generateSuggestion(), ]); const suggestedState = {}; - const suggestions = getSuggestions({ - visualizationMap: { - vis1: { - ...mockVisualization, - getSuggestions: () => [ - { - score: 0.5, - title: 'Test', - state: suggestedState, - previewIcon: 'empty', - }, - ], - }, + const visualizationMap = { + vis1: { + ...mockVisualization, + getSuggestions: () => [ + { + score: 0.5, + title: 'Test', + state: suggestedState, + previewIcon: 'empty', + }, + ], }, - activeVisualizationId: 'vis1', + }; + const suggestions = getSuggestions({ + visualizationMap, + activeVisualization: visualizationMap.vis1, visualizationState: {}, datasourceMap, datasourceStates, @@ -74,38 +75,39 @@ describe('suggestion helpers', () => { datasourceMap.mock.getDatasourceSuggestionsFromCurrentState.mockReturnValue([ generateSuggestion(), ]); - const suggestions = getSuggestions({ - visualizationMap: { - vis1: { - ...mockVisualization1, - getSuggestions: () => [ - { - score: 0.5, - title: 'Test', - state: {}, - previewIcon: 'empty', - }, - { - score: 0.5, - title: 'Test2', - state: {}, - previewIcon: 'empty', - }, - ], - }, - vis2: { - ...mockVisualization2, - getSuggestions: () => [ - { - score: 0.5, - title: 'Test3', - state: {}, - previewIcon: 'empty', - }, - ], - }, + const visualizationMap = { + vis1: { + ...mockVisualization1, + getSuggestions: () => [ + { + score: 0.5, + title: 'Test', + state: {}, + previewIcon: 'empty', + }, + { + score: 0.5, + title: 'Test2', + state: {}, + previewIcon: 'empty', + }, + ], + }, + vis2: { + ...mockVisualization2, + getSuggestions: () => [ + { + score: 0.5, + title: 'Test3', + state: {}, + previewIcon: 'empty', + }, + ], }, - activeVisualizationId: 'vis1', + }; + const suggestions = getSuggestions({ + visualizationMap, + activeVisualization: visualizationMap.vis1, visualizationState: {}, datasourceMap, datasourceStates, @@ -116,11 +118,12 @@ describe('suggestion helpers', () => { it('should call getDatasourceSuggestionsForField when a field is passed', () => { datasourceMap.mock.getDatasourceSuggestionsForField.mockReturnValue([generateSuggestion()]); const droppedField = {}; + const visualizationMap = { + vis1: createMockVisualization(), + }; getSuggestions({ - visualizationMap: { - vis1: createMockVisualization(), - }, - activeVisualizationId: 'vis1', + visualizationMap, + activeVisualization: visualizationMap.vis1, visualizationState: {}, datasourceMap, datasourceStates, @@ -128,7 +131,8 @@ describe('suggestion helpers', () => { }); expect(datasourceMap.mock.getDatasourceSuggestionsForField).toHaveBeenCalledWith( datasourceStates.mock.state, - droppedField + droppedField, + expect.any(Function) ); }); @@ -148,12 +152,13 @@ describe('suggestion helpers', () => { mock2: createMockDatasource('a'), mock3: createMockDatasource('a'), }; + const visualizationMap = { + vis1: createMockVisualization(), + }; const droppedField = {}; getSuggestions({ - visualizationMap: { - vis1: createMockVisualization(), - }, - activeVisualizationId: 'vis1', + visualizationMap, + activeVisualization: visualizationMap.vis1, visualizationState: {}, datasourceMap: multiDatasourceMap, datasourceStates: multiDatasourceStates, @@ -161,11 +166,13 @@ describe('suggestion helpers', () => { }); expect(multiDatasourceMap.mock.getDatasourceSuggestionsForField).toHaveBeenCalledWith( multiDatasourceStates.mock.state, - droppedField + droppedField, + expect.any(Function) ); expect(multiDatasourceMap.mock2.getDatasourceSuggestionsForField).toHaveBeenCalledWith( multiDatasourceStates.mock2.state, - droppedField + droppedField, + expect.any(Function) ); expect(multiDatasourceMap.mock3.getDatasourceSuggestionsForField).not.toHaveBeenCalled(); }); @@ -174,11 +181,14 @@ describe('suggestion helpers', () => { datasourceMap.mock.getDatasourceSuggestionsForVisualizeField.mockReturnValue([ generateSuggestion(), ]); + + const visualizationMap = { + vis1: createMockVisualization(), + }; + getSuggestions({ - visualizationMap: { - vis1: createMockVisualization(), - }, - activeVisualizationId: 'vis1', + visualizationMap, + activeVisualization: visualizationMap.vis1, visualizationState: {}, datasourceMap, datasourceStates, @@ -214,11 +224,13 @@ describe('suggestion helpers', () => { indexPatternId: '1', fieldName: 'test', }; + + const visualizationMap = { + vis1: createMockVisualization(), + }; getSuggestions({ - visualizationMap: { - vis1: createMockVisualization(), - }, - activeVisualizationId: 'vis1', + visualizationMap, + activeVisualization: visualizationMap.vis1, visualizationState: {}, datasourceMap: multiDatasourceMap, datasourceStates: multiDatasourceStates, @@ -245,38 +257,39 @@ describe('suggestion helpers', () => { datasourceMap.mock.getDatasourceSuggestionsFromCurrentState.mockReturnValue([ generateSuggestion(), ]); - const suggestions = getSuggestions({ - visualizationMap: { - vis1: { - ...mockVisualization1, - getSuggestions: () => [ - { - score: 0.2, - title: 'Test', - state: {}, - previewIcon: 'empty', - }, - { - score: 0.8, - title: 'Test2', - state: {}, - previewIcon: 'empty', - }, - ], - }, - vis2: { - ...mockVisualization2, - getSuggestions: () => [ - { - score: 0.6, - title: 'Test3', - state: {}, - previewIcon: 'empty', - }, - ], - }, + const visualizationMap = { + vis1: { + ...mockVisualization1, + getSuggestions: () => [ + { + score: 0.2, + title: 'Test', + state: {}, + previewIcon: 'empty', + }, + { + score: 0.8, + title: 'Test2', + state: {}, + previewIcon: 'empty', + }, + ], }, - activeVisualizationId: 'vis1', + vis2: { + ...mockVisualization2, + getSuggestions: () => [ + { + score: 0.6, + title: 'Test3', + state: {}, + previewIcon: 'empty', + }, + ], + }, + }; + const suggestions = getSuggestions({ + visualizationMap, + activeVisualization: visualizationMap.vis1, visualizationState: {}, datasourceMap, datasourceStates, @@ -305,12 +318,13 @@ describe('suggestion helpers', () => { { state: {}, table: table1, keptLayerIds: ['first'] }, { state: {}, table: table2, keptLayerIds: ['first'] }, ]); + const visualizationMap = { + vis1: mockVisualization1, + vis2: mockVisualization2, + }; getSuggestions({ - visualizationMap: { - vis1: mockVisualization1, - vis2: mockVisualization2, - }, - activeVisualizationId: 'vis1', + visualizationMap, + activeVisualization: visualizationMap.vis1, visualizationState: {}, datasourceMap, datasourceStates, @@ -357,18 +371,20 @@ describe('suggestion helpers', () => { previewIcon: 'empty', }, ]); - const suggestions = getSuggestions({ - visualizationMap: { - vis1: { - ...mockVisualization1, - getSuggestions: vis1Suggestions, - }, - vis2: { - ...mockVisualization2, - getSuggestions: vis2Suggestions, - }, + const visualizationMap = { + vis1: { + ...mockVisualization1, + getSuggestions: vis1Suggestions, }, - activeVisualizationId: 'vis1', + vis2: { + ...mockVisualization2, + getSuggestions: vis2Suggestions, + }, + }; + + const suggestions = getSuggestions({ + visualizationMap, + activeVisualization: visualizationMap.vis1, visualizationState: {}, datasourceMap, datasourceStates, @@ -389,12 +405,15 @@ describe('suggestion helpers', () => { generateSuggestion(0), generateSuggestion(1), ]); + + const visualizationMap = { + vis1: mockVisualization1, + vis2: mockVisualization2, + }; + getSuggestions({ - visualizationMap: { - vis1: mockVisualization1, - vis2: mockVisualization2, - }, - activeVisualizationId: 'vis1', + visualizationMap, + activeVisualization: visualizationMap.vis1, visualizationState: {}, datasourceMap, datasourceStates, @@ -419,12 +438,13 @@ describe('suggestion helpers', () => { generateSuggestion(0), generateSuggestion(1), ]); + const visualizationMap = { + vis1: mockVisualization1, + vis2: mockVisualization2, + }; getSuggestions({ - visualizationMap: { - vis1: mockVisualization1, - vis2: mockVisualization2, - }, - activeVisualizationId: 'vis1', + visualizationMap, + activeVisualization: visualizationMap.vis1, visualizationState: {}, datasourceMap, datasourceStates, @@ -451,12 +471,14 @@ describe('suggestion helpers', () => { generateSuggestion(0), generateSuggestion(1), ]); + const visualizationMap = { + vis1: mockVisualization1, + vis2: mockVisualization2, + }; + getSuggestions({ - visualizationMap: { - vis1: mockVisualization1, - vis2: mockVisualization2, - }, - activeVisualizationId: 'vis1', + visualizationMap, + activeVisualization: visualizationMap.vis1, visualizationState: {}, datasourceMap, datasourceStates, @@ -538,7 +560,8 @@ describe('suggestion helpers', () => { humanData: { label: 'myfieldLabel', }, - } + }, + expect.any(Function) ); }); diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/suggestion_helpers.ts b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/suggestion_helpers.ts index 2f3fe3795a881..a5c7871f33dfc 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/suggestion_helpers.ts +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/suggestion_helpers.ts @@ -58,7 +58,7 @@ export function getSuggestions({ datasourceMap, datasourceStates, visualizationMap, - activeVisualizationId, + activeVisualization, subVisualizationId, visualizationState, field, @@ -69,7 +69,7 @@ export function getSuggestions({ datasourceMap: DatasourceMap; datasourceStates: DatasourceStates; visualizationMap: VisualizationMap; - activeVisualizationId: string | null; + activeVisualization?: Visualization; subVisualizationId?: string; visualizationState: unknown; field?: unknown; @@ -83,16 +83,12 @@ export function getSuggestions({ const layerTypesMap = datasources.reduce((memo, [datasourceId, datasource]) => { const datasourceState = datasourceStates[datasourceId].state; - if (!activeVisualizationId || !datasourceState || !visualizationMap[activeVisualizationId]) { + if (!activeVisualization || !datasourceState) { return memo; } const layers = datasource.getLayers(datasourceState); for (const layerId of layers) { - const type = getLayerType( - visualizationMap[activeVisualizationId], - visualizationState, - layerId - ); + const type = getLayerType(activeVisualization, visualizationState, layerId); memo[layerId] = type; } return memo; @@ -112,7 +108,11 @@ export function getSuggestions({ visualizeTriggerFieldContext.fieldName ); } else if (field) { - dataSourceSuggestions = datasource.getDatasourceSuggestionsForField(datasourceState, field); + dataSourceSuggestions = datasource.getDatasourceSuggestionsForField( + datasourceState, + field, + (layerId) => isLayerSupportedByVisualization(layerId, [layerTypes.DATA]) // a field dragged to workspace should added to data layer + ); } else { dataSourceSuggestions = datasource.getDatasourceSuggestionsFromCurrentState( datasourceState, @@ -121,7 +121,6 @@ export function getSuggestions({ } return dataSourceSuggestions.map((suggestion) => ({ ...suggestion, datasourceId })); }); - // Pass all table suggestions to all visualization extensions to get visualization suggestions // and rank them by score return Object.entries(visualizationMap) @@ -139,12 +138,8 @@ export function getSuggestions({ .flatMap((datasourceSuggestion) => { const table = datasourceSuggestion.table; const currentVisualizationState = - visualizationId === activeVisualizationId ? visualizationState : undefined; - const palette = - mainPalette || - (activeVisualizationId && visualizationMap[activeVisualizationId]?.getMainPalette - ? visualizationMap[activeVisualizationId].getMainPalette?.(visualizationState) - : undefined); + visualizationId === activeVisualization?.id ? visualizationState : undefined; + const palette = mainPalette || activeVisualization?.getMainPalette?.(visualizationState); return getVisualizationSuggestions( visualization, @@ -169,14 +164,14 @@ export function getVisualizeFieldSuggestions({ datasourceMap, datasourceStates, visualizationMap, - activeVisualizationId, + activeVisualization, visualizationState, visualizeTriggerFieldContext, }: { datasourceMap: DatasourceMap; datasourceStates: DatasourceStates; visualizationMap: VisualizationMap; - activeVisualizationId: string | null; + activeVisualization: Visualization; subVisualizationId?: string; visualizationState: unknown; visualizeTriggerFieldContext?: VisualizeFieldContext; @@ -185,12 +180,12 @@ export function getVisualizeFieldSuggestions({ datasourceMap, datasourceStates, visualizationMap, - activeVisualizationId, + activeVisualization, visualizationState, visualizeTriggerFieldContext, }); if (suggestions.length) { - return suggestions.find((s) => s.visualizationId === activeVisualizationId) || suggestions[0]; + return suggestions.find((s) => s.visualizationId === activeVisualization?.id) || suggestions[0]; } } @@ -263,18 +258,19 @@ export function getTopSuggestionForField( (datasourceLayer) => datasourceLayer.getTableSpec().length > 0 ); - const mainPalette = - visualization.activeId && visualizationMap[visualization.activeId]?.getMainPalette - ? visualizationMap[visualization.activeId].getMainPalette?.(visualization.state) - : undefined; + const activeVisualization = visualization.activeId + ? visualizationMap[visualization.activeId] + : undefined; + + const mainPalette = activeVisualization?.getMainPalette?.(visualization.state); const suggestions = getSuggestions({ datasourceMap: { [datasource.id]: datasource }, datasourceStates, visualizationMap: hasData && visualization.activeId - ? { [visualization.activeId]: visualizationMap[visualization.activeId] } + ? { [visualization.activeId]: activeVisualization! } : visualizationMap, - activeVisualizationId: visualization.activeId, + activeVisualization, visualizationState: visualization.state, field, mainPalette, diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/suggestion_panel.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/suggestion_panel.tsx index 858fcedf215ef..5e5e19ea29e84 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/suggestion_panel.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/suggestion_panel.tsx @@ -201,7 +201,9 @@ export function SuggestionPanel({ datasourceMap, datasourceStates: currentDatasourceStates, visualizationMap, - activeVisualizationId: currentVisualization.activeId, + activeVisualization: currentVisualization.activeId + ? visualizationMap[currentVisualization.activeId] + : undefined, visualizationState: currentVisualization.state, activeData, }) 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 28c0567d784ea..51d4f2955a52b 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 @@ -515,11 +515,14 @@ function getTopSuggestion( props.visualizationMap[visualization.activeId].getMainPalette ? props.visualizationMap[visualization.activeId].getMainPalette!(visualization.state) : undefined; + const unfilteredSuggestions = getSuggestions({ datasourceMap: props.datasourceMap, datasourceStates, visualizationMap: { [visualizationId]: newVisualization }, - activeVisualizationId: visualization.activeId, + activeVisualization: visualization.activeId + ? props.visualizationMap[visualization.activeId] + : undefined, visualizationState: visualization.state, subVisualizationId, activeData: props.framePublicAPI.activeData, 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 e386bac026fdc..d25e6754fe03f 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 @@ -11,15 +11,11 @@ import { i18n } from '@kbn/i18n'; import { EuiListGroup, EuiFormRow, - EuiFieldText, EuiSpacer, EuiListGroupItemProps, EuiFormLabel, EuiToolTip, EuiText, - EuiTabs, - EuiTab, - EuiCallOut, } from '@elastic/eui'; import { IndexPatternDimensionEditorProps } from './dimension_panel'; import { OperationSupportMatrix } from './operation_support'; @@ -47,41 +43,29 @@ import { setTimeScaling, TimeScaling } from './time_scaling'; import { defaultFilter, Filtering, setFilter } from './filtering'; import { AdvancedOptions } from './advanced_options'; import { setTimeShift, TimeShift } from './time_shift'; -import { useDebouncedValue } from '../../shared_components'; +import { LayerType } from '../../../common'; +import { + quickFunctionsName, + staticValueOperationName, + isQuickFunction, + getParamEditor, + formulaOperationName, + DimensionEditorTabs, + CalloutWarning, + LabelInput, + getErrorMessage, +} from './dimensions_editor_helpers'; +import type { TemporaryState } from './dimensions_editor_helpers'; const operationPanels = getOperationDisplay(); export interface DimensionEditorProps extends IndexPatternDimensionEditorProps { selectedColumn?: IndexPatternColumn; + layerType: LayerType; operationSupportMatrix: OperationSupportMatrix; currentIndexPattern: IndexPattern; } -const LabelInput = ({ value, onChange }: { value: string; onChange: (value: string) => void }) => { - const { inputValue, handleInputChange, initialValue } = useDebouncedValue({ onChange, value }); - - return ( - - { - handleInputChange(e.target.value); - }} - placeholder={initialValue} - /> - - ); -}; - export function DimensionEditor(props: DimensionEditorProps) { const { selectedColumn, @@ -96,6 +80,8 @@ export function DimensionEditor(props: DimensionEditorProps) { dimensionGroups, toggleFullscreen, isFullscreen, + supportStaticValue, + supportFieldFormat = true, layerType, } = props; const services = { @@ -110,6 +96,11 @@ export function DimensionEditor(props: DimensionEditorProps) { const selectedOperationDefinition = selectedColumn && operationDefinitionMap[selectedColumn.operationType]; + const [temporaryState, setTemporaryState] = useState('none'); + + const temporaryQuickFunction = Boolean(temporaryState === quickFunctionsName); + const temporaryStaticValue = Boolean(temporaryState === staticValueOperationName); + const updateLayer = useCallback( (newLayer) => setState((prevState) => mergeLayer({ state: prevState, layerId, newLayer })), [layerId, setState] @@ -141,9 +132,64 @@ export function DimensionEditor(props: DimensionEditorProps) { ...incompleteParams } = incompleteInfo || {}; - const ParamEditor = selectedOperationDefinition?.paramEditor; + const isQuickFunctionSelected = Boolean( + supportStaticValue + ? selectedOperationDefinition && isQuickFunction(selectedOperationDefinition.type) + : !selectedOperationDefinition || isQuickFunction(selectedOperationDefinition.type) + ); + const showQuickFunctions = temporaryQuickFunction || isQuickFunctionSelected; + + const showStaticValueFunction = + temporaryStaticValue || + (temporaryState === 'none' && + supportStaticValue && + (!selectedColumn || selectedColumn?.operationType === staticValueOperationName)); + + const addStaticValueColumn = (prevLayer = props.state.layers[props.layerId]) => { + if (selectedColumn?.operationType !== staticValueOperationName) { + trackUiEvent(`indexpattern_dimension_operation_static_value`); + return insertOrReplaceColumn({ + layer: prevLayer, + indexPattern: currentIndexPattern, + columnId, + op: staticValueOperationName, + visualizationGroups: dimensionGroups, + }); + } + return prevLayer; + }; + + // this function intercepts the state update for static value function + // and. if in temporary state, it merges the "add new static value column" state with the incoming + // changes from the static value operation (which has to be a function) + // Note: it forced a rerender at this point to avoid UI glitches in async updates (another hack upstream) + // TODO: revisit this once we get rid of updateDatasourceAsync upstream + const moveDefinetelyToStaticValueAndUpdate = ( + setter: IndexPatternLayer | ((prevLayer: IndexPatternLayer) => IndexPatternLayer) + ) => { + if (temporaryStaticValue) { + setTemporaryState('none'); + if (typeof setter === 'function') { + return setState( + (prevState) => { + const layer = setter(addStaticValueColumn(prevState.layers[layerId])); + return mergeLayer({ state: prevState, layerId, newLayer: layer }); + }, + { + isDimensionComplete: true, + forceRender: true, + } + ); + } + } + return setStateWrapper(setter); + }; - const [temporaryQuickFunction, setQuickFunction] = useState(false); + const ParamEditor = getParamEditor( + temporaryStaticValue, + selectedOperationDefinition, + supportStaticValue && !showQuickFunctions + ); const possibleOperations = useMemo(() => { return Object.values(operationDefinitionMap) @@ -245,9 +291,9 @@ export function DimensionEditor(props: DimensionEditorProps) { [`aria-pressed`]: isActive, onClick() { if ( - operationDefinitionMap[operationType].input === 'none' || - operationDefinitionMap[operationType].input === 'managedReference' || - operationDefinitionMap[operationType].input === 'fullReference' + ['none', 'fullReference', 'managedReference'].includes( + operationDefinitionMap[operationType].input + ) ) { // Clear invalid state because we are reseting to a valid column if (selectedColumn?.operationType === operationType) { @@ -264,9 +310,12 @@ export function DimensionEditor(props: DimensionEditorProps) { visualizationGroups: dimensionGroups, targetGroup: props.groupId, }); - if (temporaryQuickFunction && newLayer.columns[columnId].operationType !== 'formula') { + if ( + temporaryQuickFunction && + isQuickFunction(newLayer.columns[columnId].operationType) + ) { // Only switch the tab once the formula is fully removed - setQuickFunction(false); + setTemporaryState('none'); } setStateWrapper(newLayer); trackUiEvent(`indexpattern_dimension_operation_${operationType}`); @@ -297,9 +346,12 @@ export function DimensionEditor(props: DimensionEditorProps) { }); // ); } - if (temporaryQuickFunction && newLayer.columns[columnId].operationType !== 'formula') { + if ( + temporaryQuickFunction && + isQuickFunction(newLayer.columns[columnId].operationType) + ) { // Only switch the tab once the formula is fully removed - setQuickFunction(false); + setTemporaryState('none'); } setStateWrapper(newLayer); trackUiEvent(`indexpattern_dimension_operation_${operationType}`); @@ -314,7 +366,7 @@ export function DimensionEditor(props: DimensionEditorProps) { } if (temporaryQuickFunction) { - setQuickFunction(false); + setTemporaryState('none'); } const newLayer = replaceColumn({ layer: props.state.layers[props.layerId], @@ -348,29 +400,10 @@ export function DimensionEditor(props: DimensionEditorProps) { !currentFieldIsInvalid && !incompleteInfo && selectedColumn && - selectedColumn.operationType !== 'formula'; + isQuickFunction(selectedColumn.operationType); const quickFunctions = ( <> - {temporaryQuickFunction && selectedColumn?.operationType === 'formula' && ( - <> - -

    - {i18n.translate('xpack.lens.indexPattern.formulaWarningText', { - defaultMessage: 'To overwrite your formula, select a quick function', - })} -

    -
    - - )}
    {i18n.translate('xpack.lens.indexPattern.functionsLabel', { @@ -608,24 +641,28 @@ export function DimensionEditor(props: DimensionEditorProps) { ); - const formulaTab = ParamEditor ? ( - + const customParamEditor = ParamEditor ? ( + <> + + ) : null; + const TabContent = showQuickFunctions ? quickFunctions : customParamEditor; + const onFormatChange = useCallback( (newFormat) => { updateLayer( @@ -640,58 +677,69 @@ export function DimensionEditor(props: DimensionEditorProps) { [columnId, layerId, state.layers, updateLayer] ); + const hasFormula = + !isFullscreen && operationSupportMatrix.operationWithoutField.has(formulaOperationName); + + const hasTabs = hasFormula || supportStaticValue; + return (
    - {!isFullscreen && operationSupportMatrix.operationWithoutField.has('formula') ? ( - - { - if (selectedColumn?.operationType === 'formula') { - setQuickFunction(true); + {hasTabs ? ( + { + if (tabClicked === 'quickFunctions') { + if (selectedColumn && !isQuickFunction(selectedColumn.operationType)) { + setTemporaryState(quickFunctionsName); + return; } - }} - > - {i18n.translate('xpack.lens.indexPattern.quickFunctionsLabel', { - defaultMessage: 'Quick functions', - })} - - { - if (selectedColumn?.operationType !== 'formula') { - setQuickFunction(false); + } + + 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: 'formula', + op: formulaOperationName, visualizationGroups: dimensionGroups, }); setStateWrapper(newLayer); trackUiEvent(`indexpattern_dimension_operation_formula`); - return; - } else { - setQuickFunction(false); } - }} - > - {i18n.translate('xpack.lens.indexPattern.formulaLabel', { - defaultMessage: 'Formula', - })} - - + } + }} + /> ) : null} - {isFullscreen - ? formulaTab - : selectedOperationDefinition?.type === 'formula' && !temporaryQuickFunction - ? formulaTab - : quickFunctions} + + {TabContent} - {!isFullscreen && !currentFieldIsInvalid && !temporaryQuickFunction && ( + {!isFullscreen && !currentFieldIsInvalid && temporaryState === 'none' && (
    {!incompleteInfo && selectedColumn && ( )} - {!isFullscreen && + {supportFieldFormat && + !isFullscreen && selectedColumn && (selectedColumn.dataType === 'number' || selectedColumn.operationType === 'range') ? ( @@ -735,26 +784,3 @@ export function DimensionEditor(props: DimensionEditorProps) {
    ); } - -function getErrorMessage( - selectedColumn: IndexPatternColumn | undefined, - incompleteOperation: boolean, - input: 'none' | 'field' | 'fullReference' | 'managedReference' | undefined, - fieldInvalid: boolean -) { - if (selectedColumn && incompleteOperation) { - if (input === 'field') { - return i18n.translate('xpack.lens.indexPattern.invalidOperationLabel', { - defaultMessage: 'This field does not work with the selected function.', - }); - } - return i18n.translate('xpack.lens.indexPattern.chooseFieldLabel', { - defaultMessage: 'To use this function, select a field.', - }); - } - if (fieldInvalid) { - return i18n.translate('xpack.lens.indexPattern.invalidFieldLabel', { - defaultMessage: 'Invalid field. Check your index pattern or pick another field.', - }); - } -} diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_panel.test.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_panel.test.tsx index 5d56661f15915..d823def1da114 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_panel.test.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_panel.test.tsx @@ -52,6 +52,13 @@ jest.mock('lodash', () => { }; }); jest.mock('../../id_generator'); +// Mock the Monaco Editor component +jest.mock('../operations/definitions/formula/editor/formula_editor', () => { + return { + WrappedFormulaEditor: () =>
    , + FormulaEditor: () =>
    , + }; +}); const fields = [ { @@ -211,6 +218,7 @@ describe('IndexPatternDimensionEditorPanel', () => { dimensionGroups: [], groupId: 'a', isFullscreen: false, + supportStaticValue: false, toggleFullscreen: jest.fn(), }; @@ -402,8 +410,9 @@ describe('IndexPatternDimensionEditorPanel', () => { const items: EuiListGroupItemProps[] = wrapper.find(EuiListGroup).prop('listItems') || []; - expect(items.find(({ id }) => id === 'math')).toBeUndefined(); - expect(items.find(({ id }) => id === 'formula')).toBeUndefined(); + ['math', 'formula', 'static_value'].forEach((hiddenOp) => { + expect(items.some(({ id }) => id === hiddenOp)).toBe(false); + }); }); it('should indicate that reference-based operations are not compatible when they are incomplete', () => { @@ -2217,4 +2226,130 @@ describe('IndexPatternDimensionEditorPanel', () => { 0 ); }); + + it('should not show tabs when formula and static_value operations are not available', () => { + const stateWithInvalidCol: IndexPatternPrivateState = getStateWithColumns({ + col1: { + label: 'Average of memory', + dataType: 'number', + isBucketed: false, + // Private + operationType: 'average', + sourceField: 'memory', + params: { + format: { id: 'bytes', params: { decimals: 2 } }, + }, + }, + }); + + const props = { + ...defaultProps, + filterOperations: jest.fn((op) => { + // the formula operation will fall into this metadata category + return !(op.dataType === 'number' && op.scale === 'ratio'); + }), + }; + + wrapper = mount( + + ); + + expect(wrapper.find('[data-test-subj="lens-dimensionTabs"]').exists()).toBeFalsy(); + }); + + it('should show the formula tab when supported', () => { + const stateWithFormulaColumn: IndexPatternPrivateState = getStateWithColumns({ + col1: { + label: 'Formula', + dataType: 'number', + isBucketed: false, + operationType: 'formula', + references: ['ref1'], + params: {}, + }, + }); + + wrapper = mount( + + ); + + expect( + wrapper.find('[data-test-subj="lens-dimensionTabs-formula"]').first().prop('isSelected') + ).toBeTruthy(); + }); + + it('should now show the static_value tab when not supported', () => { + const stateWithFormulaColumn: IndexPatternPrivateState = getStateWithColumns({ + col1: { + label: 'Formula', + dataType: 'number', + isBucketed: false, + operationType: 'formula', + references: ['ref1'], + params: {}, + }, + }); + + wrapper = mount( + + ); + + expect(wrapper.find('[data-test-subj="lens-dimensionTabs-static_value"]').exists()).toBeFalsy(); + }); + + it('should show the static value tab when supported', () => { + const staticWithFormulaColumn: IndexPatternPrivateState = getStateWithColumns({ + col1: { + label: 'Formula', + dataType: 'number', + isBucketed: false, + operationType: 'formula', + references: ['ref1'], + params: {}, + }, + }); + + wrapper = mount( + + ); + + expect( + wrapper.find('[data-test-subj="lens-dimensionTabs-static_value"]').exists() + ).toBeTruthy(); + }); + + it('should select the quick function tab by default', () => { + const stateWithNoColumn: IndexPatternPrivateState = getStateWithColumns({}); + + wrapper = mount( + + ); + + expect( + wrapper + .find('[data-test-subj="lens-dimensionTabs-quickFunctions"]') + .first() + .prop('isSelected') + ).toBeTruthy(); + }); + + it('should select the static value tab when supported by default', () => { + const stateWithNoColumn: IndexPatternPrivateState = getStateWithColumns({}); + + wrapper = mount( + + ); + + expect( + wrapper.find('[data-test-subj="lens-dimensionTabs-static_value"]').first().prop('isSelected') + ).toBeTruthy(); + }); }); diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_panel.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_panel.tsx index f3e51516d161c..ac8296cca968e 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_panel.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_panel.tsx @@ -16,7 +16,7 @@ import { IndexPatternColumn } from '../indexpattern'; import { isColumnInvalid } from '../utils'; import { IndexPatternPrivateState } from '../types'; import { DimensionEditor } from './dimension_editor'; -import type { DateRange } from '../../../common'; +import { DateRange, layerTypes } from '../../../common'; import { getOperationSupportMatrix } from './operation_support'; export type IndexPatternDimensionTriggerProps = @@ -49,11 +49,11 @@ export const IndexPatternDimensionTriggerComponent = function IndexPatternDimens const layerId = props.layerId; const layer = props.state.layers[layerId]; const currentIndexPattern = props.state.indexPatterns[layer.indexPatternId]; - const { columnId, uniqueLabel } = props; + const { columnId, uniqueLabel, invalid, invalidMessage } = props; const currentColumnHasErrors = useMemo( - () => isColumnInvalid(layer, columnId, currentIndexPattern), - [layer, columnId, currentIndexPattern] + () => invalid || isColumnInvalid(layer, columnId, currentIndexPattern), + [layer, columnId, currentIndexPattern, invalid] ); const selectedColumn: IndexPatternColumn | null = layer.columns[props.columnId] ?? null; @@ -67,15 +67,17 @@ export const IndexPatternDimensionTriggerComponent = function IndexPatternDimens return ( - {i18n.translate('xpack.lens.configure.invalidConfigTooltip', { - defaultMessage: 'Invalid configuration.', - })} -
    - {i18n.translate('xpack.lens.configure.invalidConfigTooltipClick', { - defaultMessage: 'Click for more details.', - })} -

    + invalidMessage ?? ( +

    + {i18n.translate('xpack.lens.configure.invalidConfigTooltip', { + defaultMessage: 'Invalid configuration.', + })} +
    + {i18n.translate('xpack.lens.configure.invalidConfigTooltipClick', { + defaultMessage: 'Click for more details.', + })} +

    + ) } anchorClassName="eui-displayBlock" > @@ -127,6 +129,7 @@ export const IndexPatternDimensionEditorComponent = function IndexPatternDimensi return ( void; +}) => { + const { inputValue, handleInputChange, initialValue } = useDebouncedValue({ onChange, value }); + + return ( + + { + handleInputChange(e.target.value); + }} + placeholder={initialValue} + /> + + ); +}; + +export function getParamEditor( + temporaryStaticValue: boolean, + selectedOperationDefinition: typeof operationDefinitionMap[string] | undefined, + showDefaultStaticValue: boolean +) { + if (temporaryStaticValue) { + return operationDefinitionMap[staticValueOperationName].paramEditor; + } + if (selectedOperationDefinition?.paramEditor) { + return selectedOperationDefinition.paramEditor; + } + if (showDefaultStaticValue) { + return operationDefinitionMap[staticValueOperationName].paramEditor; + } + return null; +} + +export const CalloutWarning = ({ + currentOperationType, + temporaryStateType, +}: { + currentOperationType: keyof typeof operationDefinitionMap | undefined; + temporaryStateType: TemporaryState; +}) => { + if ( + temporaryStateType === 'none' || + (currentOperationType != null && isQuickFunction(currentOperationType)) + ) { + return null; + } + if ( + currentOperationType === staticValueOperationName && + temporaryStateType === 'quickFunctions' + ) { + return ( + <> + +

    + {i18n.translate('xpack.lens.indexPattern.staticValueWarningText', { + defaultMessage: 'To overwrite your static value, select a quick function', + })} +

    +
    + + ); + } + return ( + <> + + {temporaryStateType !== 'quickFunctions' ? ( +

    + {i18n.translate('xpack.lens.indexPattern.formulaWarningStaticValueText', { + defaultMessage: 'To overwrite your formula, change the value in the input field', + })} +

    + ) : ( +

    + {i18n.translate('xpack.lens.indexPattern.formulaWarningText', { + defaultMessage: 'To overwrite your formula, select a quick function', + })} +

    + )} +
    + + ); +}; + +type DimensionEditorTabsType = + | typeof quickFunctionsName + | typeof staticValueOperationName + | typeof formulaOperationName; + +export const DimensionEditorTabs = ({ + tabsEnabled, + tabsState, + onClick, +}: { + tabsEnabled: Record; + tabsState: Record; + onClick: (tabClicked: DimensionEditorTabsType) => void; +}) => { + 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} + + ); +}; + +export function getErrorMessage( + selectedColumn: IndexPatternColumn | undefined, + incompleteOperation: boolean, + input: 'none' | 'field' | 'fullReference' | 'managedReference' | undefined, + fieldInvalid: boolean +) { + if (selectedColumn && incompleteOperation) { + if (input === 'field') { + return i18n.translate('xpack.lens.indexPattern.invalidOperationLabel', { + defaultMessage: 'This field does not work with the selected function.', + }); + } + return i18n.translate('xpack.lens.indexPattern.chooseFieldLabel', { + defaultMessage: 'To use this function, select a field.', + }); + } + if (fieldInvalid) { + return i18n.translate('xpack.lens.indexPattern.invalidFieldLabel', { + defaultMessage: 'Invalid field. Check your index pattern or pick another field.', + }); + } +} diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/droppable/droppable.test.ts b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/droppable/droppable.test.ts index 26aac5dab31e3..85807721f80f6 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/droppable/droppable.test.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/droppable/droppable.test.ts @@ -17,6 +17,7 @@ import { OperationMetadata, DropType } from '../../../types'; import { IndexPatternColumn, MedianIndexPatternColumn } from '../../operations'; import { getFieldByNameFactory } from '../../pure_helpers'; import { generateId } from '../../../id_generator'; +import { layerTypes } from '../../../../common'; jest.mock('../../../id_generator'); @@ -263,7 +264,6 @@ describe('IndexPatternDimensionEditorPanel', () => { dateRange: { fromDate: 'now-1d', toDate: 'now' }, columnId: 'col1', layerId: 'first', - layerType: 'data', uniqueLabel: 'stuff', groupId: 'group1', filterOperations: () => true, @@ -287,6 +287,8 @@ describe('IndexPatternDimensionEditorPanel', () => { dimensionGroups: [], isFullscreen: false, toggleFullscreen: () => {}, + supportStaticValue: false, + layerType: layerTypes.DATA, }; jest.clearAllMocks(); diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/droppable/on_drop_handler.ts b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/droppable/on_drop_handler.ts index e09c3e904f535..b518f667a0bfb 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/droppable/on_drop_handler.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/droppable/on_drop_handler.ts @@ -121,8 +121,12 @@ function onMoveCompatible( indexPattern, }); - let updatedColumnOrder = getColumnOrder(modifiedLayer); - updatedColumnOrder = reorderByGroups(dimensionGroups, groupId, updatedColumnOrder, columnId); + const updatedColumnOrder = reorderByGroups( + dimensionGroups, + groupId, + getColumnOrder(modifiedLayer), + columnId + ); // Time to replace setState( 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 06c8a50cd2dfa..1dfc7d40f6f3e 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.test.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.test.ts @@ -1623,4 +1623,87 @@ describe('IndexPattern Data Source', () => { expect(indexPatternDatasource.isTimeBased(state)).toEqual(false); }); }); + + describe('#initializeDimension', () => { + it('should return the same state if no static value is passed', () => { + const state = enrichBaseState({ + currentIndexPatternId: '1', + layers: { + first: { + indexPatternId: '1', + columnOrder: ['metric'], + columns: { + metric: { + label: 'Count of records', + dataType: 'number', + isBucketed: false, + sourceField: 'Records', + operationType: 'count', + }, + }, + }, + }, + }); + expect( + indexPatternDatasource.initializeDimension!(state, 'first', { + columnId: 'newStatic', + label: 'MyNewColumn', + groupId: 'a', + dataType: 'number', + }) + ).toBe(state); + }); + + it('should add a new static value column if a static value is passed', () => { + const state = enrichBaseState({ + currentIndexPatternId: '1', + layers: { + first: { + indexPatternId: '1', + columnOrder: ['metric'], + columns: { + metric: { + label: 'Count of records', + dataType: 'number', + isBucketed: false, + sourceField: 'Records', + operationType: 'count', + }, + }, + }, + }, + }); + expect( + indexPatternDatasource.initializeDimension!(state, 'first', { + columnId: 'newStatic', + label: 'MyNewColumn', + groupId: 'a', + dataType: 'number', + staticValue: 0, // use a falsy value to check also this corner case + }) + ).toEqual({ + ...state, + layers: { + ...state.layers, + first: { + ...state.layers.first, + incompleteColumns: {}, + columnOrder: ['metric', 'newStatic'], + columns: { + ...state.layers.first.columns, + newStatic: { + dataType: 'number', + isBucketed: false, + label: 'Static value: 0', + operationType: 'static_value', + 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 6a45e3c987f3d..2138b06a4c344 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.tsx @@ -44,7 +44,7 @@ import { import { isDraggedField, normalizeOperationDataType } from './utils'; import { LayerPanel } from './layerpanel'; -import { IndexPatternColumn, getErrorMessages } from './operations'; +import { IndexPatternColumn, getErrorMessages, insertNewColumn } from './operations'; import { IndexPatternField, IndexPatternPrivateState, IndexPatternPersistedState } from './types'; import { KibanaContextProvider } from '../../../../../src/plugins/kibana_react/public'; import { DataPublicPluginStart } from '../../../../../src/plugins/data/public'; @@ -192,6 +192,27 @@ export function getIndexPatternDatasource({ }); }, + initializeDimension(state, layerId, { columnId, groupId, label, dataType, staticValue }) { + const indexPattern = state.indexPatterns[state.layers[layerId]?.indexPatternId]; + if (staticValue == null) { + return state; + } + return mergeLayer({ + state, + layerId, + newLayer: insertNewColumn({ + layer: state.layers[layerId], + op: 'static_value', + columnId, + field: undefined, + indexPattern, + visualizationGroups: [], + initialParams: { params: { value: staticValue } }, + targetGroup: groupId, + }), + }); + }, + toExpression: (state, layerId) => toExpression(state, layerId, uiSettings), renderDataPanel( @@ -404,9 +425,14 @@ export function getIndexPatternDatasource({ }, }; }, - getDatasourceSuggestionsForField(state, draggedField) { + getDatasourceSuggestionsForField(state, draggedField, filterLayers) { return isDraggedField(draggedField) - ? getDatasourceSuggestionsForField(state, draggedField.indexPatternId, draggedField.field) + ? getDatasourceSuggestionsForField( + state, + draggedField.indexPatternId, + draggedField.field, + filterLayers + ) : []; }, getDatasourceSuggestionsFromCurrentState, diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern_suggestions.test.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern_suggestions.test.tsx index 4b8bbc09c6799..a5d6db4be3319 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern_suggestions.test.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern_suggestions.test.tsx @@ -1198,6 +1198,91 @@ describe('IndexPattern Data Source suggestions', () => { }) ); }); + + it('should apply layers filter if passed and model the suggestion based on that', () => { + (generateId as jest.Mock).mockReturnValue('newid'); + const initialState = stateWithNonEmptyTables(); + + const modifiedState: IndexPatternPrivateState = { + ...initialState, + layers: { + thresholdLayer: { + indexPatternId: '1', + columnOrder: ['threshold'], + columns: { + threshold: { + dataType: 'number', + isBucketed: false, + label: 'Static Value: 0', + operationType: 'static_value', + params: { value: '0' }, + references: [], + scale: 'ratio', + }, + }, + }, + currentLayer: { + indexPatternId: '1', + columnOrder: ['metric', 'ref'], + columns: { + metric: { + label: '', + customLabel: true, + dataType: 'number', + isBucketed: false, + operationType: 'average', + sourceField: 'bytes', + }, + ref: { + label: '', + customLabel: true, + dataType: 'number', + isBucketed: false, + operationType: 'cumulative_sum', + references: ['metric'], + }, + }, + }, + }, + }; + + const suggestions = getSuggestionSubset( + getDatasourceSuggestionsForField( + modifiedState, + '1', + documentField, + (layerId) => layerId !== 'thresholdLayer' + ) + ); + // should ignore the threshold layer + expect(suggestions).toContainEqual( + expect.objectContaining({ + table: expect.objectContaining({ + changeType: 'extended', + columns: [ + { + columnId: 'ref', + operation: { + dataType: 'number', + isBucketed: false, + label: '', + scale: undefined, + }, + }, + { + columnId: 'newid', + operation: { + dataType: 'number', + isBucketed: false, + label: 'Count of records', + scale: 'ratio', + }, + }, + ], + }), + }) + ); + }); }); describe('finding the layer that is using the current index pattern', () => { diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern_suggestions.ts b/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern_suggestions.ts index b0793bf912bb2..0fe0ef617dc27 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern_suggestions.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern_suggestions.ts @@ -95,10 +95,14 @@ function buildSuggestion({ export function getDatasourceSuggestionsForField( state: IndexPatternPrivateState, indexPatternId: string, - field: IndexPatternField + field: IndexPatternField, + filterLayers?: (layerId: string) => boolean ): IndexPatternSuggestion[] { const layers = Object.keys(state.layers); - const layerIds = layers.filter((id) => state.layers[id].indexPatternId === indexPatternId); + let layerIds = layers.filter((id) => state.layers[id].indexPatternId === indexPatternId); + if (filterLayers) { + layerIds = layerIds.filter(filterLayers); + } if (layerIds.length === 0) { // The field we're suggesting on does not match any existing layer. diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/formula.test.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/formula.test.tsx index c2ba893a9b90e..499170349c3d5 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/formula.test.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/formula.test.tsx @@ -355,6 +355,33 @@ describe('formula', () => { references: [], }); }); + + it('should move into Formula previous static_value operation', () => { + expect( + formulaOperation.buildColumn({ + previousColumn: { + label: 'Static value: 0', + dataType: 'number', + isBucketed: false, + operationType: 'static_value', + references: [], + params: { + value: '0', + }, + }, + layer, + indexPattern, + }) + ).toEqual({ + label: '0', + dataType: 'number', + operationType: 'formula', + isBucketed: false, + scale: 'ratio', + params: { isFormulaBroken: false, formula: '0' }, + references: [], + }); + }); }); describe('regenerateLayerFromAst()', () => { diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/generate.ts b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/generate.ts index 589f547434b91..3db9ebc6f969d 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/generate.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/generate.ts @@ -38,6 +38,11 @@ export function generateFormula( previousFormula: string, operationDefinitionMap: Record | undefined ) { + if (previousColumn.operationType === 'static_value') { + if (previousColumn.params && 'value' in previousColumn.params) { + return String(previousColumn.params.value); // make sure it's a string + } + } if ('references' in previousColumn) { const metric = layer.columns[previousColumn.references[0]]; if (metric && 'sourceField' in metric && metric.dataType === 'number') { diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/helpers.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/helpers.tsx index 45abbcd3d9cf9..a399183694863 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/helpers.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/helpers.tsx @@ -7,7 +7,7 @@ import { i18n } from '@kbn/i18n'; import { IndexPatternColumn, operationDefinitionMap } from '.'; -import { FieldBasedIndexPatternColumn } from './column_types'; +import { FieldBasedIndexPatternColumn, ReferenceBasedIndexPatternColumn } from './column_types'; import { IndexPattern } from '../../types'; export function getInvalidFieldMessage( @@ -81,8 +81,7 @@ export function isValidNumber( const inputValueAsNumber = Number(inputValue); return ( inputValue !== '' && - inputValue !== null && - inputValue !== undefined && + inputValue != null && !Number.isNaN(inputValueAsNumber) && Number.isFinite(inputValueAsNumber) && (!integer || Number.isInteger(inputValueAsNumber)) && @@ -91,7 +90,9 @@ export function isValidNumber( ); } -export function getFormatFromPreviousColumn(previousColumn: IndexPatternColumn | undefined) { +export function getFormatFromPreviousColumn( + previousColumn: IndexPatternColumn | ReferenceBasedIndexPatternColumn | undefined +) { return previousColumn?.dataType === 'number' && previousColumn.params && 'format' in previousColumn.params && 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 326b71f72c060..0212c73f46879 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 @@ -49,6 +49,7 @@ import { formulaOperation, FormulaIndexPatternColumn, } from './formula'; +import { staticValueOperation, StaticValueIndexPatternColumn } from './static_value'; import { lastValueOperation, LastValueIndexPatternColumn } from './last_value'; import { FrameDatasourceAPI, OperationMetadata } from '../../../types'; import type { BaseIndexPatternColumn, ReferenceBasedIndexPatternColumn } from './column_types'; @@ -87,7 +88,8 @@ export type IndexPatternColumn = | DerivativeIndexPatternColumn | MovingAverageIndexPatternColumn | MathIndexPatternColumn - | FormulaIndexPatternColumn; + | FormulaIndexPatternColumn + | StaticValueIndexPatternColumn; export type FieldBasedIndexPatternColumn = Extract; @@ -119,6 +121,7 @@ export { CountIndexPatternColumn } from './count'; export { LastValueIndexPatternColumn } from './last_value'; export { RangeIndexPatternColumn } from './ranges'; export { FormulaIndexPatternColumn, MathIndexPatternColumn } from './formula'; +export { StaticValueIndexPatternColumn } from './static_value'; // List of all operation definitions registered to this data source. // If you want to implement a new operation, add the definition to this array and @@ -147,6 +150,7 @@ const internalOperationDefinitions = [ overallMinOperation, overallMaxOperation, overallAverageOperation, + staticValueOperation, ]; export { termsOperation } from './terms'; @@ -168,6 +172,7 @@ export { overallMinOperation, } from './calculations'; export { formulaOperation } from './formula/formula'; +export { staticValueOperation } from './static_value'; /** * Properties passed to the operation-specific part of the popover editor 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 new file mode 100644 index 0000000000000..0a6620eecf308 --- /dev/null +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/static_value.test.tsx @@ -0,0 +1,404 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { shallow, mount } from 'enzyme'; +import { IUiSettingsClient, SavedObjectsClientContract, HttpSetup } from 'kibana/public'; +import { IStorageWrapper } from 'src/plugins/kibana_utils/public'; +import { dataPluginMock } from '../../../../../../../src/plugins/data/public/mocks'; +import { createMockedIndexPattern } from '../../mocks'; +import { staticValueOperation } from './index'; +import { IndexPattern, IndexPatternLayer } from '../../types'; +import { StaticValueIndexPatternColumn } from './static_value'; +import { EuiFieldNumber } from '@elastic/eui'; +import { act } from 'react-dom/test-utils'; + +jest.mock('lodash', () => { + const original = jest.requireActual('lodash'); + + return { + ...original, + debounce: (fn: unknown) => fn, + }; +}); + +const uiSettingsMock = {} as IUiSettingsClient; + +const defaultProps = { + storage: {} as IStorageWrapper, + uiSettings: uiSettingsMock, + savedObjectsClient: {} as SavedObjectsClientContract, + dateRange: { fromDate: 'now-1d', toDate: 'now' }, + data: dataPluginMock.createStartContract(), + http: {} as HttpSetup, + indexPattern: { + ...createMockedIndexPattern(), + hasRestrictions: false, + } as IndexPattern, + operationDefinitionMap: {}, + isFullscreen: false, + toggleFullscreen: jest.fn(), + setIsCloseable: jest.fn(), + layerId: '1', +}; + +describe('static_value', () => { + let layer: IndexPatternLayer; + + beforeEach(() => { + layer = { + indexPatternId: '1', + columnOrder: ['col1', 'col2'], + columns: { + col1: { + label: 'Top value of category', + dataType: 'string', + isBucketed: true, + operationType: 'terms', + params: { + orderBy: { type: 'alphabetical' }, + size: 3, + orderDirection: 'asc', + }, + sourceField: 'category', + }, + col2: { + label: 'Static value: 23', + dataType: 'number', + isBucketed: false, + operationType: 'static_value', + references: [], + params: { + value: '23', + }, + }, + }, + }; + }); + + function getLayerWithStaticValue(newValue: string): IndexPatternLayer { + return { + ...layer, + columns: { + ...layer.columns, + col2: { + ...layer.columns.col2, + label: `Static value: ${newValue}`, + params: { + value: newValue, + }, + } as StaticValueIndexPatternColumn, + }, + }; + } + + describe('getDefaultLabel', () => { + it('should return the label for the given value', () => { + expect( + staticValueOperation.getDefaultLabel( + { + label: 'Static value: 23', + dataType: 'number', + isBucketed: false, + operationType: 'static_value', + references: [], + params: { + value: '23', + }, + }, + createMockedIndexPattern(), + layer.columns + ) + ).toBe('Static value: 23'); + }); + + it('should return the default label for non valid value', () => { + expect( + staticValueOperation.getDefaultLabel( + { + label: 'Static value', + dataType: 'number', + isBucketed: false, + operationType: 'static_value', + references: [], + params: { + value: '', + }, + }, + createMockedIndexPattern(), + layer.columns + ) + ).toBe('Static value'); + }); + }); + + describe('getErrorMessage', () => { + it('should return no error for valid values', () => { + expect( + staticValueOperation.getErrorMessage!( + getLayerWithStaticValue('23'), + 'col2', + createMockedIndexPattern() + ) + ).toBeUndefined(); + // test for potential falsy value + expect( + staticValueOperation.getErrorMessage!( + getLayerWithStaticValue('0'), + 'col2', + createMockedIndexPattern() + ) + ).toBeUndefined(); + }); + + it('should return error for invalid values', () => { + for (const value of ['NaN', 'Infinity', 'string']) { + expect( + staticValueOperation.getErrorMessage!( + getLayerWithStaticValue(value), + 'col2', + createMockedIndexPattern() + ) + ).toEqual(expect.arrayContaining([expect.stringMatching('is not a valid number')])); + } + }); + }); + + describe('toExpression', () => { + it('should return a mathColumn operation with valid value', () => { + for (const value of ['23', '0', '-1']) { + expect( + staticValueOperation.toExpression( + getLayerWithStaticValue(value), + 'col2', + createMockedIndexPattern() + ) + ).toEqual([ + { + type: 'function', + function: 'mathColumn', + arguments: { + id: ['col2'], + name: [`Static value: ${value}`], + expression: [value], + }, + }, + ]); + } + }); + + it('should fallback to mapColumn for invalid value', () => { + for (const value of ['NaN', '', 'Infinity']) { + expect( + staticValueOperation.toExpression( + getLayerWithStaticValue(value), + 'col2', + createMockedIndexPattern() + ) + ).toEqual([ + { + type: 'function', + function: 'mapColumn', + arguments: { + id: ['col2'], + name: [`Static value`], + expression: ['100'], + }, + }, + ]); + } + }); + }); + + describe('buildColumn', () => { + it('should set default static value', () => { + expect( + staticValueOperation.buildColumn({ + indexPattern: createMockedIndexPattern(), + layer: { columns: {}, columnOrder: [], indexPatternId: '' }, + }) + ).toEqual({ + label: 'Static value', + dataType: 'number', + operationType: 'static_value', + isBucketed: false, + scale: 'ratio', + params: { value: '100' }, + references: [], + }); + }); + + it('should merge a previousColumn', () => { + expect( + staticValueOperation.buildColumn({ + indexPattern: createMockedIndexPattern(), + layer: { columns: {}, columnOrder: [], indexPatternId: '' }, + previousColumn: { + label: 'Static value', + dataType: 'number', + operationType: 'static_value', + isBucketed: false, + scale: 'ratio', + params: { value: '23' }, + references: [], + }, + }) + ).toEqual({ + label: 'Static value: 23', + dataType: 'number', + operationType: 'static_value', + isBucketed: false, + scale: 'ratio', + params: { value: '23' }, + references: [], + }); + }); + + it('should create a static_value from passed arguments', () => { + expect( + staticValueOperation.buildColumn( + { + indexPattern: createMockedIndexPattern(), + layer: { columns: {}, columnOrder: [], indexPatternId: '' }, + }, + { value: '23' } + ) + ).toEqual({ + label: 'Static value: 23', + dataType: 'number', + operationType: 'static_value', + isBucketed: false, + scale: 'ratio', + params: { value: '23' }, + references: [], + }); + }); + + it('should prioritize passed arguments over previousColumn', () => { + expect( + staticValueOperation.buildColumn( + { + indexPattern: createMockedIndexPattern(), + layer: { columns: {}, columnOrder: [], indexPatternId: '' }, + previousColumn: { + label: 'Static value', + dataType: 'number', + operationType: 'static_value', + isBucketed: false, + scale: 'ratio', + params: { value: '23' }, + references: [], + }, + }, + { value: '53' } + ) + ).toEqual({ + label: 'Static value: 53', + dataType: 'number', + operationType: 'static_value', + isBucketed: false, + scale: 'ratio', + params: { value: '53' }, + references: [], + }); + }); + }); + + describe('paramEditor', () => { + const ParamEditor = staticValueOperation.paramEditor!; + it('should render current static_value', () => { + const updateLayerSpy = jest.fn(); + const instance = shallow( + + ); + + const input = instance.find('[data-test-subj="lns-indexPattern-static_value-input"]'); + + expect(input.prop('value')).toEqual('23'); + }); + + it('should update state on change', async () => { + const updateLayerSpy = jest.fn(); + const instance = mount( + + ); + + const input = instance + .find('[data-test-subj="lns-indexPattern-static_value-input"]') + .find(EuiFieldNumber); + + await act(async () => { + input.prop('onChange')!({ + currentTarget: { value: '27' }, + } as React.ChangeEvent); + }); + + instance.update(); + + expect(updateLayerSpy.mock.calls[0]).toEqual([expect.any(Function)]); + // check that the result of the setter call is correct + expect(updateLayerSpy.mock.calls[0][0](layer)).toEqual({ + ...layer, + columns: { + ...layer.columns, + col2: { + ...layer.columns.col2, + params: { + value: '27', + }, + label: 'Static value: 27', + }, + }, + }); + }); + + it('should not update on invalid input, but show invalid value locally', async () => { + const updateLayerSpy = jest.fn(); + const instance = mount( + + ); + + const input = instance + .find('[data-test-subj="lns-indexPattern-static_value-input"]') + .find(EuiFieldNumber); + + await act(async () => { + input.prop('onChange')!({ + currentTarget: { value: '' }, + } as React.ChangeEvent); + }); + + instance.update(); + + expect(updateLayerSpy).not.toHaveBeenCalled(); + expect( + instance + .find('[data-test-subj="lns-indexPattern-static_value-input"]') + .find(EuiFieldNumber) + .prop('value') + ).toEqual(''); + }); + }); +}); 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 new file mode 100644 index 0000000000000..a76c5f64d1750 --- /dev/null +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/static_value.tsx @@ -0,0 +1,222 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import React, { useCallback } from 'react'; +import { i18n } from '@kbn/i18n'; +import { EuiFieldNumber, EuiFormLabel, EuiSpacer } from '@elastic/eui'; +import { OperationDefinition } from './index'; +import { ReferenceBasedIndexPatternColumn } from './column_types'; +import type { IndexPattern } from '../../types'; +import { useDebouncedValue } from '../../../shared_components'; +import { getFormatFromPreviousColumn, isValidNumber } from './helpers'; + +const defaultLabel = i18n.translate('xpack.lens.indexPattern.staticValueLabelDefault', { + defaultMessage: 'Static value', +}); + +const defaultValue = 100; + +function isEmptyValue(value: number | string | undefined) { + return value == null || value === ''; +} + +function ofName(value: number | string | undefined) { + if (isEmptyValue(value)) { + return defaultLabel; + } + return i18n.translate('xpack.lens.indexPattern.staticValueLabelWithValue', { + defaultMessage: 'Static value: {value}', + values: { value }, + }); +} + +export interface StaticValueIndexPatternColumn extends ReferenceBasedIndexPatternColumn { + operationType: 'static_value'; + params: { + value?: string; + format?: { + id: string; + params?: { + decimals: number; + }; + }; + }; +} + +export const staticValueOperation: OperationDefinition< + StaticValueIndexPatternColumn, + 'managedReference' +> = { + type: 'static_value', + displayName: defaultLabel, + getDefaultLabel: (column) => ofName(column.params.value), + input: 'managedReference', + hidden: true, + getDisabledStatus(indexPattern: IndexPattern) { + return undefined; + }, + getErrorMessage(layer, columnId) { + const column = layer.columns[columnId] as StaticValueIndexPatternColumn; + + return !isValidNumber(column.params.value) + ? [ + i18n.translate('xpack.lens.indexPattern.staticValueError', { + defaultMessage: 'The static value of {value} is not a valid number', + values: { value: column.params.value }, + }), + ] + : undefined; + }, + getPossibleOperation() { + return { + dataType: 'number', + isBucketed: false, + scale: 'ratio', + }; + }, + toExpression: (layer, columnId) => { + const currentColumn = layer.columns[columnId] as StaticValueIndexPatternColumn; + const params = currentColumn.params; + // TODO: improve this logic + const useDisplayLabel = currentColumn.label !== defaultLabel; + const label = isValidNumber(params.value) + ? useDisplayLabel + ? currentColumn.label + : params?.value ?? defaultLabel + : defaultLabel; + + return [ + { + type: 'function', + function: isValidNumber(params.value) ? 'mathColumn' : 'mapColumn', + arguments: { + id: [columnId], + name: [label || defaultLabel], + expression: [isValidNumber(params.value) ? params.value! : String(defaultValue)], + }, + }, + ]; + }, + buildColumn({ previousColumn, layer, indexPattern }, columnParams, operationDefinitionMap) { + const existingStaticValue = + previousColumn?.params && + 'value' in previousColumn.params && + isValidNumber(previousColumn.params.value) + ? previousColumn.params.value + : undefined; + const previousParams: StaticValueIndexPatternColumn['params'] = { + ...{ value: existingStaticValue }, + ...getFormatFromPreviousColumn(previousColumn), + ...columnParams, + }; + return { + label: ofName(previousParams.value), + dataType: 'number', + operationType: 'static_value', + isBucketed: false, + scale: 'ratio', + params: { ...previousParams, value: previousParams.value ?? String(defaultValue) }, + references: [], + }; + }, + isTransferable: (column) => { + return true; + }, + createCopy(layer, sourceId, targetId, indexPattern, operationDefinitionMap) { + const currentColumn = layer.columns[sourceId] as StaticValueIndexPatternColumn; + return { + ...layer, + columns: { + ...layer.columns, + [targetId]: { ...currentColumn }, + }, + }; + }, + + paramEditor: function StaticValueEditor({ + layer, + updateLayer, + currentColumn, + columnId, + activeData, + layerId, + indexPattern, + }) { + const onChange = useCallback( + (newValue) => { + // even if debounced it's triggering for empty string with the previous valid value + if (currentColumn.params.value === newValue) { + return; + } + // Because of upstream specific UX flows, we need fresh layer state here + // so need to use the updater pattern + updateLayer((newLayer) => { + const newColumn = newLayer.columns[columnId] as StaticValueIndexPatternColumn; + return { + ...newLayer, + columns: { + ...newLayer.columns, + [columnId]: { + ...newColumn, + label: newColumn?.customLabel ? newColumn.label : ofName(newValue), + params: { + ...newColumn.params, + value: newValue, + }, + }, + }, + }; + }); + }, + [columnId, updateLayer, currentColumn?.params?.value] + ); + + // Pick the data from the current activeData (to be used when the current operation is not static_value) + const activeDataValue = + activeData && + activeData[layerId] && + activeData[layerId]?.rows?.length === 1 && + activeData[layerId].rows[0][columnId]; + + const fallbackValue = + currentColumn?.operationType !== 'static_value' && activeDataValue != null + ? activeDataValue + : String(defaultValue); + + const { inputValue, handleInputChange } = useDebouncedValue( + { + value: currentColumn?.params?.value || fallbackValue, + onChange, + }, + { allowFalsyValue: true } + ); + + const onChangeHandler = useCallback( + (e: React.ChangeEvent) => { + const value = e.currentTarget.value; + handleInputChange(isValidNumber(value) ? value : undefined); + }, + [handleInputChange] + ); + + return ( +
    + + {i18n.translate('xpack.lens.indexPattern.staticValue.label', { + defaultMessage: 'Threshold value', + })} + + + +
    + ); + }, +}; diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/layer_helpers.ts b/x-pack/plugins/lens/public/indexpattern_datasource/operations/layer_helpers.ts index 11c8206fee021..baacc7bb64d16 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/layer_helpers.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/layer_helpers.ts @@ -184,6 +184,7 @@ export function insertNewColumn({ targetGroup, shouldResetLabel, incompleteParams, + initialParams, }: ColumnChange): IndexPatternLayer { const operationDefinition = operationDefinitionMap[op]; @@ -197,7 +198,7 @@ export function insertNewColumn({ const baseOptions = { indexPattern, - previousColumn: { ...incompleteParams, ...layer.columns[columnId] }, + previousColumn: { ...incompleteParams, ...initialParams, ...layer.columns[columnId] }, }; if (operationDefinition.input === 'none' || operationDefinition.input === 'managedReference') { @@ -396,9 +397,17 @@ export function replaceColumn({ tempLayer = resetIncomplete(tempLayer, columnId); - if (previousDefinition.input === 'managedReference') { + if ( + previousDefinition.input === 'managedReference' && + operationDefinition.input !== previousDefinition.input + ) { // If the transition is incomplete, leave the managed state until it's finished. - tempLayer = deleteColumn({ layer: tempLayer, columnId, indexPattern }); + tempLayer = removeOrphanedColumns( + previousDefinition, + previousColumn, + tempLayer, + indexPattern + ); const hypotheticalLayer = insertNewColumn({ layer: tempLayer, @@ -641,21 +650,31 @@ function removeOrphanedColumns( previousDefinition: | OperationDefinition | OperationDefinition - | OperationDefinition, + | OperationDefinition + | OperationDefinition, previousColumn: IndexPatternColumn, tempLayer: IndexPatternLayer, indexPattern: IndexPattern ) { + let newLayer: IndexPatternLayer = tempLayer; + if (previousDefinition.input === 'managedReference') { + const [columnId] = + Object.entries(tempLayer.columns).find(([_, currColumn]) => currColumn === previousColumn) || + []; + if (columnId != null) { + newLayer = deleteColumn({ layer: tempLayer, columnId, indexPattern }); + } + } if (previousDefinition.input === 'fullReference') { (previousColumn as ReferenceBasedIndexPatternColumn).references.forEach((id: string) => { - tempLayer = deleteColumn({ + newLayer = deleteColumn({ layer: tempLayer, columnId: id, indexPattern, }); }); } - return tempLayer; + return newLayer; } export function canTransition({ diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/operations.test.ts b/x-pack/plugins/lens/public/indexpattern_datasource/operations/operations.test.ts index 2ed6e2b3a7bcb..08136ed501cfc 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/operations.test.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/operations.test.ts @@ -378,6 +378,10 @@ describe('getOperationTypesForField', () => { "operationType": "formula", "type": "managedReference", }, + Object { + "operationType": "static_value", + "type": "managedReference", + }, ], }, Object { diff --git a/x-pack/plugins/lens/public/lens_inspector_service.ts b/x-pack/plugins/lens/public/lens_inspector_service.ts index 6266e7c21f792..d9573962f12d4 100644 --- a/x-pack/plugins/lens/public/lens_inspector_service.ts +++ b/x-pack/plugins/lens/public/lens_inspector_service.ts @@ -7,6 +7,7 @@ import type { Adapters, + InspectorOptions, Start as InspectorStartContract, } from '../../../../src/plugins/inspector/public'; @@ -16,7 +17,7 @@ export const getLensInspectorService = (inspector: InspectorStartContract) => { const adapters: Adapters = createDefaultInspectorAdapters(); return { adapters, - inspect: () => inspector.open(adapters), + inspect: (options?: InspectorOptions) => inspector.open(adapters, options), }; }; diff --git a/x-pack/plugins/lens/public/mocks.tsx b/x-pack/plugins/lens/public/mocks.tsx index 47a2e00055ce3..402440f3302f6 100644 --- a/x-pack/plugins/lens/public/mocks.tsx +++ b/x-pack/plugins/lens/public/mocks.tsx @@ -59,9 +59,9 @@ export function mockDatasourceStates() { }; } -export function createMockVisualization(): jest.Mocked { +export function createMockVisualization(id = 'vis1'): jest.Mocked { return { - id: 'TEST_VIS', + id, clearLayer: jest.fn((state, _layerId) => state), removeLayer: jest.fn(), getLayerIds: jest.fn((_state) => ['layer1']), @@ -70,9 +70,9 @@ export function createMockVisualization(): jest.Mocked { visualizationTypes: [ { icon: 'empty', - id: 'TEST_VIS', + id, label: 'TEST', - groupLabel: 'TEST_VISGroup', + groupLabel: `${id}Group`, }, ], getVisualizationTypeId: jest.fn((_state) => 'empty'), @@ -122,7 +122,7 @@ export function createMockDatasource(id: string): DatasourceMock { return { id: 'mockindexpattern', clearLayer: jest.fn((state, _layerId) => state), - getDatasourceSuggestionsForField: jest.fn((_state, _item) => []), + getDatasourceSuggestionsForField: jest.fn((_state, _item, filterFn) => []), getDatasourceSuggestionsForVisualizeField: jest.fn((_state, _indexpatternId, _fieldName) => []), getDatasourceSuggestionsFromCurrentState: jest.fn((_state) => []), getPersistableState: jest.fn((x) => ({ diff --git a/x-pack/plugins/lens/public/plugin.ts b/x-pack/plugins/lens/public/plugin.ts index 5326927d2c6c5..7891b5990989c 100644 --- a/x-pack/plugins/lens/public/plugin.ts +++ b/x-pack/plugins/lens/public/plugin.ts @@ -233,10 +233,10 @@ export class LensPlugin { const getPresentationUtilContext = () => startServices().plugins.presentationUtil.ContextProvider; - const ensureDefaultIndexPattern = async () => { + const ensureDefaultDataView = async () => { // make sure a default index pattern exists // if not, the page will be redirected to management and visualize won't be rendered - await startServices().plugins.data.indexPatterns.ensureDefaultIndexPattern(); + await startServices().plugins.data.indexPatterns.ensureDefaultDataView(); }; core.application.register({ @@ -261,7 +261,7 @@ export class LensPlugin { const frameStart = this.editorFrameService!.start(coreStart, deps); this.stopReportManager = stopReportManager; - await ensureDefaultIndexPattern(); + await ensureDefaultDataView(); return mountApp(core, params, { createEditorFrame: frameStart.createInstance, attributeService: getLensAttributeService(coreStart, deps), diff --git a/x-pack/plugins/lens/public/shared_components/debounced_value.ts b/x-pack/plugins/lens/public/shared_components/debounced_value.ts index fa8fc22dedd57..412199a371f1f 100644 --- a/x-pack/plugins/lens/public/shared_components/debounced_value.ts +++ b/x-pack/plugins/lens/public/shared_components/debounced_value.ts @@ -11,6 +11,10 @@ import { debounce } from 'lodash'; /** * Debounces value changes and updates inputValue on root state changes if no debounced changes * are in flight because the user is currently modifying the value. + * + * * allowFalsyValue: update upstream with all falsy values but null or undefined + * + * When testing this function mock the "debounce" function in lodash (see this module test for an example) */ export const useDebouncedValue = ( diff --git a/x-pack/plugins/lens/public/shared_components/index.ts b/x-pack/plugins/lens/public/shared_components/index.ts index c200a18a25caf..f947ce699dce4 100644 --- a/x-pack/plugins/lens/public/shared_components/index.ts +++ b/x-pack/plugins/lens/public/shared_components/index.ts @@ -14,3 +14,4 @@ export * from './coloring'; export { useDebouncedValue } from './debounced_value'; export * from './helpers'; export { LegendActionPopover } from './legend_action_popover'; +export * from './static_header'; diff --git a/x-pack/plugins/lens/public/shared_components/static_header.tsx b/x-pack/plugins/lens/public/shared_components/static_header.tsx new file mode 100644 index 0000000000000..2250358234a7f --- /dev/null +++ b/x-pack/plugins/lens/public/shared_components/static_header.tsx @@ -0,0 +1,31 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { EuiFlexGroup, EuiFlexItem, EuiIcon, EuiTitle, IconType } from '@elastic/eui'; + +export const StaticHeader = ({ label, icon }: { label: string; icon?: IconType }) => { + return ( + + {icon && ( + + {' '} + + )} + + +
    {label}
    +
    +
    +
    + ); +}; diff --git a/x-pack/plugins/lens/public/state_management/init_middleware/load_initial.ts b/x-pack/plugins/lens/public/state_management/init_middleware/load_initial.ts index 0f5f1c15d4fa7..7db03a17a3a8f 100644 --- a/x-pack/plugins/lens/public/state_management/init_middleware/load_initial.ts +++ b/x-pack/plugins/lens/public/state_management/init_middleware/load_initial.ts @@ -149,7 +149,7 @@ export function loadInitial( datasourceMap, datasourceStates, visualizationMap, - activeVisualizationId: Object.keys(visualizationMap)[0] || null, + activeVisualization: visualizationMap?.[Object.keys(visualizationMap)[0]] || null, visualizationState: null, visualizeTriggerFieldContext: initialContext, }); diff --git a/x-pack/plugins/lens/public/types.ts b/x-pack/plugins/lens/public/types.ts index a4a483fa95d37..cf6634c200d55 100644 --- a/x-pack/plugins/lens/public/types.ts +++ b/x-pack/plugins/lens/public/types.ts @@ -234,7 +234,11 @@ export interface Datasource { toExpression: (state: T, layerId: string) => ExpressionAstExpression | string | null; - getDatasourceSuggestionsForField: (state: T, field: unknown) => Array>; + getDatasourceSuggestionsForField: ( + state: T, + field: unknown, + filterFn: (layerId: string) => boolean + ) => Array>; getDatasourceSuggestionsForVisualizeField: ( state: T, indexPatternId: string, @@ -326,6 +330,8 @@ export type DatasourceDimensionProps = SharedDimensionProps & { onRemove?: (accessor: string) => void; state: T; activeData?: Record; + invalid?: boolean; + invalidMessage?: string; }; // The only way a visualization has to restrict the query building @@ -335,6 +341,7 @@ export type DatasourceDimensionEditorProps = DatasourceDimensionPro newState: Parameters>[0], publishToVisualization?: { isDimensionComplete?: boolean; + forceRender?: boolean; } ) => void; core: Pick; @@ -343,6 +350,8 @@ export type DatasourceDimensionEditorProps = DatasourceDimensionPro toggleFullscreen: () => void; isFullscreen: boolean; layerType: LayerType | undefined; + supportStaticValue: boolean; + supportFieldFormat?: boolean; }; export type DatasourceDimensionTriggerProps = DatasourceDimensionProps; @@ -434,7 +443,7 @@ export interface VisualizationToolbarProps { export type VisualizationDimensionEditorProps = VisualizationConfigProps & { groupId: string; accessor: string; - setState: (newState: T) => void; + setState(newState: T | ((currState: T) => T)): void; panelRef: MutableRefObject; }; @@ -466,13 +475,16 @@ export type VisualizationDimensionGroupConfig = SharedDimensionProps & { // this dimension group in the hierarchy. If not specified, the position of the dimension in the array is used. specified nesting // orders are always higher in the hierarchy than non-specified ones. nestingOrder?: number; + // some type of layers can produce groups even if invalid. Keep this information to visually show the user that. + invalid?: boolean; + invalidMessage?: string; }; interface VisualizationDimensionChangeProps { layerId: string; columnId: string; prevState: T; - frame: Pick; + frame: Pick; } /** @@ -655,6 +667,7 @@ export interface Visualization { getConfiguration: (props: VisualizationConfigProps) => { groups: VisualizationDimensionGroupConfig[]; supportStaticValue?: boolean; + supportFieldFormat?: boolean; }; /** diff --git a/x-pack/plugins/lens/public/xy_visualization/axes_configuration.ts b/x-pack/plugins/lens/public/xy_visualization/axes_configuration.ts index 95c9140624e63..9c83e2c58146e 100644 --- a/x-pack/plugins/lens/public/xy_visualization/axes_configuration.ts +++ b/x-pack/plugins/lens/public/xy_visualization/axes_configuration.ts @@ -30,16 +30,17 @@ export function isFormatterCompatible( return formatter1.id === formatter2.id; } -export function getAxesConfiguration( - layers: XYLayerConfig[], - shouldRotate: boolean, - tables?: Record, - formatFactory?: FormatFactory -): GroupsConfiguration { - const series: { auto: FormattedMetric[]; left: FormattedMetric[]; right: FormattedMetric[] } = { +export function groupAxesByType(layers: XYLayerConfig[], tables?: Record) { + const series: { + auto: FormattedMetric[]; + left: FormattedMetric[]; + right: FormattedMetric[]; + bottom: FormattedMetric[]; + } = { auto: [], left: [], right: [], + bottom: [], }; layers?.forEach((layer) => { @@ -89,6 +90,16 @@ export function getAxesConfiguration( series.right.push(currentSeries); } }); + return series; +} + +export function getAxesConfiguration( + layers: XYLayerConfig[], + shouldRotate: boolean, + tables?: Record, + formatFactory?: FormatFactory +): GroupsConfiguration { + const series = groupAxesByType(layers, tables); const axisGroups: GroupsConfiguration = []; diff --git a/x-pack/plugins/lens/public/xy_visualization/expression.tsx b/x-pack/plugins/lens/public/xy_visualization/expression.tsx index 026d9da71beea..863289c31bba4 100644 --- a/x-pack/plugins/lens/public/xy_visualization/expression.tsx +++ b/x-pack/plugins/lens/public/xy_visualization/expression.tsx @@ -59,6 +59,7 @@ import { getAxesConfiguration, GroupsConfiguration, validateExtent } from './axe import { getColorAssignments } from './color_assignment'; import { getXDomain, XyEndzones } from './x_domain'; import { getLegendAction } from './get_legend_action'; +import { ThresholdAnnotations } from './expression_thresholds'; declare global { interface Window { @@ -251,6 +252,7 @@ export function XYChart({ const icon: IconType = layers.length > 0 ? getIconForSeriesType(layers[0].seriesType) : 'bar'; return ; } + const thresholdLayers = layers.filter((layer) => layer.layerType === layerTypes.THRESHOLD); // use formatting hint of first x axis column to format ticks const xAxisColumn = data.tables[filteredLayers[0].layerId].columns.find( @@ -832,6 +834,20 @@ export function XYChart({ } }) )} + {thresholdLayers.length ? ( + groupId === 'left')?.formatter, + right: yAxesConfiguration.find(({ groupId }) => groupId === 'right')?.formatter, + bottom: xAxisFormatter, + }} + /> + ) : null} ); } diff --git a/x-pack/plugins/lens/public/xy_visualization/expression_thresholds.tsx b/x-pack/plugins/lens/public/xy_visualization/expression_thresholds.tsx new file mode 100644 index 0000000000000..171e2f1cfba9e --- /dev/null +++ b/x-pack/plugins/lens/public/xy_visualization/expression_thresholds.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 from 'react'; +import { groupBy } from 'lodash'; +import { EuiIcon } from '@elastic/eui'; +import { RectAnnotation, AnnotationDomainType, LineAnnotation } from '@elastic/charts'; +import type { PaletteRegistry, SeriesLayer } from 'src/plugins/charts/public'; +import type { FieldFormat } from 'src/plugins/field_formats/common'; +import type { LayerArgs } from '../../common/expressions'; +import type { LensMultiTable } from '../../common/types'; +import type { ColorAssignments } from './color_assignment'; + +export const ThresholdAnnotations = ({ + thresholdLayers, + data, + colorAssignments, + formatters, + paletteService, + syncColors, +}: { + thresholdLayers: LayerArgs[]; + data: LensMultiTable; + colorAssignments: ColorAssignments; + formatters: Record<'left' | 'right' | 'bottom', FieldFormat | undefined>; + paletteService: PaletteRegistry; + syncColors: boolean; +}) => { + return ( + <> + {thresholdLayers.flatMap((thresholdLayer) => { + if (!thresholdLayer.yConfig) { + return []; + } + const { columnToLabel, palette, yConfig: yConfigs, layerId } = thresholdLayer; + const columnToLabelMap: Record = columnToLabel + ? JSON.parse(columnToLabel) + : {}; + const table = data.tables[layerId]; + const colorAssignment = colorAssignments[palette.name]; + + const row = table.rows[0]; + + const yConfigByValue = yConfigs.sort( + ({ forAccessor: idA }, { forAccessor: idB }) => row[idA] - row[idB] + ); + + const groupedByDirection = groupBy(yConfigByValue, 'fill'); + + return yConfigByValue.flatMap((yConfig, i) => { + // Find the formatter for the given axis + const groupId = + yConfig.axisMode === 'bottom' + ? undefined + : yConfig.axisMode === 'right' + ? 'right' + : 'left'; + + const formatter = formatters[groupId || 'bottom']; + + const seriesLayers: SeriesLayer[] = [ + { + name: columnToLabelMap[yConfig.forAccessor], + totalSeriesAtDepth: colorAssignment.totalSeriesCount, + rankAtDepth: colorAssignment.getRank( + thresholdLayer, + String(yConfig.forAccessor), + String(yConfig.forAccessor) + ), + }, + ]; + const defaultColor = paletteService.get(palette.name).getCategoricalColor( + seriesLayers, + { + maxDepth: 1, + behindText: false, + totalSeries: colorAssignment.totalSeriesCount, + syncColors, + }, + palette.params + ); + + const props = { + groupId, + marker: yConfig.icon ? : undefined, + }; + const annotations = []; + + const dashStyle = + yConfig.lineStyle === 'dashed' + ? [(yConfig.lineWidth || 1) * 3, yConfig.lineWidth || 1] + : yConfig.lineStyle === 'dotted' + ? [yConfig.lineWidth || 1, yConfig.lineWidth || 1] + : undefined; + + const sharedStyle = { + strokeWidth: yConfig.lineWidth || 1, + stroke: (yConfig.color || defaultColor) ?? '#f00', + dash: dashStyle, + }; + + annotations.push( + ({ + dataValue: row[yConfig.forAccessor], + header: columnToLabelMap[yConfig.forAccessor], + details: formatter?.convert(row[yConfig.forAccessor]) || row[yConfig.forAccessor], + }))} + domainType={ + yConfig.axisMode === 'bottom' + ? AnnotationDomainType.XDomain + : AnnotationDomainType.YDomain + } + style={{ + line: { + ...sharedStyle, + opacity: 1, + }, + }} + /> + ); + + if (yConfig.fill && yConfig.fill !== 'none') { + const isFillAbove = yConfig.fill === 'above'; + const indexFromSameType = groupedByDirection[yConfig.fill].findIndex( + ({ forAccessor }) => forAccessor === yConfig.forAccessor + ); + const shouldCheckNextThreshold = + indexFromSameType < groupedByDirection[yConfig.fill].length - 1; + annotations.push( + { + if (yConfig.axisMode === 'bottom') { + return { + coordinates: { + x0: isFillAbove ? row[yConfig.forAccessor] : undefined, + y0: undefined, + x1: isFillAbove + ? shouldCheckNextThreshold + ? row[ + groupedByDirection[yConfig.fill!][indexFromSameType + 1].forAccessor + ] + : undefined + : row[yConfig.forAccessor], + y1: undefined, + }, + header: columnToLabelMap[yConfig.forAccessor], + details: + formatter?.convert(row[yConfig.forAccessor]) || row[yConfig.forAccessor], + }; + } + return { + coordinates: { + x0: undefined, + y0: isFillAbove ? row[yConfig.forAccessor] : undefined, + x1: undefined, + y1: isFillAbove + ? shouldCheckNextThreshold + ? row[ + groupedByDirection[yConfig.fill!][indexFromSameType + 1].forAccessor + ] + : undefined + : row[yConfig.forAccessor], + }, + header: columnToLabelMap[yConfig.forAccessor], + details: + formatter?.convert(row[yConfig.forAccessor]) || row[yConfig.forAccessor], + }; + })} + style={{ + ...sharedStyle, + fill: (yConfig.color || defaultColor) ?? '#f00', + opacity: 0.1, + }} + /> + ); + } + return annotations; + }); + })} + + ); +}; diff --git a/x-pack/plugins/lens/public/xy_visualization/state_helpers.ts b/x-pack/plugins/lens/public/xy_visualization/state_helpers.ts index e3b16f5981f88..4edf7fdf5e512 100644 --- a/x-pack/plugins/lens/public/xy_visualization/state_helpers.ts +++ b/x-pack/plugins/lens/public/xy_visualization/state_helpers.ts @@ -18,6 +18,14 @@ export function isHorizontalSeries(seriesType: SeriesType) { ); } +export function isPercentageSeries(seriesType: SeriesType) { + return ( + seriesType === 'bar_percentage_stacked' || + seriesType === 'bar_horizontal_percentage_stacked' || + seriesType === 'area_percentage_stacked' + ); +} + export function isHorizontalChart(layers: Array<{ seriesType: SeriesType }>) { return layers.every((l) => isHorizontalSeries(l.seriesType)); } diff --git a/x-pack/plugins/lens/public/xy_visualization/threshold_helpers.tsx b/x-pack/plugins/lens/public/xy_visualization/threshold_helpers.tsx new file mode 100644 index 0000000000000..ec47350709473 --- /dev/null +++ b/x-pack/plugins/lens/public/xy_visualization/threshold_helpers.tsx @@ -0,0 +1,165 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { layerTypes } from '../../common'; +import type { XYLayerConfig, YConfig } from '../../common/expressions'; +import { Datatable } from '../../../../../src/plugins/expressions/public'; +import type { DatasourcePublicAPI, FramePublicAPI } from '../types'; +import { groupAxesByType } from './axes_configuration'; +import { isPercentageSeries } from './state_helpers'; +import type { XYState } from './types'; +import { checkScaleOperation } from './visualization_helpers'; + +export interface ThresholdBase { + label: 'x' | 'yRight' | 'yLeft'; +} + +/** + * Return the threshold layers groups to show based on multiple criteria: + * * what groups are current defined in data layers + * * what existing threshold are currently defined in data thresholds + */ +export function getGroupsToShow( + thresholdLayers: T[], + state: XYState | undefined, + datasourceLayers: Record, + tables: Record | undefined +): Array { + if (!state) { + return []; + } + const dataLayers = state.layers.filter( + ({ layerType = layerTypes.DATA }) => layerType === layerTypes.DATA + ); + const groupsAvailable = getGroupsAvailableInData(dataLayers, datasourceLayers, tables); + return thresholdLayers + .filter(({ label, config }: T) => groupsAvailable[label] || config?.length) + .map((layer) => ({ ...layer, valid: groupsAvailable[layer.label] })); +} + +/** + * Returns the threshold layers groups to show based on what groups are current defined in data layers. + */ +export function getGroupsRelatedToData( + thresholdLayers: T[], + state: XYState | undefined, + datasourceLayers: Record, + tables: Record | undefined +): T[] { + if (!state) { + return []; + } + const dataLayers = state.layers.filter( + ({ layerType = layerTypes.DATA }) => layerType === layerTypes.DATA + ); + const groupsAvailable = getGroupsAvailableInData(dataLayers, datasourceLayers, tables); + return thresholdLayers.filter(({ label }: T) => groupsAvailable[label]); +} +/** + * Returns a dictionary with the groups filled in all the data layers + */ +export function getGroupsAvailableInData( + dataLayers: XYState['layers'], + datasourceLayers: Record, + tables: Record | undefined +) { + const hasNumberHistogram = dataLayers.some( + checkScaleOperation('interval', 'number', datasourceLayers) + ); + const { right, left } = groupAxesByType(dataLayers, tables); + return { + x: dataLayers.some(({ xAccessor }) => xAccessor != null) && hasNumberHistogram, + yLeft: left.length > 0, + yRight: right.length > 0, + }; +} + +export function getStaticValue( + dataLayers: XYState['layers'], + groupId: 'x' | 'yLeft' | 'yRight', + { activeData }: Pick, + layerHasNumberHistogram: (layer: XYLayerConfig) => boolean +) { + const fallbackValue = 100; + if (!activeData) { + return fallbackValue; + } + + // filter and organize data dimensions into threshold groups + // now pick the columnId in the active data + const { dataLayer, accessor } = getAccessorCriteriaForGroup(groupId, dataLayers, activeData); + if (groupId === 'x' && dataLayer && !layerHasNumberHistogram(dataLayer)) { + return fallbackValue; + } + return ( + computeStaticValueForGroup( + dataLayer, + accessor, + activeData, + groupId !== 'x' // histogram axis should compute the min based on the current data + ) || fallbackValue + ); +} + +function getAccessorCriteriaForGroup( + groupId: 'x' | 'yLeft' | 'yRight', + dataLayers: XYState['layers'], + activeData: FramePublicAPI['activeData'] +) { + switch (groupId) { + case 'x': + const dataLayer = dataLayers.find(({ xAccessor }) => xAccessor); + return { + dataLayer, + accessor: dataLayer?.xAccessor, + }; + case 'yLeft': + const { left } = groupAxesByType(dataLayers, activeData); + return { + dataLayer: dataLayers.find(({ layerId }) => layerId === left[0]?.layer), + accessor: left[0]?.accessor, + }; + case 'yRight': + const { right } = groupAxesByType(dataLayers, activeData); + return { + dataLayer: dataLayers.find(({ layerId }) => layerId === right[0]?.layer), + accessor: right[0]?.accessor, + }; + } +} + +function computeStaticValueForGroup( + dataLayer: XYLayerConfig | undefined, + accessorId: string | undefined, + activeData: NonNullable, + minZeroBased: boolean +) { + const defaultThresholdFactor = 3 / 4; + + if (dataLayer && accessorId) { + if (isPercentageSeries(dataLayer?.seriesType)) { + return defaultThresholdFactor; + } + const tableId = Object.keys(activeData).find((key) => + activeData[key].columns.some(({ id }) => id === accessorId) + ); + if (tableId) { + const columnMax = activeData[tableId].rows.reduce( + (max, row) => Math.max(row[accessorId], max), + -Infinity + ); + const columnMin = activeData[tableId].rows.reduce( + (max, row) => Math.min(row[accessorId], max), + Infinity + ); + // Custom axis bounds can go below 0, so consider also lower values than 0 + const finalMinValue = minZeroBased ? Math.min(0, columnMin) : columnMin; + const interval = columnMax - finalMinValue; + return Number((finalMinValue + interval * defaultThresholdFactor).toFixed(2)); + } + } +} diff --git a/x-pack/plugins/lens/public/xy_visualization/to_expression.ts b/x-pack/plugins/lens/public/xy_visualization/to_expression.ts index 5290e0298ae5e..aa99aa9b7b316 100644 --- a/x-pack/plugins/lens/public/xy_visualization/to_expression.ts +++ b/x-pack/plugins/lens/public/xy_visualization/to_expression.ts @@ -322,6 +322,10 @@ export const buildExpression = ( forAccessor: [yConfig.forAccessor], axisMode: yConfig.axisMode ? [yConfig.axisMode] : [], color: yConfig.color ? [yConfig.color] : [], + lineStyle: yConfig.lineStyle ? [yConfig.lineStyle] : [], + lineWidth: yConfig.lineWidth ? [yConfig.lineWidth] : [], + fill: [yConfig.fill || 'none'], + icon: yConfig.icon ? [yConfig.icon] : [], }, }, ], 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 7aef40b1481dc..8907db4954f99 100644 --- a/x-pack/plugins/lens/public/xy_visualization/visualization.test.ts +++ b/x-pack/plugins/lens/public/xy_visualization/visualization.test.ts @@ -15,6 +15,7 @@ import { createMockDatasource, createMockFramePublicAPI } from '../mocks'; import { LensIconChartBar } from '../assets/chart_bar'; import { chartPluginMock } from '../../../../../src/plugins/charts/public/mocks'; import { fieldFormatsServiceMock } from '../../../../../src/plugins/field_formats/public/mocks'; +import { Datatable } from 'src/plugins/expressions'; function exampleState(): State { return { @@ -216,8 +217,8 @@ describe('xy_visualization', () => { }); describe('#getSupportedLayers', () => { - it('should return a single layer type', () => { - expect(xyVisualization.getSupportedLayers()).toHaveLength(1); + it('should return a double layer types', () => { + expect(xyVisualization.getSupportedLayers()).toHaveLength(2); }); it('should return the icon for the visualization type', () => { @@ -317,6 +318,42 @@ describe('xy_visualization', () => { accessors: [], }); }); + + it('should add a dimension to a threshold layer', () => { + expect( + xyVisualization.setDimension({ + frame, + prevState: { + ...exampleState(), + layers: [ + { + layerId: 'threshold', + layerType: layerTypes.THRESHOLD, + seriesType: 'line', + accessors: [], + }, + ], + }, + layerId: 'threshold', + groupId: 'xThreshold', + columnId: 'newCol', + }).layers[0] + ).toEqual({ + layerId: 'threshold', + layerType: layerTypes.THRESHOLD, + seriesType: 'line', + accessors: ['newCol'], + yConfig: [ + { + axisMode: 'bottom', + forAccessor: 'newCol', + icon: undefined, + lineStyle: 'solid', + lineWidth: 1, + }, + ], + }); + }); }); describe('#removeDimension', () => { @@ -504,6 +541,300 @@ describe('xy_visualization', () => { expect(ops.filter(filterOperations).map((x) => x.dataType)).toEqual(['number']); }); + describe('thresholds', () => { + beforeEach(() => { + frame.datasourceLayers = { + first: mockDatasource.publicAPIMock, + threshold: mockDatasource.publicAPIMock, + }; + }); + + function getStateWithBaseThreshold(): State { + return { + ...exampleState(), + layers: [ + { + layerId: 'first', + layerType: layerTypes.DATA, + seriesType: 'area', + splitAccessor: undefined, + xAccessor: undefined, + accessors: ['a'], + }, + { + layerId: 'threshold', + layerType: layerTypes.THRESHOLD, + seriesType: 'line', + accessors: [], + yConfig: [{ axisMode: 'left', forAccessor: 'a' }], + }, + ], + }; + } + + it('should support static value', () => { + const state = getStateWithBaseThreshold(); + state.layers[0].accessors = []; + state.layers[1].yConfig = undefined; + + expect( + xyVisualization.getConfiguration({ + state: getStateWithBaseThreshold(), + frame, + layerId: 'threshold', + }).supportStaticValue + ).toBeTruthy(); + }); + + it('should return no threshold groups for a empty data layer', () => { + const state = getStateWithBaseThreshold(); + state.layers[0].accessors = []; + state.layers[1].yConfig = undefined; + + const options = xyVisualization.getConfiguration({ + state, + frame, + layerId: 'threshold', + }).groups; + + expect(options).toHaveLength(0); + }); + + it('should return a group for the vertical left axis', () => { + const options = xyVisualization.getConfiguration({ + state: getStateWithBaseThreshold(), + frame, + layerId: 'threshold', + }).groups; + + expect(options).toHaveLength(1); + expect(options[0].groupId).toBe('yThresholdLeft'); + }); + + it('should return a group for the vertical right axis', () => { + const state = getStateWithBaseThreshold(); + state.layers[0].yConfig = [{ axisMode: 'right', forAccessor: 'a' }]; + state.layers[1].yConfig![0].axisMode = 'right'; + + const options = xyVisualization.getConfiguration({ + state, + frame, + layerId: 'threshold', + }).groups; + + expect(options).toHaveLength(1); + expect(options[0].groupId).toBe('yThresholdRight'); + }); + + it('should compute no groups for thresholds when the only data accessor available is a date histogram', () => { + const state = getStateWithBaseThreshold(); + state.layers[0].xAccessor = 'b'; + state.layers[0].accessors = []; + state.layers[1].yConfig = []; // empty the configuration + // set the xAccessor as date_histogram + frame.datasourceLayers.threshold.getOperationForColumnId = jest.fn((accessor) => { + if (accessor === 'b') { + return { + dataType: 'date', + isBucketed: true, + scale: 'interval', + label: 'date_histogram', + }; + } + return null; + }); + + const options = xyVisualization.getConfiguration({ + state, + frame, + layerId: 'threshold', + }).groups; + + expect(options).toHaveLength(0); + }); + + it('should mark horizontal group is invalid when xAccessor is changed to a date histogram', () => { + const state = getStateWithBaseThreshold(); + state.layers[0].xAccessor = 'b'; + state.layers[0].accessors = []; + state.layers[1].yConfig![0].axisMode = 'bottom'; + // set the xAccessor as date_histogram + frame.datasourceLayers.threshold.getOperationForColumnId = jest.fn((accessor) => { + if (accessor === 'b') { + return { + dataType: 'date', + isBucketed: true, + scale: 'interval', + label: 'date_histogram', + }; + } + return null; + }); + + const options = xyVisualization.getConfiguration({ + state, + frame, + layerId: 'threshold', + }).groups; + + expect(options[0]).toEqual( + expect.objectContaining({ + invalid: true, + groupId: 'xThreshold', + }) + ); + }); + + it('should return groups in a specific order (left, right, bottom)', () => { + const state = getStateWithBaseThreshold(); + state.layers[0].xAccessor = 'c'; + state.layers[0].accessors = ['a', 'b']; + // invert them on purpose + state.layers[0].yConfig = [ + { axisMode: 'right', forAccessor: 'b' }, + { axisMode: 'left', forAccessor: 'a' }, + ]; + state.layers[1].yConfig = [ + { forAccessor: 'c', axisMode: 'bottom' }, + { forAccessor: 'b', axisMode: 'right' }, + { forAccessor: 'a', axisMode: 'left' }, + ]; + // set the xAccessor as number histogram + frame.datasourceLayers.threshold.getOperationForColumnId = jest.fn((accessor) => { + if (accessor === 'c') { + return { + dataType: 'number', + isBucketed: true, + scale: 'interval', + label: 'histogram', + }; + } + return null; + }); + + const [left, right, bottom] = xyVisualization.getConfiguration({ + state, + frame, + layerId: 'threshold', + }).groups; + + expect(left.groupId).toBe('yThresholdLeft'); + expect(right.groupId).toBe('yThresholdRight'); + expect(bottom.groupId).toBe('xThreshold'); + }); + + it('should ignore terms operation for xAccessor', () => { + const state = getStateWithBaseThreshold(); + state.layers[0].xAccessor = 'b'; + state.layers[0].accessors = []; + state.layers[1].yConfig = []; // empty the configuration + // set the xAccessor as top values + frame.datasourceLayers.threshold.getOperationForColumnId = jest.fn((accessor) => { + if (accessor === 'b') { + return { + dataType: 'string', + isBucketed: true, + scale: 'ordinal', + label: 'top values', + }; + } + return null; + }); + + const options = xyVisualization.getConfiguration({ + state, + frame, + layerId: 'threshold', + }).groups; + + expect(options).toHaveLength(0); + }); + + it('should mark horizontal group is invalid when accessor is changed to a terms operation', () => { + const state = getStateWithBaseThreshold(); + state.layers[0].xAccessor = 'b'; + state.layers[0].accessors = []; + state.layers[1].yConfig![0].axisMode = 'bottom'; + // set the xAccessor as date_histogram + frame.datasourceLayers.threshold.getOperationForColumnId = jest.fn((accessor) => { + if (accessor === 'b') { + return { + dataType: 'string', + isBucketed: true, + scale: 'ordinal', + label: 'top values', + }; + } + return null; + }); + + const options = xyVisualization.getConfiguration({ + state, + frame, + layerId: 'threshold', + }).groups; + + expect(options[0]).toEqual( + expect.objectContaining({ + invalid: true, + groupId: 'xThreshold', + }) + ); + }); + + it('differ vertical axis if the formatters are not compatibles between each other', () => { + const tables: Record = { + first: { + type: 'datatable', + rows: [], + columns: [ + { + id: 'xAccessorId', + name: 'horizontal axis', + meta: { + type: 'date', + params: { params: { id: 'date', params: { pattern: 'HH:mm' } } }, + }, + }, + { + id: 'yAccessorId', + name: 'left axis', + meta: { + type: 'number', + params: { id: 'number' }, + }, + }, + { + id: 'yAccessorId2', + name: 'right axis', + meta: { + type: 'number', + params: { id: 'bytes' }, + }, + }, + ], + }, + }; + + const state = getStateWithBaseThreshold(); + state.layers[0].accessors = ['yAccessorId', 'yAccessorId2']; + state.layers[1].yConfig = []; // empty the configuration + + const options = xyVisualization.getConfiguration({ + state, + frame: { ...frame, activeData: tables }, + layerId: 'threshold', + }).groups; + + expect(options).toEqual( + expect.arrayContaining([ + expect.objectContaining({ groupId: 'yThresholdLeft' }), + expect.objectContaining({ groupId: 'yThresholdRight' }), + ]) + ); + }); + }); + describe('color assignment', () => { function callConfig(layerConfigOverride: Partial) { const baseState = exampleState(); diff --git a/x-pack/plugins/lens/public/xy_visualization/visualization.tsx b/x-pack/plugins/lens/public/xy_visualization/visualization.tsx index 026c2827cedbd..ed1cc015806c5 100644 --- a/x-pack/plugins/lens/public/xy_visualization/visualization.tsx +++ b/x-pack/plugins/lens/public/xy_visualization/visualization.tsx @@ -6,7 +6,7 @@ */ import React from 'react'; -import { uniq } from 'lodash'; +import { groupBy, uniq } from 'lodash'; import { render } from 'react-dom'; import { Position } from '@elastic/charts'; import { FormattedMessage, I18nProvider } from '@kbn/i18n/react'; @@ -14,15 +14,10 @@ import { i18n } from '@kbn/i18n'; import { PaletteRegistry } from 'src/plugins/charts/public'; import { FieldFormatsStart } from 'src/plugins/field_formats/public'; import { getSuggestions } from './xy_suggestions'; -import { XyToolbar, DimensionEditor, LayerHeader } from './xy_config_panel'; -import type { - Visualization, - OperationMetadata, - VisualizationType, - AccessorConfig, - DatasourcePublicAPI, -} from '../types'; -import { State, visualizationTypes, XYState } from './types'; +import { XyToolbar, DimensionEditor } from './xy_config_panel'; +import { LayerHeader } from './xy_config_panel/layer_header'; +import type { Visualization, OperationMetadata, VisualizationType, AccessorConfig } from '../types'; +import { State, visualizationTypes } from './types'; import { SeriesType, XYLayerConfig } from '../../common/expressions'; import { LayerType, layerTypes } from '../../common'; import { isHorizontalChart } from './state_helpers'; @@ -32,6 +27,19 @@ import { LensIconChartMixedXy } from '../assets/chart_mixed_xy'; import { LensIconChartBarHorizontal } from '../assets/chart_bar_horizontal'; import { getAccessorColorConfig, getColorAssignments } from './color_assignment'; import { getColumnToLabelMap } from './state_helpers'; +import { LensIconChartBarThreshold } from '../assets/chart_bar_threshold'; +import { generateId } from '../id_generator'; +import { + getGroupsAvailableInData, + getGroupsRelatedToData, + getGroupsToShow, + getStaticValue, +} from './threshold_helpers'; +import { + checkScaleOperation, + checkXAccessorCompatibility, + getAxisName, +} from './visualization_helpers'; const defaultIcon = LensIconChartBarStacked; const defaultSeriesType = 'bar_stacked'; @@ -186,6 +194,39 @@ export const getXyVisualization = ({ }, getSupportedLayers(state, frame) { + const thresholdGroupIds = [ + { + id: 'yThresholdLeft', + label: 'yLeft' as const, + }, + { + id: 'yThresholdRight', + label: 'yRight' as const, + }, + { + id: 'xThreshold', + label: 'x' as const, + }, + ]; + + const dataLayers = + state?.layers.filter(({ layerType = layerTypes.DATA }) => layerType === layerTypes.DATA) || + []; + const filledDataLayers = dataLayers.filter( + ({ accessors, xAccessor }) => accessors.length || xAccessor + ); + const layerHasNumberHistogram = checkScaleOperation( + 'interval', + 'number', + frame?.datasourceLayers || {} + ); + const thresholdGroups = getGroupsRelatedToData( + thresholdGroupIds, + state, + frame?.datasourceLayers || {}, + frame?.activeData + ); + const layers = [ { type: layerTypes.DATA, @@ -194,6 +235,36 @@ export const getXyVisualization = ({ }), icon: LensIconChartMixedXy, }, + { + type: layerTypes.THRESHOLD, + label: i18n.translate('xpack.lens.xyChart.addThresholdLayerLabel', { + defaultMessage: 'Add threshold layer', + }), + icon: LensIconChartBarThreshold, + disabled: + !filledDataLayers.length || + (!dataLayers.some(layerHasNumberHistogram) && + dataLayers.every(({ accessors }) => !accessors.length)), + tooltipContent: filledDataLayers.length + ? undefined + : i18n.translate('xpack.lens.xyChart.addThresholdLayerLabelDisabledHelp', { + defaultMessage: 'Add some data to enable threshold layer', + }), + initialDimensions: state + ? thresholdGroups.map(({ id, label }) => ({ + groupId: id, + columnId: generateId(), + dataType: 'number', + label: getAxisName(label, { isHorizontal: isHorizontalChart(state?.layers || []) }), + staticValue: getStaticValue( + dataLayers, + label, + { activeData: frame?.activeData }, + layerHasNumberHistogram + ), + })) + : undefined, + }, ]; return layers; @@ -233,8 +304,70 @@ export const getXyVisualization = ({ const isDataLayer = !layer.layerType || layer.layerType === layerTypes.DATA; if (!isDataLayer) { + const idToIndex = sortedAccessors.reduce>((memo, id, index) => { + memo[id] = index; + return memo; + }, {}); + const { bottom, left, right } = groupBy( + [...(layer.yConfig || [])].sort( + ({ forAccessor: forA }, { forAccessor: forB }) => idToIndex[forA] - idToIndex[forB] + ), + ({ axisMode }) => { + return axisMode; + } + ); + const groupsToShow = getGroupsToShow( + [ + // When a threshold layer panel is added, a static threshold should automatically be included by default + // in the first available axis, in the following order: vertical left, vertical right, horizontal. + { + config: left, + id: 'yThresholdLeft', + label: 'yLeft', + dataTestSubj: 'lnsXY_yThresholdLeftPanel', + }, + { + config: right, + id: 'yThresholdRight', + label: 'yRight', + dataTestSubj: 'lnsXY_yThresholdRightPanel', + }, + { + config: bottom, + id: 'xThreshold', + label: 'x', + dataTestSubj: 'lnsXY_xThresholdPanel', + }, + ], + state, + frame.datasourceLayers, + frame?.activeData + ); return { - groups: [], + supportFieldFormat: false, + supportStaticValue: true, + // Each thresholds layer panel will have sections for each available axis + // (horizontal axis, vertical axis left, vertical axis right). + // Only axes that support numeric thresholds should be shown + groups: groupsToShow.map(({ config = [], id, label, dataTestSubj, valid }) => ({ + groupId: id, + groupLabel: getAxisName(label, { isHorizontal }), + accessors: config.map(({ forAccessor, color }) => ({ + columnId: forAccessor, + color: color || mappedAccessors.find(({ columnId }) => columnId === forAccessor)?.color, + triggerIcon: 'color', + })), + filterOperations: isNumericMetric, + supportsMoreColumns: true, + required: false, + enableDimensionEditor: true, + dataTestSubj, + invalid: !valid, + invalidMessage: i18n.translate('xpack.lens.configure.invalidThresholdDimension', { + defaultMessage: + 'This threshold is assigned to an axis that no longer exists. You may move this threshold to another available axis or remove it.', + }), + })), }; } @@ -305,6 +438,30 @@ export const getXyVisualization = ({ newLayer.splitAccessor = columnId; } + if (newLayer.layerType === layerTypes.THRESHOLD) { + newLayer.accessors = [...newLayer.accessors.filter((a) => a !== columnId), columnId]; + const hasYConfig = newLayer.yConfig?.some(({ forAccessor }) => forAccessor === columnId); + if (!hasYConfig) { + newLayer.yConfig = [ + ...(newLayer.yConfig || []), + // TODO: move this + // add a default config if none is available + { + forAccessor: columnId, + axisMode: + groupId === 'xThreshold' + ? 'bottom' + : groupId === 'yThresholdRight' + ? 'right' + : 'left', + icon: undefined, + lineStyle: 'solid', + lineWidth: 1, + }, + ]; + } + } + return { ...prevState, layers: prevState.layers.map((l) => (l.layerId === layerId ? newLayer : l)), @@ -331,7 +488,24 @@ export const getXyVisualization = ({ newLayer.yConfig = newLayer.yConfig.filter(({ forAccessor }) => forAccessor !== columnId); } - const newLayers = prevState.layers.map((l) => (l.layerId === layerId ? newLayer : l)); + let newLayers = prevState.layers.map((l) => (l.layerId === layerId ? newLayer : l)); + // // check if there's any threshold layer and pull it off if all data layers have no dimensions set + const layersByType = groupBy(newLayers, ({ layerType }) => layerType); + // // check for data layers if they all still have xAccessors + const groupsAvailable = getGroupsAvailableInData( + layersByType[layerTypes.DATA], + frame.datasourceLayers, + frame?.activeData + ); + if ( + (Object.keys(groupsAvailable) as Array<'x' | 'yLeft' | 'yRight'>).every( + (id) => !groupsAvailable[id] + ) + ) { + newLayers = newLayers.filter( + ({ layerType, accessors }) => layerType === layerTypes.DATA || accessors.length + ); + } return { ...prevState, @@ -510,19 +684,6 @@ function validateLayersForDimension( }; } -function getAxisName(axis: 'x' | 'y', { isHorizontal }: { isHorizontal: boolean }) { - const vertical = i18n.translate('xpack.lens.xyChart.verticalAxisLabel', { - defaultMessage: 'Vertical axis', - }); - const horizontal = i18n.translate('xpack.lens.xyChart.horizontalAxisLabel', { - defaultMessage: 'Horizontal axis', - }); - if (axis === 'x') { - return isHorizontal ? vertical : horizontal; - } - return isHorizontal ? horizontal : vertical; -} - // i18n ids cannot be dynamically generated, hence the function below function getMessageIdsForDimension(dimension: string, layers: number[], isHorizontal: boolean) { const layersList = layers.map((i: number) => i + 1).join(', '); @@ -566,76 +727,6 @@ function newLayerState( }; } -// min requirement for the bug: -// * 2 or more layers -// * at least one with date histogram -// * at least one with interval function -function checkXAccessorCompatibility( - state: XYState, - datasourceLayers: Record -) { - const errors = []; - const hasDateHistogramSet = state.layers.some( - checkScaleOperation('interval', 'date', datasourceLayers) - ); - const hasNumberHistogram = state.layers.some( - checkScaleOperation('interval', 'number', datasourceLayers) - ); - const hasOrdinalAxis = state.layers.some( - checkScaleOperation('ordinal', undefined, datasourceLayers) - ); - if (state.layers.length > 1 && hasDateHistogramSet && hasNumberHistogram) { - errors.push({ - shortMessage: i18n.translate('xpack.lens.xyVisualization.dataTypeFailureXShort', { - defaultMessage: `Wrong data type for {axis}.`, - values: { - axis: getAxisName('x', { isHorizontal: isHorizontalChart(state.layers) }), - }, - }), - longMessage: i18n.translate('xpack.lens.xyVisualization.dataTypeFailureXLong', { - defaultMessage: `Data type mismatch for the {axis}. Cannot mix date and number interval types.`, - values: { - axis: getAxisName('x', { isHorizontal: isHorizontalChart(state.layers) }), - }, - }), - }); - } - if (state.layers.length > 1 && (hasDateHistogramSet || hasNumberHistogram) && hasOrdinalAxis) { - errors.push({ - shortMessage: i18n.translate('xpack.lens.xyVisualization.dataTypeFailureXShort', { - defaultMessage: `Wrong data type for {axis}.`, - values: { - axis: getAxisName('x', { isHorizontal: isHorizontalChart(state.layers) }), - }, - }), - longMessage: i18n.translate('xpack.lens.xyVisualization.dataTypeFailureXOrdinalLong', { - defaultMessage: `Data type mismatch for the {axis}, use a different function.`, - values: { - axis: getAxisName('x', { isHorizontal: isHorizontalChart(state.layers) }), - }, - }), - }); - } - return errors; -} - -function checkScaleOperation( - scaleType: 'ordinal' | 'interval' | 'ratio', - dataType: 'date' | 'number' | 'string' | undefined, - datasourceLayers: Record -) { - return (layer: XYLayerConfig) => { - const datasourceAPI = datasourceLayers[layer.layerId]; - if (!layer.xAccessor) { - return false; - } - const operation = datasourceAPI?.getOperationForColumnId(layer.xAccessor); - return Boolean( - operation && (!dataType || operation.dataType === dataType) && operation.scale === scaleType - ); - }; -} - function getLayersByType(state: State, byType?: string) { return state.layers.filter(({ layerType = layerTypes.DATA }) => byType ? layerType === byType : true diff --git a/x-pack/plugins/lens/public/xy_visualization/visualization_helpers.tsx b/x-pack/plugins/lens/public/xy_visualization/visualization_helpers.tsx new file mode 100644 index 0000000000000..22c3c7e895323 --- /dev/null +++ b/x-pack/plugins/lens/public/xy_visualization/visualization_helpers.tsx @@ -0,0 +1,116 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { i18n } from '@kbn/i18n'; +import { DatasourcePublicAPI } from '../types'; +import { XYState } from './types'; +import { isHorizontalChart } from './state_helpers'; +import { XYLayerConfig } from '../../common/expressions'; + +export function getAxisName( + axis: 'x' | 'y' | 'yLeft' | 'yRight', + { isHorizontal }: { isHorizontal: boolean } +) { + const vertical = i18n.translate('xpack.lens.xyChart.verticalAxisLabel', { + defaultMessage: 'Vertical axis', + }); + const horizontal = i18n.translate('xpack.lens.xyChart.horizontalAxisLabel', { + defaultMessage: 'Horizontal axis', + }); + if (axis === 'x') { + return isHorizontal ? vertical : horizontal; + } + if (axis === 'y') { + return isHorizontal ? horizontal : vertical; + } + const verticalLeft = i18n.translate('xpack.lens.xyChart.verticalLeftAxisLabel', { + defaultMessage: 'Vertical left axis', + }); + const verticalRight = i18n.translate('xpack.lens.xyChart.verticalRightAxisLabel', { + defaultMessage: 'Vertical right axis', + }); + const horizontalTop = i18n.translate('xpack.lens.xyChart.horizontalLeftAxisLabel', { + defaultMessage: 'Horizontal top axis', + }); + const horizontalBottom = i18n.translate('xpack.lens.xyChart.horizontalRightAxisLabel', { + defaultMessage: 'Horizontal bottom axis', + }); + if (axis === 'yLeft') { + return isHorizontal ? horizontalTop : verticalLeft; + } + return isHorizontal ? horizontalBottom : verticalRight; +} + +// min requirement for the bug: +// * 2 or more layers +// * at least one with date histogram +// * at least one with interval function +export function checkXAccessorCompatibility( + state: XYState, + datasourceLayers: Record +) { + const errors = []; + const hasDateHistogramSet = state.layers.some( + checkScaleOperation('interval', 'date', datasourceLayers) + ); + const hasNumberHistogram = state.layers.some( + checkScaleOperation('interval', 'number', datasourceLayers) + ); + const hasOrdinalAxis = state.layers.some( + checkScaleOperation('ordinal', undefined, datasourceLayers) + ); + if (state.layers.length > 1 && hasDateHistogramSet && hasNumberHistogram) { + errors.push({ + shortMessage: i18n.translate('xpack.lens.xyVisualization.dataTypeFailureXShort', { + defaultMessage: `Wrong data type for {axis}.`, + values: { + axis: getAxisName('x', { isHorizontal: isHorizontalChart(state.layers) }), + }, + }), + longMessage: i18n.translate('xpack.lens.xyVisualization.dataTypeFailureXLong', { + defaultMessage: `Data type mismatch for the {axis}. Cannot mix date and number interval types.`, + values: { + axis: getAxisName('x', { isHorizontal: isHorizontalChart(state.layers) }), + }, + }), + }); + } + if (state.layers.length > 1 && (hasDateHistogramSet || hasNumberHistogram) && hasOrdinalAxis) { + errors.push({ + shortMessage: i18n.translate('xpack.lens.xyVisualization.dataTypeFailureXShort', { + defaultMessage: `Wrong data type for {axis}.`, + values: { + axis: getAxisName('x', { isHorizontal: isHorizontalChart(state.layers) }), + }, + }), + longMessage: i18n.translate('xpack.lens.xyVisualization.dataTypeFailureXOrdinalLong', { + defaultMessage: `Data type mismatch for the {axis}, use a different function.`, + values: { + axis: getAxisName('x', { isHorizontal: isHorizontalChart(state.layers) }), + }, + }), + }); + } + return errors; +} + +export function checkScaleOperation( + scaleType: 'ordinal' | 'interval' | 'ratio', + dataType: 'date' | 'number' | 'string' | undefined, + datasourceLayers: Record +) { + return (layer: XYLayerConfig) => { + const datasourceAPI = datasourceLayers[layer.layerId]; + if (!layer.xAccessor) { + return false; + } + const operation = datasourceAPI?.getOperationForColumnId(layer.xAccessor); + return Boolean( + operation && (!dataType || operation.dataType === dataType) && operation.scale === scaleType + ); + }; +} diff --git a/x-pack/plugins/lens/public/xy_visualization/axis_settings_popover.test.tsx b/x-pack/plugins/lens/public/xy_visualization/xy_config_panel/axis_settings_popover.test.tsx similarity index 98% rename from x-pack/plugins/lens/public/xy_visualization/axis_settings_popover.test.tsx rename to x-pack/plugins/lens/public/xy_visualization/xy_config_panel/axis_settings_popover.test.tsx index aa287795c8181..ebe0e536a4d77 100644 --- a/x-pack/plugins/lens/public/xy_visualization/axis_settings_popover.test.tsx +++ b/x-pack/plugins/lens/public/xy_visualization/xy_config_panel/axis_settings_popover.test.tsx @@ -8,8 +8,8 @@ import React from 'react'; import { shallowWithIntl as shallow } from '@kbn/test/jest'; import { AxisSettingsPopover, AxisSettingsPopoverProps } from './axis_settings_popover'; -import { ToolbarPopover } from '../shared_components'; -import { layerTypes } from '../../common'; +import { ToolbarPopover } from '../../shared_components'; +import { layerTypes } from '../../../common'; describe('Axes Settings', () => { let props: AxisSettingsPopoverProps; diff --git a/x-pack/plugins/lens/public/xy_visualization/axis_settings_popover.tsx b/x-pack/plugins/lens/public/xy_visualization/xy_config_panel/axis_settings_popover.tsx similarity index 96% rename from x-pack/plugins/lens/public/xy_visualization/axis_settings_popover.tsx rename to x-pack/plugins/lens/public/xy_visualization/xy_config_panel/axis_settings_popover.tsx index 2285cd1a7a43a..e0a30bdb2c511 100644 --- a/x-pack/plugins/lens/public/xy_visualization/axis_settings_popover.tsx +++ b/x-pack/plugins/lens/public/xy_visualization/xy_config_panel/axis_settings_popover.tsx @@ -20,15 +20,15 @@ import { EuiFieldNumber, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { XYLayerConfig, AxesSettingsConfig, AxisExtentConfig } from '../../common/expressions'; -import { ToolbarPopover, useDebouncedValue } from '../shared_components'; -import { isHorizontalChart } from './state_helpers'; -import { EuiIconAxisBottom } from '../assets/axis_bottom'; -import { EuiIconAxisLeft } from '../assets/axis_left'; -import { EuiIconAxisRight } from '../assets/axis_right'; -import { EuiIconAxisTop } from '../assets/axis_top'; -import { ToolbarButtonProps } from '../../../../../src/plugins/kibana_react/public'; -import { validateExtent } from './axes_configuration'; +import { XYLayerConfig, AxesSettingsConfig, AxisExtentConfig } from '../../../common/expressions'; +import { ToolbarPopover, useDebouncedValue } from '../../shared_components'; +import { isHorizontalChart } from '../state_helpers'; +import { EuiIconAxisBottom } from '../../assets/axis_bottom'; +import { EuiIconAxisLeft } from '../../assets/axis_left'; +import { EuiIconAxisRight } from '../../assets/axis_right'; +import { EuiIconAxisTop } from '../../assets/axis_top'; +import { ToolbarButtonProps } from '../../../../../../src/plugins/kibana_react/public'; +import { validateExtent } from '../axes_configuration'; type AxesSettingsConfigKeys = keyof AxesSettingsConfig; 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 new file mode 100644 index 0000000000000..5a6458a4654d0 --- /dev/null +++ b/x-pack/plugins/lens/public/xy_visualization/xy_config_panel/color_picker.tsx @@ -0,0 +1,175 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import './xy_config_panel.scss'; +import React, { useMemo, useState } from 'react'; +import { i18n } from '@kbn/i18n'; +import { debounce } from 'lodash'; +import { EuiFormRow, EuiColorPicker, EuiColorPickerProps, EuiToolTip, EuiIcon } from '@elastic/eui'; +import type { PaletteRegistry } from 'src/plugins/charts/public'; +import type { VisualizationDimensionEditorProps } from '../../types'; +import { State } from '../types'; +import { FormatFactory } from '../../../common'; +import { getSeriesColor } from '../state_helpers'; +import { getAccessorColorConfig, getColorAssignments } from '../color_assignment'; +import { getSortedAccessors } from '../to_expression'; +import { updateLayer } from '.'; +import { TooltipWrapper } from '../../shared_components'; + +const tooltipContent = { + auto: i18n.translate('xpack.lens.configPanel.color.tooltip.auto', { + defaultMessage: 'Lens automatically picks colors for you unless you specify a custom color.', + }), + custom: i18n.translate('xpack.lens.configPanel.color.tooltip.custom', { + defaultMessage: 'Clear the custom color to return to “Auto” mode.', + }), + disabled: i18n.translate('xpack.lens.configPanel.color.tooltip.disabled', { + defaultMessage: + 'Individual series cannot be custom colored when the layer includes a “Break down by.“', + }), +}; + +export const ColorPicker = ({ + state, + setState, + layerId, + accessor, + frame, + formatFactory, + paletteService, + label, + disableHelpTooltip, +}: VisualizationDimensionEditorProps & { + formatFactory: FormatFactory; + paletteService: PaletteRegistry; + label?: string; + disableHelpTooltip?: boolean; +}) => { + const index = state.layers.findIndex((l) => l.layerId === layerId); + const layer = state.layers[index]; + const disabled = Boolean(layer.splitAccessor); + + const overwriteColor = getSeriesColor(layer, accessor); + const currentColor = useMemo(() => { + if (overwriteColor || !frame.activeData) return overwriteColor; + + const datasource = frame.datasourceLayers[layer.layerId]; + const sortedAccessors: string[] = getSortedAccessors(datasource, layer); + + const colorAssignments = getColorAssignments( + state.layers, + { tables: frame.activeData }, + formatFactory + ); + const mappedAccessors = getAccessorColorConfig( + colorAssignments, + frame, + { + ...layer, + accessors: sortedAccessors.filter((sorted) => layer.accessors.includes(sorted)), + }, + paletteService + ); + + return mappedAccessors.find((a) => a.columnId === accessor)?.color || null; + }, [overwriteColor, frame, paletteService, state.layers, accessor, formatFactory, layer]); + + const [color, setColor] = useState(currentColor); + + const handleColor: EuiColorPickerProps['onChange'] = (text, output) => { + setColor(text); + if (output.isValid || text === '') { + updateColorInState(text, output); + } + }; + + const updateColorInState: EuiColorPickerProps['onChange'] = useMemo( + () => + debounce((text, output) => { + const newYConfigs = [...(layer.yConfig || [])]; + const existingIndex = newYConfigs.findIndex((yConfig) => yConfig.forAccessor === accessor); + if (existingIndex !== -1) { + if (text === '') { + newYConfigs[existingIndex] = { ...newYConfigs[existingIndex], color: undefined }; + } else { + newYConfigs[existingIndex] = { ...newYConfigs[existingIndex], color: output.hex }; + } + } else { + newYConfigs.push({ + forAccessor: accessor, + color: output.hex, + }); + } + setState(updateLayer(state, { ...layer, yConfig: newYConfigs }, index)); + }, 256), + [state, setState, layer, accessor, index] + ); + + const inputLabel = + label ?? + i18n.translate('xpack.lens.xyChart.seriesColor.label', { + defaultMessage: 'Series color', + }); + + const colorPicker = ( + + ); + + return ( + + + {inputLabel} + {!disableHelpTooltip && ( + <> + {''} + + + )} + + + } + > + {disabled ? ( + + {colorPicker} + + ) : ( + colorPicker + )} + + ); +}; diff --git a/x-pack/plugins/lens/public/xy_visualization/xy_config_panel.tsx b/x-pack/plugins/lens/public/xy_visualization/xy_config_panel/index.tsx similarity index 72% rename from x-pack/plugins/lens/public/xy_visualization/xy_config_panel.tsx rename to x-pack/plugins/lens/public/xy_visualization/xy_config_panel/index.tsx index cd90fa52cd402..1427a3d28ea39 100644 --- a/x-pack/plugins/lens/public/xy_visualization/xy_config_panel.tsx +++ b/x-pack/plugins/lens/public/xy_visualization/xy_config_panel/index.tsx @@ -6,24 +6,15 @@ */ import './xy_config_panel.scss'; -import React, { useMemo, useState, memo, useCallback } from 'react'; +import React, { memo, useCallback } from 'react'; import { i18n } from '@kbn/i18n'; import { Position, ScaleType, VerticalAlignment, HorizontalAlignment } from '@elastic/charts'; -import { debounce } from 'lodash'; import { EuiButtonGroup, EuiFlexGroup, EuiFlexItem, EuiFormRow, htmlIdGenerator, - EuiColorPicker, - EuiColorPickerProps, - EuiToolTip, - EuiIcon, - EuiPopover, - EuiSelectable, - EuiText, - EuiPopoverTitle, } from '@elastic/eui'; import type { PaletteRegistry } from 'src/plugins/charts/public'; import type { @@ -31,30 +22,34 @@ import type { VisualizationToolbarProps, VisualizationDimensionEditorProps, FramePublicAPI, -} from '../types'; -import { State, visualizationTypes, XYState } from './types'; -import type { FormatFactory } from '../../common'; +} from '../../types'; +import { State, visualizationTypes, XYState } from '../types'; +import type { FormatFactory } from '../../../common'; import { SeriesType, YAxisMode, AxesSettingsConfig, AxisExtentConfig, -} from '../../common/expressions'; -import { isHorizontalChart, isHorizontalSeries, getSeriesColor } from './state_helpers'; -import { trackUiEvent } from '../lens_ui_telemetry'; -import { LegendSettingsPopover } from '../shared_components'; +} from '../../../common/expressions'; +import { isHorizontalChart, isHorizontalSeries } from '../state_helpers'; +import { trackUiEvent } from '../../lens_ui_telemetry'; +import { LegendSettingsPopover } from '../../shared_components'; import { AxisSettingsPopover } from './axis_settings_popover'; -import { getAxesConfiguration, GroupsConfiguration } from './axes_configuration'; -import { PalettePicker, TooltipWrapper } from '../shared_components'; -import { getAccessorColorConfig, getColorAssignments } from './color_assignment'; -import { getScaleType, getSortedAccessors } from './to_expression'; -import { VisualOptionsPopover } from './visual_options_popover/visual_options_popover'; -import { ToolbarButton } from '../../../../../src/plugins/kibana_react/public'; +import { getAxesConfiguration, GroupsConfiguration } from '../axes_configuration'; +import { VisualOptionsPopover } from './visual_options_popover'; +import { getScaleType } from '../to_expression'; +import { ColorPicker } from './color_picker'; +import { ThresholdPanel } from './threshold_panel'; +import { PalettePicker, TooltipWrapper } from '../../shared_components'; type UnwrapArray = T extends Array ? P : T; type AxesSettingsConfigKeys = keyof AxesSettingsConfig; -function updateLayer(state: State, layer: UnwrapArray, index: number): State { +export function updateLayer( + state: State, + layer: UnwrapArray, + index: number +): State { const newLayers = [...state.layers]; newLayers[index] = layer; @@ -92,90 +87,6 @@ const legendOptions: Array<{ }, ]; -export function LayerHeader(props: VisualizationLayerWidgetProps) { - const [isPopoverOpen, setPopoverIsOpen] = useState(false); - const { state, layerId } = props; - const horizontalOnly = isHorizontalChart(state.layers); - const index = state.layers.findIndex((l) => l.layerId === layerId); - const layer = state.layers[index]; - if (!layer) { - return null; - } - - const currentVisType = visualizationTypes.find(({ id }) => id === layer.seriesType)!; - - const createTrigger = function () { - return ( - setPopoverIsOpen(!isPopoverOpen)} - fullWidth - size="s" - > - <> - - - {currentVisType.fullLabel || currentVisType.label} - - - - ); - }; - - return ( - <> - setPopoverIsOpen(false)} - display="block" - panelPaddingSize="s" - ownFocus - > - - {i18n.translate('xpack.lens.layerPanel.layerVisualizationType', { - defaultMessage: 'Layer visualization type', - })} - -
    - - singleSelection="always" - options={visualizationTypes - .filter((t) => isHorizontalSeries(t.id as SeriesType) === horizontalOnly) - .map((t) => ({ - value: t.id, - key: t.id, - checked: t.id === currentVisType.id ? 'on' : undefined, - prepend: , - label: t.fullLabel || t.label, - 'data-test-subj': `lnsXY_seriesType-${t.id}`, - }))} - onChange={(newOptions) => { - const chosenType = newOptions.find(({ checked }) => checked === 'on'); - if (!chosenType) { - return; - } - const id = chosenType.value!; - trackUiEvent('xy_change_layer_display'); - props.setState(updateLayer(state, { ...layer, seriesType: id as SeriesType }, index)); - setPopoverIsOpen(false); - }} - > - {(list) => <>{list}} - -
    -
    - - ); -} - export function LayerContextMenu(props: VisualizationLayerWidgetProps) { const { state, layerId } = props; const horizontalOnly = isHorizontalChart(state.layers); @@ -622,7 +533,7 @@ export const XyToolbar = memo(function XyToolbar(props: VisualizationToolbarProp ); }); -const idPrefix = htmlIdGenerator()(); +export const idPrefix = htmlIdGenerator()(); export function DimensionEditor( props: VisualizationDimensionEditorProps & { @@ -653,6 +564,10 @@ export function DimensionEditor( ); } + if (layer.layerType === 'threshold') { + return ; + } + return ( <> @@ -728,140 +643,3 @@ export function DimensionEditor( ); } - -const tooltipContent = { - auto: i18n.translate('xpack.lens.configPanel.color.tooltip.auto', { - defaultMessage: 'Lens automatically picks colors for you unless you specify a custom color.', - }), - custom: i18n.translate('xpack.lens.configPanel.color.tooltip.custom', { - defaultMessage: 'Clear the custom color to return to “Auto” mode.', - }), - disabled: i18n.translate('xpack.lens.configPanel.color.tooltip.disabled', { - defaultMessage: - 'Individual series cannot be custom colored when the layer includes a “Break down by.“', - }), -}; - -const ColorPicker = ({ - state, - setState, - layerId, - accessor, - frame, - formatFactory, - paletteService, -}: VisualizationDimensionEditorProps & { - formatFactory: FormatFactory; - paletteService: PaletteRegistry; -}) => { - const index = state.layers.findIndex((l) => l.layerId === layerId); - const layer = state.layers[index]; - const disabled = !!layer.splitAccessor; - - const overwriteColor = getSeriesColor(layer, accessor); - const currentColor = useMemo(() => { - if (overwriteColor || !frame.activeData) return overwriteColor; - - const datasource = frame.datasourceLayers[layer.layerId]; - const sortedAccessors: string[] = getSortedAccessors(datasource, layer); - - const colorAssignments = getColorAssignments( - state.layers, - { tables: frame.activeData }, - formatFactory - ); - const mappedAccessors = getAccessorColorConfig( - colorAssignments, - frame, - { - ...layer, - accessors: sortedAccessors.filter((sorted) => layer.accessors.includes(sorted)), - }, - paletteService - ); - - return mappedAccessors.find((a) => a.columnId === accessor)?.color || null; - }, [overwriteColor, frame, paletteService, state.layers, accessor, formatFactory, layer]); - - const [color, setColor] = useState(currentColor); - - const handleColor: EuiColorPickerProps['onChange'] = (text, output) => { - setColor(text); - if (output.isValid || text === '') { - updateColorInState(text, output); - } - }; - - const updateColorInState: EuiColorPickerProps['onChange'] = useMemo( - () => - debounce((text, output) => { - const newYConfigs = [...(layer.yConfig || [])]; - const existingIndex = newYConfigs.findIndex((yConfig) => yConfig.forAccessor === accessor); - if (existingIndex !== -1) { - if (text === '') { - newYConfigs[existingIndex] = { ...newYConfigs[existingIndex], color: undefined }; - } else { - newYConfigs[existingIndex] = { ...newYConfigs[existingIndex], color: output.hex }; - } - } else { - newYConfigs.push({ - forAccessor: accessor, - color: output.hex, - }); - } - setState(updateLayer(state, { ...layer, yConfig: newYConfigs }, index)); - }, 256), - [state, setState, layer, accessor, index] - ); - - const colorPicker = ( - - ); - - return ( - - - {i18n.translate('xpack.lens.xyChart.seriesColor.label', { - defaultMessage: 'Series color', - })}{' '} - - -
    - } - > - {disabled ? ( - - {colorPicker} - - ) : ( - colorPicker - )} - - ); -}; 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 new file mode 100644 index 0000000000000..dde4de0dd4bc3 --- /dev/null +++ b/x-pack/plugins/lens/public/xy_visualization/xy_config_panel/layer_header.tsx @@ -0,0 +1,115 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 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'; +import type { VisualizationLayerWidgetProps } from '../../types'; +import { State, visualizationTypes } from '../types'; +import { layerTypes } from '../../../common'; +import { SeriesType } from '../../../common/expressions'; +import { isHorizontalChart, isHorizontalSeries } from '../state_helpers'; +import { trackUiEvent } from '../../lens_ui_telemetry'; +import { StaticHeader } from '../../shared_components'; +import { ToolbarButton } from '../../../../../../src/plugins/kibana_react/public'; +import { LensIconChartBarThreshold } from '../../assets/chart_bar_threshold'; +import { updateLayer } from '.'; + +export function LayerHeader(props: VisualizationLayerWidgetProps) { + const [isPopoverOpen, setPopoverIsOpen] = useState(false); + const { state, layerId } = props; + const horizontalOnly = isHorizontalChart(state.layers); + const index = state.layers.findIndex((l) => l.layerId === layerId); + const layer = state.layers[index]; + if (!layer) { + return null; + } + // if it's a threshold just draw a static text + if (layer.layerType === layerTypes.THRESHOLD) { + return ( + + ); + } + const currentVisType = visualizationTypes.find(({ id }) => id === layer.seriesType)!; + + const createTrigger = function () { + return ( + setPopoverIsOpen(!isPopoverOpen)} + fullWidth + size="s" + > + <> + + + {currentVisType.fullLabel || currentVisType.label} + + + + ); + }; + + return ( + <> + setPopoverIsOpen(false)} + display="block" + panelPaddingSize="s" + ownFocus + > + + {i18n.translate('xpack.lens.layerPanel.layerVisualizationType', { + defaultMessage: 'Layer visualization type', + })} + +
    + + singleSelection="always" + options={visualizationTypes + .filter((t) => isHorizontalSeries(t.id as SeriesType) === horizontalOnly) + .map((t) => ({ + value: t.id, + key: t.id, + checked: t.id === currentVisType.id ? 'on' : undefined, + prepend: , + label: t.fullLabel || t.label, + 'data-test-subj': `lnsXY_seriesType-${t.id}`, + }))} + onChange={(newOptions) => { + const chosenType = newOptions.find(({ checked }) => checked === 'on'); + if (!chosenType) { + return; + } + const id = chosenType.value!; + trackUiEvent('xy_change_layer_display'); + props.setState(updateLayer(state, { ...layer, seriesType: id as SeriesType }, index)); + setPopoverIsOpen(false); + }} + > + {(list) => <>{list}} + +
    +
    + + ); +} diff --git a/x-pack/plugins/lens/public/xy_visualization/xy_config_panel/threshold_panel.tsx b/x-pack/plugins/lens/public/xy_visualization/xy_config_panel/threshold_panel.tsx new file mode 100644 index 0000000000000..1e5b90e41b623 --- /dev/null +++ b/x-pack/plugins/lens/public/xy_visualization/xy_config_panel/threshold_panel.tsx @@ -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 './xy_config_panel.scss'; +import React, { useCallback } from 'react'; +import { i18n } from '@kbn/i18n'; +import { EuiButtonGroup, EuiComboBox, EuiFormRow, EuiIcon, EuiRange } from '@elastic/eui'; +import type { PaletteRegistry } from 'src/plugins/charts/public'; +import type { VisualizationDimensionEditorProps } from '../../types'; +import { State } from '../types'; +import { FormatFactory } from '../../../common'; +import { YConfig } from '../../../common/expressions'; +import { LineStyle, FillStyle } from '../../../common/expressions/xy_chart'; + +import { ColorPicker } from './color_picker'; +import { updateLayer, idPrefix } from '.'; +import { useDebouncedValue } from '../../shared_components'; + +const icons = [ + { + value: 'none', + label: i18n.translate('xpack.lens.xyChart.thresholds.noIconLabel', { defaultMessage: 'None' }), + }, + { + value: 'asterisk', + label: i18n.translate('xpack.lens.xyChart.thresholds.asteriskIconLabel', { + defaultMessage: 'Asterisk', + }), + }, + { + value: 'bell', + label: i18n.translate('xpack.lens.xyChart.thresholds.bellIconLabel', { + defaultMessage: 'Bell', + }), + }, + { + value: 'bolt', + label: i18n.translate('xpack.lens.xyChart.thresholds.boltIconLabel', { + defaultMessage: 'Bolt', + }), + }, + { + value: 'bug', + label: i18n.translate('xpack.lens.xyChart.thresholds.bugIconLabel', { + defaultMessage: 'Bug', + }), + }, + { + value: 'editorComment', + label: i18n.translate('xpack.lens.xyChart.thresholds.commentIconLabel', { + defaultMessage: 'Comment', + }), + }, + { + value: 'alert', + label: i18n.translate('xpack.lens.xyChart.thresholds.alertIconLabel', { + defaultMessage: 'Alert', + }), + }, + { + value: 'flag', + label: i18n.translate('xpack.lens.xyChart.thresholds.flagIconLabel', { + defaultMessage: 'Flag', + }), + }, + { + value: 'tag', + label: i18n.translate('xpack.lens.xyChart.thresholds.tagIconLabel', { + defaultMessage: 'Tag', + }), + }, +]; + +const IconView = (props: { value?: string; label: string }) => { + if (!props.value) return null; + return ( + + + {` ${props.label}`} + + ); +}; + +const IconSelect = ({ + value, + onChange, +}: { + value?: string; + onChange: (newIcon: string) => void; +}) => { + const selectedIcon = icons.find((option) => value === option.value) || icons[0]; + + return ( + { + onChange(selection[0].value!); + }} + singleSelection={{ asPlainText: true }} + renderOption={IconView} + compressed + /> + ); +}; + +export const ThresholdPanel = ( + props: VisualizationDimensionEditorProps & { + formatFactory: FormatFactory; + paletteService: PaletteRegistry; + } +) => { + const { state, setState, layerId, accessor } = props; + const index = state.layers.findIndex((l) => l.layerId === layerId); + const layer = state.layers[index]; + + const setYConfig = useCallback( + (yConfig: Partial | undefined) => { + if (yConfig == null) { + return; + } + setState((currState) => { + const currLayer = currState.layers[index]; + const newYConfigs = [...(currLayer.yConfig || [])]; + const existingIndex = newYConfigs.findIndex( + (yAxisConfig) => yAxisConfig.forAccessor === accessor + ); + if (existingIndex !== -1) { + newYConfigs[existingIndex] = { ...newYConfigs[existingIndex], ...yConfig }; + } else { + newYConfigs.push({ forAccessor: accessor, ...yConfig }); + } + return updateLayer(currState, { ...currLayer, yConfig: newYConfigs }, index); + }); + }, + [accessor, index, setState] + ); + + const currentYConfig = layer.yConfig?.find((yConfig) => yConfig.forAccessor === accessor); + + return ( + <> + + + { + const newMode = id.replace(idPrefix, '') as LineStyle; + setYConfig({ forAccessor: accessor, lineStyle: newMode }); + }} + /> + + + { + setYConfig({ forAccessor: accessor, lineWidth: value }); + }} + /> + + + { + const newMode = id.replace(idPrefix, '') as FillStyle; + setYConfig({ forAccessor: accessor, fill: newMode }); + }} + /> + + + { + setYConfig({ forAccessor: accessor, icon: newIcon }); + }} + /> + + + ); +}; + +const minRange = 1; +const maxRange = 10; + +function getSafeValue(value: number | '', prevValue: number, min: number, max: number) { + if (value === '') { + return prevValue; + } + return Math.max(minRange, Math.min(value, maxRange)); +} + +const LineThicknessSlider = ({ + value, + onChange, +}: { + value: number; + onChange: (value: number) => void; +}) => { + const onChangeWrapped = useCallback( + (newValue) => { + if (Number.isInteger(newValue)) { + onChange(getSafeValue(newValue, newValue, minRange, maxRange)); + } + }, + [onChange] + ); + const { inputValue, handleInputChange } = useDebouncedValue( + { value, onChange: onChangeWrapped }, + { allowFalsyValue: true } + ); + + return ( + { + const newValue = e.currentTarget.value; + handleInputChange(newValue === '' ? '' : Number(newValue)); + }} + onBlur={() => { + handleInputChange(getSafeValue(inputValue, value, minRange, maxRange)); + }} + /> + ); +}; diff --git a/x-pack/plugins/lens/public/xy_visualization/visual_options_popover/fill_opacity_option.test.tsx b/x-pack/plugins/lens/public/xy_visualization/xy_config_panel/visual_options_popover/fill_opacity_option.test.tsx similarity index 100% rename from x-pack/plugins/lens/public/xy_visualization/visual_options_popover/fill_opacity_option.test.tsx rename to x-pack/plugins/lens/public/xy_visualization/xy_config_panel/visual_options_popover/fill_opacity_option.test.tsx diff --git a/x-pack/plugins/lens/public/xy_visualization/visual_options_popover/fill_opacity_option.tsx b/x-pack/plugins/lens/public/xy_visualization/xy_config_panel/visual_options_popover/fill_opacity_option.tsx similarity index 95% rename from x-pack/plugins/lens/public/xy_visualization/visual_options_popover/fill_opacity_option.tsx rename to x-pack/plugins/lens/public/xy_visualization/xy_config_panel/visual_options_popover/fill_opacity_option.tsx index eb8d35c54a99b..09b381dd03f7a 100644 --- a/x-pack/plugins/lens/public/xy_visualization/visual_options_popover/fill_opacity_option.tsx +++ b/x-pack/plugins/lens/public/xy_visualization/xy_config_panel/visual_options_popover/fill_opacity_option.tsx @@ -8,7 +8,7 @@ import React from 'react'; import { i18n } from '@kbn/i18n'; import { EuiFormRow, EuiRange } from '@elastic/eui'; -import { useDebouncedValue } from '../../shared_components'; +import { useDebouncedValue } from '../../../shared_components'; export interface FillOpacityOptionProps { /** diff --git a/x-pack/plugins/lens/public/xy_visualization/visual_options_popover/visual_options_popover.tsx b/x-pack/plugins/lens/public/xy_visualization/xy_config_panel/visual_options_popover/index.tsx similarity index 93% rename from x-pack/plugins/lens/public/xy_visualization/visual_options_popover/visual_options_popover.tsx rename to x-pack/plugins/lens/public/xy_visualization/xy_config_panel/visual_options_popover/index.tsx index 6d0e5c2d55b70..2a19897445e63 100644 --- a/x-pack/plugins/lens/public/xy_visualization/visual_options_popover/visual_options_popover.tsx +++ b/x-pack/plugins/lens/public/xy_visualization/xy_config_panel/visual_options_popover/index.tsx @@ -7,14 +7,14 @@ import React from 'react'; import { i18n } from '@kbn/i18n'; -import { ToolbarPopover, TooltipWrapper } from '../../shared_components'; +import { ToolbarPopover, TooltipWrapper } from '../../../shared_components'; import { MissingValuesOptions } from './missing_values_option'; import { LineCurveOption } from './line_curve_option'; import { FillOpacityOption } from './fill_opacity_option'; -import { XYState } from '../types'; -import { hasHistogramSeries } from '../state_helpers'; -import { ValidLayer } from '../../../common/expressions'; -import type { FramePublicAPI } from '../../types'; +import { XYState } from '../../types'; +import { hasHistogramSeries } from '../../state_helpers'; +import { ValidLayer } from '../../../../common/expressions'; +import type { FramePublicAPI } from '../../../types'; function getValueLabelDisableReason({ isAreaPercentage, diff --git a/x-pack/plugins/lens/public/xy_visualization/visual_options_popover/line_curve_option.test.tsx b/x-pack/plugins/lens/public/xy_visualization/xy_config_panel/visual_options_popover/line_curve_option.test.tsx similarity index 100% rename from x-pack/plugins/lens/public/xy_visualization/visual_options_popover/line_curve_option.test.tsx rename to x-pack/plugins/lens/public/xy_visualization/xy_config_panel/visual_options_popover/line_curve_option.test.tsx diff --git a/x-pack/plugins/lens/public/xy_visualization/visual_options_popover/line_curve_option.tsx b/x-pack/plugins/lens/public/xy_visualization/xy_config_panel/visual_options_popover/line_curve_option.tsx similarity index 95% rename from x-pack/plugins/lens/public/xy_visualization/visual_options_popover/line_curve_option.tsx rename to x-pack/plugins/lens/public/xy_visualization/xy_config_panel/visual_options_popover/line_curve_option.tsx index 6080a8c68e57d..96926412afb8a 100644 --- a/x-pack/plugins/lens/public/xy_visualization/visual_options_popover/line_curve_option.tsx +++ b/x-pack/plugins/lens/public/xy_visualization/xy_config_panel/visual_options_popover/line_curve_option.tsx @@ -8,7 +8,7 @@ import React from 'react'; import { i18n } from '@kbn/i18n'; import { EuiFormRow, EuiSwitch } from '@elastic/eui'; -import type { XYCurveType } from '../../../common/expressions'; +import type { XYCurveType } from '../../../../common/expressions'; export interface LineCurveOptionProps { /** diff --git a/x-pack/plugins/lens/public/xy_visualization/visual_options_popover/missing_value_option.test.tsx b/x-pack/plugins/lens/public/xy_visualization/xy_config_panel/visual_options_popover/missing_value_option.test.tsx similarity index 100% rename from x-pack/plugins/lens/public/xy_visualization/visual_options_popover/missing_value_option.test.tsx rename to x-pack/plugins/lens/public/xy_visualization/xy_config_panel/visual_options_popover/missing_value_option.test.tsx diff --git a/x-pack/plugins/lens/public/xy_visualization/visual_options_popover/missing_values_option.tsx b/x-pack/plugins/lens/public/xy_visualization/xy_config_panel/visual_options_popover/missing_values_option.tsx similarity index 97% rename from x-pack/plugins/lens/public/xy_visualization/visual_options_popover/missing_values_option.tsx rename to x-pack/plugins/lens/public/xy_visualization/xy_config_panel/visual_options_popover/missing_values_option.tsx index 3dba8757903e9..b12e2d2f57112 100644 --- a/x-pack/plugins/lens/public/xy_visualization/visual_options_popover/missing_values_option.tsx +++ b/x-pack/plugins/lens/public/xy_visualization/xy_config_panel/visual_options_popover/missing_values_option.tsx @@ -8,8 +8,8 @@ import React from 'react'; import { i18n } from '@kbn/i18n'; import { EuiButtonGroup, EuiFormRow, EuiIconTip, EuiSuperSelect, EuiText } from '@elastic/eui'; -import { fittingFunctionDefinitions } from '../../../common/expressions'; -import type { FittingFunction, ValueLabelConfig } from '../../../common/expressions'; +import { fittingFunctionDefinitions } from '../../../../common/expressions'; +import type { FittingFunction, ValueLabelConfig } from '../../../../common/expressions'; export interface MissingValuesOptionProps { valueLabels?: ValueLabelConfig; diff --git a/x-pack/plugins/lens/public/xy_visualization/visual_options_popover/visual_options_popover.test.tsx b/x-pack/plugins/lens/public/xy_visualization/xy_config_panel/visual_options_popover/visual_options_popover.test.tsx similarity index 96% rename from x-pack/plugins/lens/public/xy_visualization/visual_options_popover/visual_options_popover.test.tsx rename to x-pack/plugins/lens/public/xy_visualization/xy_config_panel/visual_options_popover/visual_options_popover.test.tsx index cd6a20c37dd38..0136612c46705 100644 --- a/x-pack/plugins/lens/public/xy_visualization/visual_options_popover/visual_options_popover.test.tsx +++ b/x-pack/plugins/lens/public/xy_visualization/xy_config_panel/visual_options_popover/visual_options_popover.test.tsx @@ -8,14 +8,14 @@ import React from 'react'; import { shallowWithIntl as shallow } from '@kbn/test/jest'; import { Position } from '@elastic/charts'; -import type { FramePublicAPI } from '../../types'; -import { createMockDatasource, createMockFramePublicAPI } from '../../mocks'; -import { State } from '../types'; -import { VisualOptionsPopover } from './visual_options_popover'; -import { ToolbarPopover } from '../../shared_components'; +import type { FramePublicAPI } from '../../../types'; +import { createMockDatasource, createMockFramePublicAPI } from '../../../mocks'; +import { State } from '../../types'; +import { VisualOptionsPopover } from '.'; +import { ToolbarPopover } from '../../../shared_components'; import { MissingValuesOptions } from './missing_values_option'; import { FillOpacityOption } from './fill_opacity_option'; -import { layerTypes } from '../../../common'; +import { layerTypes } from '../../../../common'; describe('Visual options popover', () => { let frame: FramePublicAPI; diff --git a/x-pack/plugins/lens/public/xy_visualization/xy_config_panel.scss b/x-pack/plugins/lens/public/xy_visualization/xy_config_panel/xy_config_panel.scss similarity index 100% rename from x-pack/plugins/lens/public/xy_visualization/xy_config_panel.scss rename to x-pack/plugins/lens/public/xy_visualization/xy_config_panel/xy_config_panel.scss diff --git a/x-pack/plugins/lens/public/xy_visualization/xy_config_panel.test.tsx b/x-pack/plugins/lens/public/xy_visualization/xy_config_panel/xy_config_panel.test.tsx similarity index 98% rename from x-pack/plugins/lens/public/xy_visualization/xy_config_panel.test.tsx rename to x-pack/plugins/lens/public/xy_visualization/xy_config_panel/xy_config_panel.test.tsx index 9ca9021382fda..e5b1870c73404 100644 --- a/x-pack/plugins/lens/public/xy_visualization/xy_config_panel.test.tsx +++ b/x-pack/plugins/lens/public/xy_visualization/xy_config_panel/xy_config_panel.test.tsx @@ -8,15 +8,15 @@ import React from 'react'; import { mountWithIntl as mount, shallowWithIntl as shallow } from '@kbn/test/jest'; import { EuiButtonGroupProps, EuiButtonGroup } from '@elastic/eui'; -import { LayerContextMenu, XyToolbar, DimensionEditor } from './xy_config_panel'; +import { LayerContextMenu, XyToolbar, DimensionEditor } from '.'; import { AxisSettingsPopover } from './axis_settings_popover'; -import { FramePublicAPI } from '../types'; -import { State } from './types'; +import { FramePublicAPI } from '../../types'; +import { State } from '../types'; import { Position } from '@elastic/charts'; -import { createMockFramePublicAPI, createMockDatasource } from '../mocks'; +import { createMockFramePublicAPI, createMockDatasource } from '../../mocks'; import { chartPluginMock } from 'src/plugins/charts/public/mocks'; import { EuiColorPicker } from '@elastic/eui'; -import { layerTypes } from '../../common'; +import { layerTypes } from '../../../common'; describe('XY Config panels', () => { let frame: FramePublicAPI; diff --git a/x-pack/plugins/lens/server/index.ts b/x-pack/plugins/lens/server/index.ts index 08f1eb1562739..e2117506e9b72 100644 --- a/x-pack/plugins/lens/server/index.ts +++ b/x-pack/plugins/lens/server/index.ts @@ -19,6 +19,7 @@ import { configSchema, ConfigSchema } from '../config'; export const config: PluginConfigDescriptor = { schema: configSchema, + deprecations: ({ deprecate }) => [deprecate('enabled', '8.0.0')], }; export const plugin = (initializerContext: PluginInitializerContext) => diff --git a/x-pack/plugins/license_management/server/index.ts b/x-pack/plugins/license_management/server/index.ts index 7a24845c981e9..e78ffe07b50c0 100644 --- a/x-pack/plugins/license_management/server/index.ts +++ b/x-pack/plugins/license_management/server/index.ts @@ -17,4 +17,5 @@ export const config: PluginConfigDescriptor = { exposeToBrowser: { ui: true, }, + deprecations: ({ deprecate }) => [deprecate('enabled', '8.0.0')], }; diff --git a/x-pack/plugins/lists/server/index.ts b/x-pack/plugins/lists/server/index.ts index 250b5e79ed109..7e1283927aa86 100644 --- a/x-pack/plugins/lists/server/index.ts +++ b/x-pack/plugins/lists/server/index.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { PluginInitializerContext } from '../../../../src/core/server'; +import { PluginConfigDescriptor, PluginInitializerContext } from '../../../../src/core/server'; import { ConfigSchema } from './config'; import { ListPlugin } from './plugin'; @@ -19,6 +19,9 @@ export { export { ExceptionListClient } from './services/exception_lists/exception_list_client'; export type { ListPluginSetup, ListsApiRequestHandlerContext } from './types'; -export const config = { schema: ConfigSchema }; +export const config: PluginConfigDescriptor = { + deprecations: ({ deprecate }) => [deprecate('enabled', '8.0.0')], + schema: ConfigSchema, +}; export const plugin = (initializerContext: PluginInitializerContext): ListPlugin => new ListPlugin(initializerContext); diff --git a/x-pack/plugins/lists/server/plugin.ts b/x-pack/plugins/lists/server/plugin.ts index 6a1b0733c1867..7fa1bc460723a 100644 --- a/x-pack/plugins/lists/server/plugin.ts +++ b/x-pack/plugins/lists/server/plugin.ts @@ -27,9 +27,7 @@ import { getUser } from './get_user'; import { initSavedObjects } from './saved_objects'; import { ExceptionListClient } from './services/exception_lists/exception_list_client'; -export class ListPlugin - implements Plugin, ListsPluginStart, {}, PluginsStart> -{ +export class ListPlugin implements Plugin { private readonly logger: Logger; private readonly config: ConfigType; private spaces: SpacesServiceStart | undefined | null; @@ -40,7 +38,7 @@ export class ListPlugin this.config = this.initializerContext.config.get(); } - public async setup(core: CoreSetup): Promise { + public setup(core: CoreSetup): ListPluginSetup { const { config } = this; initSavedObjects(core.savedObjects); @@ -70,7 +68,7 @@ export class ListPlugin }; } - public start(core: CoreStart, plugins: PluginsStart): void { + public start(core: CoreStart, plugins: PluginsStart): ListsPluginStart { this.logger.debug('Starting plugin'); this.security = plugins.security; this.spaces = plugins.spaces?.spacesService; diff --git a/x-pack/plugins/logstash/server/index.ts b/x-pack/plugins/logstash/server/index.ts index 4606a518fa8c5..33f3777297f63 100644 --- a/x-pack/plugins/logstash/server/index.ts +++ b/x-pack/plugins/logstash/server/index.ts @@ -15,4 +15,5 @@ export const config: PluginConfigDescriptor = { schema: schema.object({ enabled: schema.boolean({ defaultValue: true }), }), + deprecations: ({ deprecate }) => [deprecate('enabled', '8.0.0')], }; diff --git a/x-pack/plugins/maps/common/descriptor_types/layer_descriptor_types.ts b/x-pack/plugins/maps/common/descriptor_types/layer_descriptor_types.ts index 740da8493c53c..244ebc59efd17 100644 --- a/x-pack/plugins/maps/common/descriptor_types/layer_descriptor_types.ts +++ b/x-pack/plugins/maps/common/descriptor_types/layer_descriptor_types.ts @@ -23,6 +23,7 @@ import { KBN_IS_TILE_COMPLETE, KBN_METADATA_FEATURE, KBN_VECTOR_SHAPE_TYPE_COUNTS, + LAYER_TYPE, } from '../constants'; export type Attribution = { @@ -56,7 +57,6 @@ export type LayerDescriptor = { alpha?: number; attribution?: Attribution; id: string; - joins?: JoinDescriptor[]; label?: string | null; areLabelsOnTop?: boolean; minZoom?: number; @@ -70,9 +70,12 @@ export type LayerDescriptor = { }; export type VectorLayerDescriptor = LayerDescriptor & { + type: LAYER_TYPE.VECTOR | LAYER_TYPE.TILED_VECTOR | LAYER_TYPE.BLENDED_VECTOR; + joins?: JoinDescriptor[]; style: VectorStyleDescriptor; }; export type HeatmapLayerDescriptor = LayerDescriptor & { + type: LAYER_TYPE.HEATMAP; style: HeatmapStyleDescriptor; }; diff --git a/x-pack/plugins/maps/common/index.ts b/x-pack/plugins/maps/common/index.ts index c1b5d26fca292..8374a4d0dbaa3 100644 --- a/x-pack/plugins/maps/common/index.ts +++ b/x-pack/plugins/maps/common/index.ts @@ -12,6 +12,7 @@ export { FIELD_ORIGIN, INITIAL_LOCATION, LABEL_BORDER_SIZES, + LAYER_TYPE, MAP_SAVED_OBJECT_TYPE, SOURCE_TYPES, STYLE_TYPE, 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 d40d85f9b6192..e46bf6a1a6e7f 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 @@ -6,8 +6,8 @@ */ import { MapSavedObjectAttributes } from '../map_saved_object_type'; -import { JoinDescriptor, LayerDescriptor } from '../descriptor_types'; -import { LAYER_TYPE, SOURCE_TYPES } from '../constants'; +import { JoinDescriptor, LayerDescriptor, VectorLayerDescriptor } from '../descriptor_types'; +import { SOURCE_TYPES } from '../constants'; // enforce type property on joins. It's possible older saved-objects do not have this correctly filled in // e.g. sample-data was missing the right.type field. @@ -24,14 +24,15 @@ export function addTypeToTermJoin({ const layerList: LayerDescriptor[] = JSON.parse(attributes.layerListJSON); layerList.forEach((layer: LayerDescriptor) => { - if (layer.type !== LAYER_TYPE.VECTOR) { + if (!('joins' in layer)) { return; } - if (!layer.joins) { + const vectorLayer = layer as VectorLayerDescriptor; + if (!vectorLayer.joins) { return; } - layer.joins.forEach((join: JoinDescriptor) => { + vectorLayer.joins.forEach((join: JoinDescriptor) => { if (!join.right) { return; } diff --git a/x-pack/plugins/maps/common/migrations/references.ts b/x-pack/plugins/maps/common/migrations/references.ts index d48be6bd56fbe..41d9dc063fe47 100644 --- a/x-pack/plugins/maps/common/migrations/references.ts +++ b/x-pack/plugins/maps/common/migrations/references.ts @@ -9,7 +9,7 @@ import { SavedObjectReference } from '../../../../../src/core/types'; import { MapSavedObjectAttributes } from '../map_saved_object_type'; -import { LayerDescriptor } from '../descriptor_types'; +import { LayerDescriptor, VectorLayerDescriptor } from '../descriptor_types'; interface IndexPatternReferenceDescriptor { indexPatternId?: string; @@ -44,21 +44,24 @@ export function extractReferences({ sourceDescriptor.indexPatternRefName = refName; } - // Extract index-pattern references from join - const joins = layer.joins ? layer.joins : []; - joins.forEach((join, joinIndex) => { - if ('indexPatternId' in join.right) { - const sourceDescriptor = join.right as IndexPatternReferenceDescriptor; - const refName = `layer_${layerIndex}_join_${joinIndex}_index_pattern`; - extractedReferences.push({ - name: refName, - type: 'index-pattern', - id: sourceDescriptor.indexPatternId!, - }); - delete sourceDescriptor.indexPatternId; - sourceDescriptor.indexPatternRefName = refName; - } - }); + if ('joins' in layer) { + // Extract index-pattern references from join + const vectorLayer = layer as VectorLayerDescriptor; + const joins = vectorLayer.joins ? vectorLayer.joins : []; + joins.forEach((join, joinIndex) => { + if ('indexPatternId' in join.right) { + const sourceDescriptor = join.right as IndexPatternReferenceDescriptor; + const refName = `layer_${layerIndex}_join_${joinIndex}_index_pattern`; + extractedReferences.push({ + name: refName, + type: 'index-pattern', + id: sourceDescriptor.indexPatternId!, + }); + delete sourceDescriptor.indexPatternId; + sourceDescriptor.indexPatternRefName = refName; + } + }); + } }); return { @@ -99,16 +102,19 @@ export function injectReferences({ delete sourceDescriptor.indexPatternRefName; } - // Inject index-pattern references into join - const joins = layer.joins ? layer.joins : []; - joins.forEach((join) => { - if ('indexPatternRefName' in join.right) { - const sourceDescriptor = join.right as IndexPatternReferenceDescriptor; - const reference = findReference(sourceDescriptor.indexPatternRefName!, references); - sourceDescriptor.indexPatternId = reference.id; - delete sourceDescriptor.indexPatternRefName; - } - }); + if ('joins' in layer) { + // Inject index-pattern references into join + const vectorLayer = layer as VectorLayerDescriptor; + const joins = vectorLayer.joins ? vectorLayer.joins : []; + joins.forEach((join) => { + if ('indexPatternRefName' in join.right) { + const sourceDescriptor = join.right as IndexPatternReferenceDescriptor; + const reference = findReference(sourceDescriptor.indexPatternRefName!, references); + sourceDescriptor.indexPatternId = reference.id; + delete sourceDescriptor.indexPatternRefName; + } + }); + } }); return { diff --git a/x-pack/plugins/maps/public/api/index.ts b/x-pack/plugins/maps/public/api/index.ts index 186fd98c90bf6..feded3e16f375 100644 --- a/x-pack/plugins/maps/public/api/index.ts +++ b/x-pack/plugins/maps/public/api/index.ts @@ -6,6 +6,7 @@ */ export { MapsStartApi } from './start_api'; +export { MapsSetupApi } from './setup_api'; export { createLayerDescriptors } from './create_layer_descriptors'; export { registerLayerWizard, registerSource } from './register'; export { suggestEMSTermJoinConfig } from './ems'; diff --git a/x-pack/plugins/maps/public/api/setup_api.ts b/x-pack/plugins/maps/public/api/setup_api.ts new file mode 100644 index 0000000000000..1b4fee968aad4 --- /dev/null +++ b/x-pack/plugins/maps/public/api/setup_api.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 type { SourceRegistryEntry } from '../classes/sources/source_registry'; +import type { LayerWizard } from '../classes/layers/layer_wizard_registry'; + +export interface MapsSetupApi { + registerLayerWizard(layerWizard: LayerWizard): Promise; + registerSource(entry: SourceRegistryEntry): Promise; +} diff --git a/x-pack/plugins/maps/public/api/start_api.ts b/x-pack/plugins/maps/public/api/start_api.ts index e4213fe07a49c..eea440b8b2afc 100644 --- a/x-pack/plugins/maps/public/api/start_api.ts +++ b/x-pack/plugins/maps/public/api/start_api.ts @@ -6,8 +6,6 @@ */ import type { LayerDescriptor } from '../../common/descriptor_types'; -import type { SourceRegistryEntry } from '../classes/sources/source_registry'; -import type { LayerWizard } from '../classes/layers/layer_wizard_registry'; import type { CreateLayerDescriptorParams } from '../classes/sources/es_search_source'; import type { SampleValuesConfig, EMSTermJoinConfig } from '../ems_autosuggest'; @@ -22,7 +20,5 @@ export interface MapsStartApi { params: CreateLayerDescriptorParams ) => Promise; }; - registerLayerWizard(layerWizard: LayerWizard): Promise; - registerSource(entry: SourceRegistryEntry): Promise; suggestEMSTermJoinConfig(config: SampleValuesConfig): Promise; } 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/blended_vector_layer/blended_vector_layer.ts index d2734265f3bc3..a158892be9d09 100644 --- a/x-pack/plugins/maps/public/classes/layers/blended_vector_layer/blended_vector_layer.ts +++ b/x-pack/plugins/maps/public/classes/layers/blended_vector_layer/blended_vector_layer.ts @@ -33,7 +33,6 @@ import { SizeDynamicOptions, DynamicStylePropertyOptions, StylePropertyOptions, - LayerDescriptor, Timeslice, VectorLayerDescriptor, VectorSourceRequestMeta, @@ -179,7 +178,7 @@ export class BlendedVectorLayer extends VectorLayer implements IVectorLayer { mapColors: string[] ): VectorLayerDescriptor { const layerDescriptor = VectorLayer.createDescriptor(options, mapColors); - layerDescriptor.type = BlendedVectorLayer.type; + layerDescriptor.type = LAYER_TYPE.BLENDED_VECTOR; return layerDescriptor; } @@ -256,7 +255,7 @@ export class BlendedVectorLayer extends VectorLayer implements IVectorLayer { return false; } - async cloneDescriptor(): Promise { + async cloneDescriptor(): Promise { const clonedDescriptor = await super.cloneDescriptor(); // Use super getDisplayName instead of instance getDisplayName to avoid getting 'Clustered Clone of Clustered' diff --git a/x-pack/plugins/maps/public/classes/layers/layer.test.ts b/x-pack/plugins/maps/public/classes/layers/layer.test.ts index 83a936f377c7f..194b41680872c 100644 --- a/x-pack/plugins/maps/public/classes/layers/layer.test.ts +++ b/x-pack/plugins/maps/public/classes/layers/layer.test.ts @@ -9,21 +9,6 @@ import { AbstractLayer } from './layer'; import { ISource } from '../sources/source'; -import { - AGG_TYPE, - FIELD_ORIGIN, - LAYER_STYLE_TYPE, - SOURCE_TYPES, - VECTOR_STYLES, -} from '../../../common/constants'; -import { ESTermSourceDescriptor, VectorStyleDescriptor } from '../../../common/descriptor_types'; -import { getDefaultDynamicProperties } from '../styles/vector/vector_style_defaults'; - -jest.mock('uuid/v4', () => { - return function () { - return '12345'; - }; -}); class MockLayer extends AbstractLayer {} @@ -36,111 +21,11 @@ class MockSource { return {}; } - getDisplayName() { - return 'mySource'; - } - async supportsFitToBounds() { return this._fitToBounds; } } -describe('cloneDescriptor', () => { - describe('with joins', () => { - const styleDescriptor = { - type: LAYER_STYLE_TYPE.VECTOR, - properties: { - ...getDefaultDynamicProperties(), - }, - } as VectorStyleDescriptor; - // @ts-expect-error - styleDescriptor.properties[VECTOR_STYLES.FILL_COLOR].options.field = { - name: '__kbnjoin__count__557d0f15', - origin: FIELD_ORIGIN.JOIN, - }; - // @ts-expect-error - styleDescriptor.properties[VECTOR_STYLES.LINE_COLOR].options.field = { - name: 'bytes', - origin: FIELD_ORIGIN.SOURCE, - }; - // @ts-expect-error - styleDescriptor.properties[VECTOR_STYLES.LABEL_BORDER_COLOR].options.field = { - name: '__kbnjoin__count__6666666666', - origin: FIELD_ORIGIN.JOIN, - }; - - test('Should update data driven styling properties using join fields', async () => { - const layerDescriptor = AbstractLayer.createDescriptor({ - style: styleDescriptor, - joins: [ - { - leftField: 'iso2', - right: { - id: '557d0f15', - indexPatternId: 'myIndexPattern', - indexPatternTitle: 'logs-*', - metrics: [{ type: AGG_TYPE.COUNT }], - term: 'myTermField', - type: SOURCE_TYPES.ES_TERM_SOURCE, - applyGlobalQuery: true, - applyGlobalTime: true, - applyForceRefresh: true, - }, - }, - ], - }); - const layer = new MockLayer({ - layerDescriptor, - source: new MockSource() as unknown as ISource, - }); - const clonedDescriptor = await layer.cloneDescriptor(); - const clonedStyleProps = (clonedDescriptor.style as VectorStyleDescriptor).properties; - // Should update style field belonging to join - // @ts-expect-error - expect(clonedStyleProps[VECTOR_STYLES.FILL_COLOR].options.field.name).toEqual( - '__kbnjoin__count__12345' - ); - // Should not update style field belonging to source - // @ts-expect-error - expect(clonedStyleProps[VECTOR_STYLES.LINE_COLOR].options.field.name).toEqual('bytes'); - // Should not update style feild belonging to different join - // @ts-expect-error - expect(clonedStyleProps[VECTOR_STYLES.LABEL_BORDER_COLOR].options.field.name).toEqual( - '__kbnjoin__count__6666666666' - ); - }); - - test('Should update data driven styling properties using join fields when metrics are not provided', async () => { - const layerDescriptor = AbstractLayer.createDescriptor({ - style: styleDescriptor, - joins: [ - { - leftField: 'iso2', - right: { - id: '557d0f15', - indexPatternId: 'myIndexPattern', - indexPatternTitle: 'logs-*', - term: 'myTermField', - type: 'joinSource', - } as unknown as ESTermSourceDescriptor, - }, - ], - }); - const layer = new MockLayer({ - layerDescriptor, - source: new MockSource() as unknown as ISource, - }); - const clonedDescriptor = await layer.cloneDescriptor(); - const clonedStyleProps = (clonedDescriptor.style as VectorStyleDescriptor).properties; - // Should update style field belonging to join - // @ts-expect-error - expect(clonedStyleProps[VECTOR_STYLES.FILL_COLOR].options.field.name).toEqual( - '__kbnjoin__count__12345' - ); - }); - }); -}); - describe('isFittable', () => { [ { diff --git a/x-pack/plugins/maps/public/classes/layers/layer.tsx b/x-pack/plugins/maps/public/classes/layers/layer.tsx index 0700e54a3fe87..e1043a33f28ad 100644 --- a/x-pack/plugins/maps/public/classes/layers/layer.tsx +++ b/x-pack/plugins/maps/public/classes/layers/layer.tsx @@ -16,23 +16,16 @@ import uuid from 'uuid/v4'; import { FeatureCollection } from 'geojson'; import { DataRequest } from '../util/data_request'; import { - AGG_TYPE, - FIELD_ORIGIN, LAYER_TYPE, MAX_ZOOM, MB_SOURCE_ID_LAYER_ID_PREFIX_DELIMITER, MIN_ZOOM, SOURCE_BOUNDS_DATA_REQUEST_ID, SOURCE_DATA_REQUEST_ID, - SOURCE_TYPES, - STYLE_TYPE, } from '../../../common/constants'; import { copyPersistentState } from '../../reducers/copy_persistent_state'; import { - AggDescriptor, Attribution, - ESTermSourceDescriptor, - JoinDescriptor, LayerDescriptor, MapExtent, StyleDescriptor, @@ -42,7 +35,6 @@ import { import { ImmutableSourceProperty, ISource, SourceEditorArgs } from '../sources/source'; import { DataRequestContext } from '../../actions'; import { IStyle } from '../styles/style'; -import { getJoinAggKey } from '../../../common/get_agg_key'; import { LICENSED_FEATURES } from '../../licensed_features'; import { IESSource } from '../sources/es_source'; @@ -97,8 +89,6 @@ export interface ILayer { isPreviewLayer: () => boolean; areLabelsOnTop: () => boolean; supportsLabelsOnTop: () => boolean; - showJoinEditor(): boolean; - getJoinsDisabledReason(): string | null; isFittable(): Promise; isIncludeInFitToBounds(): boolean; getLicensedFeatures(): Promise; @@ -177,56 +167,6 @@ export class AbstractLayer implements ILayer { const displayName = await this.getDisplayName(); clonedDescriptor.label = `Clone of ${displayName}`; clonedDescriptor.sourceDescriptor = this.getSource().cloneDescriptor(); - - if (clonedDescriptor.joins) { - clonedDescriptor.joins.forEach((joinDescriptor: JoinDescriptor) => { - if (joinDescriptor.right && joinDescriptor.right.type === SOURCE_TYPES.TABLE_SOURCE) { - throw new Error( - 'Cannot clone table-source. Should only be used in MapEmbeddable, not in UX' - ); - } - const termSourceDescriptor: ESTermSourceDescriptor = - joinDescriptor.right as ESTermSourceDescriptor; - - // todo: must tie this to generic thing - const originalJoinId = joinDescriptor.right.id!; - - // right.id is uuid used to track requests in inspector - joinDescriptor.right.id = uuid(); - - // Update all data driven styling properties using join fields - if (clonedDescriptor.style && 'properties' in clonedDescriptor.style) { - const metrics = - termSourceDescriptor.metrics && termSourceDescriptor.metrics.length - ? termSourceDescriptor.metrics - : [{ type: AGG_TYPE.COUNT }]; - metrics.forEach((metricsDescriptor: AggDescriptor) => { - const originalJoinKey = getJoinAggKey({ - aggType: metricsDescriptor.type, - aggFieldName: 'field' in metricsDescriptor ? metricsDescriptor.field : '', - rightSourceId: originalJoinId, - }); - const newJoinKey = getJoinAggKey({ - aggType: metricsDescriptor.type, - aggFieldName: 'field' in metricsDescriptor ? metricsDescriptor.field : '', - rightSourceId: joinDescriptor.right.id!, - }); - - Object.keys(clonedDescriptor.style.properties).forEach((key) => { - const styleProp = clonedDescriptor.style.properties[key]; - if ( - styleProp.type === STYLE_TYPE.DYNAMIC && - styleProp.options.field && - styleProp.options.field.origin === FIELD_ORIGIN.JOIN && - styleProp.options.field.name === originalJoinKey - ) { - styleProp.options.field.name = newJoinKey; - } - }); - }); - } - }); - } return clonedDescriptor; } @@ -234,14 +174,6 @@ export class AbstractLayer implements ILayer { return `${this.getId()}${MB_SOURCE_ID_LAYER_ID_PREFIX_DELIMITER}${layerNameSuffix}`; } - showJoinEditor(): boolean { - return this.getSource().showJoinEditor(); - } - - getJoinsDisabledReason() { - return this.getSource().getJoinsDisabledReason(); - } - isPreviewLayer(): boolean { return !!this._descriptor.__isPreviewLayer; } 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 index 30ec789cf8106..9b5298685865a 100644 --- 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 @@ -48,7 +48,7 @@ export class TiledVectorLayer extends VectorLayer { mapColors?: string[] ): VectorLayerDescriptor { const layerDescriptor = super.createDescriptor(descriptor, mapColors); - layerDescriptor.type = TiledVectorLayer.type; + layerDescriptor.type = LAYER_TYPE.TILED_VECTOR; if (!layerDescriptor.style) { const styleProperties = VectorStyle.createDefaultStyleProperties(mapColors ? mapColors : []); 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 3c8449c5aa4ae..cb964f77613da 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 @@ -7,6 +7,7 @@ export { addGeoJsonMbSource, getVectorSourceBounds, syncVectorSource } from './utils'; export { + isVectorLayer, IVectorLayer, VectorLayer, VectorLayerArguments, 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 new file mode 100644 index 0000000000000..618be0b21cd73 --- /dev/null +++ b/x-pack/plugins/maps/public/classes/layers/vector_layer/vector_layer.test.tsx @@ -0,0 +1,136 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +/* eslint-disable max-classes-per-file */ + +jest.mock('../../styles/vector/vector_style', () => ({ + VectorStyle: class MockVectorStyle {}, +})); + +jest.mock('uuid/v4', () => { + return function () { + return '12345'; + }; +}); + +import { + AGG_TYPE, + FIELD_ORIGIN, + LAYER_STYLE_TYPE, + SOURCE_TYPES, + VECTOR_STYLES, +} from '../../../../common/constants'; +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'; + +class MockSource { + cloneDescriptor() { + return {}; + } + + getDisplayName() { + return 'mySource'; + } +} + +describe('cloneDescriptor', () => { + describe('with joins', () => { + const styleDescriptor = { + type: LAYER_STYLE_TYPE.VECTOR, + properties: { + ...getDefaultDynamicProperties(), + }, + } as VectorStyleDescriptor; + // @ts-expect-error + styleDescriptor.properties[VECTOR_STYLES.FILL_COLOR].options.field = { + name: '__kbnjoin__count__557d0f15', + origin: FIELD_ORIGIN.JOIN, + }; + // @ts-expect-error + styleDescriptor.properties[VECTOR_STYLES.LINE_COLOR].options.field = { + name: 'bytes', + origin: FIELD_ORIGIN.SOURCE, + }; + // @ts-expect-error + styleDescriptor.properties[VECTOR_STYLES.LABEL_BORDER_COLOR].options.field = { + name: '__kbnjoin__count__6666666666', + origin: FIELD_ORIGIN.JOIN, + }; + + test('Should update data driven styling properties using join fields', async () => { + const layerDescriptor = VectorLayer.createDescriptor({ + style: styleDescriptor, + joins: [ + { + leftField: 'iso2', + right: { + id: '557d0f15', + indexPatternId: 'myIndexPattern', + indexPatternTitle: 'logs-*', + metrics: [{ type: AGG_TYPE.COUNT }], + term: 'myTermField', + type: SOURCE_TYPES.ES_TERM_SOURCE, + applyGlobalQuery: true, + applyGlobalTime: true, + applyForceRefresh: true, + }, + }, + ], + }); + const layer = new VectorLayer({ + layerDescriptor, + source: new MockSource() as unknown as IVectorSource, + }); + const clonedDescriptor = await layer.cloneDescriptor(); + const clonedStyleProps = (clonedDescriptor.style as VectorStyleDescriptor).properties; + // Should update style field belonging to join + // @ts-expect-error + expect(clonedStyleProps[VECTOR_STYLES.FILL_COLOR].options.field.name).toEqual( + '__kbnjoin__count__12345' + ); + // Should not update style field belonging to source + // @ts-expect-error + expect(clonedStyleProps[VECTOR_STYLES.LINE_COLOR].options.field.name).toEqual('bytes'); + // Should not update style feild belonging to different join + // @ts-expect-error + expect(clonedStyleProps[VECTOR_STYLES.LABEL_BORDER_COLOR].options.field.name).toEqual( + '__kbnjoin__count__6666666666' + ); + }); + + test('Should update data driven styling properties using join fields when metrics are not provided', async () => { + const layerDescriptor = VectorLayer.createDescriptor({ + style: styleDescriptor, + joins: [ + { + leftField: 'iso2', + right: { + id: '557d0f15', + indexPatternId: 'myIndexPattern', + indexPatternTitle: 'logs-*', + term: 'myTermField', + type: 'joinSource', + } as unknown as ESTermSourceDescriptor, + }, + ], + }); + const layer = new VectorLayer({ + layerDescriptor, + source: new MockSource() as unknown as IVectorSource, + }); + const clonedDescriptor = await layer.cloneDescriptor(); + const clonedStyleProps = (clonedDescriptor.style as VectorStyleDescriptor).properties; + // Should update style field belonging to join + // @ts-expect-error + expect(clonedStyleProps[VECTOR_STYLES.FILL_COLOR].options.field.name).toEqual( + '__kbnjoin__count__12345' + ); + }); + }); +}); 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 fb310496ac9ed..3faf92715451c 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 @@ -6,6 +6,7 @@ */ import React from 'react'; +import uuid from 'uuid/v4'; import type { Map as MbMap, AnyLayer as MbLayer, @@ -19,6 +20,7 @@ import { i18n } from '@kbn/i18n'; import { AbstractLayer } from '../layer'; import { IVectorStyle, VectorStyle } from '../../styles/vector/vector_style'; import { + AGG_TYPE, FEATURE_ID_PROPERTY_NAME, SOURCE_META_DATA_REQUEST_ID, SOURCE_FORMATTERS_DATA_REQUEST_ID, @@ -29,8 +31,11 @@ import { FIELD_ORIGIN, KBN_TOO_MANY_FEATURES_IMAGE_ID, FieldFormatter, + SOURCE_TYPES, + STYLE_TYPE, SUPPORTS_FEATURE_EDITING_REQUEST_ID, KBN_IS_TILE_COMPLETE, + VECTOR_STYLES, } from '../../../../common/constants'; import { JoinTooltipProperty } from '../../tooltips/join_tooltip_property'; import { DataRequestAbortError } from '../../util/data_request'; @@ -48,8 +53,11 @@ import { TimesliceMaskConfig, } from '../../util/mb_filter_expressions'; import { + AggDescriptor, DynamicStylePropertyOptions, DataFilters, + ESTermSourceDescriptor, + JoinDescriptor, StyleMetaDescriptor, Timeslice, VectorLayerDescriptor, @@ -71,6 +79,11 @@ 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'; + +export function isVectorLayer(layer: ILayer) { + return (layer as IVectorLayer).canShowTooltip !== undefined; +} export interface VectorLayerArguments { source: IVectorSource; @@ -83,11 +96,13 @@ export interface IVectorLayer extends ILayer { getFields(): Promise; getStyleEditorFields(): Promise; getJoins(): InnerJoin[]; + getJoinsDisabledReason(): string | null; getValidJoins(): InnerJoin[]; getSource(): IVectorSource; getFeatureById(id: string | number): Feature | null; getPropertiesForTooltip(properties: GeoJsonProperties): Promise; hasJoins(): boolean; + showJoinEditor(): boolean; canShowTooltip(): boolean; supportsFeatureEditing(): boolean; getLeftJoinFields(): Promise; @@ -113,8 +128,8 @@ export class VectorLayer extends AbstractLayer implements IVectorLayer { options: Partial, mapColors?: string[] ): VectorLayerDescriptor { - const layerDescriptor = super.createDescriptor(options); - layerDescriptor.type = VectorLayer.type; + const layerDescriptor = super.createDescriptor(options) as VectorLayerDescriptor; + layerDescriptor.type = LAYER_TYPE.VECTOR; if (!options.style) { const styleProperties = VectorStyle.createDefaultStyleProperties(mapColors ? mapColors : []); @@ -125,7 +140,7 @@ export class VectorLayer extends AbstractLayer implements IVectorLayer { layerDescriptor.joins = []; } - return layerDescriptor as VectorLayerDescriptor; + return layerDescriptor; } constructor({ @@ -147,6 +162,62 @@ export class VectorLayer extends AbstractLayer implements IVectorLayer { ); } + async cloneDescriptor(): Promise { + const clonedDescriptor = (await super.cloneDescriptor()) as VectorLayerDescriptor; + if (clonedDescriptor.joins) { + clonedDescriptor.joins.forEach((joinDescriptor: JoinDescriptor) => { + if (joinDescriptor.right && joinDescriptor.right.type === SOURCE_TYPES.TABLE_SOURCE) { + throw new Error( + 'Cannot clone table-source. Should only be used in MapEmbeddable, not in UX' + ); + } + const termSourceDescriptor: ESTermSourceDescriptor = + joinDescriptor.right as ESTermSourceDescriptor; + + // todo: must tie this to generic thing + const originalJoinId = joinDescriptor.right.id!; + + // right.id is uuid used to track requests in inspector + joinDescriptor.right.id = uuid(); + + // Update all data driven styling properties using join fields + if (clonedDescriptor.style && 'properties' in clonedDescriptor.style) { + const metrics = + termSourceDescriptor.metrics && termSourceDescriptor.metrics.length + ? termSourceDescriptor.metrics + : [{ type: AGG_TYPE.COUNT }]; + metrics.forEach((metricsDescriptor: AggDescriptor) => { + const originalJoinKey = getJoinAggKey({ + aggType: metricsDescriptor.type, + aggFieldName: 'field' in metricsDescriptor ? metricsDescriptor.field : '', + rightSourceId: originalJoinId, + }); + const newJoinKey = getJoinAggKey({ + aggType: metricsDescriptor.type, + aggFieldName: 'field' in metricsDescriptor ? metricsDescriptor.field : '', + rightSourceId: joinDescriptor.right.id!, + }); + + Object.keys(clonedDescriptor.style.properties).forEach((key) => { + const styleProp = clonedDescriptor.style.properties[key as VECTOR_STYLES]; + if ('type' in styleProp && styleProp.type === STYLE_TYPE.DYNAMIC) { + const options = styleProp.options as DynamicStylePropertyOptions; + if ( + options.field && + options.field.origin === FIELD_ORIGIN.JOIN && + options.field.name === originalJoinKey + ) { + options.field.name = newJoinKey; + } + } + }); + }); + } + }); + } + return clonedDescriptor; + } + getSource(): IVectorSource { return super.getSource() as IVectorSource; } @@ -176,6 +247,10 @@ export class VectorLayer extends AbstractLayer implements IVectorLayer { return this._joins.slice(); } + getJoinsDisabledReason() { + return this.getSource().getJoinsDisabledReason(); + } + getValidJoins() { return this.getJoins().filter((join) => { return join.hasCompleteConfig(); @@ -192,6 +267,10 @@ export class VectorLayer extends AbstractLayer implements IVectorLayer { return this.getValidJoins().length > 0; } + showJoinEditor(): boolean { + return this.getSource().showJoinEditor(); + } + isInitialDataLoadComplete() { const sourceDataRequest = this.getSourceDataRequest(); if (!sourceDataRequest || !sourceDataRequest.hasData()) { diff --git a/x-pack/plugins/maps/public/classes/sources/mvt_single_layer_vector_source/mvt_single_layer_vector_source.tsx b/x-pack/plugins/maps/public/classes/sources/mvt_single_layer_vector_source/mvt_single_layer_vector_source.tsx index a955cb336e55e..34a30ae9ec977 100644 --- a/x-pack/plugins/maps/public/classes/sources/mvt_single_layer_vector_source/mvt_single_layer_vector_source.tsx +++ b/x-pack/plugins/maps/public/classes/sources/mvt_single_layer_vector_source/mvt_single_layer_vector_source.tsx @@ -243,6 +243,14 @@ export class MVTSingleLayerVectorSource async getDefaultFields(): Promise>> { return {}; } + + showJoinEditor(): boolean { + return false; + } + + getJoinsDisabledReason(): string | null { + return null; + } } registerSource({ diff --git a/x-pack/plugins/maps/public/classes/sources/source.ts b/x-pack/plugins/maps/public/classes/sources/source.ts index 5b2fc16d18b41..4c2cffcf8b070 100644 --- a/x-pack/plugins/maps/public/classes/sources/source.ts +++ b/x-pack/plugins/maps/public/classes/sources/source.ts @@ -53,8 +53,6 @@ export interface ISource { isESSource(): boolean; renderSourceSettingsEditor(sourceEditorArgs: SourceEditorArgs): ReactElement | null; supportsFitToBounds(): Promise; - showJoinEditor(): boolean; - getJoinsDisabledReason(): string | null; cloneDescriptor(): AbstractSourceDescriptor; getFieldNames(): string[]; getApplyGlobalQuery(): boolean; @@ -155,14 +153,6 @@ export class AbstractSource implements ISource { return 0; } - showJoinEditor(): boolean { - return false; - } - - getJoinsDisabledReason(): string | null { - return null; - } - isESSource(): boolean { return false; } diff --git a/x-pack/plugins/maps/public/classes/sources/vector_source/vector_source.tsx b/x-pack/plugins/maps/public/classes/sources/vector_source/vector_source.tsx index bc3807a8247b1..3c0adf64216e6 100644 --- a/x-pack/plugins/maps/public/classes/sources/vector_source/vector_source.tsx +++ b/x-pack/plugins/maps/public/classes/sources/vector_source/vector_source.tsx @@ -59,6 +59,8 @@ export interface IVectorSource extends ISource { getFields(): Promise; getFieldByName(fieldName: string): IField | null; getLeftJoinFields(): Promise; + showJoinEditor(): boolean; + getJoinsDisabledReason(): string | null; /* * Vector layer avoids unnecessarily re-fetching source data. @@ -122,6 +124,10 @@ export class AbstractVectorSource extends AbstractSource implements IVectorSourc return []; } + getJoinsDisabledReason(): string | null { + return null; + } + async getGeoJsonWithMeta( layerName: string, searchFilters: VectorSourceRequestMeta, diff --git a/x-pack/plugins/maps/public/connected_components/edit_layer_panel/__snapshots__/edit_layer_panel.test.tsx.snap b/x-pack/plugins/maps/public/connected_components/edit_layer_panel/__snapshots__/edit_layer_panel.test.tsx.snap index 24f15674a0504..5fb1cc6f72585 100644 --- a/x-pack/plugins/maps/public/connected_components/edit_layer_panel/__snapshots__/edit_layer_panel.test.tsx.snap +++ b/x-pack/plugins/maps/public/connected_components/edit_layer_panel/__snapshots__/edit_layer_panel.test.tsx.snap @@ -93,6 +93,7 @@ exports[`EditLayerPanel is rendered 1`] = `
    mockSourceSettings @@ -112,6 +114,7 @@ exports[`EditLayerPanel is rendered 1`] = ` { return true; }, + canShowTooltip: () => { + return true; + }, supportsElasticsearchFilters: () => { return false; }, @@ -76,6 +79,9 @@ const mockLayer = { hasErrors: () => { return false; }, + supportsFitToBounds: () => { + return true; + }, } as unknown as ILayer; const defaultProps = { diff --git a/x-pack/plugins/maps/public/connected_components/edit_layer_panel/edit_layer_panel.tsx b/x-pack/plugins/maps/public/connected_components/edit_layer_panel/edit_layer_panel.tsx index da72969684216..424c4b8e16bec 100644 --- a/x-pack/plugins/maps/public/connected_components/edit_layer_panel/edit_layer_panel.tsx +++ b/x-pack/plugins/maps/public/connected_components/edit_layer_panel/edit_layer_panel.tsx @@ -33,7 +33,7 @@ import { Storage } from '../../../../../../src/plugins/kibana_utils/public'; import { LAYER_TYPE } from '../../../common/constants'; import { getData, getCore } from '../../kibana_services'; import { ILayer } from '../../classes/layers/layer'; -import { IVectorLayer } from '../../classes/layers/vector_layer'; +import { isVectorLayer, IVectorLayer } from '../../classes/layers/vector_layer'; import { ImmutableSourceProperty, OnSourceChangeArgs } from '../../classes/sources/source'; import { IField } from '../../classes/fields/field'; @@ -111,11 +111,12 @@ export class EditLayerPanel extends Component { }; async _loadLeftJoinFields() { - if ( - !this.props.selectedLayer || - !this.props.selectedLayer.showJoinEditor() || - (this.props.selectedLayer as IVectorLayer).getLeftJoinFields === undefined - ) { + if (!this.props.selectedLayer || !isVectorLayer(this.props.selectedLayer)) { + return; + } + + const vectorLayer = this.props.selectedLayer as IVectorLayer; + if (!vectorLayer.showJoinEditor() || vectorLayer.getLeftJoinFields === undefined) { return; } @@ -182,7 +183,11 @@ export class EditLayerPanel extends Component { } _renderJoinSection() { - if (!this.props.selectedLayer || !this.props.selectedLayer.showJoinEditor()) { + if (!this.props.selectedLayer || !isVectorLayer(this.props.selectedLayer)) { + return; + } + const vectorLayer = this.props.selectedLayer as IVectorLayer; + if (!vectorLayer.showJoinEditor()) { return null; } @@ -190,7 +195,7 @@ export class EditLayerPanel extends Component { diff --git a/x-pack/plugins/maps/public/connected_components/edit_layer_panel/index.ts b/x-pack/plugins/maps/public/connected_components/edit_layer_panel/index.ts index b78ffb3874e30..84caa45741a62 100644 --- a/x-pack/plugins/maps/public/connected_components/edit_layer_panel/index.ts +++ b/x-pack/plugins/maps/public/connected_components/edit_layer_panel/index.ts @@ -13,11 +13,18 @@ import { LAYER_TYPE } from '../../../common/constants'; import { getSelectedLayer } from '../../selectors/map_selectors'; import { updateSourceProp } from '../../actions'; import { MapStoreState } from '../../reducers/store'; +import { isVectorLayer, IVectorLayer } from '../../classes/layers/vector_layer'; function mapStateToProps(state: MapStoreState) { const selectedLayer = getSelectedLayer(state); + let key = 'none'; + if (selectedLayer) { + key = isVectorLayer(selectedLayer) + ? `${selectedLayer.getId()}${(selectedLayer as IVectorLayer).showJoinEditor()}` + : selectedLayer.getId(); + } return { - key: selectedLayer ? `${selectedLayer.getId()}${selectedLayer.showJoinEditor()}` : '', + key, selectedLayer, }; } diff --git a/x-pack/plugins/maps/public/connected_components/edit_layer_panel/join_editor/join_editor.test.tsx b/x-pack/plugins/maps/public/connected_components/edit_layer_panel/join_editor/join_editor.test.tsx index 27a345cdf2dda..6da05ef0b4092 100644 --- a/x-pack/plugins/maps/public/connected_components/edit_layer_panel/join_editor/join_editor.test.tsx +++ b/x-pack/plugins/maps/public/connected_components/edit_layer_panel/join_editor/join_editor.test.tsx @@ -6,7 +6,7 @@ */ import React from 'react'; -import { ILayer } from '../../../classes/layers/layer'; +import { IVectorLayer } from '../../../classes/layers/vector_layer'; import { JoinEditor } from './join_editor'; import { shallow } from 'enzyme'; import { JoinDescriptor } from '../../../../common/descriptor_types'; @@ -48,7 +48,7 @@ const defaultProps = { test('Should render join editor', () => { const component = shallow( - + ); expect(component).toMatchSnapshot(); }); @@ -57,7 +57,7 @@ test('Should render callout when joins are disabled', () => { const component = shallow( ); expect(component).toMatchSnapshot(); diff --git a/x-pack/plugins/maps/public/connected_components/edit_layer_panel/join_editor/join_editor.tsx b/x-pack/plugins/maps/public/connected_components/edit_layer_panel/join_editor/join_editor.tsx index e0d630994566d..e99ec6a688092 100644 --- a/x-pack/plugins/maps/public/connected_components/edit_layer_panel/join_editor/join_editor.tsx +++ b/x-pack/plugins/maps/public/connected_components/edit_layer_panel/join_editor/join_editor.tsx @@ -20,7 +20,7 @@ import { import { FormattedMessage } from '@kbn/i18n/react'; import { i18n } from '@kbn/i18n'; import { Join } from './resources/join'; -import { ILayer } from '../../../classes/layers/layer'; +import { IVectorLayer } from '../../../classes/layers/vector_layer'; import { JoinDescriptor } from '../../../../common/descriptor_types'; import { SOURCE_TYPES } from '../../../../common/constants'; @@ -31,10 +31,10 @@ export interface JoinField { export interface Props { joins: JoinDescriptor[]; - layer: ILayer; + layer: IVectorLayer; layerDisplayName: string; leftJoinFields: JoinField[]; - onChange: (layer: ILayer, joins: JoinDescriptor[]) => void; + onChange: (layer: IVectorLayer, joins: JoinDescriptor[]) => void; } export function JoinEditor({ joins, layer, onChange, leftJoinFields, layerDisplayName }: Props) { diff --git a/x-pack/plugins/maps/public/connected_components/mb_map/tooltip_control/tooltip_control.tsx b/x-pack/plugins/maps/public/connected_components/mb_map/tooltip_control/tooltip_control.tsx index 3567501455734..c2ad75d9cb335 100644 --- a/x-pack/plugins/maps/public/connected_components/mb_map/tooltip_control/tooltip_control.tsx +++ b/x-pack/plugins/maps/public/connected_components/mb_map/tooltip_control/tooltip_control.tsx @@ -35,7 +35,7 @@ import { TooltipPopover } from './tooltip_popover'; import { FeatureGeometryFilterForm } from './features_tooltip'; import { EXCLUDE_TOO_MANY_FEATURES_BOX } from '../../../classes/util/mb_filter_expressions'; import { ILayer } from '../../../classes/layers/layer'; -import { IVectorLayer } from '../../../classes/layers/vector_layer'; +import { IVectorLayer, isVectorLayer } from '../../../classes/layers/vector_layer'; import { RenderToolTipContent } from '../../../classes/tooltips/tooltip_property'; function justifyAnchorLocation( @@ -58,10 +58,6 @@ function justifyAnchorLocation( return popupAnchorLocation; } -function isVectorLayer(layer: ILayer) { - return (layer as IVectorLayer).canShowTooltip !== undefined; -} - export interface Props { addFilters: ((filters: Filter[], actionId: string) => Promise) | null; closeOnClickTooltip: (tooltipId: string) => void; diff --git a/x-pack/plugins/maps/public/connected_components/right_side_controls/layer_control/layer_toc/toc_entry/__snapshots__/toc_entry.test.tsx.snap b/x-pack/plugins/maps/public/connected_components/right_side_controls/layer_control/layer_toc/toc_entry/__snapshots__/toc_entry.test.tsx.snap index 42618d099ffcf..5beaf12029d2f 100644 --- a/x-pack/plugins/maps/public/connected_components/right_side_controls/layer_control/layer_toc/toc_entry/__snapshots__/toc_entry.test.tsx.snap +++ b/x-pack/plugins/maps/public/connected_components/right_side_controls/layer_control/layer_toc/toc_entry/__snapshots__/toc_entry.test.tsx.snap @@ -24,10 +24,11 @@ exports[`TOCEntry is rendered 1`] = ` "isVisible": [Function], "renderLegendDetails": [Function], "showAtZoomLevel": [Function], + "supportsFitToBounds": [Function], } } openLayerSettings={[Function]} - supportsFitToBounds={false} + supportsFitToBounds={true} />
    +
    +
    +
    +
    + { return true; }, + supportsFitToBounds: () => { + return true; + }, } as unknown as ILayer; const defaultProps = { diff --git a/x-pack/plugins/maps/public/index.ts b/x-pack/plugins/maps/public/index.ts index 29cc20c706296..19850e004e6b8 100644 --- a/x-pack/plugins/maps/public/index.ts +++ b/x-pack/plugins/maps/public/index.ts @@ -18,11 +18,26 @@ export const plugin: PluginInitializer = ( }; export { MAP_SAVED_OBJECT_TYPE } from '../common/constants'; +export type { PreIndexedShape } from '../common/elasticsearch_util'; -export type { RenderTooltipContentParams } from './classes/tooltips/tooltip_property'; +export type { + ITooltipProperty, + RenderTooltipContentParams, +} from './classes/tooltips/tooltip_property'; -export { MapsStartApi } from './api'; +export type { MapsSetupApi, MapsStartApi } from './api'; export type { MapEmbeddable, MapEmbeddableInput, MapEmbeddableOutput } from './embeddable'; export type { EMSTermJoinConfig, SampleValuesConfig } from './ems_autosuggest'; + +export type { IVectorSource, GeoJsonWithMeta } from './classes/sources/vector_source/vector_source'; +export type { ImmutableSourceProperty, SourceEditorArgs } from './classes/sources/source'; +export type { Attribution } from '../common/descriptor_types'; +export type { + BoundsRequestMeta, + SourceTooltipConfig, +} from './classes/sources/vector_source/vector_source'; +export type { IField } from './classes/fields/field'; +export type { LayerWizard, RenderWizardArguments } from './classes/layers/layer_wizard_registry'; +export type { DataRequest } from './classes/util/data_request'; diff --git a/x-pack/plugins/maps/public/plugin.ts b/x-pack/plugins/maps/public/plugin.ts index 1b71c004edf5e..cef40f11b4443 100644 --- a/x-pack/plugins/maps/public/plugin.ts +++ b/x-pack/plugins/maps/public/plugin.ts @@ -50,6 +50,7 @@ import { createLayerDescriptors, registerLayerWizard, registerSource, + MapsSetupApi, MapsStartApi, suggestEMSTermJoinConfig, } from './api'; @@ -128,7 +129,7 @@ export class MapsPlugin this._initializerContext = initializerContext; } - public setup(core: CoreSetup, plugins: MapsPluginSetupDependencies) { + public setup(core: CoreSetup, plugins: MapsPluginSetupDependencies): MapsSetupApi { registerLicensedFeatures(plugins.licensing); const config = this._initializerContext.config.get(); @@ -178,6 +179,11 @@ export class MapsPlugin return renderApp(params, UsageTracker); }, }); + + return { + registerLayerWizard, + registerSource, + }; } public start(core: CoreStart, plugins: MapsPluginStartDependencies): MapsStartApi { @@ -195,8 +201,6 @@ export class MapsPlugin return { createLayerDescriptors, - registerLayerWizard, - registerSource, suggestEMSTermJoinConfig, }; } diff --git a/x-pack/plugins/maps/server/index.ts b/x-pack/plugins/maps/server/index.ts index a884b2354b583..291bb28eed446 100644 --- a/x-pack/plugins/maps/server/index.ts +++ b/x-pack/plugins/maps/server/index.ts @@ -23,7 +23,8 @@ export const config: PluginConfigDescriptor = { preserveDrawingBuffer: true, }, schema: configSchema, - deprecations: () => [ + deprecations: ({ deprecate }) => [ + deprecate('enabled', '8.0.0'), ( completeConfig: Record, rootPath: string, diff --git a/x-pack/plugins/maps/server/maps_telemetry/util.ts b/x-pack/plugins/maps/server/maps_telemetry/util.ts index 792c3d7b4f8ae..6b9b455904e40 100644 --- a/x-pack/plugins/maps/server/maps_telemetry/util.ts +++ b/x-pack/plugins/maps/server/maps_telemetry/util.ts @@ -10,6 +10,7 @@ import { ESGeoGridSourceDescriptor, ESSearchSourceDescriptor, LayerDescriptor, + VectorLayerDescriptor, } from '../../common/descriptor_types'; import { GRID_RESOLUTION, @@ -270,8 +271,7 @@ export function getTermJoinsPerCluster( ): TELEMETRY_TERM_JOIN_COUNTS_PER_CLUSTER { return getCountsByCluster(layerLists, (layerDescriptor: LayerDescriptor) => { return layerDescriptor.type === LAYER_TYPE.VECTOR && - layerDescriptor.joins && - layerDescriptor.joins.length + (layerDescriptor as VectorLayerDescriptor)?.joins?.length ? TELEMETRY_TERM_JOIN : null; }); diff --git a/x-pack/plugins/metrics_entities/server/index.ts b/x-pack/plugins/metrics_entities/server/index.ts index b4d35eb90f486..c8f9d81347bdb 100644 --- a/x-pack/plugins/metrics_entities/server/index.ts +++ b/x-pack/plugins/metrics_entities/server/index.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { PluginInitializerContext } from '../../../../src/core/server'; +import { PluginConfigDescriptor, PluginInitializerContext } from '../../../../src/core/server'; import { ConfigSchema } from './config'; import { MetricsEntitiesPlugin } from './plugin'; @@ -13,7 +13,10 @@ import { MetricsEntitiesPlugin } from './plugin'; // This exports static code and TypeScript types, // as well as, Kibana Platform `plugin()` initializer. -export const config = { schema: ConfigSchema }; +export const config: PluginConfigDescriptor = { + deprecations: ({ deprecate }) => [deprecate('enabled', '8.0.0')], + schema: ConfigSchema, +}; export const plugin = (initializerContext: PluginInitializerContext): MetricsEntitiesPlugin => { return new MetricsEntitiesPlugin(initializerContext); }; diff --git a/x-pack/plugins/ml/public/application/components/validate_job/validate_job_view.test.js b/x-pack/plugins/ml/public/application/components/validate_job/validate_job_view.test.js index 067b03d938ee8..8ec83d8679e87 100644 --- a/x-pack/plugins/ml/public/application/components/validate_job/validate_job_view.test.js +++ b/x-pack/plugins/ml/public/application/components/validate_job/validate_job_view.test.js @@ -41,7 +41,7 @@ function prepareTest(messages) { }; const kibana = { services: { - notifications: { toasts: { addDanger: jest.fn() } }, + notifications: { toasts: { addDanger: jest.fn(), addError: jest.fn() } }, }, }; diff --git a/x-pack/plugins/ml/public/application/explorer/anomalies_map.tsx b/x-pack/plugins/ml/public/application/explorer/anomalies_map.tsx index 182a8a37fadfc..28f346c0148c6 100644 --- a/x-pack/plugins/ml/public/application/explorer/anomalies_map.tsx +++ b/x-pack/plugins/ml/public/application/explorer/anomalies_map.tsx @@ -18,6 +18,7 @@ import { } from '@elastic/eui'; import { FIELD_ORIGIN, + LAYER_TYPE, SOURCE_TYPES, STYLE_TYPE, COLOR_MAP_TYPE, @@ -125,7 +126,7 @@ export const getChoroplethAnomaliesLayer = ( isTimeAware: true, }, visible: false, - type: 'VECTOR', + type: LAYER_TYPE.VECTOR, }; }; diff --git a/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_charts_container.js b/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_charts_container.js index c714b388c826f..ddb46edc7b921 100644 --- a/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_charts_container.js +++ b/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_charts_container.js @@ -79,8 +79,12 @@ function ExplorerChartContainer({ let isCancelled = false; const generateLink = async () => { if (!isCancelled && series.functionDescription !== ML_JOB_AGGREGATION.LAT_LONG) { - const singleMetricViewerLink = await getExploreSeriesLink(mlLocator, series, timefilter); - setExplorerSeriesLink(singleMetricViewerLink); + try { + const singleMetricViewerLink = await getExploreSeriesLink(mlLocator, series, timefilter); + setExplorerSeriesLink(singleMetricViewerLink); + } catch (error) { + setExplorerSeriesLink(''); + } } }; generateLink(); diff --git a/x-pack/plugins/ml/server/models/bucket_span_estimator/bucket_span_estimator.test.ts b/x-pack/plugins/ml/server/models/bucket_span_estimator/bucket_span_estimator.test.ts index fbe0ff650cc2d..6ffb74131bf6e 100644 --- a/x-pack/plugins/ml/server/models/bucket_span_estimator/bucket_span_estimator.test.ts +++ b/x-pack/plugins/ml/server/models/bucket_span_estimator/bucket_span_estimator.test.ts @@ -12,6 +12,10 @@ import { BucketSpanEstimatorData } from '../../../common/types/job_service'; import { estimateBucketSpanFactory } from './bucket_span_estimator'; +jest.mock('../../lib/log', () => ({ + mlLog: { warn: jest.fn() }, +})); + const callAs = { search: () => Promise.resolve({ body: {} }), cluster: { getSettings: () => Promise.resolve({ body: {} }) }, diff --git a/x-pack/plugins/monitoring/server/lib/ccs_utils.test.js b/x-pack/plugins/monitoring/common/ccs_utils.test.js similarity index 100% rename from x-pack/plugins/monitoring/server/lib/ccs_utils.test.js rename to x-pack/plugins/monitoring/common/ccs_utils.test.js diff --git a/x-pack/plugins/monitoring/server/lib/ccs_utils.ts b/x-pack/plugins/monitoring/common/ccs_utils.ts similarity index 96% rename from x-pack/plugins/monitoring/server/lib/ccs_utils.ts rename to x-pack/plugins/monitoring/common/ccs_utils.ts index 1d899456913b9..7efe6e43ddbbd 100644 --- a/x-pack/plugins/monitoring/server/lib/ccs_utils.ts +++ b/x-pack/plugins/monitoring/common/ccs_utils.ts @@ -6,7 +6,8 @@ */ import { isFunction, get } from 'lodash'; -import type { MonitoringConfig } from '../config'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import type { MonitoringConfig } from '../server/config'; type Config = Partial & { get?: (key: string) => any; diff --git a/x-pack/plugins/monitoring/common/types/alerts.ts b/x-pack/plugins/monitoring/common/types/alerts.ts index 17bbffce19a18..1f68b0c55a046 100644 --- a/x-pack/plugins/monitoring/common/types/alerts.ts +++ b/x-pack/plugins/monitoring/common/types/alerts.ts @@ -48,12 +48,16 @@ export interface CommonAlertParams { duration: string; threshold?: number; limit?: string; + filterQuery?: string; + filterQueryText?: string; [key: string]: unknown; } export interface ThreadPoolRejectionsAlertParams { threshold: number; duration: string; + filterQuery?: string; + filterQueryText?: string; } export interface AlertEnableAction { 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 1de9a175026a6..64eab04cbd5ce 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 @@ -15,6 +15,7 @@ import { RULE_REQUIRES_APP_CONTEXT, } from '../../../common/constants'; import { AlertTypeParams } from '../../../../alerting/common'; +import { MonitoringConfig } from '../../types'; interface ValidateOptions extends AlertTypeParams { duration: string; @@ -36,7 +37,9 @@ const validate = (inputValues: ValidateOptions): ValidationResult => { return validationResult; }; -export function createCCRReadExceptionsAlertType(): AlertTypeModel { +export function createCCRReadExceptionsAlertType( + config: MonitoringConfig +): AlertTypeModel { return { id: RULE_CCR_READ_EXCEPTIONS, description: RULE_DETAILS[RULE_CCR_READ_EXCEPTIONS].description, @@ -45,7 +48,11 @@ export function createCCRReadExceptionsAlertType(): AlertTypeModel ( - + ), validate, defaultActionMessage: '{{context.internalFullMessage}}', diff --git a/x-pack/plugins/monitoring/public/alerts/components/param_details_form/expression.tsx b/x-pack/plugins/monitoring/public/alerts/components/param_details_form/expression.tsx index df17ce1a911a0..827eed955d535 100644 --- a/x-pack/plugins/monitoring/public/alerts/components/param_details_form/expression.tsx +++ b/x-pack/plugins/monitoring/public/alerts/components/param_details_form/expression.tsx @@ -5,14 +5,23 @@ * 2.0. */ -import React, { Fragment } from 'react'; -import { EuiForm, EuiSpacer } from '@elastic/eui'; +import React, { Fragment, useCallback } from 'react'; +import { EuiForm, EuiFormRow, EuiSpacer } from '@elastic/eui'; +import { DataPublicPluginStart } from 'src/plugins/data/public'; +import { debounce } from 'lodash'; +import { i18n } from '@kbn/i18n'; import { CommonAlertParamDetails } from '../../../../common/types/alerts'; import { AlertParamDuration } from '../../flyout_expressions/alert_param_duration'; import { AlertParamType } from '../../../../common/enums'; import { AlertParamPercentage } from '../../flyout_expressions/alert_param_percentage'; import { AlertParamNumber } from '../../flyout_expressions/alert_param_number'; import { AlertParamTextField } from '../../flyout_expressions/alert_param_textfield'; +import { MonitoringConfig } from '../../../types'; +import { useDerivedIndexPattern } from './use_derived_index_pattern'; +import { KueryBar } from '../../../components/kuery_bar'; +import { convertKueryToElasticSearchQuery } from '../../../lib/kuery'; + +const FILTER_TYPING_DEBOUNCE_MS = 500; export interface Props { alertParams: { [property: string]: any }; @@ -20,10 +29,14 @@ export interface Props { setAlertProperty: (property: string, value: any) => void; errors: { [key: string]: string[] }; paramDetails: CommonAlertParamDetails; + data: DataPublicPluginStart; + config?: MonitoringConfig; } export const Expression: React.FC = (props) => { - const { alertParams, paramDetails, setAlertParams, errors } = props; + const { alertParams, paramDetails, setAlertParams, errors, config, data } = props; + + const { derivedIndexPattern } = useDerivedIndexPattern(data, config); const alertParamsUi = Object.keys(paramDetails).map((alertParamName) => { const details = paramDetails[alertParamName]; @@ -77,10 +90,44 @@ export const Expression: React.FC = (props) => { } }); + const onFilterChange = useCallback( + (filter: string) => { + setAlertParams('filterQueryText', filter); + setAlertParams( + 'filterQuery', + convertKueryToElasticSearchQuery(filter, derivedIndexPattern) || '' + ); + }, + [setAlertParams, derivedIndexPattern] + ); + + /* eslint-disable-next-line react-hooks/exhaustive-deps */ + const debouncedOnFilterChange = useCallback(debounce(onFilterChange, FILTER_TYPING_DEBOUNCE_MS), [ + onFilterChange, + ]); + return ( - {alertParamsUi} - + + {alertParamsUi} + + + + + + ); }; diff --git a/x-pack/plugins/monitoring/public/alerts/components/param_details_form/use_derived_index_pattern.tsx b/x-pack/plugins/monitoring/public/alerts/components/param_details_form/use_derived_index_pattern.tsx new file mode 100644 index 0000000000000..1a4d88d690b84 --- /dev/null +++ b/x-pack/plugins/monitoring/public/alerts/components/param_details_form/use_derived_index_pattern.tsx @@ -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 { useEffect, useState } from 'react'; +import { DataPublicPluginStart, IFieldType, IIndexPattern } from 'src/plugins/data/public'; +import { + INDEX_PATTERN_BEATS, + INDEX_PATTERN_ELASTICSEARCH, + INDEX_PATTERN_KIBANA, + INDEX_PATTERN_LOGSTASH, +} from '../../../../common/constants'; +import { prefixIndexPattern } from '../../../../common/ccs_utils'; +import { MonitoringConfig } from '../../../types'; + +const INDEX_PATTERNS = `${INDEX_PATTERN_ELASTICSEARCH},${INDEX_PATTERN_KIBANA},${INDEX_PATTERN_LOGSTASH},${INDEX_PATTERN_BEATS}`; + +export const useDerivedIndexPattern = ( + data: DataPublicPluginStart, + config?: MonitoringConfig +): { loading: boolean; derivedIndexPattern: IIndexPattern } => { + const indexPattern = prefixIndexPattern(config || ({} as MonitoringConfig), INDEX_PATTERNS, '*'); + const [loading, setLoading] = useState(true); + const [fields, setFields] = useState([]); + useEffect(() => { + (async function fetchData() { + const result = await data.indexPatterns.getFieldsForWildcard({ + pattern: indexPattern, + }); + setFields(result); + setLoading(false); + })(); + }, [indexPattern, data.indexPatterns]); + return { + loading, + derivedIndexPattern: { + title: indexPattern, + fields, + }, + }; +}; 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 ec7a583ec2ba1..f0e0a413435f9 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 @@ -11,8 +11,11 @@ import { AlertTypeModel } from '../../../../triggers_actions_ui/public/types'; 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'; -export function createCpuUsageAlertType(): AlertTypeModel { +export function createCpuUsageAlertType( + config: MonitoringConfig +): AlertTypeModel { return { id: RULE_CPU_USAGE, description: RULE_DETAILS[RULE_CPU_USAGE].description, @@ -21,7 +24,11 @@ export function createCpuUsageAlertType(): AlertTypeModel ( - + ), validate, defaultActionMessage: '{{context.internalFullMessage}}', diff --git a/x-pack/plugins/monitoring/public/alerts/disk_usage_alert/index.tsx b/x-pack/plugins/monitoring/public/alerts/disk_usage_alert/index.tsx index 779945da0c9e0..5f9f9536ae567 100644 --- a/x-pack/plugins/monitoring/public/alerts/disk_usage_alert/index.tsx +++ b/x-pack/plugins/monitoring/public/alerts/disk_usage_alert/index.tsx @@ -16,8 +16,11 @@ import { RULE_DETAILS, RULE_REQUIRES_APP_CONTEXT, } from '../../../common/constants'; +import { MonitoringConfig } from '../../types'; -export function createDiskUsageAlertType(): AlertTypeModel { +export function createDiskUsageAlertType( + config: MonitoringConfig +): AlertTypeModel { return { id: RULE_DISK_USAGE, description: RULE_DETAILS[RULE_DISK_USAGE].description, @@ -26,7 +29,11 @@ export function createDiskUsageAlertType(): AlertTypeModel ( - + ), validate, defaultActionMessage: '{{context.internalFullMessage}}', diff --git a/x-pack/plugins/monitoring/public/alerts/large_shard_size_alert/index.tsx b/x-pack/plugins/monitoring/public/alerts/large_shard_size_alert/index.tsx index e0f595abe7602..afaf20d60d882 100644 --- a/x-pack/plugins/monitoring/public/alerts/large_shard_size_alert/index.tsx +++ b/x-pack/plugins/monitoring/public/alerts/large_shard_size_alert/index.tsx @@ -15,6 +15,7 @@ import { RULE_REQUIRES_APP_CONTEXT, } from '../../../common/constants'; import { AlertTypeParams } from '../../../../alerting/common'; +import { MonitoringConfig } from '../../types'; interface ValidateOptions extends AlertTypeParams { indexPattern: string; @@ -36,7 +37,9 @@ const validate = (inputValues: ValidateOptions): ValidationResult => { return validationResult; }; -export function createLargeShardSizeAlertType(): AlertTypeModel { +export function createLargeShardSizeAlertType( + config: MonitoringConfig +): AlertTypeModel { return { id: RULE_LARGE_SHARD_SIZE, description: RULE_DETAILS[RULE_LARGE_SHARD_SIZE].description, @@ -45,7 +48,11 @@ export function createLargeShardSizeAlertType(): AlertTypeModel return `${docLinks.links.monitoring.alertsKibanaLargeShardSize}`; }, alertParamsExpression: (props: Props) => ( - + ), validate, defaultActionMessage: '{{context.internalFullMessage}}', diff --git a/x-pack/plugins/monitoring/public/alerts/legacy_alert/expression.tsx b/x-pack/plugins/monitoring/public/alerts/legacy_alert/expression.tsx new file mode 100644 index 0000000000000..fe6adf66c1d4f --- /dev/null +++ b/x-pack/plugins/monitoring/public/alerts/legacy_alert/expression.tsx @@ -0,0 +1,56 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import React, { useCallback } from 'react'; +import { debounce } from 'lodash'; +import { EuiSpacer, EuiForm, EuiFormRow } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { useDerivedIndexPattern } from '../components/param_details_form/use_derived_index_pattern'; +import { convertKueryToElasticSearchQuery } from '../../lib/kuery'; +import { KueryBar } from '../../components/kuery_bar'; +import { Props } from '../components/param_details_form/expression'; + +const FILTER_TYPING_DEBOUNCE_MS = 500; + +export const Expression = ({ alertParams, config, setAlertParams, data }: Props) => { + const { derivedIndexPattern } = useDerivedIndexPattern(data, config); + const onFilterChange = useCallback( + (filter: string) => { + setAlertParams('filterQueryText', filter); + setAlertParams( + 'filterQuery', + convertKueryToElasticSearchQuery(filter, derivedIndexPattern) || '' + ); + }, + [setAlertParams, derivedIndexPattern] + ); + + /* eslint-disable-next-line react-hooks/exhaustive-deps */ + const debouncedOnFilterChange = useCallback(debounce(onFilterChange, FILTER_TYPING_DEBOUNCE_MS), [ + onFilterChange, + ]); + return ( + + + + + + + ); +}; 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 cac4873bc0c79..a6c22035c5a5a 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 @@ -5,9 +5,7 @@ * 2.0. */ -import React, { Fragment } from 'react'; -import { i18n } from '@kbn/i18n'; -import { EuiTextColor, EuiSpacer } from '@elastic/eui'; +import React from 'react'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths import { AlertTypeModel } from '../../../../triggers_actions_ui/public/types'; import { @@ -15,8 +13,11 @@ import { 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'; -export function createLegacyAlertTypes(): AlertTypeModel[] { +export function createLegacyAlertTypes(config: MonitoringConfig): AlertTypeModel[] { return LEGACY_RULES.map((legacyAlert) => { return { id: legacyAlert, @@ -25,17 +26,7 @@ export function createLegacyAlertTypes(): AlertTypeModel[] { documentationUrl(docLinks) { return `${docLinks.links.monitoring.alertsKibanaClusterAlerts}`; }, - alertParamsExpression: () => ( - - - - {i18n.translate('xpack.monitoring.alerts.legacyAlert.expressionText', { - defaultMessage: 'There is nothing to configure.', - })} - - - - ), + alertParamsExpression: (props: Props) => , defaultActionMessage: '{{context.internalFullMessage}}', validate: () => ({ errors: {} }), requiresAppContext: RULE_REQUIRES_APP_CONTEXT, 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 3e55b6d5454ff..2fe0c9b77c0eb 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 @@ -16,8 +16,11 @@ import { RULE_DETAILS, RULE_REQUIRES_APP_CONTEXT, } from '../../../common/constants'; +import { MonitoringConfig } from '../../types'; -export function createMemoryUsageAlertType(): AlertTypeModel { +export function createMemoryUsageAlertType( + config: MonitoringConfig +): AlertTypeModel { return { id: RULE_MEMORY_USAGE, description: RULE_DETAILS[RULE_MEMORY_USAGE].description, @@ -26,7 +29,11 @@ export function createMemoryUsageAlertType(): AlertTypeModel ( - + ), validate, defaultActionMessage: '{{context.internalFullMessage}}', 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 7fd9438e1cea3..e8a15ad835581 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 @@ -13,6 +13,7 @@ import { Expression, Props } from '../components/param_details_form/expression'; import { AlertTypeModel } from '../../../../triggers_actions_ui/public/types'; import { CommonAlertParamDetails } from '../../../common/types/alerts'; import { RULE_REQUIRES_APP_CONTEXT } from '../../../common/constants'; +import { MonitoringConfig } from '../../types'; interface ThreadPoolTypes { [key: string]: unknown; @@ -26,7 +27,8 @@ interface ThreadPoolRejectionAlertDetails { export function createThreadPoolRejectionsAlertType( alertId: string, - threadPoolAlertDetails: ThreadPoolRejectionAlertDetails + threadPoolAlertDetails: ThreadPoolRejectionAlertDetails, + config: MonitoringConfig ): AlertTypeModel { return { id: alertId, @@ -38,7 +40,7 @@ export function createThreadPoolRejectionsAlertType( alertParamsExpression: (props: Props) => ( <> - + ), validate: (inputValues: ThreadPoolTypes) => { diff --git a/x-pack/plugins/monitoring/public/application/hooks/use_table.ts b/x-pack/plugins/monitoring/public/application/hooks/use_table.ts new file mode 100644 index 0000000000000..60264f3657fe3 --- /dev/null +++ b/x-pack/plugins/monitoring/public/application/hooks/use_table.ts @@ -0,0 +1,162 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { useState, useCallback } from 'react'; +import { EUI_SORT_ASCENDING } from '../../../common/constants'; +import { euiTableStorageGetter, euiTableStorageSetter } from '../../components/table'; +import { Storage } from '../../../../../../src/plugins/kibana_utils/public'; + +interface Pagination { + pageSize: number; + initialPageSize: number; + pageIndex: number; + initialPageIndex: number; + pageSizeOptions: number[]; + totalItemCount: number; +} + +interface Page { + size: number; + index: number; +} + +interface Sorting { + sort: { + field: string; + direction: string; + }; +} + +const PAGE_SIZE_OPTIONS = [5, 10, 20, 50]; + +const DEFAULT_PAGINATION = { + pageSize: 20, + initialPageSize: 20, + pageIndex: 0, + initialPageIndex: 0, + pageSizeOptions: PAGE_SIZE_OPTIONS, + totalItemCount: 0, +}; + +const getPaginationInitialState = (page: Page | undefined) => { + const pagination = DEFAULT_PAGINATION; + + if (page) { + pagination.initialPageSize = page.size; + pagination.pageSize = page.size; + pagination.initialPageIndex = page.index; + pagination.pageIndex = page.index; + } + + return { + ...pagination, + pageSizeOptions: PAGE_SIZE_OPTIONS, + }; +}; + +export function useTable(storageKey: string) { + const storage = new Storage(window.localStorage); + const getLocalStorageData = euiTableStorageGetter(storageKey); + const setLocalStorageData = euiTableStorageSetter(storageKey); + + const storageData = getLocalStorageData(storage); + // get initial state from localstorage + const [pagination, setPagination] = useState( + getPaginationInitialState(storageData.page) + ); + + const updateTotalItemCount = useCallback( + (num) => { + // only update pagination state if different + if (num === pagination.totalItemCount) return; + setPagination({ + ...pagination, + totalItemCount: num, + }); + }, + [setPagination, pagination] + ); + + // get initial state from localStorage + const [sorting, setSorting] = useState(storageData.sort || { sort: {} }); + const cleanSortingData = (sortData: Sorting) => { + const sort = sortData || { sort: {} }; + + if (!sort.sort.field) { + sort.sort.field = 'name'; + } + if (!sort.sort.direction) { + sort.sort.direction = EUI_SORT_ASCENDING; + } + + return sort; + }; + + const [query, setQuery] = useState(''); + + const onTableChange = () => { + // we are already updating the state in fetchMoreData. We would need to check in react + // if both methods are needed or we can clean one of them + // For now I just keep it so existing react components don't break + }; + + const getPaginationRouteOptions = useCallback(() => { + if (!pagination || !sorting) { + return {}; + } + + return { + pagination: { + size: pagination.pageSize, + index: pagination.pageIndex, + }, + ...sorting, + queryText: query, + }; + }, [pagination, query, sorting]); + + const getPaginationTableProps = () => { + return { + sorting, + pagination, + onTableChange, + fetchMoreData: ({ + page, + sort, + queryText, + }: { + page: Page; + sort: Sorting; + queryText: string; + }) => { + setPagination({ + ...pagination, + ...{ + initialPageSize: page.size, + pageSize: page.size, + initialPageIndex: page.index, + pageIndex: page.index, + pageSizeOptions: PAGE_SIZE_OPTIONS, + }, + }); + setSorting(cleanSortingData(sort)); + setQuery(queryText); + + setLocalStorageData(storage, { + page, + sort, + }); + }, + }; + }; + + return { + getPaginationRouteOptions, + getPaginationTableProps, + updateTotalItemCount, + }; +} diff --git a/x-pack/plugins/monitoring/public/application/index.tsx b/x-pack/plugins/monitoring/public/application/index.tsx index 6db9343035237..85dc5286efa42 100644 --- a/x-pack/plugins/monitoring/public/application/index.tsx +++ b/x-pack/plugins/monitoring/public/application/index.tsx @@ -18,8 +18,11 @@ import { GlobalStateProvider } from './global_state_context'; import { ExternalConfigContext, ExternalConfig } from './external_config_context'; import { createPreserveQueryHistory } from './preserve_query_history'; import { RouteInit } from './route_init'; +import { NoDataPage } from './pages/no_data'; import { ElasticsearchOverviewPage } from './pages/elasticsearch/overview'; -import { CODE_PATH_ELASTICSEARCH } from '../../common/constants'; +import { BeatsOverviewPage } from './pages/beats/overview'; +import { CODE_PATH_ELASTICSEARCH, CODE_PATH_BEATS } from '../../common/constants'; +import { ElasticsearchNodesPage } from './pages/elasticsearch/nodes_page'; import { MonitoringTimeContainer } from './hooks/use_monitoring_time'; import { BreadcrumbContainer } from './hooks/use_breadcrumbs'; @@ -54,7 +57,7 @@ const MonitoringApp: React.FC<{ - + {/* ElasticSearch Views */} + + + + {/* Beats Views */} + + = () => { - return
    No data page
    ; -}; - const Home: React.FC<{}> = () => { return
    Home page (Cluster listing)
    ; }; diff --git a/x-pack/plugins/monitoring/public/application/pages/beats/beats_template.tsx b/x-pack/plugins/monitoring/public/application/pages/beats/beats_template.tsx new file mode 100644 index 0000000000000..90df1b49aa5d7 --- /dev/null +++ b/x-pack/plugins/monitoring/public/application/pages/beats/beats_template.tsx @@ -0,0 +1,35 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import React from 'react'; +import { i18n } from '@kbn/i18n'; +import { PageTemplate } from '../page_template'; +import { TabMenuItem, PageTemplateProps } from '../page_template'; + +interface BeatsTemplateProps extends PageTemplateProps { + cluster: any; +} + +export const BeatsTemplate: React.FC = ({ cluster, ...props }) => { + const tabs: TabMenuItem[] = [ + { + id: 'overview', + label: i18n.translate('xpack.monitoring.beatsNavigation.overviewLinkText', { + defaultMessage: 'Overview', + }), + route: '/beats', + }, + { + id: 'instances', + label: i18n.translate('xpack.monitoring.beatsNavigation.instancesLinkText', { + defaultMessage: 'Instances', + }), + route: '/beats/beats', + }, + ]; + + return ; +}; diff --git a/x-pack/plugins/monitoring/public/application/pages/beats/overview.tsx b/x-pack/plugins/monitoring/public/application/pages/beats/overview.tsx new file mode 100644 index 0000000000000..3efad7b82549c --- /dev/null +++ b/x-pack/plugins/monitoring/public/application/pages/beats/overview.tsx @@ -0,0 +1,85 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { useContext, useState, useCallback, useEffect } from 'react'; +import { i18n } from '@kbn/i18n'; +import { find } from 'lodash'; +import { ComponentProps } from '../../route_init'; +import { BeatsTemplate } from './beats_template'; +import { GlobalStateContext } from '../../global_state_context'; +import { useCharts } from '../../hooks/use_charts'; +import { useKibana } from '../../../../../../../src/plugins/kibana_react/public'; +// @ts-ignore +import { BeatsOverview } from '../../../components/beats/overview'; +import { BreadcrumbContainer } from '../../hooks/use_breadcrumbs'; + +export const BeatsOverviewPage: React.FC = ({ clusters }) => { + const globalState = useContext(GlobalStateContext); + const { zoomInfo, onBrush } = useCharts(); + const { services } = useKibana<{ data: any }>(); + const clusterUuid = globalState.cluster_uuid; + const ccs = globalState.ccs; + const { generate: generateBreadcrumbs } = useContext(BreadcrumbContainer.Context); + const cluster = find(clusters, { + cluster_uuid: clusterUuid, + }) as any; + + const [data, setData] = useState(null); + + const title = i18n.translate('xpack.monitoring.beats.overview.routeTitle', { + defaultMessage: 'Beats - Overview', + }); + + const pageTitle = i18n.translate('xpack.monitoring.beats.overview.pageTitle', { + defaultMessage: 'Beats overview', + }); + + useEffect(() => { + if (cluster) { + generateBreadcrumbs(cluster.cluster_name, { + inBeats: true, + }); + } + }, [cluster, generateBreadcrumbs]); + + const getPageData = useCallback(async () => { + const bounds = services.data?.query.timefilter.timefilter.getBounds(); + const url = `../api/monitoring/v1/clusters/${clusterUuid}/beats`; + + const response = await services.http?.fetch(url, { + method: 'POST', + body: JSON.stringify({ + ccs, + timeRange: { + min: bounds.min.toISOString(), + max: bounds.max.toISOString(), + }, + }), + }); + + setData(response); + }, [ccs, clusterUuid, services.data?.query.timefilter.timefilter, services.http]); + + const renderOverview = (overviewData: any) => { + if (overviewData === null) { + return null; + } + return ; + }; + + return ( + +
    {renderOverview(data)}
    +
    + ); +}; diff --git a/x-pack/plugins/monitoring/public/application/pages/elasticsearch/nodes_page.tsx b/x-pack/plugins/monitoring/public/application/pages/elasticsearch/nodes_page.tsx new file mode 100644 index 0000000000000..652fe83231441 --- /dev/null +++ b/x-pack/plugins/monitoring/public/application/pages/elasticsearch/nodes_page.tsx @@ -0,0 +1,102 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import React, { useContext, useState, useCallback } from 'react'; +import { i18n } from '@kbn/i18n'; +import { find } from 'lodash'; +import { ElasticsearchTemplate } from './elasticsearch_template'; +import { useKibana } from '../../../../../../../src/plugins/kibana_react/public'; +import { GlobalStateContext } from '../../global_state_context'; +import { ExternalConfigContext } from '../../external_config_context'; +import { ElasticsearchNodes } from '../../../components/elasticsearch'; +import { ComponentProps } from '../../route_init'; +import { SetupModeRenderer } from '../../setup_mode/setup_mode_renderer'; +import { SetupModeContext } from '../../../components/setup_mode/setup_mode_context'; +import { useTable } from '../../hooks/use_table'; + +interface SetupModeProps { + setupMode: any; + flyoutComponent: any; + bottomBarComponent: any; +} + +export const ElasticsearchNodesPage: React.FC = ({ clusters }) => { + const globalState = useContext(GlobalStateContext); + const { showCgroupMetricsElasticsearch } = useContext(ExternalConfigContext); + const { services } = useKibana<{ data: any }>(); + const { getPaginationRouteOptions, updateTotalItemCount, getPaginationTableProps } = + useTable('elasticsearch.nodes'); + const clusterUuid = globalState.cluster_uuid; + const ccs = globalState.ccs; + const cluster = find(clusters, { + cluster_uuid: clusterUuid, + }); + const [data, setData] = useState({} as any); + + const title = i18n.translate('xpack.monitoring.elasticsearch.nodes.routeTitle', { + defaultMessage: 'Elasticsearch - Nodes', + }); + + const pageTitle = i18n.translate('xpack.monitoring.elasticsearch.nodes.pageTitle', { + defaultMessage: 'Elasticsearch nodes', + }); + + const getPageData = useCallback(async () => { + const bounds = services.data?.query.timefilter.timefilter.getBounds(); + const url = `../api/monitoring/v1/clusters/${clusterUuid}/elasticsearch/nodes`; + const response = await services.http?.fetch(url, { + method: 'POST', + body: JSON.stringify({ + ccs, + timeRange: { + min: bounds.min.toISOString(), + max: bounds.max.toISOString(), + }, + ...getPaginationRouteOptions(), + }), + }); + + setData(response); + updateTotalItemCount(response.totalNodeCount); + }, [ + ccs, + clusterUuid, + services.data?.query.timefilter.timefilter, + services.http, + getPaginationRouteOptions, + updateTotalItemCount, + ]); + + return ( + +
    + ( + + {flyoutComponent} + + {bottomBarComponent} + + )} + /> +
    +
    + ); +}; diff --git a/x-pack/plugins/monitoring/public/application/pages/no_data/enabler.ts b/x-pack/plugins/monitoring/public/application/pages/no_data/enabler.ts new file mode 100644 index 0000000000000..6225e3863b2f7 --- /dev/null +++ b/x-pack/plugins/monitoring/public/application/pages/no_data/enabler.ts @@ -0,0 +1,57 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +// From x-pack/plugins/monitoring/public/lib/elasticsearch_settings/enabler.js +export class Enabler { + http: any; + updateModel: any; + + constructor(http: any, updateModel: (properties: any) => void) { + this.http = http; + this.updateModel = updateModel; + } + + async enableCollectionInterval() { + try { + this.updateModel({ isCollectionIntervalUpdating: true }); + + await this.http.fetch('../api/monitoring/v1/elasticsearch_settings/set/collection_interval', { + method: 'PUT', + }); + this.updateModel({ + isCollectionIntervalUpdated: true, + isCollectionIntervalUpdating: false, + }); + } catch (err) { + this.updateModel({ + errors: (err as any).data, + isCollectionIntervalUpdated: false, + isCollectionIntervalUpdating: false, + }); + } + } + + async enableCollectionEnabled() { + try { + this.updateModel({ isCollectionEnabledUpdating: true }); + await this.http.fetch('../api/monitoring/v1/elasticsearch_settings/set/collection_enabled', { + method: 'PUT', + }); + + this.updateModel({ + isCollectionEnabledUpdated: true, + isCollectionEnabledUpdating: false, + }); + } catch (err) { + this.updateModel({ + errors: (err as any).data, + isCollectionEnabledUpdated: false, + isCollectionEnabledUpdating: false, + }); + } + } +} diff --git a/x-pack/plugins/monitoring/public/application/pages/no_data/index.ts b/x-pack/plugins/monitoring/public/application/pages/no_data/index.ts new file mode 100644 index 0000000000000..7fa176d0e6adf --- /dev/null +++ b/x-pack/plugins/monitoring/public/application/pages/no_data/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 { NoDataPage } from './no_data_page'; diff --git a/x-pack/plugins/monitoring/public/application/pages/no_data/no_data_page.tsx b/x-pack/plugins/monitoring/public/application/pages/no_data/no_data_page.tsx new file mode 100644 index 0000000000000..b05bd783b2ff2 --- /dev/null +++ b/x-pack/plugins/monitoring/public/application/pages/no_data/no_data_page.tsx @@ -0,0 +1,240 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { useCallback, useContext, useState } from 'react'; +import { Redirect } from 'react-router-dom'; + +import { i18n } from '@kbn/i18n'; +// @ts-ignore +import { NoData } from '../../../components/no_data'; +import { PageTemplate } from '../page_template'; +import { useKibana } from '../../../../../../../src/plugins/kibana_react/public'; +import { CODE_PATH_LICENSE, STANDALONE_CLUSTER_CLUSTER_UUID } from '../../../../common/constants'; +import { Legacy } from '../../../legacy_shims'; +import { Enabler } from './enabler'; +import { BreadcrumbContainer } from '../../hooks/use_breadcrumbs'; +import { initSetupModeState } from '../../setup_mode/setup_mode'; +import { GlobalStateContext } from '../../global_state_context'; + +const CODE_PATHS = [CODE_PATH_LICENSE]; + +interface NoDataPageSetupDeps { + http: any; + data: any; +} + +interface SettingsChecker { + message: string; + api: string; + next?: SettingsChecker; +} + +const clusterCheckers: SettingsChecker[] = [ + { + message: i18n.translate('xpack.monitoring.noData.checker.clusterSettings', { + defaultMessage: 'Checking cluster settings API on production cluster', + }), + api: '../api/monitoring/v1/elasticsearch_settings/check/cluster', + }, + { + message: i18n.translate('xpack.monitoring.noData.checker.nodesSettings', { + defaultMessage: 'Checking nodes settings API on production cluster', + }), + api: '../api/monitoring/v1/elasticsearch_settings/check/nodes', + }, +]; + +export const NoDataPage = () => { + const title = i18n.translate('xpack.monitoring.noData.routeTitle', { + defaultMessage: 'Setup Monitoring', + }); + + const { services } = useKibana(); + const [shouldRedirect, setShouldRedirect] = useState(false); + + const [model, setModel] = useState({ + errors: [], // errors can happen from trying to check or set ES settings + checkMessage: null, // message to show while waiting for api response + isLoading: true, // flag for in-progress state of checking for no data reason + isCollectionEnabledUpdating: false, // flags to indicate whether to show a spinner while waiting for ajax + isCollectionEnabledUpdated: false, + isCollectionIntervalUpdating: false, + isCollectionIntervalUpdated: false, + } as any); + + const { update: updateBreadcrumbs } = useContext(BreadcrumbContainer.Context); + updateBreadcrumbs([ + { + 'data-test-subj': 'breadcrumbClusters', + text: 'Clusters', + href: '#/home', + ignoreGlobalState: true, + }, + ]); + + const globalState = useContext(GlobalStateContext); + initSetupModeState(globalState, services.http); + + // From x-pack/plugins/monitoring/public/views/no_data/model_updater.js + const updateModel = useCallback( + (properties: any) => { + setModel((previousModel: any) => { + const updated = { ...previousModel }; + const keys = Object.keys(properties); + + keys.forEach((key) => { + if (Array.isArray(updated[key])) { + updated[key].push(properties[key]); + } else { + updated[key] = properties[key]; + } + }); + + return updated; + }); + }, + [setModel] + ); + + const getPageData = useCallback(async () => { + let catchReason; + try { + const clusters = await getClusters(services); + + if (clusters && clusters.length) { + setShouldRedirect(true); + return; + } + } catch (err) { + if (err && err.status === 503) { + catchReason = { + property: 'custom', + message: err.data.message, + }; + } + } + + if (catchReason) { + updateModel({ reason: catchReason }); + } else { + await startChecks(clusterCheckers, services.http, updateModel); + } + }, [services, updateModel]); + + const enabler = new Enabler(services.http, updateModel); + + return ( + + {shouldRedirect ? ( + + ) : ( + + )} + + ); +}; + +async function getClusters(services: NoDataPageSetupDeps): Promise { + const url = '../api/monitoring/v1/clusters'; + const bounds = services.data?.query.timefilter.timefilter.getBounds(); + const min = bounds.min.toISOString(); + const max = bounds.max.toISOString(); + + const response = await services.http?.fetch(url, { + method: 'POST', + body: JSON.stringify({ + css: undefined, + timeRange: { + min, + max, + }, + codePaths: CODE_PATHS, + }), + }); + + return formatClusters(response); +} + +// From x-pack/plugins/monitoring/public/lib/elasticsearch_settings/start_checks.js +const mapCheckers = (_checkers: SettingsChecker[]) => { + return _checkers.map((current, checkerIndex) => { + const next = _checkers[checkerIndex + 1]; + if (next !== undefined) { + current.next = next; + } + + return current; + }); +}; + +// From x-pack/plugins/monitoring/public/lib/elasticsearch_settings/start_checks.js +function startChecks( + checkers: SettingsChecker[], + http: { fetch: any }, + updateModel: (properties: any) => void +) { + const runCheck = async (currentChecker: SettingsChecker): Promise => { + updateModel({ checkMessage: currentChecker.message }); + + const { found, reason, error, errorReason } = await executeCheck(currentChecker, http); + + if (error) { + updateModel({ errors: errorReason }); + if (currentChecker.next) { + return runCheck(currentChecker.next); + } + } else if (found) { + return updateModel({ + reason, + isLoading: false, + checkMessage: null, + }); + } else if (currentChecker.next) { + return runCheck(currentChecker.next); + } + + // dead end + updateModel({ + reason: null, + isLoading: false, + checkMessage: null, + }); + }; + + const _checkers = mapCheckers(checkers); + return runCheck(_checkers[0]); +} + +async function executeCheck(checker: SettingsChecker, http: { fetch: any }): Promise { + try { + const response = await http.fetch(checker.api, { + method: 'GET', + }); + const { found, reason } = response; + + return { found, reason }; + } catch (err: any) { + const { data } = err; + + return { + error: true, + found: false, + errorReason: data, + }; + } +} + +function formatClusters(clusters: any): any[] { + return clusters.map(formatCluster); +} + +function formatCluster(cluster: any) { + if (cluster.cluster_uuid === STANDALONE_CLUSTER_CLUSTER_UUID) { + cluster.cluster_name = 'Standalone Cluster'; + } + return cluster; +} diff --git a/x-pack/plugins/monitoring/public/components/elasticsearch/index.d.ts b/x-pack/plugins/monitoring/public/components/elasticsearch/index.d.ts index 4460b8432134b..615e79a0bf154 100644 --- a/x-pack/plugins/monitoring/public/components/elasticsearch/index.d.ts +++ b/x-pack/plugins/monitoring/public/components/elasticsearch/index.d.ts @@ -6,3 +6,4 @@ */ export const ElasticsearchOverview: FunctionComponent; +export const ElasticsearchNodes: FunctionComponent; diff --git a/x-pack/plugins/monitoring/public/components/kuery_bar/autocomplete_field.tsx b/x-pack/plugins/monitoring/public/components/kuery_bar/autocomplete_field.tsx new file mode 100644 index 0000000000000..522256ea49b98 --- /dev/null +++ b/x-pack/plugins/monitoring/public/components/kuery_bar/autocomplete_field.tsx @@ -0,0 +1,316 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { EuiFieldSearch, EuiOutsideClickDetector, EuiPanel } from '@elastic/eui'; +import React from 'react'; +import { QuerySuggestion } from '../../../../../../src/plugins/data/public'; +import { euiStyled } from '../../../../../../src/plugins/kibana_react/common'; +import { composeStateUpdaters } from '../../lib/typed_react'; +import { SuggestionItem } from './suggestion_item'; + +interface AutocompleteFieldProps { + isLoadingSuggestions: boolean; + isValid: boolean; + loadSuggestions: (value: string, cursorPosition: number, maxCount?: number) => void; + onSubmit?: (value: string) => void; + onChange?: (value: string) => void; + placeholder?: string; + suggestions: QuerySuggestion[]; + value: string; + disabled?: boolean; + autoFocus?: boolean; + 'aria-label'?: string; +} + +interface AutocompleteFieldState { + areSuggestionsVisible: boolean; + isFocused: boolean; + selectedIndex: number | null; +} + +export class AutocompleteField extends React.Component< + AutocompleteFieldProps, + AutocompleteFieldState +> { + public readonly state: AutocompleteFieldState = { + areSuggestionsVisible: false, + isFocused: false, + selectedIndex: null, + }; + + private inputElement: HTMLInputElement | null = null; + + public render() { + const { + suggestions, + isLoadingSuggestions, + isValid, + placeholder, + value, + disabled, + 'aria-label': ariaLabel, + } = this.props; + const { areSuggestionsVisible, selectedIndex } = this.state; + + return ( + + + + {areSuggestionsVisible && !isLoadingSuggestions && suggestions.length > 0 ? ( + + {suggestions.map((suggestion, suggestionIndex) => ( + + ))} + + ) : null} + + + ); + } + + public componentDidMount() { + if (this.inputElement && this.props.autoFocus) { + this.inputElement.focus(); + } + } + + public componentDidUpdate(prevProps: AutocompleteFieldProps) { + const hasNewValue = prevProps.value !== this.props.value; + const hasNewSuggestions = prevProps.suggestions !== this.props.suggestions; + + if (hasNewValue) { + this.updateSuggestions(); + } + + if (hasNewValue && this.props.value === '') { + this.submit(); + } + + if (hasNewSuggestions && this.state.isFocused) { + this.showSuggestions(); + } + } + + private handleChangeInputRef = (element: HTMLInputElement | null) => { + this.inputElement = element; + }; + + private handleChange = (evt: React.ChangeEvent) => { + this.changeValue(evt.currentTarget.value); + }; + + private handleKeyDown = (evt: React.KeyboardEvent) => { + const { suggestions } = this.props; + + switch (evt.key) { + case 'ArrowUp': + evt.preventDefault(); + if (suggestions.length > 0) { + this.setState( + composeStateUpdaters(withSuggestionsVisible, withPreviousSuggestionSelected) + ); + } + break; + case 'ArrowDown': + evt.preventDefault(); + if (suggestions.length > 0) { + this.setState(composeStateUpdaters(withSuggestionsVisible, withNextSuggestionSelected)); + } else { + this.updateSuggestions(); + } + break; + case 'Enter': + evt.preventDefault(); + if (this.state.selectedIndex !== null) { + this.applySelectedSuggestion(); + } else { + this.submit(); + } + break; + case 'Escape': + evt.preventDefault(); + this.setState(withSuggestionsHidden); + break; + } + }; + + private handleKeyUp = (evt: React.KeyboardEvent) => { + switch (evt.key) { + case 'ArrowLeft': + case 'ArrowRight': + case 'Home': + case 'End': + this.updateSuggestions(); + break; + } + }; + + private handleFocus = () => { + this.setState(composeStateUpdaters(withSuggestionsVisible, withFocused)); + }; + + private handleBlur = () => { + this.setState(composeStateUpdaters(withSuggestionsHidden, withUnfocused)); + }; + + private selectSuggestionAt = (index: number) => () => { + this.setState(withSuggestionAtIndexSelected(index)); + }; + + private applySelectedSuggestion = () => { + if (this.state.selectedIndex !== null) { + this.applySuggestionAt(this.state.selectedIndex)(); + } + }; + + private applySuggestionAt = (index: number) => () => { + const { value, suggestions } = this.props; + const selectedSuggestion = suggestions[index]; + + if (!selectedSuggestion) { + return; + } + + const newValue = + value.substr(0, selectedSuggestion.start) + + selectedSuggestion.text + + value.substr(selectedSuggestion.end); + + this.setState(withSuggestionsHidden); + this.changeValue(newValue); + this.focusInputElement(); + }; + + private changeValue = (value: string) => { + const { onChange } = this.props; + + if (onChange) { + onChange(value); + } + }; + + private focusInputElement = () => { + if (this.inputElement) { + this.inputElement.focus(); + } + }; + + private showSuggestions = () => { + this.setState(withSuggestionsVisible); + }; + + private submit = () => { + const { isValid, onSubmit, value } = this.props; + + if (isValid && onSubmit) { + onSubmit(value); + } + + this.setState(withSuggestionsHidden); + }; + + private updateSuggestions = () => { + const inputCursorPosition = this.inputElement ? this.inputElement.selectionStart || 0 : 0; + this.props.loadSuggestions(this.props.value, inputCursorPosition, 200); + }; +} + +const withPreviousSuggestionSelected = ( + state: AutocompleteFieldState, + props: AutocompleteFieldProps +): AutocompleteFieldState => ({ + ...state, + selectedIndex: + props.suggestions.length === 0 + ? null + : state.selectedIndex !== null + ? (state.selectedIndex + props.suggestions.length - 1) % props.suggestions.length + : Math.max(props.suggestions.length - 1, 0), +}); + +const withNextSuggestionSelected = ( + state: AutocompleteFieldState, + props: AutocompleteFieldProps +): AutocompleteFieldState => ({ + ...state, + selectedIndex: + props.suggestions.length === 0 + ? null + : state.selectedIndex !== null + ? (state.selectedIndex + 1) % props.suggestions.length + : 0, +}); + +const withSuggestionAtIndexSelected = + (suggestionIndex: number) => + (state: AutocompleteFieldState, props: AutocompleteFieldProps): AutocompleteFieldState => ({ + ...state, + selectedIndex: + props.suggestions.length === 0 + ? null + : suggestionIndex >= 0 && suggestionIndex < props.suggestions.length + ? suggestionIndex + : 0, + }); + +const withSuggestionsVisible = (state: AutocompleteFieldState) => ({ + ...state, + areSuggestionsVisible: true, +}); + +const withSuggestionsHidden = (state: AutocompleteFieldState) => ({ + ...state, + areSuggestionsVisible: false, + selectedIndex: null, +}); + +const withFocused = (state: AutocompleteFieldState) => ({ + ...state, + isFocused: true, +}); + +const withUnfocused = (state: AutocompleteFieldState) => ({ + ...state, + isFocused: false, +}); + +const AutocompleteContainer = euiStyled.div` + position: relative; +`; + +const SuggestionsPanel = euiStyled(EuiPanel).attrs(() => ({ + paddingSize: 'none', + hasShadow: true, +}))` + position: absolute; + width: 100%; + margin-top: 2px; + overflow-x: hidden; + overflow-y: scroll; + z-index: ${(props) => props.theme.eui.euiZLevel1}; + max-height: 322px; +`; diff --git a/x-pack/plugins/monitoring/public/components/kuery_bar/index.tsx b/x-pack/plugins/monitoring/public/components/kuery_bar/index.tsx new file mode 100644 index 0000000000000..ca0a8122772f3 --- /dev/null +++ b/x-pack/plugins/monitoring/public/components/kuery_bar/index.tsx @@ -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 { i18n } from '@kbn/i18n'; + +import React, { useEffect, useState } from 'react'; +import { WithKueryAutocompletion } from './with_kuery_autocompletion'; +import { AutocompleteField } from './autocomplete_field'; +import { esKuery, IIndexPattern, QuerySuggestion } from '../../../../../../src/plugins/data/public'; + +type LoadSuggestionsFn = ( + e: string, + p: number, + m?: number, + transform?: (s: QuerySuggestion[]) => QuerySuggestion[] +) => void; +export type CurryLoadSuggestionsType = (loadSuggestions: LoadSuggestionsFn) => LoadSuggestionsFn; + +interface Props { + derivedIndexPattern: IIndexPattern; + onSubmit: (query: string) => void; + onChange?: (query: string) => void; + value?: string | null; + placeholder?: string; + curryLoadSuggestions?: CurryLoadSuggestionsType; +} + +function validateQuery(query: string) { + try { + esKuery.fromKueryExpression(query); + } catch (err) { + return false; + } + return true; +} + +export const KueryBar = ({ + derivedIndexPattern, + onSubmit, + onChange, + value, + placeholder, + curryLoadSuggestions = defaultCurryLoadSuggestions, +}: Props) => { + const [draftQuery, setDraftQuery] = useState(value || ''); + const [isValid, setValidation] = useState(true); + + // This ensures that if value changes out side this component it will update. + useEffect(() => { + if (value) { + setDraftQuery(value); + } + }, [value]); + + const handleChange = (query: string) => { + setValidation(validateQuery(query)); + setDraftQuery(query); + if (onChange) { + onChange(query); + } + }; + + const filteredDerivedIndexPattern = { + ...derivedIndexPattern, + fields: derivedIndexPattern.fields, + }; + + const defaultPlaceholder = i18n.translate('xpack.monitoring.alerts.kqlSearchFieldPlaceholder', { + defaultMessage: 'Search for monitoring data', + }); + + return ( + + {({ isLoadingSuggestions, loadSuggestions, suggestions }) => ( + + )} + + ); +}; + +const defaultCurryLoadSuggestions: CurryLoadSuggestionsType = + (loadSuggestions) => + (...args) => + loadSuggestions(...args); diff --git a/x-pack/plugins/monitoring/public/components/kuery_bar/suggestion_item.tsx b/x-pack/plugins/monitoring/public/components/kuery_bar/suggestion_item.tsx new file mode 100644 index 0000000000000..3681bf26987cc --- /dev/null +++ b/x-pack/plugins/monitoring/public/components/kuery_bar/suggestion_item.tsx @@ -0,0 +1,119 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { EuiIcon } from '@elastic/eui'; +import { transparentize } from 'polished'; +import React from 'react'; +import { euiStyled } from '../../../../../../src/plugins/kibana_react/common'; +import { QuerySuggestion, QuerySuggestionTypes } from '../../../../../../src/plugins/data/public'; + +interface Props { + isSelected?: boolean; + onClick?: React.MouseEventHandler; + onMouseEnter?: React.MouseEventHandler; + suggestion: QuerySuggestion; +} + +export const SuggestionItem: React.FC = (props) => { + const { isSelected, onClick, onMouseEnter, suggestion } = props; + + return ( + + + + + {suggestion.text} + {suggestion.description} + + ); +}; + +SuggestionItem.defaultProps = { + isSelected: false, +}; + +const SuggestionItemContainer = euiStyled.div<{ + isSelected?: boolean; +}>` + display: flex; + flex-direction: row; + font-size: ${(props) => props.theme.eui.euiFontSizeS}; + height: ${(props) => props.theme.eui.euiSizeXL}; + white-space: nowrap; + background-color: ${(props) => + props.isSelected ? props.theme.eui.euiColorLightestShade : 'transparent'}; +`; + +const SuggestionItemField = euiStyled.div` + align-items: center; + cursor: pointer; + display: flex; + flex-direction: row; + height: ${(props) => props.theme.eui.euiSizeXL}; + padding: ${(props) => props.theme.eui.euiSizeXS}; +`; + +const SuggestionItemIconField = euiStyled(SuggestionItemField)<{ + suggestionType: QuerySuggestionTypes; +}>` + background-color: ${(props) => + transparentize(0.9, getEuiIconColor(props.theme, props.suggestionType))}; + color: ${(props) => getEuiIconColor(props.theme, props.suggestionType)}; + flex: 0 0 auto; + justify-content: center; + width: ${(props) => props.theme.eui.euiSizeXL}; +`; + +const SuggestionItemTextField = euiStyled(SuggestionItemField)` + flex: 2 0 0; + font-family: ${(props) => props.theme.eui.euiCodeFontFamily}; +`; + +const SuggestionItemDescriptionField = euiStyled(SuggestionItemField)` + flex: 3 0 0; + + p { + display: inline; + + span { + font-family: ${(props) => props.theme.eui.euiCodeFontFamily}; + } + } +`; + +const getEuiIconType = (suggestionType: QuerySuggestionTypes) => { + switch (suggestionType) { + case QuerySuggestionTypes.Field: + return 'kqlField'; + case QuerySuggestionTypes.Value: + return 'kqlValue'; + case QuerySuggestionTypes.RecentSearch: + return 'search'; + case QuerySuggestionTypes.Conjunction: + return 'kqlSelector'; + case QuerySuggestionTypes.Operator: + return 'kqlOperand'; + default: + return 'empty'; + } +}; + +const getEuiIconColor = (theme: any, suggestionType: QuerySuggestionTypes): string => { + switch (suggestionType) { + case QuerySuggestionTypes.Field: + return theme?.eui.euiColorVis7; + case QuerySuggestionTypes.Value: + return theme?.eui.euiColorVis0; + case QuerySuggestionTypes.Operator: + return theme?.eui.euiColorVis1; + case QuerySuggestionTypes.Conjunction: + return theme?.eui.euiColorVis2; + case QuerySuggestionTypes.RecentSearch: + default: + return theme?.eui.euiColorMediumShade; + } +}; diff --git a/x-pack/plugins/monitoring/public/components/kuery_bar/with_kuery_autocompletion.tsx b/x-pack/plugins/monitoring/public/components/kuery_bar/with_kuery_autocompletion.tsx new file mode 100644 index 0000000000000..8d79bf4039846 --- /dev/null +++ b/x-pack/plugins/monitoring/public/components/kuery_bar/with_kuery_autocompletion.tsx @@ -0,0 +1,111 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license 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 { QuerySuggestion, IIndexPattern, DataPublicPluginStart } from 'src/plugins/data/public'; +import { + withKibana, + KibanaReactContextValue, + KibanaServices, +} from '../../../../../../src/plugins/kibana_react/public'; +import { RendererFunction } from '../../lib/typed_react'; + +interface WithKueryAutocompletionLifecycleProps { + kibana: KibanaReactContextValue<{ data: DataPublicPluginStart } & KibanaServices>; + children: RendererFunction<{ + isLoadingSuggestions: boolean; + loadSuggestions: (expression: string, cursorPosition: number, maxSuggestions?: number) => void; + suggestions: QuerySuggestion[]; + }>; + indexPattern: IIndexPattern; +} + +interface WithKueryAutocompletionLifecycleState { + // lacking cancellation support in the autocompletion api, + // this is used to keep older, slower requests from clobbering newer ones + currentRequest: { + expression: string; + cursorPosition: number; + } | null; + suggestions: QuerySuggestion[]; +} + +class WithKueryAutocompletionComponent extends React.Component< + WithKueryAutocompletionLifecycleProps, + WithKueryAutocompletionLifecycleState +> { + public readonly state: WithKueryAutocompletionLifecycleState = { + currentRequest: null, + suggestions: [], + }; + + public render() { + const { currentRequest, suggestions } = this.state; + + return this.props.children({ + isLoadingSuggestions: currentRequest !== null, + loadSuggestions: this.loadSuggestions, + suggestions, + }); + } + + private loadSuggestions = async ( + expression: string, + cursorPosition: number, + maxSuggestions?: number, + transformSuggestions?: (s: QuerySuggestion[]) => QuerySuggestion[] + ) => { + const { indexPattern } = this.props; + const language = 'kuery'; + const hasQuerySuggestions = + this.props.kibana.services.data?.autocomplete.hasQuerySuggestions(language); + + if (!hasQuerySuggestions) { + return; + } + + this.setState({ + currentRequest: { + expression, + cursorPosition, + }, + suggestions: [], + }); + + const suggestions = + (await this.props.kibana.services.data.autocomplete.getQuerySuggestions({ + language, + query: expression, + selectionStart: cursorPosition, + selectionEnd: cursorPosition, + indexPatterns: [indexPattern], + boolFilter: [], + })) || []; + + const transformedSuggestions = transformSuggestions + ? transformSuggestions(suggestions) + : suggestions; + + this.setState((state) => + state.currentRequest && + state.currentRequest.expression !== expression && + state.currentRequest.cursorPosition !== cursorPosition + ? state // ignore this result, since a newer request is in flight + : { + ...state, + currentRequest: null, + suggestions: maxSuggestions + ? transformedSuggestions.slice(0, maxSuggestions) + : transformedSuggestions, + } + ); + }; +} + +export const WithKueryAutocompletion = withKibana( + WithKueryAutocompletionComponent +); diff --git a/x-pack/plugins/monitoring/public/components/no_data/no_data.js b/x-pack/plugins/monitoring/public/components/no_data/no_data.js index 42ecbd3152cca..0deb55a049b5b 100644 --- a/x-pack/plugins/monitoring/public/components/no_data/no_data.js +++ b/x-pack/plugins/monitoring/public/components/no_data/no_data.js @@ -32,9 +32,9 @@ import { CloudDeployment } from './blurbs'; import { getSafeForExternalLink } from '../../lib/get_safe_for_external_link'; function NoDataMessage(props) { - const { isLoading, reason, checkMessage } = props; + const { isLoading, reason, checkMessage, isCollectionEnabledUpdated } = props; - if (isLoading) { + if ((isCollectionEnabledUpdated && !reason) || isLoading) { return ; } diff --git a/x-pack/plugins/monitoring/public/components/table/index.d.ts b/x-pack/plugins/monitoring/public/components/table/index.d.ts new file mode 100644 index 0000000000000..6b54b3d97e5f1 --- /dev/null +++ b/x-pack/plugins/monitoring/public/components/table/index.d.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 const euiTableStorageGetter: (string) => any; +export const euiTableStorageSetter: (string) => any; diff --git a/x-pack/plugins/monitoring/public/lib/kuery.ts b/x-pack/plugins/monitoring/public/lib/kuery.ts new file mode 100644 index 0000000000000..19706d7664c22 --- /dev/null +++ b/x-pack/plugins/monitoring/public/lib/kuery.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 { esKuery, IIndexPattern } from '../../../../../src/plugins/data/public'; + +export const convertKueryToElasticSearchQuery = ( + kueryExpression: string, + indexPattern: IIndexPattern +) => { + try { + return kueryExpression + ? JSON.stringify( + esKuery.toElasticsearchQuery(esKuery.fromKueryExpression(kueryExpression), indexPattern) + ) + : ''; + } catch (err) { + return ''; + } +}; diff --git a/x-pack/plugins/monitoring/public/lib/setup_mode.tsx b/x-pack/plugins/monitoring/public/lib/setup_mode.tsx index f622f2944a31a..fca7f94731bc5 100644 --- a/x-pack/plugins/monitoring/public/lib/setup_mode.tsx +++ b/x-pack/plugins/monitoring/public/lib/setup_mode.tsx @@ -159,6 +159,8 @@ export const disableElasticsearchInternalCollection = async () => { }; export const toggleSetupMode = (inSetupMode: boolean) => { + if (isReactMigrationEnabled()) return setupModeReact.toggleSetupMode(inSetupMode); + checkAngularState(); const globalState = angularState.injector.get('globalState'); diff --git a/x-pack/plugins/monitoring/public/lib/typed_react.tsx b/x-pack/plugins/monitoring/public/lib/typed_react.tsx new file mode 100644 index 0000000000000..b5b7a440c117c --- /dev/null +++ b/x-pack/plugins/monitoring/public/lib/typed_react.tsx @@ -0,0 +1,82 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { omit } from 'lodash'; +import React from 'react'; +import { InferableComponentEnhancerWithProps, ConnectedComponent } from 'react-redux'; + +export type RendererResult = React.ReactElement | null; +export type RendererFunction = (args: RenderArgs) => Result; + +export type ChildFunctionRendererProps = { + children: RendererFunction; + initializeOnMount?: boolean; + resetOnUnmount?: boolean; +} & RenderArgs; + +interface ChildFunctionRendererOptions { + onInitialize?: (props: RenderArgs) => void; + onCleanup?: (props: RenderArgs) => void; +} + +export const asChildFunctionRenderer = ( + hoc: InferableComponentEnhancerWithProps, + { onInitialize, onCleanup }: ChildFunctionRendererOptions = {} +): ConnectedComponent< + React.ComponentClass<{}>, + { + children: RendererFunction; + initializeOnMount?: boolean; + resetOnUnmount?: boolean; + } & OwnProps +> => + hoc( + class ChildFunctionRenderer extends React.Component> { + public displayName = 'ChildFunctionRenderer'; + + public componentDidMount() { + if (this.props.initializeOnMount && onInitialize) { + onInitialize(this.getRendererArgs()); + } + } + + public componentWillUnmount() { + if (this.props.resetOnUnmount && onCleanup) { + onCleanup(this.getRendererArgs()); + } + } + + public render() { + return (this.props.children as ChildFunctionRendererProps['children'])( + this.getRendererArgs() + ); + } + + private getRendererArgs = () => + omit(this.props, ['children', 'initializeOnMount', 'resetOnUnmount']) as Pick< + ChildFunctionRendererProps, + keyof InjectedProps + >; + } as any + ); + +export type StateUpdater = ( + prevState: Readonly, + prevProps: Readonly +) => State | null; + +export type PropsOfContainer = Container extends InferableComponentEnhancerWithProps< + infer InjectedProps, + any +> + ? InjectedProps + : never; + +export function composeStateUpdaters(...updaters: Array>) { + return (state: State, props: Props) => + updaters.reduce((currentState, updater) => updater(currentState, props) || currentState, state); +} diff --git a/x-pack/plugins/monitoring/public/plugin.ts b/x-pack/plugins/monitoring/public/plugin.ts index ad71cdbeb106c..aee5072947531 100644 --- a/x-pack/plugins/monitoring/public/plugin.ts +++ b/x-pack/plugins/monitoring/public/plugin.ts @@ -28,14 +28,6 @@ import { RULE_THREAD_POOL_WRITE_REJECTIONS, RULE_DETAILS, } from '../common/constants'; -import { createCpuUsageAlertType } from './alerts/cpu_usage_alert'; -import { createMissingMonitoringDataAlertType } from './alerts/missing_monitoring_data_alert'; -import { createLegacyAlertTypes } from './alerts/legacy_alert'; -import { createDiskUsageAlertType } from './alerts/disk_usage_alert'; -import { createThreadPoolRejectionsAlertType } from './alerts/thread_pool_rejections_alert'; -import { createMemoryUsageAlertType } from './alerts/memory_usage_alert'; -import { createCCRReadExceptionsAlertType } from './alerts/ccr_read_exceptions_alert'; -import { createLargeShardSizeAlertType } from './alerts/large_shard_size_alert'; import { setConfig } from './external_config'; interface MonitoringSetupPluginDependencies { @@ -49,11 +41,11 @@ const HASH_CHANGE = 'hashchange'; export class MonitoringPlugin implements - Plugin + Plugin { constructor(private initializerContext: PluginInitializerContext) {} - public setup( + public async setup( core: CoreSetup, plugins: MonitoringSetupPluginDependencies ) { @@ -86,7 +78,7 @@ export class MonitoringPlugin }); } - this.registerAlerts(plugins); + await this.registerAlerts(plugins, monitoring); const app: App = { id, @@ -152,7 +144,6 @@ export class MonitoringPlugin }; core.application.register(app); - return true; } public start(core: CoreStart, plugins: any) {} @@ -192,29 +183,48 @@ export class MonitoringPlugin ]; } - private registerAlerts(plugins: MonitoringSetupPluginDependencies) { + private async registerAlerts( + plugins: MonitoringSetupPluginDependencies, + config: MonitoringConfig + ) { const { triggersActionsUi: { ruleTypeRegistry }, } = plugins; - ruleTypeRegistry.register(createCpuUsageAlertType()); - ruleTypeRegistry.register(createDiskUsageAlertType()); - ruleTypeRegistry.register(createMemoryUsageAlertType()); + + 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)); ruleTypeRegistry.register(createMissingMonitoringDataAlertType()); ruleTypeRegistry.register( createThreadPoolRejectionsAlertType( RULE_THREAD_POOL_SEARCH_REJECTIONS, - RULE_DETAILS[RULE_THREAD_POOL_SEARCH_REJECTIONS] + RULE_DETAILS[RULE_THREAD_POOL_SEARCH_REJECTIONS], + config ) ); ruleTypeRegistry.register( createThreadPoolRejectionsAlertType( RULE_THREAD_POOL_WRITE_REJECTIONS, - RULE_DETAILS[RULE_THREAD_POOL_WRITE_REJECTIONS] + RULE_DETAILS[RULE_THREAD_POOL_WRITE_REJECTIONS], + config ) ); - ruleTypeRegistry.register(createCCRReadExceptionsAlertType()); - ruleTypeRegistry.register(createLargeShardSizeAlertType()); - const legacyAlertTypes = createLegacyAlertTypes(); + ruleTypeRegistry.register(createCCRReadExceptionsAlertType(config)); + ruleTypeRegistry.register(createLargeShardSizeAlertType(config)); + const legacyAlertTypes = createLegacyAlertTypes(config); for (const legacyAlertType of legacyAlertTypes) { ruleTypeRegistry.register(legacyAlertType); } diff --git a/x-pack/plugins/monitoring/server/alerts/ccr_read_exceptions_rule.ts b/x-pack/plugins/monitoring/server/alerts/ccr_read_exceptions_rule.ts index 3a50aca7d4b84..e3a3537ea2eaf 100644 --- a/x-pack/plugins/monitoring/server/alerts/ccr_read_exceptions_rule.ts +++ b/x-pack/plugins/monitoring/server/alerts/ccr_read_exceptions_rule.ts @@ -88,7 +88,8 @@ export class CCRReadExceptionsRule extends BaseRule { esIndexPattern, startMs, endMs, - Globals.app.config.ui.max_bucket_size + Globals.app.config.ui.max_bucket_size, + params.filterQuery ); return stats.map((stat) => { @@ -278,7 +279,7 @@ export class CCRReadExceptionsRule extends BaseRule { state: AlertingDefaults.ALERT_STATE.firing, remoteCluster, followerIndex, - /* continue to send "remoteClusters" and "followerIndices" values for users still using it though + /* continue to send "remoteClusters" and "followerIndices" values for users still using it though we have replaced it with "remoteCluster" and "followerIndex" in the template due to alerts per index instead of all indices see https://github.com/elastic/kibana/issues/100136#issuecomment-865229431 */ diff --git a/x-pack/plugins/monitoring/server/alerts/cluster_health_rule.ts b/x-pack/plugins/monitoring/server/alerts/cluster_health_rule.ts index 7fac3b74a1b66..b9b9b90845eea 100644 --- a/x-pack/plugins/monitoring/server/alerts/cluster_health_rule.ts +++ b/x-pack/plugins/monitoring/server/alerts/cluster_health_rule.ts @@ -73,7 +73,12 @@ export class ClusterHealthRule extends BaseRule { if (availableCcs) { esIndexPattern = getCcsIndexPattern(esIndexPattern, availableCcs); } - const healths = await fetchClusterHealth(esClient, clusters, esIndexPattern); + const healths = await fetchClusterHealth( + esClient, + clusters, + esIndexPattern, + params.filterQuery + ); return healths.map((clusterHealth) => { const shouldFire = clusterHealth.health !== AlertClusterHealthType.Green; const severity = diff --git a/x-pack/plugins/monitoring/server/alerts/cpu_usage_rule.ts b/x-pack/plugins/monitoring/server/alerts/cpu_usage_rule.ts index 2e57a3c22de1b..7e38efcb819ea 100644 --- a/x-pack/plugins/monitoring/server/alerts/cpu_usage_rule.ts +++ b/x-pack/plugins/monitoring/server/alerts/cpu_usage_rule.ts @@ -76,7 +76,8 @@ export class CpuUsageRule extends BaseRule { esIndexPattern, startMs, endMs, - Globals.app.config.ui.max_bucket_size + Globals.app.config.ui.max_bucket_size, + params.filterQuery ); return stats.map((stat) => { if (Globals.app.config.ui.container.elasticsearch.enabled) { @@ -203,7 +204,7 @@ export class CpuUsageRule extends BaseRule { internalShortMessage, internalFullMessage: Globals.app.isCloud ? internalShortMessage : internalFullMessage, state: AlertingDefaults.ALERT_STATE.firing, - /* continue to send "nodes" and "count" values for users before https://github.com/elastic/kibana/pull/102544 + /* continue to send "nodes" and "count" values for users before https://github.com/elastic/kibana/pull/102544 see https://github.com/elastic/kibana/issues/100136#issuecomment-865229431 */ nodes: `${firingNode.nodeName}:${firingNode.cpuUsage}`, diff --git a/x-pack/plugins/monitoring/server/alerts/disk_usage_rule.ts b/x-pack/plugins/monitoring/server/alerts/disk_usage_rule.ts index ae3025c1db92c..bac70baebb4e2 100644 --- a/x-pack/plugins/monitoring/server/alerts/disk_usage_rule.ts +++ b/x-pack/plugins/monitoring/server/alerts/disk_usage_rule.ts @@ -72,7 +72,8 @@ export class DiskUsageRule extends BaseRule { clusters, esIndexPattern, duration as string, - Globals.app.config.ui.max_bucket_size + Globals.app.config.ui.max_bucket_size, + params.filterQuery ); return stats.map((stat) => { @@ -212,7 +213,7 @@ export class DiskUsageRule extends BaseRule { internalShortMessage, internalFullMessage: Globals.app.isCloud ? internalShortMessage : internalFullMessage, state: AlertingDefaults.ALERT_STATE.firing, - /* continue to send "nodes" and "count" values for users before https://github.com/elastic/kibana/pull/102544 + /* continue to send "nodes" and "count" values for users before https://github.com/elastic/kibana/pull/102544 see https://github.com/elastic/kibana/issues/100136#issuecomment-865229431 */ nodes: `${firingNode.nodeName}:${firingNode.diskUsage}`, diff --git a/x-pack/plugins/monitoring/server/alerts/elasticsearch_version_mismatch_rule.ts b/x-pack/plugins/monitoring/server/alerts/elasticsearch_version_mismatch_rule.ts index 6a5abcb4975f4..352cac531f8e8 100644 --- a/x-pack/plugins/monitoring/server/alerts/elasticsearch_version_mismatch_rule.ts +++ b/x-pack/plugins/monitoring/server/alerts/elasticsearch_version_mismatch_rule.ts @@ -66,7 +66,8 @@ export class ElasticsearchVersionMismatchRule extends BaseRule { esClient, clusters, esIndexPattern, - Globals.app.config.ui.max_bucket_size + Globals.app.config.ui.max_bucket_size, + params.filterQuery ); return elasticsearchVersions.map((elasticsearchVersion) => { diff --git a/x-pack/plugins/monitoring/server/alerts/kibana_version_mismatch_rule.ts b/x-pack/plugins/monitoring/server/alerts/kibana_version_mismatch_rule.ts index 90275ea4d23a8..6d9410ed0e5a0 100644 --- a/x-pack/plugins/monitoring/server/alerts/kibana_version_mismatch_rule.ts +++ b/x-pack/plugins/monitoring/server/alerts/kibana_version_mismatch_rule.ts @@ -79,7 +79,8 @@ export class KibanaVersionMismatchRule extends BaseRule { esClient, clusters, kibanaIndexPattern, - Globals.app.config.ui.max_bucket_size + Globals.app.config.ui.max_bucket_size, + params.filterQuery ); return kibanaVersions.map((kibanaVersion) => { diff --git a/x-pack/plugins/monitoring/server/alerts/large_shard_size_rule.ts b/x-pack/plugins/monitoring/server/alerts/large_shard_size_rule.ts index 86f96daa3b21d..b0370a23159d7 100644 --- a/x-pack/plugins/monitoring/server/alerts/large_shard_size_rule.ts +++ b/x-pack/plugins/monitoring/server/alerts/large_shard_size_rule.ts @@ -75,7 +75,8 @@ export class LargeShardSizeRule extends BaseRule { esIndexPattern, threshold!, shardIndexPatterns, - Globals.app.config.ui.max_bucket_size + Globals.app.config.ui.max_bucket_size, + params.filterQuery ); return stats.map((stat) => { @@ -211,7 +212,7 @@ export class LargeShardSizeRule extends BaseRule { internalShortMessage, internalFullMessage, state: AlertingDefaults.ALERT_STATE.firing, - /* continue to send "shardIndices" values for users still using it though + /* continue to send "shardIndices" values for users still using it though we have replaced it with shardIndex in the template due to alerts per index instead of all indices see https://github.com/elastic/kibana/issues/100136#issuecomment-865229431 */ diff --git a/x-pack/plugins/monitoring/server/alerts/license_expiration_rule.ts b/x-pack/plugins/monitoring/server/alerts/license_expiration_rule.ts index 67ea8bd57b491..c26929b05ab26 100644 --- a/x-pack/plugins/monitoring/server/alerts/license_expiration_rule.ts +++ b/x-pack/plugins/monitoring/server/alerts/license_expiration_rule.ts @@ -87,7 +87,7 @@ export class LicenseExpirationRule extends BaseRule { if (availableCcs) { esIndexPattern = getCcsIndexPattern(esIndexPattern, availableCcs); } - const licenses = await fetchLicenses(esClient, clusters, esIndexPattern); + const licenses = await fetchLicenses(esClient, clusters, esIndexPattern, params.filterQuery); return licenses.map((license) => { const { clusterUuid, type, expiryDateMS, status, ccs } = license; diff --git a/x-pack/plugins/monitoring/server/alerts/logstash_version_mismatch_rule.ts b/x-pack/plugins/monitoring/server/alerts/logstash_version_mismatch_rule.ts index 0f9ad4dd4b117..e59ed9efbefb2 100644 --- a/x-pack/plugins/monitoring/server/alerts/logstash_version_mismatch_rule.ts +++ b/x-pack/plugins/monitoring/server/alerts/logstash_version_mismatch_rule.ts @@ -66,7 +66,8 @@ export class LogstashVersionMismatchRule extends BaseRule { esClient, clusters, logstashIndexPattern, - Globals.app.config.ui.max_bucket_size + Globals.app.config.ui.max_bucket_size, + params.filterQuery ); return logstashVersions.map((logstashVersion) => { diff --git a/x-pack/plugins/monitoring/server/alerts/memory_usage_rule.ts b/x-pack/plugins/monitoring/server/alerts/memory_usage_rule.ts index 384610e659d47..d94e1234ce813 100644 --- a/x-pack/plugins/monitoring/server/alerts/memory_usage_rule.ts +++ b/x-pack/plugins/monitoring/server/alerts/memory_usage_rule.ts @@ -82,7 +82,8 @@ export class MemoryUsageRule extends BaseRule { esIndexPattern, startMs, endMs, - Globals.app.config.ui.max_bucket_size + Globals.app.config.ui.max_bucket_size, + params.filterQuery ); return stats.map((stat) => { @@ -223,7 +224,7 @@ export class MemoryUsageRule extends BaseRule { internalShortMessage, internalFullMessage: Globals.app.isCloud ? internalShortMessage : internalFullMessage, state: AlertingDefaults.ALERT_STATE.firing, - /* continue to send "nodes" and "count" values for users before https://github.com/elastic/kibana/pull/102544 + /* continue to send "nodes" and "count" values for users before https://github.com/elastic/kibana/pull/102544 see https://github.com/elastic/kibana/issues/100136#issuecomment-865229431 */ nodes: `${firingNode.nodeName}:${firingNode.memoryUsage.toFixed(2)}`, diff --git a/x-pack/plugins/monitoring/server/alerts/missing_monitoring_data_rule.ts b/x-pack/plugins/monitoring/server/alerts/missing_monitoring_data_rule.ts index 32e4ff738c71b..1b45b19fe89f8 100644 --- a/x-pack/plugins/monitoring/server/alerts/missing_monitoring_data_rule.ts +++ b/x-pack/plugins/monitoring/server/alerts/missing_monitoring_data_rule.ts @@ -75,7 +75,8 @@ export class MissingMonitoringDataRule extends BaseRule { indexPattern, Globals.app.config.ui.max_bucket_size, now, - now - limit - LIMIT_BUFFER + now - limit - LIMIT_BUFFER, + params.filterQuery ); return missingData.map((missing) => { return { @@ -198,7 +199,7 @@ export class MissingMonitoringDataRule extends BaseRule { internalShortMessage, internalFullMessage: Globals.app.isCloud ? internalShortMessage : internalFullMessage, state: AlertingDefaults.ALERT_STATE.firing, - /* continue to send "nodes" and "count" values for users before https://github.com/elastic/kibana/pull/102544 + /* continue to send "nodes" and "count" values for users before https://github.com/elastic/kibana/pull/102544 see https://github.com/elastic/kibana/issues/100136#issuecomment-865229431 */ nodes: `node: ${firingNode.nodeName}`, diff --git a/x-pack/plugins/monitoring/server/alerts/nodes_changed_rule.ts b/x-pack/plugins/monitoring/server/alerts/nodes_changed_rule.ts index 90bd70f32c8cb..6645466f30c73 100644 --- a/x-pack/plugins/monitoring/server/alerts/nodes_changed_rule.ts +++ b/x-pack/plugins/monitoring/server/alerts/nodes_changed_rule.ts @@ -114,7 +114,8 @@ export class NodesChangedRule extends BaseRule { const nodesFromClusterStats = await fetchNodesFromClusterStats( esClient, clusters, - esIndexPattern + esIndexPattern, + params.filterQuery ); return nodesFromClusterStats.map((nodes) => { const { removed, added, restarted } = getNodeStates(nodes); diff --git a/x-pack/plugins/monitoring/server/alerts/thread_pool_rejections_rule_base.ts b/x-pack/plugins/monitoring/server/alerts/thread_pool_rejections_rule_base.ts index c478b2f687c02..678f8b429167f 100644 --- a/x-pack/plugins/monitoring/server/alerts/thread_pool_rejections_rule_base.ts +++ b/x-pack/plugins/monitoring/server/alerts/thread_pool_rejections_rule_base.ts @@ -86,7 +86,8 @@ export class ThreadPoolRejectionsRuleBase extends BaseRule { esIndexPattern, Globals.app.config.ui.max_bucket_size, this.threadPoolType, - duration + duration, + params.filterQuery ); return stats.map((stat) => { @@ -257,7 +258,7 @@ export class ThreadPoolRejectionsRuleBase extends BaseRule { internalFullMessage: Globals.app.isCloud ? internalShortMessage : internalFullMessage, threadPoolType: type, state: AlertingDefaults.ALERT_STATE.firing, - /* continue to send "count" value for users before https://github.com/elastic/kibana/pull/102544 + /* continue to send "count" value for users before https://github.com/elastic/kibana/pull/102544 see https://github.com/elastic/kibana/issues/100136#issuecomment-865229431 */ count: 1, diff --git a/x-pack/plugins/monitoring/server/deprecations.test.js b/x-pack/plugins/monitoring/server/deprecations.test.js index 5aff34389207e..fe0c1850968e0 100644 --- a/x-pack/plugins/monitoring/server/deprecations.test.js +++ b/x-pack/plugins/monitoring/server/deprecations.test.js @@ -11,12 +11,13 @@ import { deprecations as deprecationsModule } from './deprecations'; // TODO: tests were not running before and are not up to date describe.skip('monitoring plugin deprecations', function () { let transformDeprecations; + const deprecate = jest.fn(() => jest.fn()); const rename = jest.fn(() => jest.fn()); const renameFromRoot = jest.fn(() => jest.fn()); const fromPath = 'monitoring'; beforeAll(function () { - const deprecations = deprecationsModule({ rename, renameFromRoot }); + const deprecations = deprecationsModule({ deprecate, rename, renameFromRoot }); transformDeprecations = (settings, fromPath, addDeprecation = noop) => { deprecations.forEach((deprecation) => deprecation(settings, fromPath, addDeprecation)); }; diff --git a/x-pack/plugins/monitoring/server/deprecations.ts b/x-pack/plugins/monitoring/server/deprecations.ts index ac1c2a0d7ac10..3554abd569581 100644 --- a/x-pack/plugins/monitoring/server/deprecations.ts +++ b/x-pack/plugins/monitoring/server/deprecations.ts @@ -18,10 +18,12 @@ import { CLUSTER_ALERTS_ADDRESS_CONFIG_KEY } from '../common/constants'; * @return {Array} array of rename operations and callback function for rename logging */ export const deprecations = ({ + deprecate, rename, renameFromRoot, }: ConfigDeprecationFactory): ConfigDeprecation[] => { return [ + deprecate('enabled', '8.0.0'), // 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'), diff --git a/x-pack/plugins/monitoring/server/kibana_monitoring/collectors/lib/fetch_stack_product_usage.ts b/x-pack/plugins/monitoring/server/kibana_monitoring/collectors/lib/fetch_stack_product_usage.ts index 527ed503c8faf..0d3aab8283688 100644 --- a/x-pack/plugins/monitoring/server/kibana_monitoring/collectors/lib/fetch_stack_product_usage.ts +++ b/x-pack/plugins/monitoring/server/kibana_monitoring/collectors/lib/fetch_stack_product_usage.ts @@ -10,7 +10,7 @@ import { ElasticsearchClient } from 'src/core/server'; import { estypes } from '@elastic/elasticsearch'; import { MonitoringConfig } from '../../../config'; // @ts-ignore -import { prefixIndexPattern } from '../../../lib/ccs_utils'; +import { prefixIndexPattern } from '../../../../common/ccs_utils'; import { StackProductUsage } from '../types'; interface ESResponse { diff --git a/x-pack/plugins/monitoring/server/kibana_monitoring/collectors/lib/get_stack_products_usage.ts b/x-pack/plugins/monitoring/server/kibana_monitoring/collectors/lib/get_stack_products_usage.ts index 7cce1b392112f..25a1892a9f38d 100644 --- a/x-pack/plugins/monitoring/server/kibana_monitoring/collectors/lib/get_stack_products_usage.ts +++ b/x-pack/plugins/monitoring/server/kibana_monitoring/collectors/lib/get_stack_products_usage.ts @@ -12,7 +12,7 @@ import { MonitoringConfig } from '../../../config'; // @ts-ignore import { getIndexPatterns } from '../../../lib/cluster/get_index_patterns'; // @ts-ignore -import { prefixIndexPattern } from '../../../lib/ccs_utils'; +import { prefixIndexPattern } from '../../../../common/ccs_utils'; import { INDEX_PATTERN_ELASTICSEARCH, INDEX_PATTERN_KIBANA, diff --git a/x-pack/plugins/monitoring/server/lib/alerts/fetch_ccr_read_exceptions.ts b/x-pack/plugins/monitoring/server/lib/alerts/fetch_ccr_read_exceptions.ts index 560751d1297d5..e7a5923207d60 100644 --- a/x-pack/plugins/monitoring/server/lib/alerts/fetch_ccr_read_exceptions.ts +++ b/x-pack/plugins/monitoring/server/lib/alerts/fetch_ccr_read_exceptions.ts @@ -14,7 +14,8 @@ export async function fetchCCRReadExceptions( index: string, startMs: number, endMs: number, - size: number + size: number, + filterQuery?: string ): Promise { const params = { index, @@ -93,6 +94,15 @@ export async function fetchCCRReadExceptions( }, }; + try { + if (filterQuery) { + const filterQueryObject = JSON.parse(filterQuery); + params.body.query.bool.filter.push(filterQueryObject); + } + } catch (e) { + // meh + } + const { body: response } = await esClient.search(params); const stats: CCRReadExceptionsStats[] = []; // @ts-expect-error declare aggegations type explicitly diff --git a/x-pack/plugins/monitoring/server/lib/alerts/fetch_cluster_health.ts b/x-pack/plugins/monitoring/server/lib/alerts/fetch_cluster_health.ts index 85bfbd9dbd049..b2004f0c7c710 100644 --- a/x-pack/plugins/monitoring/server/lib/alerts/fetch_cluster_health.ts +++ b/x-pack/plugins/monitoring/server/lib/alerts/fetch_cluster_health.ts @@ -11,7 +11,8 @@ import { ElasticsearchSource, ElasticsearchResponse } from '../../../common/type export async function fetchClusterHealth( esClient: ElasticsearchClient, clusters: AlertCluster[], - index: string + index: string, + filterQuery?: string ): Promise { const params = { index, @@ -59,6 +60,15 @@ export async function fetchClusterHealth( }, }; + try { + if (filterQuery) { + const filterQueryObject = JSON.parse(filterQuery); + params.body.query.bool.filter.push(filterQueryObject); + } + } catch (e) { + // meh + } + const result = await esClient.search(params); const response: ElasticsearchResponse = result.body as ElasticsearchResponse; return (response.hits?.hits ?? []).map((hit) => { diff --git a/x-pack/plugins/monitoring/server/lib/alerts/fetch_cpu_usage_node_stats.test.ts b/x-pack/plugins/monitoring/server/lib/alerts/fetch_cpu_usage_node_stats.test.ts index 90cd456f18037..8f0083f1f533f 100644 --- a/x-pack/plugins/monitoring/server/lib/alerts/fetch_cpu_usage_node_stats.test.ts +++ b/x-pack/plugins/monitoring/server/lib/alerts/fetch_cpu_usage_node_stats.test.ts @@ -201,7 +201,9 @@ describe('fetchCpuUsageNodeStats', () => { {} as estypes.SearchResponse ); }); - await fetchCpuUsageNodeStats(esClient, clusters, index, startMs, endMs, size); + const filterQuery = + '{"bool":{"should":[{"exists":{"field":"cluster_uuid"}}],"minimum_should_match":1}}'; + await fetchCpuUsageNodeStats(esClient, clusters, index, startMs, endMs, size, filterQuery); expect(params).toStrictEqual({ index: '.monitoring-es-*', filter_path: ['aggregations'], @@ -213,6 +215,9 @@ describe('fetchCpuUsageNodeStats', () => { { terms: { cluster_uuid: ['abc123'] } }, { term: { type: 'node_stats' } }, { range: { timestamp: { format: 'epoch_millis', gte: 0, lte: 0 } } }, + { + bool: { should: [{ exists: { field: 'cluster_uuid' } }], minimum_should_match: 1 }, + }, ], }, }, diff --git a/x-pack/plugins/monitoring/server/lib/alerts/fetch_cpu_usage_node_stats.ts b/x-pack/plugins/monitoring/server/lib/alerts/fetch_cpu_usage_node_stats.ts index 6f7d27916a7b1..2ad42870e9958 100644 --- a/x-pack/plugins/monitoring/server/lib/alerts/fetch_cpu_usage_node_stats.ts +++ b/x-pack/plugins/monitoring/server/lib/alerts/fetch_cpu_usage_node_stats.ts @@ -29,7 +29,8 @@ export async function fetchCpuUsageNodeStats( index: string, startMs: number, endMs: number, - size: number + size: number, + filterQuery?: string ): Promise { // Using pure MS didn't seem to work well with the date_histogram interval // but minutes does @@ -140,6 +141,15 @@ export async function fetchCpuUsageNodeStats( }, }; + try { + if (filterQuery) { + const filterQueryObject = JSON.parse(filterQuery); + params.body.query.bool.filter.push(filterQueryObject); + } + } catch (e) { + // meh + } + const { body: response } = await esClient.search(params); const stats: AlertCpuUsageNodeStats[] = []; const clusterBuckets = get( diff --git a/x-pack/plugins/monitoring/server/lib/alerts/fetch_disk_usage_node_stats.ts b/x-pack/plugins/monitoring/server/lib/alerts/fetch_disk_usage_node_stats.ts index 70f05991d4229..2d4872c0bd895 100644 --- a/x-pack/plugins/monitoring/server/lib/alerts/fetch_disk_usage_node_stats.ts +++ b/x-pack/plugins/monitoring/server/lib/alerts/fetch_disk_usage_node_stats.ts @@ -14,7 +14,8 @@ export async function fetchDiskUsageNodeStats( clusters: AlertCluster[], index: string, duration: string, - size: number + size: number, + filterQuery?: string ): Promise { const clustersIds = clusters.map((cluster) => cluster.clusterUuid); const params = { @@ -99,6 +100,15 @@ export async function fetchDiskUsageNodeStats( }, }; + try { + if (filterQuery) { + const filterQueryObject = JSON.parse(filterQuery); + params.body.query.bool.filter.push(filterQueryObject); + } + } catch (e) { + // meh + } + const { body: response } = await esClient.search(params); const stats: AlertDiskUsageNodeStats[] = []; // @ts-expect-error declare type for aggregations explicitly diff --git a/x-pack/plugins/monitoring/server/lib/alerts/fetch_elasticsearch_versions.ts b/x-pack/plugins/monitoring/server/lib/alerts/fetch_elasticsearch_versions.ts index f2f311ac870a5..6ca2e89048df9 100644 --- a/x-pack/plugins/monitoring/server/lib/alerts/fetch_elasticsearch_versions.ts +++ b/x-pack/plugins/monitoring/server/lib/alerts/fetch_elasticsearch_versions.ts @@ -12,7 +12,8 @@ export async function fetchElasticsearchVersions( esClient: ElasticsearchClient, clusters: AlertCluster[], index: string, - size: number + size: number, + filterQuery?: string ): Promise { const params = { index, @@ -60,6 +61,15 @@ export async function fetchElasticsearchVersions( }, }; + try { + if (filterQuery) { + const filterQueryObject = JSON.parse(filterQuery); + params.body.query.bool.filter.push(filterQueryObject); + } + } catch (e) { + // meh + } + const result = await esClient.search(params); const response: ElasticsearchResponse = result.body as ElasticsearchResponse; return (response.hits?.hits ?? []).map((hit) => { diff --git a/x-pack/plugins/monitoring/server/lib/alerts/fetch_index_shard_size.ts b/x-pack/plugins/monitoring/server/lib/alerts/fetch_index_shard_size.ts index 7e7ea5e6bfdd2..98bb546b43ab9 100644 --- a/x-pack/plugins/monitoring/server/lib/alerts/fetch_index_shard_size.ts +++ b/x-pack/plugins/monitoring/server/lib/alerts/fetch_index_shard_size.ts @@ -35,7 +35,8 @@ export async function fetchIndexShardSize( index: string, threshold: number, shardIndexPatterns: string, - size: number + size: number, + filterQuery?: string ): Promise { const params = { index, @@ -104,6 +105,15 @@ export async function fetchIndexShardSize( }, }; + try { + if (filterQuery) { + const filterQueryObject = JSON.parse(filterQuery); + params.body.query.bool.must.push(filterQueryObject); + } + } catch (e) { + // meh + } + const { body: response } = await esClient.search(params); // @ts-expect-error declare aggegations type explicitly const { buckets: clusterBuckets } = response.aggregations?.clusters; diff --git a/x-pack/plugins/monitoring/server/lib/alerts/fetch_kibana_versions.ts b/x-pack/plugins/monitoring/server/lib/alerts/fetch_kibana_versions.ts index e57b45e2570fa..71813f3a526de 100644 --- a/x-pack/plugins/monitoring/server/lib/alerts/fetch_kibana_versions.ts +++ b/x-pack/plugins/monitoring/server/lib/alerts/fetch_kibana_versions.ts @@ -16,7 +16,8 @@ export async function fetchKibanaVersions( esClient: ElasticsearchClient, clusters: AlertCluster[], index: string, - size: number + size: number, + filterQuery?: string ): Promise { const params = { index, @@ -89,6 +90,15 @@ export async function fetchKibanaVersions( }, }; + try { + if (filterQuery) { + const filterQueryObject = JSON.parse(filterQuery); + params.body.query.bool.filter.push(filterQueryObject); + } + } catch (e) { + // meh + } + const { body: response } = await esClient.search(params); const indexName = get(response, 'aggregations.index.buckets[0].key', ''); const clusterList = get(response, 'aggregations.cluster.buckets', []) as ESAggResponse[]; diff --git a/x-pack/plugins/monitoring/server/lib/alerts/fetch_licenses.ts b/x-pack/plugins/monitoring/server/lib/alerts/fetch_licenses.ts index 38ff82cf29832..b7bdf2fb6be72 100644 --- a/x-pack/plugins/monitoring/server/lib/alerts/fetch_licenses.ts +++ b/x-pack/plugins/monitoring/server/lib/alerts/fetch_licenses.ts @@ -11,7 +11,8 @@ import { ElasticsearchSource } from '../../../common/types/es'; export async function fetchLicenses( esClient: ElasticsearchClient, clusters: AlertCluster[], - index: string + index: string, + filterQuery?: string ): Promise { const params = { index, @@ -59,6 +60,15 @@ export async function fetchLicenses( }, }; + try { + if (filterQuery) { + const filterQueryObject = JSON.parse(filterQuery); + params.body.query.bool.filter.push(filterQueryObject); + } + } catch (e) { + // meh + } + const { body: response } = await esClient.search(params); return ( response?.hits?.hits.map((hit) => { diff --git a/x-pack/plugins/monitoring/server/lib/alerts/fetch_logstash_versions.ts b/x-pack/plugins/monitoring/server/lib/alerts/fetch_logstash_versions.ts index 774ee2551ec07..112c2fe798b10 100644 --- a/x-pack/plugins/monitoring/server/lib/alerts/fetch_logstash_versions.ts +++ b/x-pack/plugins/monitoring/server/lib/alerts/fetch_logstash_versions.ts @@ -16,7 +16,8 @@ export async function fetchLogstashVersions( esClient: ElasticsearchClient, clusters: AlertCluster[], index: string, - size: number + size: number, + filterQuery?: string ): Promise { const params = { index, @@ -89,6 +90,15 @@ export async function fetchLogstashVersions( }, }; + try { + if (filterQuery) { + const filterQueryObject = JSON.parse(filterQuery); + params.body.query.bool.filter.push(filterQueryObject); + } + } catch (e) { + // meh + } + const { body: response } = await esClient.search(params); const indexName = get(response, 'aggregations.index.buckets[0].key', ''); const clusterList = get(response, 'aggregations.cluster.buckets', []) as ESAggResponse[]; diff --git a/x-pack/plugins/monitoring/server/lib/alerts/fetch_memory_usage_node_stats.ts b/x-pack/plugins/monitoring/server/lib/alerts/fetch_memory_usage_node_stats.ts index f34a8dcff1db7..9403ae5d79a70 100644 --- a/x-pack/plugins/monitoring/server/lib/alerts/fetch_memory_usage_node_stats.ts +++ b/x-pack/plugins/monitoring/server/lib/alerts/fetch_memory_usage_node_stats.ts @@ -15,7 +15,8 @@ export async function fetchMemoryUsageNodeStats( index: string, startMs: number, endMs: number, - size: number + size: number, + filterQuery?: string ): Promise { const clustersIds = clusters.map((cluster) => cluster.clusterUuid); const params = { @@ -92,6 +93,15 @@ export async function fetchMemoryUsageNodeStats( }, }; + try { + if (filterQuery) { + const filterQueryObject = JSON.parse(filterQuery); + params.body.query.bool.filter.push(filterQueryObject); + } + } catch (e) { + // meh + } + const { body: response } = await esClient.search(params); const stats: AlertMemoryUsageNodeStats[] = []; // @ts-expect-error declare type for aggregations explicitly diff --git a/x-pack/plugins/monitoring/server/lib/alerts/fetch_missing_monitoring_data.ts b/x-pack/plugins/monitoring/server/lib/alerts/fetch_missing_monitoring_data.ts index 856ca7c919885..cdf0f21b52b09 100644 --- a/x-pack/plugins/monitoring/server/lib/alerts/fetch_missing_monitoring_data.ts +++ b/x-pack/plugins/monitoring/server/lib/alerts/fetch_missing_monitoring_data.ts @@ -47,7 +47,8 @@ export async function fetchMissingMonitoringData( index: string, size: number, nowInMs: number, - startMs: number + startMs: number, + filterQuery?: string ): Promise { const endMs = nowInMs; const params = { @@ -117,6 +118,15 @@ export async function fetchMissingMonitoringData( }, }; + try { + if (filterQuery) { + const filterQueryObject = JSON.parse(filterQuery); + params.body.query.bool.filter.push(filterQueryObject); + } + } catch (e) { + // meh + } + const { body: response } = await esClient.search(params); const clusterBuckets = get( response, diff --git a/x-pack/plugins/monitoring/server/lib/alerts/fetch_nodes_from_cluster_stats.ts b/x-pack/plugins/monitoring/server/lib/alerts/fetch_nodes_from_cluster_stats.ts index dcc8e6516c69b..3dc3e315318fc 100644 --- a/x-pack/plugins/monitoring/server/lib/alerts/fetch_nodes_from_cluster_stats.ts +++ b/x-pack/plugins/monitoring/server/lib/alerts/fetch_nodes_from_cluster_stats.ts @@ -26,7 +26,8 @@ function formatNode( export async function fetchNodesFromClusterStats( esClient: ElasticsearchClient, clusters: AlertCluster[], - index: string + index: string, + filterQuery?: string ): Promise { const params = { index, @@ -88,6 +89,15 @@ export async function fetchNodesFromClusterStats( }, }; + try { + if (filterQuery) { + const filterQueryObject = JSON.parse(filterQuery); + params.body.query.bool.filter.push(filterQueryObject); + } + } catch (e) { + // meh + } + const { body: response } = await esClient.search(params); const nodes: AlertClusterStatsNodes[] = []; // @ts-expect-error declare type for aggregations explicitly diff --git a/x-pack/plugins/monitoring/server/lib/alerts/fetch_thread_pool_rejections_stats.ts b/x-pack/plugins/monitoring/server/lib/alerts/fetch_thread_pool_rejections_stats.ts index 132f7692a7579..0d1d052b5f866 100644 --- a/x-pack/plugins/monitoring/server/lib/alerts/fetch_thread_pool_rejections_stats.ts +++ b/x-pack/plugins/monitoring/server/lib/alerts/fetch_thread_pool_rejections_stats.ts @@ -36,7 +36,8 @@ export async function fetchThreadPoolRejectionStats( index: string, size: number, threadType: string, - duration: string + duration: string, + filterQuery?: string ): Promise { const clustersIds = clusters.map((cluster) => cluster.clusterUuid); const params = { @@ -94,6 +95,15 @@ export async function fetchThreadPoolRejectionStats( }, }; + try { + if (filterQuery) { + const filterQueryObject = JSON.parse(filterQuery); + params.body.query.bool.filter.push(filterQueryObject); + } + } catch (e) { + // meh + } + const { body: response } = await esClient.search(params); const stats: AlertThreadPoolRejectionsStats[] = []; // @ts-expect-error declare type for aggregations explicitly diff --git a/x-pack/plugins/monitoring/server/lib/cluster/get_clusters_stats.ts b/x-pack/plugins/monitoring/server/lib/cluster/get_clusters_stats.ts index 6eb21165d7256..a2201ca958e35 100644 --- a/x-pack/plugins/monitoring/server/lib/cluster/get_clusters_stats.ts +++ b/x-pack/plugins/monitoring/server/lib/cluster/get_clusters_stats.ts @@ -12,7 +12,7 @@ import { createQuery } from '../create_query'; // @ts-ignore import { ElasticsearchMetric } from '../metrics'; // @ts-ignore -import { parseCrossClusterPrefix } from '../ccs_utils'; +import { parseCrossClusterPrefix } from '../../../common/ccs_utils'; import { getClustersState } from './get_clusters_state'; import { ElasticsearchResponse, ElasticsearchModifiedSource } from '../../../common/types/es'; import { LegacyRequest } from '../../types'; diff --git a/x-pack/plugins/monitoring/server/lib/cluster/get_index_patterns.ts b/x-pack/plugins/monitoring/server/lib/cluster/get_index_patterns.ts index d908d6180772e..ccfe380edec09 100644 --- a/x-pack/plugins/monitoring/server/lib/cluster/get_index_patterns.ts +++ b/x-pack/plugins/monitoring/server/lib/cluster/get_index_patterns.ts @@ -6,7 +6,7 @@ */ import { LegacyServer } from '../../types'; -import { prefixIndexPattern } from '../ccs_utils'; +import { prefixIndexPattern } from '../../../common/ccs_utils'; import { INDEX_PATTERN_ELASTICSEARCH, INDEX_PATTERN_KIBANA, diff --git a/x-pack/plugins/monitoring/server/lib/logs/init_infra_source.ts b/x-pack/plugins/monitoring/server/lib/logs/init_infra_source.ts index c0fa931676870..727e47b62bc92 100644 --- a/x-pack/plugins/monitoring/server/lib/logs/init_infra_source.ts +++ b/x-pack/plugins/monitoring/server/lib/logs/init_infra_source.ts @@ -6,7 +6,7 @@ */ // @ts-ignore -import { prefixIndexPattern } from '../ccs_utils'; +import { prefixIndexPattern } from '../../../common/ccs_utils'; import { INFRA_SOURCE_ID } from '../../../common/constants'; import { MonitoringConfig } from '../../config'; import { InfraPluginSetup } from '../../../../infra/server'; diff --git a/x-pack/plugins/monitoring/server/plugin.ts b/x-pack/plugins/monitoring/server/plugin.ts index 7990f9485c19c..3f19a8d9b79b9 100644 --- a/x-pack/plugins/monitoring/server/plugin.ts +++ b/x-pack/plugins/monitoring/server/plugin.ts @@ -210,7 +210,7 @@ export class MonitoringPlugin } } - async start(coreStart: CoreStart, { licensing }: PluginsStart) { + start(coreStart: CoreStart, { licensing }: PluginsStart) { const config = this.config!; this.cluster = instantiateClient( config.ui.elasticsearch, diff --git a/x-pack/plugins/monitoring/server/routes/api/v1/apm/instance.js b/x-pack/plugins/monitoring/server/routes/api/v1/apm/instance.js index 4884b8151f61f..a0b00167101fe 100644 --- a/x-pack/plugins/monitoring/server/routes/api/v1/apm/instance.js +++ b/x-pack/plugins/monitoring/server/routes/api/v1/apm/instance.js @@ -6,7 +6,7 @@ */ import { schema } from '@kbn/config-schema'; -import { prefixIndexPattern } from '../../../../lib/ccs_utils'; +import { prefixIndexPattern } from '../../../../../common/ccs_utils'; import { getMetrics } from '../../../../lib/details/get_metrics'; import { metricSet } from './metric_set_instance'; import { handleError } from '../../../../lib/errors'; diff --git a/x-pack/plugins/monitoring/server/routes/api/v1/apm/instances.js b/x-pack/plugins/monitoring/server/routes/api/v1/apm/instances.js index 53afa4c3f01b4..95f378ff5b98d 100644 --- a/x-pack/plugins/monitoring/server/routes/api/v1/apm/instances.js +++ b/x-pack/plugins/monitoring/server/routes/api/v1/apm/instances.js @@ -6,7 +6,7 @@ */ import { schema } from '@kbn/config-schema'; -import { prefixIndexPattern } from '../../../../lib/ccs_utils'; +import { prefixIndexPattern } from '../../../../../common/ccs_utils'; import { getStats, getApms } from '../../../../lib/apm'; import { handleError } from '../../../../lib/errors'; import { INDEX_PATTERN_BEATS } from '../../../../../common/constants'; diff --git a/x-pack/plugins/monitoring/server/routes/api/v1/apm/overview.js b/x-pack/plugins/monitoring/server/routes/api/v1/apm/overview.js index 7a772594b4bc2..ea7f3f41b842e 100644 --- a/x-pack/plugins/monitoring/server/routes/api/v1/apm/overview.js +++ b/x-pack/plugins/monitoring/server/routes/api/v1/apm/overview.js @@ -6,7 +6,7 @@ */ import { schema } from '@kbn/config-schema'; -import { prefixIndexPattern } from '../../../../lib/ccs_utils'; +import { prefixIndexPattern } from '../../../../../common/ccs_utils'; import { getMetrics } from '../../../../lib/details/get_metrics'; import { metricSet } from './metric_set_overview'; import { handleError } from '../../../../lib/errors'; diff --git a/x-pack/plugins/monitoring/server/routes/api/v1/beats/beat_detail.js b/x-pack/plugins/monitoring/server/routes/api/v1/beats/beat_detail.js index 919efe98f3df3..851380fede77d 100644 --- a/x-pack/plugins/monitoring/server/routes/api/v1/beats/beat_detail.js +++ b/x-pack/plugins/monitoring/server/routes/api/v1/beats/beat_detail.js @@ -6,7 +6,7 @@ */ import { schema } from '@kbn/config-schema'; -import { prefixIndexPattern } from '../../../../lib/ccs_utils'; +import { prefixIndexPattern } from '../../../../../common/ccs_utils'; import { getBeatSummary } from '../../../../lib/beats'; import { getMetrics } from '../../../../lib/details/get_metrics'; import { handleError } from '../../../../lib/errors'; diff --git a/x-pack/plugins/monitoring/server/routes/api/v1/beats/beats.js b/x-pack/plugins/monitoring/server/routes/api/v1/beats/beats.js index 57b24e59e66ab..fa35ccb9371c2 100644 --- a/x-pack/plugins/monitoring/server/routes/api/v1/beats/beats.js +++ b/x-pack/plugins/monitoring/server/routes/api/v1/beats/beats.js @@ -6,7 +6,7 @@ */ import { schema } from '@kbn/config-schema'; -import { prefixIndexPattern } from '../../../../lib/ccs_utils'; +import { prefixIndexPattern } from '../../../../../common/ccs_utils'; import { getStats, getBeats } from '../../../../lib/beats'; import { handleError } from '../../../../lib/errors'; import { INDEX_PATTERN_BEATS } from '../../../../../common/constants'; diff --git a/x-pack/plugins/monitoring/server/routes/api/v1/beats/overview.js b/x-pack/plugins/monitoring/server/routes/api/v1/beats/overview.js index 5f1bb1778bc9a..4abf46b3ad1ce 100644 --- a/x-pack/plugins/monitoring/server/routes/api/v1/beats/overview.js +++ b/x-pack/plugins/monitoring/server/routes/api/v1/beats/overview.js @@ -6,7 +6,7 @@ */ import { schema } from '@kbn/config-schema'; -import { prefixIndexPattern } from '../../../../lib/ccs_utils'; +import { prefixIndexPattern } from '../../../../../common/ccs_utils'; import { getMetrics } from '../../../../lib/details/get_metrics'; import { getLatestStats, getStats } from '../../../../lib/beats'; import { handleError } from '../../../../lib/errors'; diff --git a/x-pack/plugins/monitoring/server/routes/api/v1/elasticsearch/ccr.ts b/x-pack/plugins/monitoring/server/routes/api/v1/elasticsearch/ccr.ts index 73b646126ce98..898cfc82463d9 100644 --- a/x-pack/plugins/monitoring/server/routes/api/v1/elasticsearch/ccr.ts +++ b/x-pack/plugins/monitoring/server/routes/api/v1/elasticsearch/ccr.ts @@ -11,7 +11,7 @@ import { get, groupBy } from 'lodash'; // @ts-ignore import { handleError } from '../../../../lib/errors/handle_error'; // @ts-ignore -import { prefixIndexPattern } from '../../../../lib/ccs_utils'; +import { prefixIndexPattern } from '../../../../../common/ccs_utils'; import { INDEX_PATTERN_ELASTICSEARCH } from '../../../../../common/constants'; import { ElasticsearchResponse, diff --git a/x-pack/plugins/monitoring/server/routes/api/v1/elasticsearch/ccr_shard.ts b/x-pack/plugins/monitoring/server/routes/api/v1/elasticsearch/ccr_shard.ts index 5ecb84d97618b..d07a660222407 100644 --- a/x-pack/plugins/monitoring/server/routes/api/v1/elasticsearch/ccr_shard.ts +++ b/x-pack/plugins/monitoring/server/routes/api/v1/elasticsearch/ccr_shard.ts @@ -10,7 +10,7 @@ import { schema } from '@kbn/config-schema'; // @ts-ignore import { handleError } from '../../../../lib/errors/handle_error'; // @ts-ignore -import { prefixIndexPattern } from '../../../../lib/ccs_utils'; +import { prefixIndexPattern } from '../../../../../common/ccs_utils'; // @ts-ignore import { getMetrics } from '../../../../lib/details/get_metrics'; import { INDEX_PATTERN_ELASTICSEARCH } from '../../../../../common/constants'; diff --git a/x-pack/plugins/monitoring/server/routes/api/v1/elasticsearch/index_detail.js b/x-pack/plugins/monitoring/server/routes/api/v1/elasticsearch/index_detail.js index 89ca911f44268..e99ae04ab282c 100644 --- a/x-pack/plugins/monitoring/server/routes/api/v1/elasticsearch/index_detail.js +++ b/x-pack/plugins/monitoring/server/routes/api/v1/elasticsearch/index_detail.js @@ -12,7 +12,7 @@ import { getIndexSummary } from '../../../../lib/elasticsearch/indices'; import { getMetrics } from '../../../../lib/details/get_metrics'; import { getShardAllocation, getShardStats } from '../../../../lib/elasticsearch/shards'; import { handleError } from '../../../../lib/errors/handle_error'; -import { prefixIndexPattern } from '../../../../lib/ccs_utils'; +import { prefixIndexPattern } from '../../../../../common/ccs_utils'; import { metricSet } from './metric_set_index_detail'; import { INDEX_PATTERN_ELASTICSEARCH } from '../../../../../common/constants'; import { getLogs } from '../../../../lib/logs/get_logs'; diff --git a/x-pack/plugins/monitoring/server/routes/api/v1/elasticsearch/indices.js b/x-pack/plugins/monitoring/server/routes/api/v1/elasticsearch/indices.js index 8099ecf3462cc..76e769ac030ba 100644 --- a/x-pack/plugins/monitoring/server/routes/api/v1/elasticsearch/indices.js +++ b/x-pack/plugins/monitoring/server/routes/api/v1/elasticsearch/indices.js @@ -10,7 +10,7 @@ import { getClusterStats } from '../../../../lib/cluster/get_cluster_stats'; import { getClusterStatus } from '../../../../lib/cluster/get_cluster_status'; import { getIndices } from '../../../../lib/elasticsearch/indices'; import { handleError } from '../../../../lib/errors/handle_error'; -import { prefixIndexPattern } from '../../../../lib/ccs_utils'; +import { prefixIndexPattern } from '../../../../../common/ccs_utils'; import { INDEX_PATTERN_ELASTICSEARCH } from '../../../../../common/constants'; import { getIndicesUnassignedShardStats } from '../../../../lib/elasticsearch/shards/get_indices_unassigned_shard_stats'; diff --git a/x-pack/plugins/monitoring/server/routes/api/v1/elasticsearch/ml_jobs.js b/x-pack/plugins/monitoring/server/routes/api/v1/elasticsearch/ml_jobs.js index e23c23f7a819d..5853cc3d6ee9d 100644 --- a/x-pack/plugins/monitoring/server/routes/api/v1/elasticsearch/ml_jobs.js +++ b/x-pack/plugins/monitoring/server/routes/api/v1/elasticsearch/ml_jobs.js @@ -10,7 +10,7 @@ import { getClusterStats } from '../../../../lib/cluster/get_cluster_stats'; import { getClusterStatus } from '../../../../lib/cluster/get_cluster_status'; import { getMlJobs } from '../../../../lib/elasticsearch/get_ml_jobs'; import { handleError } from '../../../../lib/errors/handle_error'; -import { prefixIndexPattern } from '../../../../lib/ccs_utils'; +import { prefixIndexPattern } from '../../../../../common/ccs_utils'; import { INDEX_PATTERN_ELASTICSEARCH } from '../../../../../common/constants'; import { getIndicesUnassignedShardStats } from '../../../../lib/elasticsearch/shards/get_indices_unassigned_shard_stats'; diff --git a/x-pack/plugins/monitoring/server/routes/api/v1/elasticsearch/node_detail.js b/x-pack/plugins/monitoring/server/routes/api/v1/elasticsearch/node_detail.js index 2122f8ceb2215..5f77d0394a4f1 100644 --- a/x-pack/plugins/monitoring/server/routes/api/v1/elasticsearch/node_detail.js +++ b/x-pack/plugins/monitoring/server/routes/api/v1/elasticsearch/node_detail.js @@ -12,7 +12,7 @@ import { getNodeSummary } from '../../../../lib/elasticsearch/nodes'; import { getShardStats, getShardAllocation } from '../../../../lib/elasticsearch/shards'; import { getMetrics } from '../../../../lib/details/get_metrics'; import { handleError } from '../../../../lib/errors/handle_error'; -import { prefixIndexPattern } from '../../../../lib/ccs_utils'; +import { prefixIndexPattern } from '../../../../../common/ccs_utils'; import { metricSets } from './metric_set_node_detail'; import { INDEX_PATTERN_ELASTICSEARCH } from '../../../../../common/constants'; import { getLogs } from '../../../../lib/logs/get_logs'; diff --git a/x-pack/plugins/monitoring/server/routes/api/v1/elasticsearch/nodes.js b/x-pack/plugins/monitoring/server/routes/api/v1/elasticsearch/nodes.js index db12e28916b65..7ea2e6e1e1440 100644 --- a/x-pack/plugins/monitoring/server/routes/api/v1/elasticsearch/nodes.js +++ b/x-pack/plugins/monitoring/server/routes/api/v1/elasticsearch/nodes.js @@ -11,7 +11,7 @@ import { getClusterStatus } from '../../../../lib/cluster/get_cluster_status'; import { getNodes } from '../../../../lib/elasticsearch/nodes'; import { getNodesShardCount } from '../../../../lib/elasticsearch/shards/get_nodes_shard_count'; import { handleError } from '../../../../lib/errors/handle_error'; -import { prefixIndexPattern } from '../../../../lib/ccs_utils'; +import { prefixIndexPattern } from '../../../../../common/ccs_utils'; import { INDEX_PATTERN_ELASTICSEARCH } from '../../../../../common/constants'; import { getPaginatedNodes } from '../../../../lib/elasticsearch/nodes/get_nodes/get_paginated_nodes'; import { LISTING_METRICS_NAMES } from '../../../../lib/elasticsearch/nodes/get_nodes/nodes_listing_metrics'; diff --git a/x-pack/plugins/monitoring/server/routes/api/v1/elasticsearch/overview.js b/x-pack/plugins/monitoring/server/routes/api/v1/elasticsearch/overview.js index c76513df721ba..a0fc524768eb9 100644 --- a/x-pack/plugins/monitoring/server/routes/api/v1/elasticsearch/overview.js +++ b/x-pack/plugins/monitoring/server/routes/api/v1/elasticsearch/overview.js @@ -11,7 +11,7 @@ import { getClusterStatus } from '../../../../lib/cluster/get_cluster_status'; import { getLastRecovery } from '../../../../lib/elasticsearch/get_last_recovery'; import { getMetrics } from '../../../../lib/details/get_metrics'; import { handleError } from '../../../../lib/errors/handle_error'; -import { prefixIndexPattern } from '../../../../lib/ccs_utils'; +import { prefixIndexPattern } from '../../../../../common/ccs_utils'; import { metricSet } from './metric_set_overview'; import { INDEX_PATTERN_ELASTICSEARCH } from '../../../../../common/constants'; import { getLogs } from '../../../../lib/logs'; diff --git a/x-pack/plugins/monitoring/server/routes/api/v1/elasticsearch_settings/check/internal_monitoring.ts b/x-pack/plugins/monitoring/server/routes/api/v1/elasticsearch_settings/check/internal_monitoring.ts index d05d60866d119..3cd2b8b73b315 100644 --- a/x-pack/plugins/monitoring/server/routes/api/v1/elasticsearch_settings/check/internal_monitoring.ts +++ b/x-pack/plugins/monitoring/server/routes/api/v1/elasticsearch_settings/check/internal_monitoring.ts @@ -14,7 +14,7 @@ import { INDEX_PATTERN_LOGSTASH, } from '../../../../../../common/constants'; // @ts-ignore -import { prefixIndexPattern } from '../../../../../lib/ccs_utils'; +import { prefixIndexPattern } from '../../../../../../common/ccs_utils'; // @ts-ignore import { handleError } from '../../../../../lib/errors'; import { RouteDependencies, LegacyServer } from '../../../../../types'; diff --git a/x-pack/plugins/monitoring/server/routes/api/v1/kibana/instance.ts b/x-pack/plugins/monitoring/server/routes/api/v1/kibana/instance.ts index d16f568b475b4..613ca39275c2d 100644 --- a/x-pack/plugins/monitoring/server/routes/api/v1/kibana/instance.ts +++ b/x-pack/plugins/monitoring/server/routes/api/v1/kibana/instance.ts @@ -12,7 +12,7 @@ import { handleError } from '../../../../lib/errors'; // @ts-ignore import { getMetrics } from '../../../../lib/details/get_metrics'; // @ts-ignore -import { prefixIndexPattern } from '../../../../lib/ccs_utils'; +import { prefixIndexPattern } from '../../../../../common/ccs_utils'; // @ts-ignore import { metricSet } from './metric_set_instance'; import { INDEX_PATTERN_KIBANA } from '../../../../../common/constants'; diff --git a/x-pack/plugins/monitoring/server/routes/api/v1/kibana/instances.js b/x-pack/plugins/monitoring/server/routes/api/v1/kibana/instances.js index 59618f0a217b5..f9b3498cd684e 100644 --- a/x-pack/plugins/monitoring/server/routes/api/v1/kibana/instances.js +++ b/x-pack/plugins/monitoring/server/routes/api/v1/kibana/instances.js @@ -6,7 +6,7 @@ */ import { schema } from '@kbn/config-schema'; -import { prefixIndexPattern } from '../../../../lib/ccs_utils'; +import { prefixIndexPattern } from '../../../../../common/ccs_utils'; import { getKibanaClusterStatus } from './_get_kibana_cluster_status'; import { getKibanas } from '../../../../lib/kibana/get_kibanas'; import { handleError } from '../../../../lib/errors'; diff --git a/x-pack/plugins/monitoring/server/routes/api/v1/kibana/overview.js b/x-pack/plugins/monitoring/server/routes/api/v1/kibana/overview.js index cca36d2aad1a7..f9a9443c3533b 100644 --- a/x-pack/plugins/monitoring/server/routes/api/v1/kibana/overview.js +++ b/x-pack/plugins/monitoring/server/routes/api/v1/kibana/overview.js @@ -6,7 +6,7 @@ */ import { schema } from '@kbn/config-schema'; -import { prefixIndexPattern } from '../../../../lib/ccs_utils'; +import { prefixIndexPattern } from '../../../../../common/ccs_utils'; import { getKibanaClusterStatus } from './_get_kibana_cluster_status'; import { getMetrics } from '../../../../lib/details/get_metrics'; import { metricSet } from './metric_set_overview'; diff --git a/x-pack/plugins/monitoring/server/routes/api/v1/logstash/node.js b/x-pack/plugins/monitoring/server/routes/api/v1/logstash/node.js index b81b4ea796c63..d3ecea95430ca 100644 --- a/x-pack/plugins/monitoring/server/routes/api/v1/logstash/node.js +++ b/x-pack/plugins/monitoring/server/routes/api/v1/logstash/node.js @@ -9,7 +9,7 @@ import { schema } from '@kbn/config-schema'; import { getNodeInfo } from '../../../../lib/logstash/get_node_info'; import { handleError } from '../../../../lib/errors'; import { getMetrics } from '../../../../lib/details/get_metrics'; -import { prefixIndexPattern } from '../../../../lib/ccs_utils'; +import { prefixIndexPattern } from '../../../../../common/ccs_utils'; import { metricSets } from './metric_set_node'; import { INDEX_PATTERN_LOGSTASH } from '../../../../../common/constants'; diff --git a/x-pack/plugins/monitoring/server/routes/api/v1/logstash/nodes.js b/x-pack/plugins/monitoring/server/routes/api/v1/logstash/nodes.js index 74b89ab41be92..051fb7d38fd41 100644 --- a/x-pack/plugins/monitoring/server/routes/api/v1/logstash/nodes.js +++ b/x-pack/plugins/monitoring/server/routes/api/v1/logstash/nodes.js @@ -9,7 +9,7 @@ import { schema } from '@kbn/config-schema'; import { getClusterStatus } from '../../../../lib/logstash/get_cluster_status'; import { getNodes } from '../../../../lib/logstash/get_nodes'; import { handleError } from '../../../../lib/errors'; -import { prefixIndexPattern } from '../../../../lib/ccs_utils'; +import { prefixIndexPattern } from '../../../../../common/ccs_utils'; import { INDEX_PATTERN_LOGSTASH } from '../../../../../common/constants'; /* diff --git a/x-pack/plugins/monitoring/server/routes/api/v1/logstash/overview.js b/x-pack/plugins/monitoring/server/routes/api/v1/logstash/overview.js index 23dd64a1afb74..89a6a93fb207d 100644 --- a/x-pack/plugins/monitoring/server/routes/api/v1/logstash/overview.js +++ b/x-pack/plugins/monitoring/server/routes/api/v1/logstash/overview.js @@ -9,7 +9,7 @@ import { schema } from '@kbn/config-schema'; import { getClusterStatus } from '../../../../lib/logstash/get_cluster_status'; import { getMetrics } from '../../../../lib/details/get_metrics'; import { handleError } from '../../../../lib/errors'; -import { prefixIndexPattern } from '../../../../lib/ccs_utils'; +import { prefixIndexPattern } from '../../../../../common/ccs_utils'; import { metricSet } from './metric_set_overview'; import { INDEX_PATTERN_LOGSTASH } from '../../../../../common/constants'; diff --git a/x-pack/plugins/monitoring/server/routes/api/v1/logstash/pipeline.js b/x-pack/plugins/monitoring/server/routes/api/v1/logstash/pipeline.js index 4243b2d6c3a5c..6b81059f0c256 100644 --- a/x-pack/plugins/monitoring/server/routes/api/v1/logstash/pipeline.js +++ b/x-pack/plugins/monitoring/server/routes/api/v1/logstash/pipeline.js @@ -10,7 +10,7 @@ import { handleError } from '../../../../lib/errors'; import { getPipelineVersions } from '../../../../lib/logstash/get_pipeline_versions'; import { getPipeline } from '../../../../lib/logstash/get_pipeline'; import { getPipelineVertex } from '../../../../lib/logstash/get_pipeline_vertex'; -import { prefixIndexPattern } from '../../../../lib/ccs_utils'; +import { prefixIndexPattern } from '../../../../../common/ccs_utils'; import { INDEX_PATTERN_LOGSTASH } from '../../../../../common/constants'; function getPipelineVersion(versions, pipelineHash) { diff --git a/x-pack/plugins/monitoring/server/routes/api/v1/logstash/pipelines/cluster_pipeline_ids.js b/x-pack/plugins/monitoring/server/routes/api/v1/logstash/pipelines/cluster_pipeline_ids.js index c881ff7b3d23c..7f14b74da207d 100644 --- a/x-pack/plugins/monitoring/server/routes/api/v1/logstash/pipelines/cluster_pipeline_ids.js +++ b/x-pack/plugins/monitoring/server/routes/api/v1/logstash/pipelines/cluster_pipeline_ids.js @@ -7,7 +7,7 @@ import { schema } from '@kbn/config-schema'; import { handleError } from '../../../../../lib/errors'; -import { prefixIndexPattern } from '../../../../../lib/ccs_utils'; +import { prefixIndexPattern } from '../../../../../../common/ccs_utils'; import { INDEX_PATTERN_LOGSTASH } from '../../../../../../common/constants'; import { getLogstashPipelineIds } from '../../../../../lib/logstash/get_pipeline_ids'; diff --git a/x-pack/plugins/monitoring/server/routes/api/v1/logstash/pipelines/cluster_pipelines.js b/x-pack/plugins/monitoring/server/routes/api/v1/logstash/pipelines/cluster_pipelines.js index 1f7a5e1d436b1..b7d86e86e7a07 100644 --- a/x-pack/plugins/monitoring/server/routes/api/v1/logstash/pipelines/cluster_pipelines.js +++ b/x-pack/plugins/monitoring/server/routes/api/v1/logstash/pipelines/cluster_pipelines.js @@ -8,7 +8,7 @@ import { schema } from '@kbn/config-schema'; import { getClusterStatus } from '../../../../../lib/logstash/get_cluster_status'; import { handleError } from '../../../../../lib/errors'; -import { prefixIndexPattern } from '../../../../../lib/ccs_utils'; +import { prefixIndexPattern } from '../../../../../../common/ccs_utils'; import { INDEX_PATTERN_LOGSTASH } from '../../../../../../common/constants'; import { getPaginatedPipelines } from '../../../../../lib/logstash/get_paginated_pipelines'; diff --git a/x-pack/plugins/monitoring/server/routes/api/v1/logstash/pipelines/node_pipelines.js b/x-pack/plugins/monitoring/server/routes/api/v1/logstash/pipelines/node_pipelines.js index 47b8fd81a4d44..f31e88b5b8b08 100644 --- a/x-pack/plugins/monitoring/server/routes/api/v1/logstash/pipelines/node_pipelines.js +++ b/x-pack/plugins/monitoring/server/routes/api/v1/logstash/pipelines/node_pipelines.js @@ -8,7 +8,7 @@ import { schema } from '@kbn/config-schema'; import { getNodeInfo } from '../../../../../lib/logstash/get_node_info'; import { handleError } from '../../../../../lib/errors'; -import { prefixIndexPattern } from '../../../../../lib/ccs_utils'; +import { prefixIndexPattern } from '../../../../../../common/ccs_utils'; import { INDEX_PATTERN_LOGSTASH } from '../../../../../../common/constants'; import { getPaginatedPipelines } from '../../../../../lib/logstash/get_paginated_pipelines'; diff --git a/x-pack/plugins/observability/kibana.json b/x-pack/plugins/observability/kibana.json index 45fe0258dd142..07299f2e6ff1c 100644 --- a/x-pack/plugins/observability/kibana.json +++ b/x-pack/plugins/observability/kibana.json @@ -15,6 +15,7 @@ "home", "lens", "licensing", + "spaces", "usageCollection" ], "requiredPlugins": [ diff --git a/x-pack/plugins/observability/server/index.ts b/x-pack/plugins/observability/server/index.ts index 9a62602859c54..97a17b0d11153 100644 --- a/x-pack/plugins/observability/server/index.ts +++ b/x-pack/plugins/observability/server/index.ts @@ -9,7 +9,7 @@ /* eslint-disable @kbn/eslint/no_export_all */ import { schema, TypeOf } from '@kbn/config-schema'; -import { PluginInitializerContext } from 'src/core/server'; +import { PluginConfigDescriptor, PluginInitializerContext } from 'src/core/server'; import { ObservabilityPlugin, ObservabilityPluginSetup } from './plugin'; import { createOrUpdateIndex, Mappings } from './utils/create_or_update_index'; import { ScopedAnnotationsClient } from './lib/annotations/bootstrap_annotations'; @@ -18,9 +18,9 @@ export { rangeQuery, kqlQuery } from './utils/queries'; export * from './types'; -export const config = { +export const config: PluginConfigDescriptor = { exposeToBrowser: { - unsafe: { alertingExperience: { enabled: true }, cases: { enabled: true } }, + unsafe: true, }, schema: schema.object({ enabled: schema.boolean({ defaultValue: true }), @@ -33,6 +33,7 @@ export const config = { cases: schema.object({ enabled: schema.boolean({ defaultValue: false }) }), }), }), + deprecations: ({ deprecate }) => [deprecate('enabled', '8.0.0')], }; export type ObservabilityConfig = TypeOf; diff --git a/x-pack/plugins/observability/tsconfig.json b/x-pack/plugins/observability/tsconfig.json index 4e912ee4535b8..dc935f3f77787 100644 --- a/x-pack/plugins/observability/tsconfig.json +++ b/x-pack/plugins/observability/tsconfig.json @@ -26,6 +26,7 @@ { "path": "../cases/tsconfig.json" }, { "path": "../lens/tsconfig.json" }, { "path": "../rule_registry/tsconfig.json" }, + { "path": "../spaces/tsconfig.json" }, { "path": "../timelines/tsconfig.json"}, { "path": "../translations/tsconfig.json" } ] diff --git a/x-pack/plugins/osquery/server/index.ts b/x-pack/plugins/osquery/server/index.ts index 30bc5ed5bd835..385515c285093 100644 --- a/x-pack/plugins/osquery/server/index.ts +++ b/x-pack/plugins/osquery/server/index.ts @@ -5,11 +5,12 @@ * 2.0. */ -import { PluginInitializerContext } from '../../../../src/core/server'; +import { PluginConfigDescriptor, PluginInitializerContext } from '../../../../src/core/server'; import { OsqueryPlugin } from './plugin'; -import { ConfigSchema } from './config'; +import { ConfigSchema, ConfigType } from './config'; -export const config = { +export const config: PluginConfigDescriptor = { + deprecations: ({ deprecate }) => [deprecate('enabled', '8.0.0')], schema: ConfigSchema, exposeToBrowser: { enabled: true, diff --git a/x-pack/plugins/remote_clusters/server/config.ts b/x-pack/plugins/remote_clusters/server/config.ts index e0fadea5d41f7..8f379ec5613c8 100644 --- a/x-pack/plugins/remote_clusters/server/config.ts +++ b/x-pack/plugins/remote_clusters/server/config.ts @@ -18,6 +18,7 @@ export const configSchema = schema.object({ export type ConfigType = TypeOf; export const config: PluginConfigDescriptor = { + deprecations: ({ deprecate }) => [deprecate('enabled', '8.0.0')], schema: configSchema, exposeToBrowser: { ui: true, diff --git a/x-pack/plugins/reporting/public/panel_actions/get_csv_panel_action.test.ts b/x-pack/plugins/reporting/public/panel_actions/get_csv_panel_action.test.ts index ceb3ed180cd3b..bedf310725ae2 100644 --- a/x-pack/plugins/reporting/public/panel_actions/get_csv_panel_action.test.ts +++ b/x-pack/plugins/reporting/public/panel_actions/get_csv_panel_action.test.ts @@ -11,8 +11,10 @@ import { CoreStart } from 'src/core/public'; import type { SearchSource } from 'src/plugins/data/common'; import type { SavedSearch } from 'src/plugins/discover/public'; import { coreMock } from '../../../../../src/core/public/mocks'; +import { dataPluginMock } from '../../../../../src/plugins/data/public/mocks'; import type { ILicense, LicensingPluginSetup } from '../../../licensing/public'; import { ReportingAPIClient } from '../lib/reporting_api_client'; +import type { ReportingPublicPluginStartDendencies } from '../plugin'; import type { ActionContext } from './get_csv_panel_action'; import { ReportingCsvPanelAction } from './get_csv_panel_action'; @@ -25,8 +27,8 @@ describe('GetCsvReportPanelAction', () => { let context: ActionContext; let mockLicense$: (state?: LicenseResults) => Rx.Observable; let mockSearchSource: SearchSource; - let mockStartServicesPayload: [CoreStart, object, unknown]; - let mockStartServices$: Rx.Subject; + let mockStartServicesPayload: [CoreStart, ReportingPublicPluginStartDendencies, unknown]; + let mockStartServices$: Rx.Observable; beforeAll(() => { if (typeof window.URL.revokeObjectURL === 'undefined') { @@ -48,14 +50,17 @@ describe('GetCsvReportPanelAction', () => { }) as unknown as LicensingPluginSetup['license$']; }; - mockStartServices$ = new Rx.Subject<[CoreStart, object, unknown]>(); mockStartServicesPayload = [ { + ...core, application: { capabilities: { dashboard: { downloadCsv: true } } }, } as unknown as CoreStart, - {}, + { + data: dataPluginMock.createStartContract(), + } as ReportingPublicPluginStartDendencies, null, ]; + mockStartServices$ = Rx.from(Promise.resolve(mockStartServicesPayload)); mockSearchSource = { createCopy: () => mockSearchSource, @@ -93,7 +98,7 @@ describe('GetCsvReportPanelAction', () => { usesUiCapabilities: true, }); - mockStartServices$.next(mockStartServicesPayload); + await mockStartServices$.pipe(first()).toPromise(); await panel.execute(context); @@ -130,7 +135,7 @@ describe('GetCsvReportPanelAction', () => { usesUiCapabilities: true, }); - mockStartServices$.next(mockStartServicesPayload); + await mockStartServices$.pipe(first()).toPromise(); await panel.execute(context); @@ -153,7 +158,7 @@ describe('GetCsvReportPanelAction', () => { usesUiCapabilities: true, }); - mockStartServices$.next(mockStartServicesPayload); + await mockStartServices$.pipe(first()).toPromise(); await panel.execute(context); @@ -169,7 +174,7 @@ describe('GetCsvReportPanelAction', () => { usesUiCapabilities: true, }); - mockStartServices$.next(mockStartServicesPayload); + await mockStartServices$.pipe(first()).toPromise(); await panel.execute(context); @@ -187,7 +192,7 @@ describe('GetCsvReportPanelAction', () => { usesUiCapabilities: true, }); - mockStartServices$.next(mockStartServicesPayload); + await mockStartServices$.pipe(first()).toPromise(); await panel.execute(context); @@ -204,14 +209,13 @@ describe('GetCsvReportPanelAction', () => { usesUiCapabilities: true, }); - mockStartServices$.next(mockStartServicesPayload); - + await mockStartServices$.pipe(first()).toPromise(); await licenseMock$.pipe(first()).toPromise(); expect(await plugin.isCompatible(context)).toEqual(false); }); - it('sets a display and icon type', () => { + it('sets a display and icon type', async () => { const panel = new ReportingCsvPanelAction({ core, apiClient, @@ -220,7 +224,7 @@ describe('GetCsvReportPanelAction', () => { usesUiCapabilities: true, }); - mockStartServices$.next(mockStartServicesPayload); + await mockStartServices$.pipe(first()).toPromise(); expect(panel.getIconType()).toMatchInlineSnapshot(`"document"`); expect(panel.getDisplayName()).toMatchInlineSnapshot(`"Download CSV"`); @@ -228,25 +232,28 @@ describe('GetCsvReportPanelAction', () => { describe('Application UI Capabilities', () => { it(`doesn't allow downloads when UI capability is not enabled`, async () => { + mockStartServicesPayload = [ + { application: { capabilities: {} } } as unknown as CoreStart, + { + data: dataPluginMock.createStartContract(), + } as ReportingPublicPluginStartDendencies, + null, + ]; + const startServices$ = Rx.from(Promise.resolve(mockStartServicesPayload)); const plugin = new ReportingCsvPanelAction({ core, apiClient, license$: mockLicense$(), - startServices$: mockStartServices$, + startServices$, usesUiCapabilities: true, }); - mockStartServices$.next([ - { application: { capabilities: {} } } as unknown as CoreStart, - {}, - null, - ]); + await startServices$.pipe(first()).toPromise(); expect(await plugin.isCompatible(context)).toEqual(false); }); it(`allows downloads when license is valid and UI capability is enabled`, async () => { - mockStartServices$ = new Rx.Subject(); const plugin = new ReportingCsvPanelAction({ core, apiClient, @@ -255,7 +262,7 @@ describe('GetCsvReportPanelAction', () => { usesUiCapabilities: true, }); - mockStartServices$.next(mockStartServicesPayload); + await mockStartServices$.pipe(first()).toPromise(); expect(await plugin.isCompatible(context)).toEqual(true); }); diff --git a/x-pack/plugins/reporting/public/panel_actions/get_csv_panel_action.tsx b/x-pack/plugins/reporting/public/panel_actions/get_csv_panel_action.tsx index eb14e32160869..ef32e64741765 100644 --- a/x-pack/plugins/reporting/public/panel_actions/get_csv_panel_action.tsx +++ b/x-pack/plugins/reporting/public/panel_actions/get_csv_panel_action.tsx @@ -7,7 +7,8 @@ import { i18n } from '@kbn/i18n'; import * as Rx from 'rxjs'; -import type { CoreSetup, IUiSettingsClient, NotificationsSetup } from 'src/core/public'; +import { first } from 'rxjs/operators'; +import type { CoreSetup, NotificationsSetup } from 'src/core/public'; import { CoreStart } from 'src/core/public'; import type { ISearchEmbeddable, SavedSearch } from '../../../../../src/plugins/discover/public'; import { @@ -22,6 +23,7 @@ import type { LicensingPluginSetup } from '../../../licensing/public'; import { CSV_REPORTING_ACTION } from '../../common/constants'; import { checkLicense } from '../lib/license_check'; import { ReportingAPIClient } from '../lib/reporting_api_client'; +import type { ReportingPublicPluginStartDendencies } from '../plugin'; function isSavedSearchEmbeddable( embeddable: IEmbeddable | ISearchEmbeddable @@ -36,7 +38,7 @@ export interface ActionContext { interface Params { apiClient: ReportingAPIClient; core: CoreSetup; - startServices$: Rx.Observable<[CoreStart, object, unknown]>; + startServices$: Rx.Observable<[CoreStart, ReportingPublicPluginStartDendencies, unknown]>; license$: LicensingPluginSetup['license$']; usesUiCapabilities: boolean; } @@ -47,16 +49,16 @@ export class ReportingCsvPanelAction implements ActionDefinition public readonly id = CSV_REPORTING_ACTION; private licenseHasDownloadCsv: boolean = false; private capabilityHasDownloadCsv: boolean = false; - private uiSettings: IUiSettingsClient; private notifications: NotificationsSetup; private apiClient: ReportingAPIClient; + private startServices$: Params['startServices$']; constructor({ core, startServices$, license$, usesUiCapabilities, apiClient }: Params) { this.isDownloading = false; - this.uiSettings = core.uiSettings; this.notifications = core.notifications; this.apiClient = apiClient; + this.startServices$ = startServices$; license$.subscribe((license) => { const results = license.check('reporting', 'basic'); @@ -65,7 +67,7 @@ export class ReportingCsvPanelAction implements ActionDefinition }); if (usesUiCapabilities) { - startServices$.subscribe(([{ application }]) => { + this.startServices$.subscribe(([{ application }]) => { this.capabilityHasDownloadCsv = application.capabilities.dashboard?.downloadCsv === true; }); } else { @@ -84,11 +86,12 @@ export class ReportingCsvPanelAction implements ActionDefinition } public async getSearchSource(savedSearch: SavedSearch, embeddable: ISearchEmbeddable) { + const [{ uiSettings }, { data }] = await this.startServices$.pipe(first()).toPromise(); const { getSharingData } = await loadSharingDataHelpers(); return await getSharingData( savedSearch.searchSource, - savedSearch, // TODO: get unsaved state (using embeddale.searchScope): https://github.com/elastic/kibana/issues/43977 - this.uiSettings + savedSearch, // TODO: get unsaved state (using embeddable.searchScope): https://github.com/elastic/kibana/issues/43977 + { uiSettings, data } ); } diff --git a/x-pack/plugins/reporting/public/plugin.ts b/x-pack/plugins/reporting/public/plugin.ts index 28226751975a9..7fd6047470a0e 100644 --- a/x-pack/plugins/reporting/public/plugin.ts +++ b/x-pack/plugins/reporting/public/plugin.ts @@ -8,6 +8,7 @@ import { i18n } from '@kbn/i18n'; import * as Rx from 'rxjs'; import { catchError, filter, map, mergeMap, takeUntil } from 'rxjs/operators'; +import type { DataPublicPluginStart } from 'src/plugins/data/public'; import { CoreSetup, CoreStart, @@ -77,6 +78,7 @@ export interface ReportingPublicPluginSetupDendencies { export interface ReportingPublicPluginStartDendencies { home: HomePublicPluginStart; + data: DataPublicPluginStart; management: ManagementStart; licensing: LicensingPluginStart; uiActions: UiActionsStart; @@ -134,7 +136,10 @@ export class ReportingPublicPlugin return this.contract; } - public setup(core: CoreSetup, setupDeps: ReportingPublicPluginSetupDendencies) { + public setup( + core: CoreSetup, + setupDeps: ReportingPublicPluginSetupDendencies + ) { const { getStartServices, uiSettings } = core; const { home, diff --git a/x-pack/plugins/reporting/server/lib/content_stream.ts b/x-pack/plugins/reporting/server/lib/content_stream.ts index 79ff9a6812137..23cc8a302dbef 100644 --- a/x-pack/plugins/reporting/server/lib/content_stream.ts +++ b/x-pack/plugins/reporting/server/lib/content_stream.ts @@ -93,11 +93,11 @@ export class ContentStream extends Duplex { this.parameters = { encoding }; } - private async decode(content: string) { + private decode(content: string) { return Buffer.from(content, this.parameters.encoding === 'base64' ? 'base64' : undefined); } - private async encode(buffer: Buffer) { + private encode(buffer: Buffer) { return buffer.toString(this.parameters.encoding === 'base64' ? 'base64' : undefined); } @@ -188,7 +188,7 @@ export class ContentStream extends Duplex { return; } - const buffer = await this.decode(content); + const buffer = this.decode(content); this.push(buffer); this.chunksRead++; @@ -252,7 +252,7 @@ export class ContentStream extends Duplex { private async flush(size = this.buffer.byteLength) { const chunk = this.buffer.slice(0, size); - const content = await this.encode(chunk); + const content = this.encode(chunk); if (!this.chunksWritten) { await this.removeChunks(); @@ -269,32 +269,29 @@ export class ContentStream extends Duplex { this.buffer = this.buffer.slice(size); } - async _write(chunk: Buffer | string, encoding: BufferEncoding, callback: Callback) { + private async flushAllFullChunks() { + const maxChunkSize = await this.getMaxChunkSize(); + + while (this.buffer.byteLength >= maxChunkSize) { + await this.flush(maxChunkSize); + } + } + + _write(chunk: Buffer | string, encoding: BufferEncoding, callback: Callback) { this.buffer = Buffer.concat([ this.buffer, Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk, encoding), ]); - try { - const maxChunkSize = await this.getMaxChunkSize(); - - while (this.buffer.byteLength >= maxChunkSize) { - await this.flush(maxChunkSize); - } - - callback(); - } catch (error) { - callback(error); - } + this.flushAllFullChunks() + .then(() => callback()) + .catch(callback); } - async _final(callback: Callback) { - try { - await this.flush(); - callback(); - } catch (error) { - callback(error); - } + _final(callback: Callback) { + this.flush() + .then(() => callback()) + .catch(callback); } getSeqNo(): number | undefined { diff --git a/x-pack/plugins/reporting/server/usage/__snapshots__/reporting_usage_collector.test.ts.snap b/x-pack/plugins/reporting/server/usage/__snapshots__/reporting_usage_collector.test.ts.snap index 12debe5c85d5e..2017ae0be59c7 100644 --- a/x-pack/plugins/reporting/server/usage/__snapshots__/reporting_usage_collector.test.ts.snap +++ b/x-pack/plugins/reporting/server/usage/__snapshots__/reporting_usage_collector.test.ts.snap @@ -37,6 +37,29 @@ Object { "type": "long", }, }, + "sizes": Object { + "1.0": Object { + "type": "long", + }, + "25.0": Object { + "type": "long", + }, + "5.0": Object { + "type": "long", + }, + "50.0": Object { + "type": "long", + }, + "75.0": Object { + "type": "long", + }, + "95.0": Object { + "type": "long", + }, + "99.0": Object { + "type": "long", + }, + }, "total": Object { "type": "long", }, @@ -73,6 +96,29 @@ Object { "type": "long", }, }, + "sizes": Object { + "1.0": Object { + "type": "long", + }, + "25.0": Object { + "type": "long", + }, + "5.0": Object { + "type": "long", + }, + "50.0": Object { + "type": "long", + }, + "75.0": Object { + "type": "long", + }, + "95.0": Object { + "type": "long", + }, + "99.0": Object { + "type": "long", + }, + }, "total": Object { "type": "long", }, @@ -118,6 +164,29 @@ Object { "type": "long", }, }, + "sizes": Object { + "1.0": Object { + "type": "long", + }, + "25.0": Object { + "type": "long", + }, + "5.0": Object { + "type": "long", + }, + "50.0": Object { + "type": "long", + }, + "75.0": Object { + "type": "long", + }, + "95.0": Object { + "type": "long", + }, + "99.0": Object { + "type": "long", + }, + }, "total": Object { "type": "long", }, @@ -154,6 +223,29 @@ Object { "type": "long", }, }, + "sizes": Object { + "1.0": Object { + "type": "long", + }, + "25.0": Object { + "type": "long", + }, + "5.0": Object { + "type": "long", + }, + "50.0": Object { + "type": "long", + }, + "75.0": Object { + "type": "long", + }, + "95.0": Object { + "type": "long", + }, + "99.0": Object { + "type": "long", + }, + }, "total": Object { "type": "long", }, @@ -190,6 +282,29 @@ Object { "type": "long", }, }, + "sizes": Object { + "1.0": Object { + "type": "long", + }, + "25.0": Object { + "type": "long", + }, + "5.0": Object { + "type": "long", + }, + "50.0": Object { + "type": "long", + }, + "75.0": Object { + "type": "long", + }, + "95.0": Object { + "type": "long", + }, + "99.0": Object { + "type": "long", + }, + }, "total": Object { "type": "long", }, @@ -230,6 +345,29 @@ Object { "type": "long", }, }, + "sizes": Object { + "1.0": Object { + "type": "long", + }, + "25.0": Object { + "type": "long", + }, + "5.0": Object { + "type": "long", + }, + "50.0": Object { + "type": "long", + }, + "75.0": Object { + "type": "long", + }, + "95.0": Object { + "type": "long", + }, + "99.0": Object { + "type": "long", + }, + }, "total": Object { "type": "long", }, @@ -266,6 +404,29 @@ Object { "type": "long", }, }, + "sizes": Object { + "1.0": Object { + "type": "long", + }, + "25.0": Object { + "type": "long", + }, + "5.0": Object { + "type": "long", + }, + "50.0": Object { + "type": "long", + }, + "75.0": Object { + "type": "long", + }, + "95.0": Object { + "type": "long", + }, + "99.0": Object { + "type": "long", + }, + }, "total": Object { "type": "long", }, @@ -305,6 +466,29 @@ Object { "type": "long", }, }, + "sizes": Object { + "1.0": Object { + "type": "long", + }, + "25.0": Object { + "type": "long", + }, + "5.0": Object { + "type": "long", + }, + "50.0": Object { + "type": "long", + }, + "75.0": Object { + "type": "long", + }, + "95.0": Object { + "type": "long", + }, + "99.0": Object { + "type": "long", + }, + }, "total": Object { "type": "long", }, @@ -341,6 +525,29 @@ Object { "type": "long", }, }, + "sizes": Object { + "1.0": Object { + "type": "long", + }, + "25.0": Object { + "type": "long", + }, + "5.0": Object { + "type": "long", + }, + "50.0": Object { + "type": "long", + }, + "75.0": Object { + "type": "long", + }, + "95.0": Object { + "type": "long", + }, + "99.0": Object { + "type": "long", + }, + }, "total": Object { "type": "long", }, @@ -377,10 +584,56 @@ Object { "type": "long", }, }, + "sizes": Object { + "1.0": Object { + "type": "long", + }, + "25.0": Object { + "type": "long", + }, + "5.0": Object { + "type": "long", + }, + "50.0": Object { + "type": "long", + }, + "75.0": Object { + "type": "long", + }, + "95.0": Object { + "type": "long", + }, + "99.0": Object { + "type": "long", + }, + }, "total": Object { "type": "long", }, }, + "output_size": Object { + "1.0": Object { + "type": "long", + }, + "25.0": Object { + "type": "long", + }, + "5.0": Object { + "type": "long", + }, + "50.0": Object { + "type": "long", + }, + "75.0": Object { + "type": "long", + }, + "95.0": Object { + "type": "long", + }, + "99.0": Object { + "type": "long", + }, + }, "printable_pdf": Object { "app": Object { "canvas workpad": Object { @@ -413,6 +666,29 @@ Object { "type": "long", }, }, + "sizes": Object { + "1.0": Object { + "type": "long", + }, + "25.0": Object { + "type": "long", + }, + "5.0": Object { + "type": "long", + }, + "50.0": Object { + "type": "long", + }, + "75.0": Object { + "type": "long", + }, + "95.0": Object { + "type": "long", + }, + "99.0": Object { + "type": "long", + }, + }, "total": Object { "type": "long", }, @@ -449,6 +725,29 @@ Object { "type": "long", }, }, + "sizes": Object { + "1.0": Object { + "type": "long", + }, + "25.0": Object { + "type": "long", + }, + "5.0": Object { + "type": "long", + }, + "50.0": Object { + "type": "long", + }, + "75.0": Object { + "type": "long", + }, + "95.0": Object { + "type": "long", + }, + "99.0": Object { + "type": "long", + }, + }, "total": Object { "type": "long", }, @@ -973,6 +1272,29 @@ Object { }, }, }, + "output_size": Object { + "1.0": Object { + "type": "long", + }, + "25.0": Object { + "type": "long", + }, + "5.0": Object { + "type": "long", + }, + "50.0": Object { + "type": "long", + }, + "75.0": Object { + "type": "long", + }, + "95.0": Object { + "type": "long", + }, + "99.0": Object { + "type": "long", + }, + }, "printable_pdf": Object { "app": Object { "canvas workpad": Object { @@ -1005,6 +1327,29 @@ Object { "type": "long", }, }, + "sizes": Object { + "1.0": Object { + "type": "long", + }, + "25.0": Object { + "type": "long", + }, + "5.0": Object { + "type": "long", + }, + "50.0": Object { + "type": "long", + }, + "75.0": Object { + "type": "long", + }, + "95.0": Object { + "type": "long", + }, + "99.0": Object { + "type": "long", + }, + }, "total": Object { "type": "long", }, @@ -1041,6 +1386,29 @@ Object { "type": "long", }, }, + "sizes": Object { + "1.0": Object { + "type": "long", + }, + "25.0": Object { + "type": "long", + }, + "5.0": Object { + "type": "long", + }, + "50.0": Object { + "type": "long", + }, + "75.0": Object { + "type": "long", + }, + "95.0": Object { + "type": "long", + }, + "99.0": Object { + "type": "long", + }, + }, "total": Object { "type": "long", }, @@ -1620,6 +1988,7 @@ Object { "preserve_layout": 0, "print": 0, }, + "output_size": undefined, "total": 4, }, "csv_searchsource": Object { @@ -1636,6 +2005,7 @@ Object { "preserve_layout": 0, "print": 0, }, + "output_size": undefined, "total": 5, }, "csv_searchsource_immediate": Object { @@ -1703,6 +2073,7 @@ Object { "preserve_layout": 0, "print": 0, }, + "output_size": undefined, "total": 4, }, "csv_searchsource": Object { @@ -1719,6 +2090,7 @@ Object { "preserve_layout": 0, "print": 0, }, + "output_size": undefined, "total": 5, }, "csv_searchsource_immediate": Object { @@ -1737,6 +2109,7 @@ Object { }, "total": 0, }, + "output_size": undefined, "printable_pdf": Object { "app": Object { "canvas workpad": 0, @@ -1784,6 +2157,7 @@ Object { }, }, }, + "output_size": undefined, "printable_pdf": Object { "app": Object { "canvas workpad": 0, @@ -2001,6 +2375,7 @@ Object { }, "total": 0, }, + "output_size": undefined, "printable_pdf": Object { "app": Object { "canvas workpad": 0, @@ -2039,6 +2414,7 @@ Object { }, "statuses": Object {}, }, + "output_size": undefined, "printable_pdf": Object { "app": Object { "canvas workpad": 0, @@ -2079,7 +2455,7 @@ Object { } `; -exports[`data modeling with normal looking usage data 1`] = ` +exports[`data modeling with sparse data 1`] = ` Object { "PNG": Object { "app": Object { @@ -2095,7 +2471,8 @@ Object { "preserve_layout": 0, "print": 0, }, - "total": 3, + "output_size": undefined, + "total": 1, }, "PNGV2": Object { "app": Object { @@ -2113,7 +2490,7 @@ Object { }, "total": 0, }, - "_all": 12, + "_all": 4, "available": true, "browser_type": undefined, "csv": Object { @@ -2124,13 +2501,14 @@ Object { "visualization": 0, }, "available": true, - "deprecated": 0, + "deprecated": 1, "layout": Object { "canvas": 0, "preserve_layout": 0, "print": 0, }, - "total": 0, + "output_size": undefined, + "total": 1, }, "csv_searchsource": Object { "app": Object { @@ -2180,6 +2558,7 @@ Object { "preserve_layout": 0, "print": 0, }, + "output_size": undefined, "total": 1, }, "PNGV2": Object { @@ -2198,7 +2577,7 @@ Object { }, "total": 0, }, - "_all": 1, + "_all": 4, "csv": Object { "app": Object { "canvas workpad": 0, @@ -2207,13 +2586,14 @@ Object { "visualization": 0, }, "available": true, - "deprecated": 0, + "deprecated": 1, "layout": Object { "canvas": 0, "preserve_layout": 0, "print": 0, }, - "total": 0, + "output_size": undefined, + "total": 1, }, "csv_searchsource": Object { "app": Object { @@ -2247,10 +2627,11 @@ Object { }, "total": 0, }, + "output_size": undefined, "printable_pdf": Object { "app": Object { - "canvas workpad": 0, - "dashboard": 0, + "canvas workpad": 1, + "dashboard": 1, "search": 0, "visualization": 0, }, @@ -2258,10 +2639,11 @@ Object { "deprecated": 0, "layout": Object { "canvas": 0, - "preserve_layout": 0, + "preserve_layout": 2, "print": 0, }, - "total": 0, + "output_size": undefined, + "total": 2, }, "printable_pdf_v2": Object { "app": Object { @@ -2280,33 +2662,39 @@ Object { "total": 0, }, "status": Object { - "completed": 0, - "completed_with_warnings": 1, + "completed": 4, "failed": 0, }, "statuses": Object { - "completed_with_warnings": Object { + "completed": Object { "PNG": Object { "dashboard": 1, }, + "csv": Object {}, + "printable_pdf": Object { + "canvas workpad": 1, + "dashboard": 1, + }, }, }, }, + "output_size": undefined, "printable_pdf": Object { "app": Object { - "canvas workpad": 6, - "dashboard": 0, + "canvas workpad": 1, + "dashboard": 1, "search": 0, - "visualization": 3, + "visualization": 0, }, "available": true, "deprecated": 0, "layout": Object { "canvas": 0, - "preserve_layout": 9, + "preserve_layout": 2, "print": 0, }, - "total": 9, + "output_size": undefined, + "total": 2, }, "printable_pdf_v2": Object { "app": Object { @@ -2325,27 +2713,17 @@ Object { "total": 0, }, "status": Object { - "completed": 10, - "completed_with_warnings": 1, - "failed": 1, + "completed": 4, + "failed": 0, }, "statuses": Object { "completed": Object { - "PNG": Object { - "visualization": 1, - }, - "printable_pdf": Object { - "canvas workpad": 6, - "visualization": 3, - }, - }, - "completed_with_warnings": Object { "PNG": Object { "dashboard": 1, }, - }, - "failed": Object { - "PNG": Object { + "csv": Object {}, + "printable_pdf": Object { + "canvas workpad": 1, "dashboard": 1, }, }, @@ -2353,7 +2731,7 @@ Object { } `; -exports[`data modeling with sparse data 1`] = ` +exports[`data modeling with usage data from the reporting/archived_reports es archive 1`] = ` Object { "PNG": Object { "app": Object { @@ -2369,6 +2747,7 @@ Object { "preserve_layout": 0, "print": 0, }, + "output_size": undefined, "total": 1, }, "PNGV2": Object { @@ -2387,7 +2766,7 @@ Object { }, "total": 0, }, - "_all": 4, + "_all": 11, "available": true, "browser_type": undefined, "csv": Object { @@ -2404,6 +2783,7 @@ Object { "preserve_layout": 0, "print": 0, }, + "output_size": undefined, "total": 1, }, "csv_searchsource": Object { @@ -2420,7 +2800,8 @@ Object { "preserve_layout": 0, "print": 0, }, - "total": 0, + "output_size": undefined, + "total": 3, }, "csv_searchsource_immediate": Object { "app": Object { @@ -2454,7 +2835,7 @@ Object { "preserve_layout": 0, "print": 0, }, - "total": 1, + "total": 0, }, "PNGV2": Object { "app": Object { @@ -2472,7 +2853,7 @@ Object { }, "total": 0, }, - "_all": 4, + "_all": 0, "csv": Object { "app": Object { "canvas workpad": 0, @@ -2481,13 +2862,13 @@ Object { "visualization": 0, }, "available": true, - "deprecated": 1, + "deprecated": 0, "layout": Object { "canvas": 0, "preserve_layout": 0, "print": 0, }, - "total": 1, + "total": 0, }, "csv_searchsource": Object { "app": Object { @@ -2521,10 +2902,11 @@ Object { }, "total": 0, }, + "output_size": undefined, "printable_pdf": Object { "app": Object { - "canvas workpad": 1, - "dashboard": 1, + "canvas workpad": 0, + "dashboard": 0, "search": 0, "visualization": 0, }, @@ -2532,10 +2914,10 @@ Object { "deprecated": 0, "layout": Object { "canvas": 0, - "preserve_layout": 2, + "preserve_layout": 0, "print": 0, }, - "total": 2, + "total": 0, }, "printable_pdf_v2": Object { "app": Object { @@ -2554,26 +2936,16 @@ Object { "total": 0, }, "status": Object { - "completed": 4, + "completed": 0, "failed": 0, }, - "statuses": Object { - "completed": Object { - "PNG": Object { - "dashboard": 1, - }, - "csv": Object {}, - "printable_pdf": Object { - "canvas workpad": 1, - "dashboard": 1, - }, - }, - }, + "statuses": Object {}, }, + "output_size": undefined, "printable_pdf": Object { "app": Object { - "canvas workpad": 1, - "dashboard": 1, + "canvas workpad": 0, + "dashboard": 6, "search": 0, "visualization": 0, }, @@ -2581,10 +2953,11 @@ Object { "deprecated": 0, "layout": Object { "canvas": 0, - "preserve_layout": 2, - "print": 0, + "preserve_layout": 5, + "print": 1, }, - "total": 2, + "output_size": undefined, + "total": 6, }, "printable_pdf_v2": Object { "app": Object { @@ -2603,17 +2976,38 @@ Object { "total": 0, }, "status": Object { - "completed": 4, - "failed": 0, + "completed": 6, + "completed_with_warnings": 2, + "failed": 2, + "pending": 1, }, "statuses": Object { "completed": Object { + "csv": Object { + "search": 1, + }, + "csv_searchsource": Object { + "search": 3, + }, + "printable_pdf": Object { + "dashboard": 2, + }, + }, + "completed_with_warnings": Object { "PNG": Object { "dashboard": 1, }, - "csv": Object {}, "printable_pdf": Object { - "canvas workpad": 1, + "dashboard": 1, + }, + }, + "failed": Object { + "printable_pdf": Object { + "dashboard": 2, + }, + }, + "pending": Object { + "printable_pdf": Object { "dashboard": 1, }, }, diff --git a/x-pack/plugins/reporting/server/usage/get_export_stats.test.ts b/x-pack/plugins/reporting/server/usage/get_export_stats.test.ts index 782f2e910038e..f74e176e6f21d 100644 --- a/x-pack/plugins/reporting/server/usage/get_export_stats.test.ts +++ b/x-pack/plugins/reporting/server/usage/get_export_stats.test.ts @@ -11,6 +11,15 @@ import { getExportTypesHandler } from './get_export_type_handler'; import { FeatureAvailabilityMap } from './types'; let featureMap: FeatureAvailabilityMap; +const sizesAggResponse = { + '1.0': 5093470.0, + '5.0': 5093470.0, + '25.0': 5093470.0, + '50.0': 8514532.0, + '75.0': 1.1935594e7, + '95.0': 1.1935594e7, + '99.0': 1.1935594e7, +}; beforeEach(() => { featureMap = { PNG: true, csv: true, csv_searchsource: true, printable_pdf: true }; @@ -67,14 +76,19 @@ test('Model of job status and status-by-pdf-app', () => { test('Model of jobTypes', () => { const result = getExportStats( { - PNG: { available: true, total: 3 }, + PNG: { available: true, total: 3, sizes: sizesAggResponse }, printable_pdf: { available: true, total: 3, + sizes: sizesAggResponse, app: { dashboard: 0, visualization: 0, 'canvas workpad': 3 }, layout: { preserve_layout: 3, print: 0 }, }, - csv_searchsource: { available: true, total: 3 }, + csv_searchsource: { + available: true, + total: 3, + sizes: sizesAggResponse, + }, }, featureMap, exportTypesHandler @@ -95,6 +109,15 @@ test('Model of jobTypes', () => { "preserve_layout": 0, "print": 0, }, + "output_size": Object { + "1.0": 5093470, + "25.0": 5093470, + "5.0": 5093470, + "50.0": 8514532, + "75.0": 11935594, + "95.0": 11935594, + "99.0": 11935594, + }, "total": 3, } `); @@ -131,6 +154,15 @@ test('Model of jobTypes', () => { "preserve_layout": 0, "print": 0, }, + "output_size": Object { + "1.0": 5093470, + "25.0": 5093470, + "5.0": 5093470, + "50.0": 8514532, + "75.0": 11935594, + "95.0": 11935594, + "99.0": 11935594, + }, "total": 3, } `); @@ -149,6 +181,15 @@ test('Model of jobTypes', () => { "preserve_layout": 3, "print": 0, }, + "output_size": Object { + "1.0": 5093470, + "25.0": 5093470, + "5.0": 5093470, + "50.0": 8514532, + "75.0": 11935594, + "95.0": 11935594, + "99.0": 11935594, + }, "total": 3, } `); @@ -156,7 +197,14 @@ test('Model of jobTypes', () => { test('PNG counts, provided count of deprecated jobs explicitly', () => { const result = getExportStats( - { PNG: { available: true, total: 15, deprecated: 5 } }, + { + PNG: { + available: true, + total: 15, + deprecated: 5, + sizes: sizesAggResponse, + }, + }, featureMap, exportTypesHandler ); @@ -175,6 +223,15 @@ test('PNG counts, provided count of deprecated jobs explicitly', () => { "preserve_layout": 0, "print": 0, }, + "output_size": Object { + "1.0": 5093470, + "25.0": 5093470, + "5.0": 5093470, + "50.0": 8514532, + "75.0": 11935594, + "95.0": 11935594, + "99.0": 11935594, + }, "total": 15, } `); @@ -182,7 +239,14 @@ test('PNG counts, provided count of deprecated jobs explicitly', () => { test('CSV counts, provides all jobs implicitly deprecated due to jobtype', () => { const result = getExportStats( - { csv: { available: true, total: 15, deprecated: 0 } }, + { + csv: { + available: true, + total: 15, + deprecated: 0, + sizes: sizesAggResponse, + }, + }, featureMap, exportTypesHandler ); @@ -201,6 +265,15 @@ test('CSV counts, provides all jobs implicitly deprecated due to jobtype', () => "preserve_layout": 0, "print": 0, }, + "output_size": Object { + "1.0": 5093470, + "25.0": 5093470, + "5.0": 5093470, + "50.0": 8514532, + "75.0": 11935594, + "95.0": 11935594, + "99.0": 11935594, + }, "total": 15, } `); diff --git a/x-pack/plugins/reporting/server/usage/get_export_stats.ts b/x-pack/plugins/reporting/server/usage/get_export_stats.ts index ffdb6cdc290d7..72c09f08017a1 100644 --- a/x-pack/plugins/reporting/server/usage/get_export_stats.ts +++ b/x-pack/plugins/reporting/server/usage/get_export_stats.ts @@ -33,6 +33,7 @@ function getAvailableTotalForFeature( available: isAvailable(featureAvailability, typeKey), total: jobType.total, deprecated, + output_size: jobType.sizes, app: { ...defaultTotalsForFeature.app, ...jobType.app }, layout: { ...defaultTotalsForFeature.layout, ...jobType.layout }, }; @@ -56,6 +57,7 @@ export const getExportStats = ( _all: rangeAll, status: rangeStatus, statuses: rangeStatusByApp, + output_size: outputSize, ...rangeStats } = rangeStatsInput; @@ -84,6 +86,7 @@ export const getExportStats = ( _all: rangeAll || 0, status: { completed: 0, failed: 0, ...rangeStatus }, statuses: rangeStatusByApp, + output_size: outputSize, } as RangeStats; return resultStats; diff --git a/x-pack/plugins/reporting/server/usage/get_reporting_usage.ts b/x-pack/plugins/reporting/server/usage/get_reporting_usage.ts index 9aba7841162c2..9a452943ff699 100644 --- a/x-pack/plugins/reporting/server/usage/get_reporting_usage.ts +++ b/x-pack/plugins/reporting/server/usage/get_reporting_usage.ts @@ -13,6 +13,7 @@ import type { GetLicense } from './'; import { getExportStats } from './get_export_stats'; import { getExportTypesHandler } from './get_export_type_handler'; import type { + AggregationBuckets, AggregationResultBuckets, AvailableTotal, FeatureAvailabilityMap, @@ -33,6 +34,8 @@ const OBJECT_TYPES_FIELD = 'meta.objectType.keyword'; const STATUS_TYPES_KEY = 'statusTypes'; const STATUS_BY_APP_KEY = 'statusByApp'; const STATUS_TYPES_FIELD = 'status'; +const OUTPUT_SIZES_KEY = 'sizes'; +const OUTPUT_SIZES_FIELD = 'output.size'; const DEFAULT_TERMS_SIZE = 10; const PRINTABLE_PDF_JOBTYPE = 'printable_pdf'; @@ -64,13 +67,14 @@ const getAppStatuses = (buckets: StatusByAppBucket[]) => }, {}); function getAggStats(aggs: AggregationResultBuckets): Partial { - const { buckets: jobBuckets } = aggs[JOB_TYPES_KEY]; + const { buckets: jobBuckets } = aggs[JOB_TYPES_KEY] as AggregationBuckets; const jobTypes = jobBuckets.reduce((accum: JobTypes, bucket) => { - const { key, doc_count: count, isDeprecated } = bucket; + const { key, doc_count: count, isDeprecated, sizes } = bucket; const deprecatedCount = isDeprecated?.doc_count; const total: Omit = { total: count, deprecated: deprecatedCount, + sizes: sizes?.values, }; return { ...accum, [key]: total }; }, {} as JobTypes); @@ -97,7 +101,13 @@ function getAggStats(aggs: AggregationResultBuckets): Partial { statusByApp = getAppStatuses(statusAppBuckets); } - return { _all: all, status: statusTypes, statuses: statusByApp, ...jobTypes }; + return { + _all: all, + status: statusTypes, + statuses: statusByApp, + output_size: get(aggs[OUTPUT_SIZES_KEY], 'values') ?? undefined, + ...jobTypes, + }; } type RangeStatSets = Partial & { @@ -135,7 +145,6 @@ export async function getReportingUsage( exportTypesRegistry: ExportTypesRegistry ): Promise { const reportingIndex = config.get('index'); - const params = { index: `${reportingIndex}-*`, filterPath: 'aggregations.*.buckets', @@ -152,8 +161,14 @@ export async function getReportingUsage( aggs: { [JOB_TYPES_KEY]: { terms: { field: JOB_TYPES_FIELD, size: DEFAULT_TERMS_SIZE }, - aggs: { isDeprecated: { filter: { term: { [OBJECT_TYPE_DEPRECATED_KEY]: true } } } }, + aggs: { + isDeprecated: { filter: { term: { [OBJECT_TYPE_DEPRECATED_KEY]: true } } }, + [OUTPUT_SIZES_KEY]: { + percentiles: { field: OUTPUT_SIZES_FIELD }, + }, + }, }, + [STATUS_TYPES_KEY]: { terms: { field: STATUS_TYPES_FIELD, size: DEFAULT_TERMS_SIZE } }, [STATUS_BY_APP_KEY]: { terms: { field: 'status', size: DEFAULT_TERMS_SIZE }, @@ -161,19 +176,24 @@ export async function getReportingUsage( jobTypes: { terms: { field: JOB_TYPES_FIELD, size: DEFAULT_TERMS_SIZE }, aggs: { - appNames: { terms: { field: OBJECT_TYPES_FIELD, size: DEFAULT_TERMS_SIZE } }, // NOTE Discover/CSV export is missing the 'meta.objectType' field, so Discover/CSV results are missing for this agg + appNames: { terms: { field: OBJECT_TYPES_FIELD, size: DEFAULT_TERMS_SIZE } }, }, }, }, }, [OBJECT_TYPES_KEY]: { filter: { term: { jobtype: PRINTABLE_PDF_JOBTYPE } }, - aggs: { pdf: { terms: { field: OBJECT_TYPES_FIELD, size: DEFAULT_TERMS_SIZE } } }, + aggs: { + pdf: { terms: { field: OBJECT_TYPES_FIELD, size: DEFAULT_TERMS_SIZE } }, + }, }, [LAYOUT_TYPES_KEY]: { filter: { term: { jobtype: PRINTABLE_PDF_JOBTYPE } }, aggs: { pdf: { terms: { field: LAYOUT_TYPES_FIELD, size: DEFAULT_TERMS_SIZE } } }, }, + [OUTPUT_SIZES_KEY]: { + percentiles: { field: OUTPUT_SIZES_FIELD }, + }, }, }, }, diff --git a/x-pack/plugins/reporting/server/usage/reporting_usage_collector.test.ts b/x-pack/plugins/reporting/server/usage/reporting_usage_collector.test.ts index e69e56d6272d5..447085810cfd0 100644 --- a/x-pack/plugins/reporting/server/usage/reporting_usage_collector.test.ts +++ b/x-pack/plugins/reporting/server/usage/reporting_usage_collector.test.ts @@ -18,7 +18,6 @@ import { getReportingUsageCollector, registerReportingUsageCollector, } from './reporting_usage_collector'; -import { SearchResponse } from './types'; const exportTypesRegistry = getExportTypesRegistry(); @@ -190,7 +189,7 @@ describe('data modeling', () => { beforeAll(async () => { mockCore = await createMockReportingCore(createMockConfigSchema()); }); - test('with normal looking usage data', async () => { + test('with usage data from the reporting/archived_reports es archive', async () => { const plugins = getPluginsMock(); const collector = getReportingUsageCollector( mockCore, @@ -202,39 +201,37 @@ describe('data modeling', () => { } ); collectorFetchContext = getMockFetchClients( - getResponseMock( - { + getResponseMock({ aggregations: { ranges: { + meta: {}, buckets: { all: { - doc_count: 12, - jobTypes: { buckets: [ { doc_count: 9, key: 'printable_pdf' }, { doc_count: 3, key: 'PNG' }, ], }, - layoutTypes: { doc_count: 9, pdf: { buckets: [{ doc_count: 9, key: 'preserve_layout' }] }, }, - objectTypes: { doc_count: 9, pdf: { buckets: [ { doc_count: 6, key: 'canvas workpad' }, { doc_count: 3, key: 'visualization' }, ], }, }, - statusByApp: { buckets: [ { doc_count: 10, jobTypes: { buckets: [ { appNames: { buckets: [ { doc_count: 6, key: 'canvas workpad' }, { doc_count: 3, key: 'visualization' }, ], }, doc_count: 9, key: 'printable_pdf', }, { appNames: { buckets: [{ doc_count: 1, key: 'visualization' }] }, doc_count: 1, key: 'PNG', }, ], }, key: 'completed', }, { doc_count: 1, jobTypes: { buckets: [ { appNames: { buckets: [{ doc_count: 1, key: 'dashboard' }] }, doc_count: 1, key: 'PNG', }, ], }, key: 'completed_with_warnings', }, { doc_count: 1, jobTypes: { buckets: [ { appNames: { buckets: [{ doc_count: 1, key: 'dashboard' }] }, doc_count: 1, key: 'PNG', }, ], }, key: 'failed', }, ], }, - statusTypes: { buckets: [ { doc_count: 10, key: 'completed' }, { doc_count: 1, key: 'completed_with_warnings' }, { doc_count: 1, key: 'failed' }, ], }, + doc_count: 11, + layoutTypes: { doc_count: 6, pdf: { doc_count_error_upper_bound: 0, sum_other_doc_count: 0, buckets: [ { key: 'preserve_layout', doc_count: 5 }, { key: 'print', doc_count: 1 }, ] } }, + statusByApp: { doc_count_error_upper_bound: 0, sum_other_doc_count: 0, buckets: [ { key: 'completed', doc_count: 6, jobTypes: { doc_count_error_upper_bound: 0, sum_other_doc_count: 0, buckets: [ { key: 'csv_searchsource', doc_count: 3, appNames: { doc_count_error_upper_bound: 0, sum_other_doc_count: 0, buckets: [ { key: 'search', doc_count: 3 }, ] } }, { key: 'printable_pdf', doc_count: 2, appNames: { doc_count_error_upper_bound: 0, sum_other_doc_count: 0, buckets: [ { key: 'dashboard', doc_count: 2 }, ] } }, { key: 'csv', doc_count: 1, appNames: { doc_count_error_upper_bound: 0, sum_other_doc_count: 0, buckets: [ { key: 'search', doc_count: 1 }, ] } }, ] } }, { key: 'completed_with_warnings', doc_count: 2, jobTypes: { doc_count_error_upper_bound: 0, sum_other_doc_count: 0, buckets: [ { key: 'PNG', doc_count: 1, appNames: { doc_count_error_upper_bound: 0, sum_other_doc_count: 0, buckets: [ { key: 'dashboard', doc_count: 1 }, ] } }, { key: 'printable_pdf', doc_count: 1, appNames: { doc_count_error_upper_bound: 0, sum_other_doc_count: 0, buckets: [ { key: 'dashboard', doc_count: 1 }, ] } }, ] } }, { key: 'failed', doc_count: 2, jobTypes: { doc_count_error_upper_bound: 0, sum_other_doc_count: 0, buckets: [ { key: 'printable_pdf', doc_count: 2, appNames: { doc_count_error_upper_bound: 0, sum_other_doc_count: 0, buckets: [ { key: 'dashboard', doc_count: 2 }, ] } }, ] } }, { key: 'pending', doc_count: 1, jobTypes: { doc_count_error_upper_bound: 0, sum_other_doc_count: 0, buckets: [ { key: 'printable_pdf', doc_count: 1, appNames: { doc_count_error_upper_bound: 0, sum_other_doc_count: 0, buckets: [ { key: 'dashboard', doc_count: 1 }, ] } }, ] } }, ] }, + objectTypes: { doc_count: 6, pdf: { doc_count_error_upper_bound: 0, sum_other_doc_count: 0, buckets: [ { key: 'dashboard', doc_count: 6 }, ] } }, + statusTypes: { doc_count_error_upper_bound: 0, sum_other_doc_count: 0, buckets: [ { key: 'completed', doc_count: 6 }, { key: 'completed_with_warnings', doc_count: 2 }, { key: 'failed', doc_count: 2 }, { key: 'pending', doc_count: 1 }, ] }, + jobTypes: { meta: {}, doc_count_error_upper_bound: 0, sum_other_doc_count: 0, buckets: [ { key: 'printable_pdf', doc_count: 6, isDeprecated: { meta: {}, doc_count: 0 }, sizeMax: { value: 1713303.0 }, sizeAvg: { value: 957215.0 }, sizeMin: { value: 43226.0 } }, { key: 'csv_searchsource', doc_count: 3, isDeprecated: { meta: {}, doc_count: 0 }, sizeMax: { value: 7557.0 }, sizeAvg: { value: 3684.6666666666665 }, sizeMin: { value: 204.0 } }, { key: 'PNG', doc_count: 1, isDeprecated: { meta: {}, doc_count: 0 }, sizeMax: { value: 37748.0 }, sizeAvg: { value: 37748.0 }, sizeMin: { value: 37748.0 } }, { key: 'csv', doc_count: 1, isDeprecated: { meta: {}, doc_count: 0 }, sizeMax: { value: 231.0 }, sizeAvg: { value: 231.0 }, sizeMin: { value: 231.0 } }, ] }, + sizeMax: { value: 1713303.0 }, + sizeMin: { value: 204.0 }, + sizeAvg: { value: 365084.75 }, }, last7Days: { - doc_count: 1, - jobTypes: { buckets: [{ doc_count: 1, key: 'PNG' }] }, - layoutTypes: { doc_count: 0, pdf: { buckets: [] } }, - objectTypes: { doc_count: 0, pdf: { buckets: [] } }, - statusByApp: { buckets: [ { doc_count: 1, jobTypes: { buckets: [ { appNames: { buckets: [{ doc_count: 1, key: 'dashboard' }] }, doc_count: 1, key: 'PNG', }, ], }, key: 'completed_with_warnings', }, ], }, - statusTypes: { buckets: [{ doc_count: 1, key: 'completed_with_warnings' }] }, - }, - lastDay: { - doc_count: 1, - jobTypes: { buckets: [{ doc_count: 1, key: 'PNG' }] }, - layoutTypes: { doc_count: 0, pdf: { buckets: [] } }, - objectTypes: { doc_count: 0, pdf: { buckets: [] } }, - statusByApp: { buckets: [ { doc_count: 1, jobTypes: { buckets: [ { appNames: { buckets: [{ doc_count: 1, key: 'dashboard' }] }, doc_count: 1, key: 'PNG', }, ], }, key: 'completed_with_warnings', }, ], }, - statusTypes: { buckets: [{ doc_count: 1, key: 'completed_with_warnings' }] }, + doc_count: 0, + layoutTypes: { doc_count: 0, pdf: { doc_count_error_upper_bound: 0, sum_other_doc_count: 0, buckets: [] } }, + statusByApp: { doc_count_error_upper_bound: 0, sum_other_doc_count: 0, buckets: [] }, + objectTypes: { doc_count: 0, pdf: { doc_count_error_upper_bound: 0, sum_other_doc_count: 0, buckets: [] } }, + statusTypes: { doc_count_error_upper_bound: 0, sum_other_doc_count: 0, buckets: [] }, + jobTypes: { meta: {}, doc_count_error_upper_bound: 0, sum_other_doc_count: 0, buckets: [] }, + sizeMax: { value: null }, + sizeMin: { value: null }, + sizeAvg: { value: null }, }, }, - }, + }, // prettier-ignore }, - } as SearchResponse) // prettier-ignore + }) ); const usageStats = await collector.fetch(collectorFetchContext); expect(usageStats).toMatchSnapshot(); @@ -258,121 +255,21 @@ describe('data modeling', () => { buckets: { all: { doc_count: 9, - layoutTypes: { - doc_count: 0, - pdf: { doc_count_error_upper_bound: 0, sum_other_doc_count: 0, buckets: [] }, - }, - statusByApp: { - doc_count_error_upper_bound: 0, - sum_other_doc_count: 0, - buckets: [ - { - key: 'completed', - doc_count: 9, - jobTypes: { - doc_count_error_upper_bound: 0, - sum_other_doc_count: 0, - buckets: [ - { - key: 'csv_searchsource', - doc_count: 5, - appNames: { - doc_count_error_upper_bound: 0, - sum_other_doc_count: 0, - buckets: [{ key: 'search', doc_count: 5 }], - }, - }, - { - key: 'csv', - doc_count: 4, - appNames: { - doc_count_error_upper_bound: 0, - sum_other_doc_count: 0, - buckets: [{ key: 'search', doc_count: 4 }], - }, - }, - ], - }, - }, - ], - }, - objectTypes: { - doc_count: 0, - pdf: { doc_count_error_upper_bound: 0, sum_other_doc_count: 0, buckets: [] }, - }, - statusTypes: { - doc_count_error_upper_bound: 0, - sum_other_doc_count: 0, - buckets: [{ key: 'completed', doc_count: 9 }], - }, - jobTypes: { - doc_count_error_upper_bound: 0, - sum_other_doc_count: 0, - buckets: [ - { key: 'csv_searchsource', doc_count: 5, isDeprecated: { doc_count: 0 } }, - { key: 'csv', doc_count: 4, isDeprecated: { doc_count: 4 } }, - ], - }, + layoutTypes: { doc_count: 0, pdf: { doc_count_error_upper_bound: 0, sum_other_doc_count: 0, buckets: [] } }, + statusByApp: { doc_count_error_upper_bound: 0, sum_other_doc_count: 0, buckets: [ { key: 'completed', doc_count: 9, jobTypes: { doc_count_error_upper_bound: 0, sum_other_doc_count: 0, buckets: [ { key: 'csv_searchsource', doc_count: 5, appNames: { doc_count_error_upper_bound: 0, sum_other_doc_count: 0, buckets: [{ key: 'search', doc_count: 5 }] } }, { key: 'csv', doc_count: 4, appNames: { doc_count_error_upper_bound: 0, sum_other_doc_count: 0, buckets: [{ key: 'search', doc_count: 4 }] } }, ] } }, ] }, + objectTypes: { doc_count: 0, pdf: { doc_count_error_upper_bound: 0, sum_other_doc_count: 0, buckets: [] } }, + statusTypes: { doc_count_error_upper_bound: 0, sum_other_doc_count: 0, buckets: [{ key: 'completed', doc_count: 9 }] }, + jobTypes: { doc_count_error_upper_bound: 0, sum_other_doc_count: 0, buckets: [ { key: 'csv_searchsource', doc_count: 5, isDeprecated: { doc_count: 0 } }, { key: 'csv', doc_count: 4, isDeprecated: { doc_count: 4 } }, ] }, }, last7Days: { doc_count: 9, - layoutTypes: { - doc_count: 0, - pdf: { doc_count_error_upper_bound: 0, sum_other_doc_count: 0, buckets: [] }, - }, - statusByApp: { - doc_count_error_upper_bound: 0, - sum_other_doc_count: 0, - buckets: [ - { - key: 'completed', - doc_count: 9, - jobTypes: { - doc_count_error_upper_bound: 0, - sum_other_doc_count: 0, - buckets: [ - { - key: 'csv_searchsource', - doc_count: 5, - appNames: { - doc_count_error_upper_bound: 0, - sum_other_doc_count: 0, - buckets: [{ key: 'search', doc_count: 5 }], - }, - }, - { - key: 'csv', - doc_count: 4, - appNames: { - doc_count_error_upper_bound: 0, - sum_other_doc_count: 0, - buckets: [{ key: 'search', doc_count: 4 }], - }, - }, - ], - }, - }, - ], - }, - objectTypes: { - doc_count: 0, - pdf: { doc_count_error_upper_bound: 0, sum_other_doc_count: 0, buckets: [] }, - }, - statusTypes: { - doc_count_error_upper_bound: 0, - sum_other_doc_count: 0, - buckets: [{ key: 'completed', doc_count: 9 }], - }, - jobTypes: { - doc_count_error_upper_bound: 0, - sum_other_doc_count: 0, - buckets: [ - { key: 'csv_searchsource', doc_count: 5, isDeprecated: { doc_count: 0 } }, - { key: 'csv', doc_count: 4, isDeprecated: { doc_count: 4 } }, - ], - }, + layoutTypes: { doc_count: 0, pdf: { doc_count_error_upper_bound: 0, sum_other_doc_count: 0, buckets: [] } }, + statusByApp: { doc_count_error_upper_bound: 0, sum_other_doc_count: 0, buckets: [ { key: 'completed', doc_count: 9, jobTypes: { doc_count_error_upper_bound: 0, sum_other_doc_count: 0, buckets: [ { key: 'csv_searchsource', doc_count: 5, appNames: { doc_count_error_upper_bound: 0, sum_other_doc_count: 0, buckets: [{ key: 'search', doc_count: 5 }] } }, { key: 'csv', doc_count: 4, appNames: { doc_count_error_upper_bound: 0, sum_other_doc_count: 0, buckets: [{ key: 'search', doc_count: 4 }] } }, ] } }, ] }, + objectTypes: { doc_count: 0, pdf: { doc_count_error_upper_bound: 0, sum_other_doc_count: 0, buckets: [] } }, + statusTypes: { doc_count_error_upper_bound: 0, sum_other_doc_count: 0, buckets: [{ key: 'completed', doc_count: 9 }] }, + jobTypes: { doc_count_error_upper_bound: 0, sum_other_doc_count: 0, buckets: [ { key: 'csv_searchsource', doc_count: 5, isDeprecated: { doc_count: 0 } }, { key: 'csv', doc_count: 4, isDeprecated: { doc_count: 4 } }, ] }, }, - }, + }, // prettier-ignore }, }, }) @@ -393,39 +290,30 @@ describe('data modeling', () => { } ); collectorFetchContext = getMockFetchClients( - getResponseMock( - { + getResponseMock({ aggregations: { ranges: { buckets: { all: { doc_count: 4, - layoutTypes: { doc_count: 2, pdf: { buckets: [{ key: 'preserve_layout', doc_count: 2 }] }, }, - statusByApp: { buckets: [ { key: 'completed', doc_count: 4, jobTypes: { buckets: [ { key: 'printable_pdf', doc_count: 2, appNames: { buckets: [ { key: 'canvas workpad', doc_count: 1 }, { key: 'dashboard', doc_count: 1 }, ], }, }, { key: 'PNG', doc_count: 1, appNames: { buckets: [{ key: 'dashboard', doc_count: 1 }] }, }, { key: 'csv', doc_count: 1, appNames: { buckets: [] } }, ], }, }, ], }, - objectTypes: { doc_count: 2, pdf: { buckets: [ { key: 'canvas workpad', doc_count: 1 }, { key: 'dashboard', doc_count: 1 }, ], }, }, + layoutTypes: { doc_count: 2, pdf: { buckets: [{ key: 'preserve_layout', doc_count: 2 }] } }, + statusByApp: { buckets: [ { key: 'completed', doc_count: 4, jobTypes: { buckets: [ { key: 'printable_pdf', doc_count: 2, appNames: { buckets: [ { key: 'canvas workpad', doc_count: 1 }, { key: 'dashboard', doc_count: 1 }, ] } }, { key: 'PNG', doc_count: 1, appNames: { buckets: [{ key: 'dashboard', doc_count: 1 }] } }, { key: 'csv', doc_count: 1, appNames: { buckets: [] } }, ] } }, ] }, + objectTypes: { doc_count: 2, pdf: { buckets: [ { key: 'canvas workpad', doc_count: 1 }, { key: 'dashboard', doc_count: 1 }, ] } }, statusTypes: { buckets: [{ key: 'completed', doc_count: 4 }] }, - jobTypes: { buckets: [ { key: 'printable_pdf', doc_count: 2 }, { key: 'PNG', doc_count: 1 }, { key: 'csv', doc_count: 1 }, ], }, + jobTypes: { buckets: [ { key: 'printable_pdf', doc_count: 2 }, { key: 'PNG', doc_count: 1 }, { key: 'csv', doc_count: 1 }, ] }, }, last7Days: { doc_count: 4, - layoutTypes: { doc_count: 2, pdf: { buckets: [{ key: 'preserve_layout', doc_count: 2 }] }, }, - statusByApp: { buckets: [ { key: 'completed', doc_count: 4, jobTypes: { buckets: [ { key: 'printable_pdf', doc_count: 2, appNames: { buckets: [ { key: 'canvas workpad', doc_count: 1 }, { key: 'dashboard', doc_count: 1 }, ], }, }, { key: 'PNG', doc_count: 1, appNames: { buckets: [{ key: 'dashboard', doc_count: 1 }] }, }, { key: 'csv', doc_count: 1, appNames: { buckets: [] } }, ], }, }, ], }, - objectTypes: { doc_count: 2, pdf: { buckets: [ { key: 'canvas workpad', doc_count: 1 }, { key: 'dashboard', doc_count: 1 }, ], }, }, + layoutTypes: { doc_count: 2, pdf: { buckets: [{ key: 'preserve_layout', doc_count: 2 }] } }, + statusByApp: { buckets: [ { key: 'completed', doc_count: 4, jobTypes: { buckets: [ { key: 'printable_pdf', doc_count: 2, appNames: { buckets: [ { key: 'canvas workpad', doc_count: 1 }, { key: 'dashboard', doc_count: 1 }, ] } }, { key: 'PNG', doc_count: 1, appNames: { buckets: [{ key: 'dashboard', doc_count: 1 }] } }, { key: 'csv', doc_count: 1, appNames: { buckets: [] } }, ] } }, ] }, + objectTypes: { doc_count: 2, pdf: { buckets: [ { key: 'canvas workpad', doc_count: 1 }, { key: 'dashboard', doc_count: 1 }, ] } }, statusTypes: { buckets: [{ key: 'completed', doc_count: 4 }] }, - jobTypes: { buckets: [ { key: 'printable_pdf', doc_count: 2 }, { key: 'PNG', doc_count: 1 }, { key: 'csv', doc_count: 1 }, ], }, + jobTypes: { buckets: [ { key: 'printable_pdf', doc_count: 2 }, { key: 'PNG', doc_count: 1 }, { key: 'csv', doc_count: 1 }, ] }, }, - lastDay: { - doc_count: 4, - layoutTypes: { doc_count: 2, pdf: { buckets: [{ key: 'preserve_layout', doc_count: 2 }] }, }, - statusByApp: { buckets: [ { key: 'completed', doc_count: 4, jobTypes: { buckets: [ { key: 'printable_pdf', doc_count: 2, appNames: { buckets: [ { key: 'canvas workpad', doc_count: 1 }, { key: 'dashboard', doc_count: 1 }, ], }, }, { key: 'PNG', doc_count: 1, appNames: { buckets: [{ key: 'dashboard', doc_count: 1 }] }, }, { key: 'csv', doc_count: 1, appNames: { buckets: [] } }, ], }, }, ], }, - objectTypes: { doc_count: 2, pdf: { buckets: [ { key: 'canvas workpad', doc_count: 1 }, { key: 'dashboard', doc_count: 1 }, ], }, }, - statusTypes: { buckets: [{ key: 'completed', doc_count: 4 }] }, - jobTypes: { buckets: [ { key: 'printable_pdf', doc_count: 2 }, { key: 'PNG', doc_count: 1 }, { key: 'csv', doc_count: 1 }, ], }, - }, - }, + }, // prettier-ignore }, }, - } as SearchResponse) // prettier-ignore + }) ); const usageStats = await collector.fetch(collectorFetchContext); expect(usageStats).toMatchSnapshot(); @@ -445,9 +333,9 @@ describe('data modeling', () => { collectorFetchContext = getMockFetchClients( getResponseMock({ - aggregations: { - ranges: { - buckets: { + aggregations: { + ranges: { + buckets: { all: { doc_count: 0, jobTypes: { buckets: [] }, @@ -455,6 +343,9 @@ describe('data modeling', () => { objectTypes: { doc_count: 0, pdf: { buckets: [] } }, statusByApp: { buckets: [] }, statusTypes: { buckets: [] }, + sizeMax: { value: null}, + sizeMin: { value: null }, + sizeAvg: { value: null}, }, last7Days: { doc_count: 0, @@ -463,19 +354,15 @@ describe('data modeling', () => { objectTypes: { doc_count: 0, pdf: { buckets: [] } }, statusByApp: { buckets: [] }, statusTypes: { buckets: [] }, + sizeMax: { value: null}, + sizeMin: { value: null }, + sizeAvg: { value: null}, + }, - lastDay: { - doc_count: 0, - jobTypes: { buckets: [] }, - layoutTypes: { doc_count: 0, pdf: { buckets: [] } }, - objectTypes: { doc_count: 0, pdf: { buckets: [] } }, - statusByApp: { buckets: [] }, - statusTypes: { buckets: [] }, - }, + }, // prettier-ignore }, }, - }, - } as SearchResponse) // prettier-ignore + }) ); const usageStats = await collector.fetch(collectorFetchContext); expect(usageStats).toMatchSnapshot(); diff --git a/x-pack/plugins/reporting/server/usage/schema.ts b/x-pack/plugins/reporting/server/usage/schema.ts index 02bf65e7c5e4d..9580ddb935dfb 100644 --- a/x-pack/plugins/reporting/server/usage/schema.ts +++ b/x-pack/plugins/reporting/server/usage/schema.ts @@ -14,6 +14,7 @@ import { LayoutCounts, RangeStats, ReportingUsageType, + SizePercentiles, } from './types'; const appCountsSchema: MakeSchemaFrom = { @@ -39,10 +40,21 @@ const byAppCountsSchema: MakeSchemaFrom = { printable_pdf_v2: appCountsSchema, }; +const sizesSchema: MakeSchemaFrom = { + '1.0': { type: 'long' }, + '5.0': { type: 'long' }, + '25.0': { type: 'long' }, + '50.0': { type: 'long' }, + '75.0': { type: 'long' }, + '95.0': { type: 'long' }, + '99.0': { type: 'long' }, +}; + const availableTotalSchema: MakeSchemaFrom = { available: { type: 'boolean' }, total: { type: 'long' }, deprecated: { type: 'long' }, + sizes: sizesSchema, app: appCountsSchema, layout: layoutCountsSchema, }; @@ -74,6 +86,7 @@ const rangeStatsSchema: MakeSchemaFrom = { pending: byAppCountsSchema, processing: byAppCountsSchema, }, + output_size: sizesSchema, }; export const reportingSchema: MakeSchemaFrom = { diff --git a/x-pack/plugins/reporting/server/usage/types.ts b/x-pack/plugins/reporting/server/usage/types.ts index 7bd79de090b37..856d3ad10cb26 100644 --- a/x-pack/plugins/reporting/server/usage/types.ts +++ b/x-pack/plugins/reporting/server/usage/types.ts @@ -5,45 +5,57 @@ * 2.0. */ -export interface KeyCountBucket { - key: string; +export interface SizePercentiles { + '1.0': number | null; + '5.0': number | null; + '25.0': number | null; + '50.0': number | null; + '75.0': number | null; + '95.0': number | null; + '99.0': number | null; +} + +interface DocCount { doc_count: number; - isDeprecated?: { - doc_count: number; - }; +} + +interface SizeStats { + sizes?: { values: SizePercentiles }; +} + +export interface KeyCountBucket extends DocCount, SizeStats { + key: string; + isDeprecated?: DocCount; } export interface AggregationBuckets { buckets: KeyCountBucket[]; } -export interface StatusByAppBucket { +export interface StatusByAppBucket extends DocCount { key: string; - doc_count: number; jobTypes: { - buckets: Array<{ - doc_count: number; - key: string; - appNames: AggregationBuckets; - }>; + buckets: Array< + { + key: string; + appNames: AggregationBuckets; + } & DocCount + >; }; } -export interface AggregationResultBuckets { - jobTypes: AggregationBuckets; +export interface AggregationResultBuckets extends DocCount, SizeStats { + jobTypes?: AggregationBuckets; layoutTypes: { - doc_count: number; - pdf: AggregationBuckets; - }; + pdf?: AggregationBuckets; + } & DocCount; objectTypes: { - doc_count: number; - pdf: AggregationBuckets; - }; + pdf?: AggregationBuckets; + } & DocCount; statusTypes: AggregationBuckets; statusByApp: { buckets: StatusByAppBucket[]; }; - doc_count: number; } export interface SearchResponse { @@ -61,6 +73,7 @@ export interface AvailableTotal { available: boolean; total: number; deprecated?: number; + sizes?: SizePercentiles; app?: { search?: number; dashboard?: number; @@ -110,7 +123,8 @@ type StatusByAppCounts = { export type RangeStats = JobTypes & { _all: number; status: StatusCounts; - statuses: StatusByAppCounts; + statuses?: StatusByAppCounts; + output_size?: SizePercentiles; }; export type ReportingUsageType = RangeStats & { diff --git a/x-pack/plugins/rollup/server/index.ts b/x-pack/plugins/rollup/server/index.ts index aa96f3ae0aac3..e77e0e6f15d72 100644 --- a/x-pack/plugins/rollup/server/index.ts +++ b/x-pack/plugins/rollup/server/index.ts @@ -13,5 +13,6 @@ export const plugin = (pluginInitializerContext: PluginInitializerContext) => new RollupPlugin(pluginInitializerContext); export const config: PluginConfigDescriptor = { + deprecations: ({ deprecate }) => [deprecate('enabled', '8.0.0')], schema: configSchema, }; diff --git a/x-pack/plugins/rollup/server/types.ts b/x-pack/plugins/rollup/server/types.ts index c774644da46ce..a3e826fefa0bf 100644 --- a/x-pack/plugins/rollup/server/types.ts +++ b/x-pack/plugins/rollup/server/types.ts @@ -7,7 +7,7 @@ import { IRouter } from 'src/core/server'; import { UsageCollectionSetup } from 'src/plugins/usage_collection/server'; -import { VisTypeTimeseriesSetup } from 'src/plugins/vis_type_timeseries/server'; +import { VisTypeTimeseriesSetup } from 'src/plugins/vis_types/timeseries/server'; import { getCapabilitiesForRollupIndices } from 'src/plugins/data/server'; import { IndexManagementPluginSetup } from '../../index_management/server'; diff --git a/x-pack/plugins/rollup/tsconfig.json b/x-pack/plugins/rollup/tsconfig.json index fbe323b2549ea..252c27a66fba2 100644 --- a/x-pack/plugins/rollup/tsconfig.json +++ b/x-pack/plugins/rollup/tsconfig.json @@ -22,7 +22,7 @@ { "path": "../../../src/plugins/home/tsconfig.json" }, { "path": "../index_management/tsconfig.json" }, { "path": "../../../src/plugins/usage_collection/tsconfig.json" }, - { "path": "../../../src/plugins/vis_type_timeseries/tsconfig.json" }, + { "path": "../../../src/plugins/vis_types/timeseries/tsconfig.json" }, // required bundles { "path": "../../../src/plugins/kibana_utils/tsconfig.json" }, { "path": "../../../src/plugins/kibana_react/tsconfig.json" }, diff --git a/x-pack/plugins/rule_registry/server/config.ts b/x-pack/plugins/rule_registry/server/config.ts index 481c5fe3cce8b..62f29a9e06294 100644 --- a/x-pack/plugins/rule_registry/server/config.ts +++ b/x-pack/plugins/rule_registry/server/config.ts @@ -6,8 +6,10 @@ */ import { schema, TypeOf } from '@kbn/config-schema'; +import { PluginConfigDescriptor } from 'src/core/server'; -export const config = { +export const config: PluginConfigDescriptor = { + deprecations: ({ deprecate }) => [deprecate('enabled', '8.0.0')], schema: schema.object({ enabled: schema.boolean({ defaultValue: true }), write: schema.object({ diff --git a/x-pack/plugins/saved_objects_tagging/server/config.ts b/x-pack/plugins/saved_objects_tagging/server/config.ts index f4f0bd1cf1aa0..183779aa6f229 100644 --- a/x-pack/plugins/saved_objects_tagging/server/config.ts +++ b/x-pack/plugins/saved_objects_tagging/server/config.ts @@ -16,6 +16,7 @@ const configSchema = schema.object({ export type SavedObjectsTaggingConfigType = TypeOf; export const config: PluginConfigDescriptor = { + deprecations: ({ deprecate }) => [deprecate('enabled', '8.0.0')], schema: configSchema, exposeToBrowser: { cache_refresh_interval: true, diff --git a/x-pack/plugins/security_solution/common/constants.ts b/x-pack/plugins/security_solution/common/constants.ts index 8076caf60f697..2e2dffa05c9fb 100644 --- a/x-pack/plugins/security_solution/common/constants.ts +++ b/x-pack/plugins/security_solution/common/constants.ts @@ -64,6 +64,7 @@ export const DEFAULT_INDICATOR_SOURCE_PATH = 'threatintel.indicator'; export const ENRICHMENT_DESTINATION_PATH = 'threat.enrichments'; export const DEFAULT_THREAT_INDEX_KEY = 'securitySolution:defaultThreatIndex'; export const DEFAULT_THREAT_INDEX_VALUE = ['filebeat-*']; +export const DEFAULT_THREAT_MATCH_QUERY = '@timestamp >= "now-30d"'; export enum SecurityPageName { administration = 'administration', @@ -197,7 +198,6 @@ export const EQL_RULE_TYPE_ID = `${RULE_TYPE_PREFIX}.eqlRule` as const; export const INDICATOR_RULE_TYPE_ID = `${RULE_TYPE_PREFIX}.indicatorRule` as const; export const ML_RULE_TYPE_ID = `${RULE_TYPE_PREFIX}.mlRule` as const; export const QUERY_RULE_TYPE_ID = `${RULE_TYPE_PREFIX}.queryRule` as const; -export const SAVED_QUERY_RULE_TYPE_ID = `${RULE_TYPE_PREFIX}.savedQueryRule` as const; export const THRESHOLD_RULE_TYPE_ID = `${RULE_TYPE_PREFIX}.thresholdRule` as const; /** diff --git a/x-pack/plugins/security_solution/common/types/timeline/pinned_event/index.ts b/x-pack/plugins/security_solution/common/types/timeline/pinned_event/index.ts index dbb19df7a6b05..df230615818ac 100644 --- a/x-pack/plugins/security_solution/common/types/timeline/pinned_event/index.ts +++ b/x-pack/plugins/security_solution/common/types/timeline/pinned_event/index.ts @@ -30,6 +30,12 @@ export const SavedPinnedEventRuntimeType = runtimeTypes.intersection([ export interface SavedPinnedEvent extends runtimeTypes.TypeOf {} +/** + * This type represents a pinned event type stored in a saved object that does not include any fields that reference + * other saved objects. + */ +export type PinnedEventWithoutExternalRefs = Omit; + /** * Note Saved object type with metadata */ 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 f8b3b426580b2..871e50821b58c 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 @@ -108,6 +108,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'; describe('indicator match', () => { describe('Detection rules, Indicator Match', () => { @@ -180,8 +181,8 @@ describe('indicator match', () => { }); describe('custom indicator query input', () => { - it('Has a default set of *:*', () => { - getCustomIndicatorQueryInput().should('have.text', '*:*'); + it(`Has a default set of ${DEFAULT_THREAT_MATCH_QUERY}`, () => { + getCustomIndicatorQueryInput().should('have.text', DEFAULT_THREAT_MATCH_QUERY); }); it('Shows invalidation text if text is removed', () => { diff --git a/x-pack/plugins/security_solution/cypress/tasks/create_new_rule.ts b/x-pack/plugins/security_solution/cypress/tasks/create_new_rule.ts index c1210bf457b69..b7fb0785736f6 100644 --- a/x-pack/plugins/security_solution/cypress/tasks/create_new_rule.ts +++ b/x-pack/plugins/security_solution/cypress/tasks/create_new_rule.ts @@ -473,6 +473,7 @@ export const fillDefineIndicatorMatchRuleAndContinue = (rule: ThreatIndicatorRul indexField: rule.indicatorMappingField, indicatorIndexField: rule.indicatorIndexField, }); + getCustomIndicatorQueryInput().type('{selectall}{enter}*:*'); getDefineContinueButton().should('exist').click({ force: true }); cy.get(CUSTOM_QUERY_INPUT).should('not.exist'); }; diff --git a/x-pack/plugins/security_solution/public/common/components/endpoint/route_capture.tsx b/x-pack/plugins/security_solution/public/common/components/endpoint/route_capture.tsx index ebd25eef87cb7..a5e0c90402df4 100644 --- a/x-pack/plugins/security_solution/public/common/components/endpoint/route_capture.tsx +++ b/x-pack/plugins/security_solution/public/common/components/endpoint/route_capture.tsx @@ -9,6 +9,8 @@ import React, { memo, useEffect } from 'react'; import { useLocation } from 'react-router-dom'; import { useDispatch } from 'react-redux'; import { AppLocation } from '../../../../common/endpoint/types'; +import { timelineActions } from '../../../timelines/store/timeline'; +import { TimelineId } from '../../../../../timelines/common'; /** * This component should be used above all routes, but below the Provider. @@ -18,6 +20,10 @@ export const RouteCapture = memo(({ children }) => { const location: AppLocation = useLocation(); const dispatch = useDispatch(); + useEffect(() => { + dispatch(timelineActions.showTimeline({ id: TimelineId.active, show: false })); + }, [dispatch, location.pathname]); + useEffect(() => { dispatch({ type: 'userChangedUrl', payload: location }); }); diff --git a/x-pack/plugins/security_solution/public/common/components/events_viewer/index.tsx b/x-pack/plugins/security_solution/public/common/components/events_viewer/index.tsx index c62337b2426d3..9e1fd3a769eee 100644 --- a/x-pack/plugins/security_solution/public/common/components/events_viewer/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/events_viewer/index.tsx @@ -177,8 +177,7 @@ const StatefulEventsViewerComponent: React.FC = ({ {tGridEnabled ? ( timelinesUi.getTGrid<'embedded'>({ - id, - type: 'embedded', + additionalFilters, browserFields, bulkActions, columns, @@ -189,9 +188,12 @@ const StatefulEventsViewerComponent: React.FC = ({ end, entityType, filters: globalFilters, + filterStatus: currentFilter, globalFullScreen, + graphEventId, graphOverlay, hasAlertsCrud, + id, indexNames: selectedPatterns, indexPattern, isLive, @@ -199,19 +201,17 @@ const StatefulEventsViewerComponent: React.FC = ({ itemsPerPage, itemsPerPageOptions: itemsPerPageOptions!, kqlMode, - query, + leadingControlColumns, onRuleChange, + query, renderCellValue, rowRenderers, setQuery, - start, sort, - additionalFilters, - graphEventId, - filterStatus: currentFilter, - leadingControlColumns, - trailingControlColumns, + start, tGridEventRenderedViewEnabled, + trailingControlColumns, + type: 'embedded', unit, }) ) : ( diff --git a/x-pack/plugins/security_solution/public/common/components/hover_actions/actions/show_top_n.tsx b/x-pack/plugins/security_solution/public/common/components/hover_actions/actions/show_top_n.tsx index 0d6e59483fbc4..e1546c5220e22 100644 --- a/x-pack/plugins/security_solution/public/common/components/hover_actions/actions/show_top_n.tsx +++ b/x-pack/plugins/security_solution/public/common/components/hover_actions/actions/show_top_n.tsx @@ -21,6 +21,7 @@ import { useSourcererScope } from '../../../containers/sourcerer'; import { TooltipWithKeyboardShortcut } from '../../accessibility'; import { getAdditionalScreenReaderOnlyContext } from '../utils'; import { SHOW_TOP_N_KEYBOARD_SHORTCUT } from '../keyboard_shortcut_constants'; +import { Filter } from '../../../../../../../../src/plugins/data/public'; const SHOW_TOP = (fieldName: string) => i18n.translate('xpack.securitySolution.hoverActions.showTopTooltip', { @@ -35,11 +36,12 @@ interface Props { Component?: typeof EuiButtonEmpty | typeof EuiButtonIcon | typeof EuiContextMenuItem; enablePopOver?: boolean; field: string; + globalFilters?: Filter[]; onClick: () => void; onFilterAdded?: () => void; ownFocus: boolean; - showTopN: boolean; showTooltip?: boolean; + showTopN: boolean; timelineId?: string | null; value?: string[] | string | null; } @@ -56,6 +58,7 @@ export const ShowTopNButton: React.FC = React.memo( showTopN, timelineId, value, + globalFilters, }) => { const activeScope: SourcererScopeName = timelineId === TimelineId.active @@ -128,9 +131,10 @@ export const ShowTopNButton: React.FC = React.memo( timelineId={timelineId ?? undefined} toggleTopN={onClick} value={value} + globalFilters={globalFilters} /> ), - [browserFields, field, indexPattern, onClick, onFilterAdded, timelineId, value] + [browserFields, field, indexPattern, onClick, onFilterAdded, timelineId, value, globalFilters] ); return showTopN ? ( diff --git a/x-pack/plugins/security_solution/public/common/components/navigation/breadcrumbs/index.test.ts b/x-pack/plugins/security_solution/public/common/components/navigation/breadcrumbs/index.test.ts index c3830513c3229..08f2ef2267f97 100644 --- a/x-pack/plugins/security_solution/public/common/components/navigation/breadcrumbs/index.test.ts +++ b/x-pack/plugins/security_solution/public/common/components/navigation/breadcrumbs/index.test.ts @@ -7,13 +7,15 @@ import '../../../mock/match_media'; import { encodeIpv6 } from '../../../lib/helpers'; -import { getBreadcrumbsForRoute, setBreadcrumbs } from '.'; +import { getBreadcrumbsForRoute, useSetBreadcrumbs } from '.'; import { HostsTableType } from '../../../../hosts/store/model'; import { RouteSpyState, SiemRouteType } from '../../../utils/route/types'; import { TabNavigationProps } from '../tab_navigation/types'; import { NetworkRouteType } from '../../../../network/pages/navigation/types'; import { TimelineTabs } from '../../../../../common/types/timeline'; import { AdministrationSubTab } from '../../../../management/types'; +import { renderHook } from '@testing-library/react-hooks'; +import { TestProviders } from '../../../mock'; const setBreadcrumbsMock = jest.fn(); const chromeMock = { @@ -425,34 +427,13 @@ describe('Navigation Breadcrumbs', () => { }, ]); }); - - test('should set "timeline.isOpen" to false when timeline is open', () => { - const breadcrumbs = getBreadcrumbsForRoute( - { - ...getMockObject('timelines', '/', undefined), - timeline: { - activeTab: TimelineTabs.query, - id: 'TIMELINE_ID', - isOpen: true, - graphEventId: 'GRAPH_EVENT_ID', - }, - }, - getUrlForAppMock - ); - expect(breadcrumbs).toEqual([ - { text: 'Security', href: 'securitySolution/overview' }, - { - text: 'Timelines', - href: "securitySolution/timelines?sourcerer=()&timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)),timeline:(linkTo:!(global),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)))&timeline=(activeTab:query,graphEventId:GRAPH_EVENT_ID,id:TIMELINE_ID,isOpen:!f)", - }, - ]); - }); }); describe('setBreadcrumbs()', () => { test('should call chrome breadcrumb service with correct breadcrumbs', () => { const navigateToUrlMock = jest.fn(); - setBreadcrumbs( + const { result } = renderHook(() => useSetBreadcrumbs(), { wrapper: TestProviders }); + result.current( getMockObject('hosts', '/', hostName), chromeMock, getUrlForAppMock, diff --git a/x-pack/plugins/security_solution/public/common/components/navigation/breadcrumbs/index.ts b/x-pack/plugins/security_solution/public/common/components/navigation/breadcrumbs/index.ts index aae97d90cb4b8..f4e3814738f92 100644 --- a/x-pack/plugins/security_solution/public/common/components/navigation/breadcrumbs/index.ts +++ b/x-pack/plugins/security_solution/public/common/components/navigation/breadcrumbs/index.ts @@ -7,6 +7,7 @@ import { getOr, omit } from 'lodash/fp'; +import { useDispatch } from 'react-redux'; import { ChromeBreadcrumb } from '../../../../../../../../src/core/public'; import { APP_NAME, APP_ID } from '../../../../../common/constants'; import { StartServices } from '../../../../types'; @@ -27,33 +28,40 @@ import { UebaRouteSpyState, } from '../../../utils/route/types'; import { getAppOverviewUrl } from '../../link_to'; - +import { timelineActions } from '../../../../../public/timelines/store/timeline'; +import { TimelineId } from '../../../../../common/types/timeline'; import { TabNavigationProps } from '../tab_navigation/types'; import { getSearch } from '../helpers'; import { GetUrlForApp, NavigateToUrl, SearchNavTab } from '../types'; -export const setBreadcrumbs = ( - spyState: RouteSpyState & TabNavigationProps, - chrome: StartServices['chrome'], - getUrlForApp: GetUrlForApp, - navigateToUrl: NavigateToUrl -) => { - const breadcrumbs = getBreadcrumbsForRoute(spyState, getUrlForApp); - if (breadcrumbs) { - chrome.setBreadcrumbs( - breadcrumbs.map((breadcrumb) => ({ - ...breadcrumb, - ...(breadcrumb.href && !breadcrumb.onClick - ? { - onClick: (ev) => { - ev.preventDefault(); - navigateToUrl(breadcrumb.href!); - }, - } - : {}), - })) - ); - } +export const useSetBreadcrumbs = () => { + const dispatch = useDispatch(); + return ( + spyState: RouteSpyState & TabNavigationProps, + chrome: StartServices['chrome'], + getUrlForApp: GetUrlForApp, + navigateToUrl: NavigateToUrl + ) => { + const breadcrumbs = getBreadcrumbsForRoute(spyState, getUrlForApp); + if (breadcrumbs) { + chrome.setBreadcrumbs( + breadcrumbs.map((breadcrumb) => ({ + ...breadcrumb, + ...(breadcrumb.href && !breadcrumb.onClick + ? { + onClick: (ev) => { + ev.preventDefault(); + + dispatch(timelineActions.showTimeline({ id: TimelineId.active, show: false })); + + navigateToUrl(breadcrumb.href!); + }, + } + : {}), + })) + ); + } + }; }; const isNetworkRoutes = (spyState: RouteSpyState): spyState is NetworkRouteSpyState => @@ -79,14 +87,10 @@ const isRulesRoutes = (spyState: RouteSpyState): spyState is AdministrationRoute // eslint-disable-next-line complexity export const getBreadcrumbsForRoute = ( - objectParam: RouteSpyState & TabNavigationProps, + object: RouteSpyState & TabNavigationProps, getUrlForApp: GetUrlForApp ): ChromeBreadcrumb[] | null => { - const spyState: RouteSpyState = omit('navTabs', objectParam); - - // Sets `timeline.isOpen` to false in the state to avoid reopening the timeline on breadcrumb click. https://github.com/elastic/kibana/issues/100322 - const object = { ...objectParam, timeline: { ...objectParam.timeline, isOpen: false } }; - + const spyState: RouteSpyState = omit('navTabs', object); const overviewPath = getUrlForApp(APP_ID, { deepLinkId: SecurityPageName.overview }); const siemRootBreadcrumb: ChromeBreadcrumb = { text: APP_NAME, diff --git a/x-pack/plugins/security_solution/public/common/components/navigation/index.test.tsx b/x-pack/plugins/security_solution/public/common/components/navigation/index.test.tsx index 5bb0805acc378..393a3d3e0aaba 100644 --- a/x-pack/plugins/security_solution/public/common/components/navigation/index.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/navigation/index.test.tsx @@ -10,7 +10,6 @@ import React from 'react'; import { CONSTANTS } from '../url_state/constants'; import { TabNavigationComponent } from './'; -import { setBreadcrumbs } from './breadcrumbs'; import { navTabs } from '../../../app/home/home_navigations'; import { HostsTableType } from '../../../hosts/store/model'; import { RouteSpyState } from '../../utils/route/types'; @@ -28,8 +27,10 @@ jest.mock('react-router-dom', () => { }; }); +const mockSetBreadcrumbs = jest.fn(); + jest.mock('./breadcrumbs', () => ({ - setBreadcrumbs: jest.fn(), + useSetBreadcrumbs: () => mockSetBreadcrumbs, })); const mockGetUrlForApp = jest.fn(); const mockNavigateToUrl = jest.fn(); @@ -102,7 +103,7 @@ describe('SIEM Navigation', () => { }; const wrapper = mount(); test('it calls setBreadcrumbs with correct path on mount', () => { - expect(setBreadcrumbs).toHaveBeenNthCalledWith( + expect(mockSetBreadcrumbs).toHaveBeenNthCalledWith( 1, { detailName: undefined, @@ -158,7 +159,7 @@ describe('SIEM Navigation', () => { tabName: 'authentications', }); wrapper.update(); - expect(setBreadcrumbs).toHaveBeenNthCalledWith( + expect(mockSetBreadcrumbs).toHaveBeenNthCalledWith( 2, { detailName: undefined, diff --git a/x-pack/plugins/security_solution/public/common/components/navigation/index.tsx b/x-pack/plugins/security_solution/public/common/components/navigation/index.tsx index 020b246d3e26f..f8b9251f4ff91 100644 --- a/x-pack/plugins/security_solution/public/common/components/navigation/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/navigation/index.tsx @@ -14,7 +14,7 @@ import { useKibana } from '../../lib/kibana'; import { RouteSpyState } from '../../utils/route/types'; import { useRouteSpy } from '../../utils/route/use_route_spy'; import { makeMapStateToProps } from '../url_state/helpers'; -import { setBreadcrumbs } from './breadcrumbs'; +import { useSetBreadcrumbs } from './breadcrumbs'; import { TabNavigation } from './tab_navigation'; import { TabNavigationComponentProps, SecuritySolutionTabNavigationProps } from './types'; @@ -42,6 +42,8 @@ export const TabNavigationComponent: React.FC< application: { getUrlForApp, navigateToUrl }, } = useKibana().services; + const setBreadcrumbs = useSetBreadcrumbs(); + useEffect(() => { if (pathName || pageName) { setBreadcrumbs( @@ -79,6 +81,7 @@ export const TabNavigationComponent: React.FC< tabName, getUrlForApp, navigateToUrl, + setBreadcrumbs, ]); return ( diff --git a/x-pack/plugins/security_solution/public/common/components/navigation/use_security_solution_navigation/index.test.tsx b/x-pack/plugins/security_solution/public/common/components/navigation/use_security_solution_navigation/index.test.tsx index b488000ac8736..820d90087ce48 100644 --- a/x-pack/plugins/security_solution/public/common/components/navigation/use_security_solution_navigation/index.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/navigation/use_security_solution_navigation/index.test.tsx @@ -17,6 +17,7 @@ import { useDeepEqualSelector } from '../../../hooks/use_selector'; import { UrlInputsModel } from '../../../store/inputs/model'; import { useRouteSpy } from '../../../utils/route/use_route_spy'; import { useIsExperimentalFeatureEnabled } from '../../../hooks/use_experimental_features'; +import { TestProviders } from '../../../mock'; jest.mock('../../../lib/kibana/kibana_react'); jest.mock('../../../lib/kibana'); @@ -96,8 +97,9 @@ describe('useSecuritySolutionNavigation', () => { }); it('should create navigation config', async () => { - const { result } = renderHook<{}, KibanaPageTemplateProps['solutionNav']>(() => - useSecuritySolutionNavigation() + const { result } = renderHook<{}, KibanaPageTemplateProps['solutionNav']>( + () => useSecuritySolutionNavigation(), + { wrapper: TestProviders } ); expect(result.current).toMatchInlineSnapshot(` @@ -243,8 +245,9 @@ describe('useSecuritySolutionNavigation', () => { // TODO: Steph/ueba remove when no longer experimental it('should include ueba when feature flag is on', async () => { (useIsExperimentalFeatureEnabled as jest.Mock).mockReturnValue(true); - const { result } = renderHook<{}, KibanaPageTemplateProps['solutionNav']>(() => - useSecuritySolutionNavigation() + const { result } = renderHook<{}, KibanaPageTemplateProps['solutionNav']>( + () => useSecuritySolutionNavigation(), + { wrapper: TestProviders } ); // @ts-ignore possibly undefined, but if undefined we want this test to fail @@ -259,8 +262,9 @@ describe('useSecuritySolutionNavigation', () => { read: true, }); - const { result } = renderHook<{}, KibanaPageTemplateProps['solutionNav']>(() => - useSecuritySolutionNavigation() + const { result } = renderHook<{}, KibanaPageTemplateProps['solutionNav']>( + () => useSecuritySolutionNavigation(), + { wrapper: TestProviders } ); const caseNavItem = (result.current?.items || [])[3].items?.find( @@ -286,8 +290,9 @@ describe('useSecuritySolutionNavigation', () => { read: false, }); - const { result } = renderHook<{}, KibanaPageTemplateProps['solutionNav']>(() => - useSecuritySolutionNavigation() + const { result } = renderHook<{}, KibanaPageTemplateProps['solutionNav']>( + () => useSecuritySolutionNavigation(), + { wrapper: TestProviders } ); const caseNavItem = (result.current?.items || [])[3].items?.find( diff --git a/x-pack/plugins/security_solution/public/common/components/navigation/use_security_solution_navigation/index.tsx b/x-pack/plugins/security_solution/public/common/components/navigation/use_security_solution_navigation/index.tsx index 6a9fe2b3749aa..1e824015db872 100644 --- a/x-pack/plugins/security_solution/public/common/components/navigation/use_security_solution_navigation/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/navigation/use_security_solution_navigation/index.tsx @@ -8,7 +8,7 @@ import { useEffect } from 'react'; import { usePrimaryNavigation } from './use_primary_navigation'; import { useKibana } from '../../../lib/kibana'; -import { setBreadcrumbs } from '../breadcrumbs'; +import { useSetBreadcrumbs } from '../breadcrumbs'; import { makeMapStateToProps } from '../../url_state/helpers'; import { useRouteSpy } from '../../../utils/route/use_route_spy'; import { navTabs } from '../../../../app/home/home_navigations'; @@ -37,6 +37,9 @@ export const useSecuritySolutionNavigation = () => { const { ueba, ...rest } = enabledNavTabs; enabledNavTabs = rest; } + + const setBreadcrumbs = useSetBreadcrumbs(); + useEffect(() => { if (pathName || pageName) { setBreadcrumbs( @@ -74,6 +77,7 @@ export const useSecuritySolutionNavigation = () => { getUrlForApp, navigateToUrl, enabledNavTabs, + setBreadcrumbs, ]); return usePrimaryNavigation({ diff --git a/x-pack/plugins/security_solution/public/common/components/top_n/index.test.tsx b/x-pack/plugins/security_solution/public/common/components/top_n/index.test.tsx index 787b7e8f88703..6962ed03e81d4 100644 --- a/x-pack/plugins/security_solution/public/common/components/top_n/index.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/top_n/index.test.tsx @@ -160,6 +160,60 @@ let testProps = { }; describe('StatefulTopN', () => { + describe('rendering globalFilter', () => { + let wrapper: ReactWrapper; + const globalFilters = [ + { + meta: { + alias: null, + negate: false, + disabled: false, + type: 'phrase', + key: 'signal.rule.id', + params: { + query: 'd62249f0-1632-11ec-b035-19607969bc20', + }, + }, + query: { + match_phrase: { + 'signal.rule.id': 'd62249f0-1632-11ec-b035-19607969bc20', + }, + }, + }, + ]; + beforeEach(() => { + wrapper = mount( + + + + ); + }); + + test(`provides filters from non Redux state when rendering in alerts table`, () => { + const props = wrapper.find('[data-test-subj="top-n"]').first().props() as Props; + + expect(props.filters).toEqual([ + { + meta: { + alias: null, + negate: false, + disabled: false, + type: 'phrase', + key: 'signal.rule.id', + params: { + query: 'd62249f0-1632-11ec-b035-19607969bc20', + }, + }, + query: { + match_phrase: { + 'signal.rule.id': 'd62249f0-1632-11ec-b035-19607969bc20', + }, + }, + }, + ]); + }); + }); + describe('rendering in a global NON-timeline context', () => { let wrapper: ReactWrapper; diff --git a/x-pack/plugins/security_solution/public/common/components/top_n/index.tsx b/x-pack/plugins/security_solution/public/common/components/top_n/index.tsx index 2286a53030784..1556f2d0f3d13 100644 --- a/x-pack/plugins/security_solution/public/common/components/top_n/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/top_n/index.tsx @@ -41,11 +41,11 @@ const makeMapStateToProps = () => { // The mapped Redux state provided to this component includes the global // filters that appear at the top of most views in the app, and all the // filters in the active timeline: - const mapStateToProps = (state: State) => { + const mapStateToProps = (state: State, ownProps: { globalFilters?: Filter[] }) => { const activeTimeline: TimelineModel = getTimeline(state, TimelineId.active) ?? timelineDefaults; const activeTimelineFilters = activeTimeline.filters ?? EMPTY_FILTERS; const activeTimelineInput: inputsModel.InputsRange = getInputsTimeline(state); - + const { globalFilters } = ownProps; return { activeTimelineEventType: activeTimeline.eventType, activeTimelineFilters: @@ -59,7 +59,7 @@ const makeMapStateToProps = () => { dataProviders: activeTimeline.activeTab === TimelineTabs.query ? activeTimeline.dataProviders : [], globalQuery: getGlobalQuerySelector(state), - globalFilters: getGlobalFiltersQuerySelector(state), + globalFilters: globalFilters ?? getGlobalFiltersQuerySelector(state), kqlMode: activeTimeline.kqlMode, }; }; @@ -82,6 +82,7 @@ export interface OwnProps { toggleTopN: () => void; onFilterAdded?: () => void; value?: string[] | string | null; + globalFilters?: Filter[]; } type PropsFromRedux = ConnectedProps; type Props = OwnProps & PropsFromRedux; diff --git a/x-pack/plugins/security_solution/public/common/components/url_state/index.test.tsx b/x-pack/plugins/security_solution/public/common/components/url_state/index.test.tsx index 18b99adca3a55..81b2111f00da9 100644 --- a/x-pack/plugins/security_solution/public/common/components/url_state/index.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/url_state/index.test.tsx @@ -23,6 +23,7 @@ import { import { UrlStateContainerPropTypes } from './types'; import { useUrlStateHooks } from './use_url_state'; import { waitFor } from '@testing-library/react'; +import { useLocation } from 'react-router-dom'; let mockProps: UrlStateContainerPropTypes; @@ -59,11 +60,12 @@ jest.mock('../../lib/kibana', () => ({ }, })); -jest.mock('react-redux', () => { - const original = jest.requireActual('react-redux'); +jest.mock('react-router-dom', () => { + const original = jest.requireActual('react-router-dom'); + return { ...original, - useDispatch: () => jest.fn(), + useLocation: jest.fn(), }; }); @@ -84,6 +86,11 @@ describe('UrlStateContainer', () => { pageName, detailName, }).relativeTimeSearch.undefinedQuery; + + (useLocation as jest.Mock).mockReturnValue({ + pathname: mockProps.pathName, + }); + mount( useUrlStateHooks(args)} />); expect(mockSetRelativeRangeDatePicker.mock.calls[1][0]).toEqual({ @@ -113,6 +120,11 @@ describe('UrlStateContainer', () => { (page, namespaceLower, namespaceUpper, examplePath, type, pageName, detailName) => { mockProps = getMockPropsObj({ page, examplePath, namespaceLower, pageName, detailName }) .absoluteTimeSearch.undefinedQuery; + + (useLocation as jest.Mock).mockReturnValue({ + pathname: mockProps.pathName, + }); + mount( useUrlStateHooks(args)} />); expect(mockSetAbsoluteRangeDatePicker.mock.calls[1][0]).toEqual({ @@ -138,6 +150,11 @@ describe('UrlStateContainer', () => { (page, namespaceLower, namespaceUpper, examplePath, type, pageName, detailName) => { mockProps = getMockPropsObj({ page, examplePath, namespaceLower, pageName, detailName }) .relativeTimeSearch.undefinedQuery; + + (useLocation as jest.Mock).mockReturnValue({ + pathname: mockProps.pathName, + }); + mount( useUrlStateHooks(args)} />); expect(mockSetFilterQuery.mock.calls[0][0]).toEqual({ @@ -162,6 +179,11 @@ describe('UrlStateContainer', () => { pageName, detailName, }).noSearch.definedQuery; + + (useLocation as jest.Mock).mockReturnValue({ + pathname: mockProps.pathName, + }); + mount( useUrlStateHooks(args)} />); expect( @@ -176,6 +198,24 @@ describe('UrlStateContainer', () => { ); }); }); + + it("it doesn't update URL state when pathName and browserPAth are out of sync", () => { + mockProps = getMockPropsObj({ + page: CONSTANTS.networkPage, + examplePath: '/network', + namespaceLower: 'network', + pageName: SecurityPageName.network, + detailName: undefined, + }).noSearch.undefinedQuery; + + (useLocation as jest.Mock).mockReturnValue({ + pathname: 'out of sync path', + }); + + mount( useUrlStateHooks(args)} />); + + expect(mockHistory.replace).not.toHaveBeenCalled(); + }); }); describe('After Initialization, keep Relative Date up to date for global only on alerts page', () => { @@ -189,6 +229,11 @@ describe('UrlStateContainer', () => { pageName, detailName, }).relativeTimeSearch.undefinedQuery; + + (useLocation as jest.Mock).mockReturnValue({ + pathname: mockProps.pathName, + }); + const wrapper = mount( useUrlStateHooks(args)} /> ); diff --git a/x-pack/plugins/security_solution/public/common/components/url_state/index_mocked.test.tsx b/x-pack/plugins/security_solution/public/common/components/url_state/index_mocked.test.tsx index 3175656f12071..f1e2cd7fa5357 100644 --- a/x-pack/plugins/security_solution/public/common/components/url_state/index_mocked.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/url_state/index_mocked.test.tsx @@ -15,6 +15,7 @@ import { CONSTANTS } from './constants'; import { getFilterQuery, getMockPropsObj, mockHistory, testCases } from './test_dependencies'; import { UrlStateContainerPropTypes } from './types'; import { useUrlStateHooks } from './use_url_state'; +import { useLocation } from 'react-router-dom'; let mockProps: UrlStateContainerPropTypes; @@ -31,13 +32,9 @@ jest.mock('../../lib/kibana', () => ({ }), })); -jest.mock('react-redux', () => { - const original = jest.requireActual('react-redux'); - return { - ...original, - useDispatch: () => jest.fn(), - }; -}); +jest.mock('react-router-dom', () => ({ + useLocation: jest.fn(), +})); describe('UrlStateContainer - lodash.throttle mocked to test update url', () => { afterEach(() => { @@ -54,6 +51,11 @@ describe('UrlStateContainer - lodash.throttle mocked to test update url', () => pageName: SecurityPageName.network, detailName: undefined, }).noSearch.definedQuery; + + (useLocation as jest.Mock).mockReturnValue({ + pathname: mockProps.pathName, + }); + const wrapper = mount( useUrlStateHooks(args)} /> ); @@ -105,6 +107,11 @@ describe('UrlStateContainer - lodash.throttle mocked to test update url', () => pageName: SecurityPageName.network, detailName: undefined, }).noSearch.undefinedQuery; + + (useLocation as jest.Mock).mockReturnValue({ + pathname: mockProps.pathName, + }); + const wrapper = mount( useUrlStateHooks(args)} /> ); @@ -137,6 +144,10 @@ describe('UrlStateContainer - lodash.throttle mocked to test update url', () => detailName: undefined, }).noSearch.undefinedQuery; + (useLocation as jest.Mock).mockReturnValue({ + pathname: mockProps.pathName, + }); + const wrapper = mount( useUrlStateHooks(args)} /> ); @@ -170,6 +181,10 @@ describe('UrlStateContainer - lodash.throttle mocked to test update url', () => detailName: undefined, }).noSearch.undefinedQuery; + (useLocation as jest.Mock).mockReturnValue({ + pathname: mockProps.pathName, + }); + const wrapper = mount( useUrlStateHooks(args)} /> ); @@ -203,6 +218,11 @@ describe('UrlStateContainer - lodash.throttle mocked to test update url', () => (page, namespaceLower, namespaceUpper, examplePath, type, pageName, detailName) => { mockProps = getMockPropsObj({ page, examplePath, namespaceLower, pageName, detailName }) .noSearch.undefinedQuery; + + (useLocation as jest.Mock).mockReturnValue({ + pathname: mockProps.pathName, + }); + mount( useUrlStateHooks(args)} />); expect(mockHistory.replace.mock.calls[0][0]).toEqual({ @@ -239,6 +259,11 @@ describe('UrlStateContainer - lodash.throttle mocked to test update url', () => pageName: SecurityPageName.network, detailName: undefined, }).noSearch.definedQuery; + + (useLocation as jest.Mock).mockReturnValue({ + pathname: mockProps.pathName, + }); + const wrapper = mount( useUrlStateHooks(args)} /> ); @@ -249,7 +274,12 @@ describe('UrlStateContainer - lodash.throttle mocked to test update url', () => "?sourcerer=()&timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)),timeline:(linkTo:!(global),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)))" ); + (useLocation as jest.Mock).mockReturnValue({ + pathname: updatedProps.pathName, + }); + wrapper.setProps({ hookProps: updatedProps }); + wrapper.update(); expect( diff --git a/x-pack/plugins/security_solution/public/common/components/url_state/test_dependencies.ts b/x-pack/plugins/security_solution/public/common/components/url_state/test_dependencies.ts index f560a35d13329..caa5eefc51400 100644 --- a/x-pack/plugins/security_solution/public/common/components/url_state/test_dependencies.ts +++ b/x-pack/plugins/security_solution/public/common/components/url_state/test_dependencies.ts @@ -169,13 +169,7 @@ interface GetMockPropsObj { detailName: string | undefined; } -export const getMockPropsObj = ({ - page, - examplePath, - namespaceLower, - pageName, - detailName, -}: GetMockPropsObj) => ({ +export const getMockPropsObj = ({ page, examplePath, pageName, detailName }: GetMockPropsObj) => ({ noSearch: { undefinedQuery: getMockProps( { diff --git a/x-pack/plugins/security_solution/public/common/components/url_state/use_url_state.tsx b/x-pack/plugins/security_solution/public/common/components/url_state/use_url_state.tsx index 87e17ba7691cc..b3505196d2366 100644 --- a/x-pack/plugins/security_solution/public/common/components/url_state/use_url_state.tsx +++ b/x-pack/plugins/security_solution/public/common/components/url_state/use_url_state.tsx @@ -9,7 +9,7 @@ import { difference, isEmpty } from 'lodash/fp'; import { useEffect, useRef, useState } from 'react'; import deepEqual from 'fast-deep-equal'; -import { useDispatch } from 'react-redux'; +import { useLocation } from 'react-router-dom'; import { useKibana } from '../../lib/kibana'; import { CONSTANTS, UrlStateType } from './constants'; import { @@ -32,9 +32,6 @@ import { UrlState, } from './types'; import { TimelineUrl } from '../../../timelines/store/timeline/model'; -import { timelineActions } from '../../../timelines/store/timeline'; -import { TimelineId } from '../../../../../timelines/common'; - function usePrevious(value: PreviousLocationUrlState) { const ref = useRef(value); useEffect(() => { @@ -61,20 +58,20 @@ const updateTimelineAtinitialization = ( export const useUrlStateHooks = ({ detailName, indexPattern, - history, navTabs, pageName, - pathName, - search, setInitialStateFromUrl, updateTimeline, updateTimelineIsLoading, urlState, + search, + pathName, + history, }: UrlStateContainerPropTypes) => { const [isInitializing, setIsInitializing] = useState(true); const { filterManager, savedQueries } = useKibana().services.data.query; + const { pathname: browserPathName } = useLocation(); const prevProps = usePrevious({ pathName, pageName, urlState }); - const dispatch = useDispatch(); const handleInitialize = (type: UrlStateType, needUpdate?: boolean) => { let mySearch = search; @@ -175,6 +172,14 @@ export const useUrlStateHooks = ({ }; useEffect(() => { + // When browser location and store location are out of sync, skip the execution. + // It happens in three scenarios: + // * When changing urlState and quickly moving to a new location. + // * Redirects as "security/hosts" -> "security/hosts/allHosts" + // * It also happens once on every location change because browserPathName gets updated before pathName + // *Warning*: Removing this return would cause redirect loops that crashes the APP. + if (browserPathName !== pathName) return; + const type: UrlStateType = getUrlType(pageName); if (isInitializing && pageName != null && pageName !== '') { handleInitialize(type); @@ -226,10 +231,9 @@ export const useUrlStateHooks = ({ }); } else if (pathName !== prevProps.pathName) { handleInitialize(type, isDetectionsPages(pageName)); - dispatch(timelineActions.showTimeline({ id: TimelineId.active, show: false })); } // eslint-disable-next-line react-hooks/exhaustive-deps - }, [isInitializing, history, pathName, pageName, prevProps, urlState, dispatch]); + }, [isInitializing, history, pathName, pageName, prevProps, urlState, browserPathName]); useEffect(() => { document.title = `${getTitle(pageName, detailName, navTabs)} - Kibana`; diff --git a/x-pack/plugins/security_solution/public/common/experimental_features_service.ts b/x-pack/plugins/security_solution/public/common/experimental_features_service.ts new file mode 100644 index 0000000000000..813341f175408 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/experimental_features_service.ts @@ -0,0 +1,30 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { ExperimentalFeatures } from '../../common/experimental_features'; + +export class ExperimentalFeaturesService { + private static experimentalFeatures?: ExperimentalFeatures; + + public static init({ experimentalFeatures }: { experimentalFeatures: ExperimentalFeatures }) { + this.experimentalFeatures = experimentalFeatures; + } + + public static get(): ExperimentalFeatures { + if (!this.experimentalFeatures) { + this.throwUninitializedError(); + } + + return this.experimentalFeatures; + } + + private static throwUninitializedError(): never { + throw new Error( + 'Experimental features services not initialized - are you trying to import this module from outside of the Security Solution app?' + ); + } +} diff --git a/x-pack/plugins/security_solution/public/common/lib/cell_actions/default_cell_actions.tsx b/x-pack/plugins/security_solution/public/common/lib/cell_actions/default_cell_actions.tsx index 8279613e67db7..149a0c62b8b6a 100644 --- a/x-pack/plugins/security_solution/public/common/lib/cell_actions/default_cell_actions.tsx +++ b/x-pack/plugins/security_solution/public/common/lib/cell_actions/default_cell_actions.tsx @@ -6,6 +6,7 @@ */ import React, { useCallback, useState, useMemo } from 'react'; +import { Filter } from '../../../../../../../src/plugins/data/public'; import type { BrowserFields, @@ -167,11 +168,13 @@ export const defaultCellActions: TGridCellAction[] = [ ({ browserFields, data, + globalFilters, timelineId, pageSize, }: { browserFields: BrowserFields; data: TimelineNonEcsData[][]; + globalFilters?: Filter[]; timelineId: string; pageSize: number; }) => @@ -205,6 +208,7 @@ export const defaultCellActions: TGridCellAction[] = [ enablePopOver data-test-subj="hover-actions-show-top-n" field={columnId} + globalFilters={globalFilters} onClick={onClick} onFilterAdded={onFilterAdded} ownFocus={false} diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/step_define_rule/index.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/step_define_rule/index.tsx index 3dfcc62e26a66..785afa49c9791 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/step_define_rule/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/step_define_rule/index.tsx @@ -11,7 +11,11 @@ import styled from 'styled-components'; import { isEqual } from 'lodash'; import { IndexPattern } from 'src/plugins/data/public'; -import { DEFAULT_INDEX_KEY, DEFAULT_THREAT_INDEX_KEY } from '../../../../../common/constants'; +import { + DEFAULT_INDEX_KEY, + DEFAULT_THREAT_INDEX_KEY, + DEFAULT_THREAT_MATCH_QUERY, +} from '../../../../../common/constants'; import { DEFAULT_TIMELINE_TITLE } from '../../../../timelines/components/timeline/translations'; import { isMlRule } from '../../../../../common/machine_learning/helpers'; import { hasMlAdminPermissions } from '../../../../../common/machine_learning/has_ml_admin_permissions'; @@ -72,7 +76,7 @@ const stepDefineDefaultValue: DefineStepRule = { saved_id: undefined, }, threatQueryBar: { - query: { query: '*:*', language: 'kuery' }, + query: { query: DEFAULT_THREAT_MATCH_QUERY, language: 'kuery' }, filters: [], saved_id: undefined, }, diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/detection_engine.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/detection_engine.tsx index 1fb7eeabb7e2e..848bdd7f8ef71 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/detection_engine.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/detection_engine.tsx @@ -178,10 +178,9 @@ const DetectionEnginePageComponent: React.FC = ({ const timelineId = TimelineId.detectionsPage; clearEventsLoading!({ id: timelineId }); clearEventsDeleted!({ id: timelineId }); - clearSelected!({ id: timelineId }); setFilterGroup(newFilterGroup); }, - [clearEventsLoading, clearEventsDeleted, clearSelected, setFilterGroup] + [clearEventsLoading, clearEventsDeleted, setFilterGroup] ); const alertsHistogramDefaultFilters = useMemo( diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/index.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/index.tsx index 4eb270c4d0e79..70d7faa47b9ee 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/index.tsx @@ -178,7 +178,8 @@ const RuleDetailsPageComponent: React.FC = ({ (getTimeline(state, TimelineId.detectionsRulesDetailsPage) ?? timelineDefaults).graphEventId ); const updatedAt = useShallowEqualSelector( - (state) => (getTimeline(state, TimelineId.detectionsPage) ?? timelineDefaults).updated + (state) => + (getTimeline(state, TimelineId.detectionsRulesDetailsPage) ?? timelineDefaults).updated ); const isAlertsLoading = useShallowEqualSelector( (state) => (getTimeline(state, TimelineId.detectionsPage) ?? timelineDefaults).isLoading @@ -350,7 +351,7 @@ const RuleDetailsPageComponent: React.FC = ({ // Callback for when open/closed filter changes const onFilterGroupChangedCallback = useCallback( (newFilterGroup: Status) => { - const timelineId = TimelineId.detectionsPage; + const timelineId = TimelineId.detectionsRulesDetailsPage; clearEventsLoading!({ id: timelineId }); clearEventsDeleted!({ id: timelineId }); clearSelected!({ id: timelineId }); @@ -390,7 +391,6 @@ const RuleDetailsPageComponent: React.FC = ({ const alertsTableDefaultFilters = useMemo( () => [ ...buildAlertsRuleIdFilter(ruleId), - ...filters, ...(ruleRegistryEnabled ? [ // TODO: Once we are past experimental phase this code should be removed @@ -399,7 +399,7 @@ const RuleDetailsPageComponent: React.FC = ({ : [...buildShowBuildingBlockFilter(showBuildingBlockAlerts)]), ...buildThreatMatchFilter(showOnlyThreatIndicatorAlerts), ], - [ruleId, filters, ruleRegistryEnabled, showBuildingBlockAlerts, showOnlyThreatIndicatorAlerts] + [ruleId, ruleRegistryEnabled, showBuildingBlockAlerts, showOnlyThreatIndicatorAlerts] ); const alertMergedFilters = useMemo( diff --git a/x-pack/plugins/security_solution/public/management/common/routing.ts b/x-pack/plugins/security_solution/public/management/common/routing.ts index b4776f328cf15..58fbd64faf8a6 100644 --- a/x-pack/plugins/security_solution/public/management/common/routing.ts +++ b/x-pack/plugins/security_solution/public/management/common/routing.ts @@ -26,6 +26,7 @@ import { appendSearch } from '../../common/components/link_to/helpers'; import { EndpointIndexUIQueryParams } from '../pages/endpoint_hosts/types'; import { TrustedAppsListPageLocation } from '../pages/trusted_apps/state'; import { EventFiltersPageLocation } from '../pages/event_filters/types'; +import { PolicyDetailsArtifactsPageLocation } from '../pages/policy/types'; // Taken from: https://github.com/microsoft/TypeScript/issues/12936#issuecomment-559034150 type ExactKeys = Exclude extends never ? T1 : never; @@ -160,6 +161,25 @@ const normalizeTrustedAppsPageLocation = ( } }; +const normalizePolicyDetailsArtifactsListPageLocation = ( + location?: Partial +): Partial => { + if (location) { + return { + ...(!isDefaultOrMissing(location.page_index, MANAGEMENT_DEFAULT_PAGE) + ? { page_index: location.page_index } + : {}), + ...(!isDefaultOrMissing(location.page_size, MANAGEMENT_DEFAULT_PAGE_SIZE) + ? { page_size: location.page_size } + : {}), + ...(!isDefaultOrMissing(location.show, undefined) ? { show: location.show } : {}), + ...(!isDefaultOrMissing(location.filter, '') ? { filter: location.filter } : ''), + }; + } else { + return {}; + } +}; + const normalizeEventFiltersPageLocation = ( location?: Partial ): Partial => { @@ -257,6 +277,34 @@ export const getTrustedAppsListPath = (location?: Partial { + const showParamValue = extractFirstParamValue( + query, + 'show' + ) as PolicyDetailsArtifactsPageLocation['show']; + + return { + ...extractListPaginationParams(query), + show: showParamValue && 'list' === showParamValue ? showParamValue : undefined, + }; +}; + +export const getPolicyDetailsArtifactsListPath = ( + policyId: string, + location?: Partial +): string => { + const path = generatePath(MANAGEMENT_ROUTING_POLICY_DETAILS_TRUSTED_APPS_PATH, { + tabName: AdministrationSubTab.policies, + policyId, + }); + + return `${path}${appendSearch( + querystring.stringify(normalizePolicyDetailsArtifactsListPageLocation(location)) + )}`; +}; + export const extractEventFiltetrsPageLocation = ( query: querystring.ParsedUrlQuery ): EventFiltersPageLocation => { diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/policy_response_friendly_names.ts b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/policy_response_friendly_names.ts index a9c2676396e83..3551d00c50c73 100644 --- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/policy_response_friendly_names.ts +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/policy_response_friendly_names.ts @@ -243,6 +243,18 @@ responseMap.set( defaultMessage: 'Events', }) ); +responseMap.set( + 'memory_protection', + i18n.translate('xpack.securitySolution.endpoint.details.policyResponse.memory_protection', { + defaultMessage: 'Memory Threat', + }) +); +responseMap.set( + 'behavior_protection', + i18n.translate('xpack.securitySolution.endpoint.details.policyResponse.behavior_protection', { + defaultMessage: 'Malicious Behavior', + }) +); /** * Maps a server provided value to corresponding i18n'd string. diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_details/action/index.ts b/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_details/action/index.ts new file mode 100644 index 0000000000000..ab84bb4f253ea --- /dev/null +++ b/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_details/action/index.ts @@ -0,0 +1,11 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { PolicySettingsAction } from './policy_settings_action'; +import { PolicyTrustedAppsAction } from './policy_trusted_apps_action'; + +export type PolicyDetailsAction = PolicySettingsAction | PolicyTrustedAppsAction; diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_details/action.ts b/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_details/action/policy_settings_action.ts similarity index 83% rename from x-pack/plugins/security_solution/public/management/pages/policy/store/policy_details/action.ts rename to x-pack/plugins/security_solution/public/management/pages/policy/store/policy_details/action/policy_settings_action.ts index 6bd39e9c24f96..eec0ab1c6445c 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_details/action.ts +++ b/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_details/action/policy_settings_action.ts @@ -5,11 +5,11 @@ * 2.0. */ -import { ILicense } from '../../../../../../../licensing/common/types'; -import { GetAgentStatusResponse } from '../../../../../../../fleet/common/types/rest_spec'; -import { PolicyData, UIPolicyConfig } from '../../../../../../common/endpoint/types'; -import { ServerApiError } from '../../../../../common/types'; -import { PolicyDetailsState } from '../../types'; +import { ILicense } from '../../../../../../../../licensing/common/types'; +import { GetAgentStatusResponse } from '../../../../../../../../fleet/common/types/rest_spec'; +import { PolicyData, UIPolicyConfig } from '../../../../../../../common/endpoint/types'; +import { ServerApiError } from '../../../../../../common/types'; +import { PolicyDetailsState } from '../../../types'; export interface ServerReturnedPolicyDetailsData { type: 'serverReturnedPolicyDetailsData'; @@ -69,7 +69,7 @@ export interface LicenseChanged { payload: ILicense; } -export type PolicyDetailsAction = +export type PolicySettingsAction = | ServerReturnedPolicyDetailsData | UserClickedPolicyDetailsSaveButton | ServerReturnedPolicyDetailsAgentSummaryData diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_details/action/policy_trusted_apps_action.ts b/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_details/action/policy_trusted_apps_action.ts new file mode 100644 index 0000000000000..46e0f8293cc33 --- /dev/null +++ b/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_details/action/policy_trusted_apps_action.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. + */ + +// TODO: defined trusted apps actions (code below only here to silence TS) +export type PolicyTrustedAppsAction = + | { + type: 'a'; + } + | { type: 'b' }; diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_details/index.ts b/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_details/index.ts index bc83c7438eba8..bc9e42ddf7f52 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_details/index.ts +++ b/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_details/index.ts @@ -11,8 +11,7 @@ import { AppAction } from '../../../../../common/store/actions'; import { Immutable } from '../../../../../../common/endpoint/types'; export { policyDetailsMiddlewareFactory } from './middleware'; -export { PolicyDetailsAction } from './action'; -export { policyDetailsReducer } from './reducer'; +export { policyDetailsReducer, initialPolicyDetailsState } from './reducer'; export interface EndpointPolicyDetailsStatePluginState { policyDetails: Immutable; @@ -21,3 +20,4 @@ export interface EndpointPolicyDetailsStatePluginState { export interface EndpointPolicyDetailsStatePluginReducer { policyDetails: ImmutableReducer; } +export { PolicyDetailsAction } from './action'; diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_details/middleware.ts b/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_details/middleware.ts deleted file mode 100644 index 3d90b8d640ac8..0000000000000 --- a/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_details/middleware.ts +++ /dev/null @@ -1,163 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { IHttpFetchError } from 'kibana/public'; -import { - DefaultPolicyNotificationMessage, - DefaultPolicyRuleNotificationMessage, -} from '../../../../../../common/endpoint/models/policy_config'; -import { PolicyDetailsState, UpdatePolicyResponse } from '../../types'; -import { - policyIdFromParams, - isOnPolicyDetailsPage, - policyDetails, - policyDetailsForUpdate, - needsToRefresh, -} from './selectors'; -import { - sendGetPackagePolicy, - sendGetFleetAgentStatusForPolicy, - sendPutPackagePolicy, -} from '../services/ingest'; -import { NewPolicyData, PolicyData } from '../../../../../../common/endpoint/types'; -import { ImmutableMiddlewareFactory } from '../../../../../common/store'; -import { getPolicyDataForUpdate } from '../../../../../../common/endpoint/service/policy/get_policy_data_for_update'; - -export const policyDetailsMiddlewareFactory: ImmutableMiddlewareFactory = ( - coreStart -) => { - const http = coreStart.http; - return ({ getState, dispatch }) => - (next) => - async (action) => { - next(action); - const state = getState(); - - if ( - action.type === 'userChangedUrl' && - needsToRefresh(state) && - isOnPolicyDetailsPage(state) - ) { - const id = policyIdFromParams(state); - let policyItem: PolicyData; - - try { - policyItem = (await sendGetPackagePolicy(http, id)).item; - // sets default user notification message if policy config message is empty - if (policyItem.inputs[0].config.policy.value.windows.popup.malware.message === '') { - policyItem.inputs[0].config.policy.value.windows.popup.malware.message = - DefaultPolicyNotificationMessage; - policyItem.inputs[0].config.policy.value.mac.popup.malware.message = - DefaultPolicyNotificationMessage; - policyItem.inputs[0].config.policy.value.linux.popup.malware.message = - DefaultPolicyNotificationMessage; - } - if (policyItem.inputs[0].config.policy.value.windows.popup.ransomware.message === '') { - policyItem.inputs[0].config.policy.value.windows.popup.ransomware.message = - DefaultPolicyNotificationMessage; - } - if ( - policyItem.inputs[0].config.policy.value.windows.popup.memory_protection.message === '' - ) { - policyItem.inputs[0].config.policy.value.windows.popup.memory_protection.message = - DefaultPolicyRuleNotificationMessage; - } - if ( - policyItem.inputs[0].config.policy.value.windows.popup.behavior_protection.message === - '' - ) { - policyItem.inputs[0].config.policy.value.windows.popup.behavior_protection.message = - DefaultPolicyRuleNotificationMessage; - } - if ( - policyItem.inputs[0].config.policy.value.mac.popup.behavior_protection.message === '' - ) { - policyItem.inputs[0].config.policy.value.mac.popup.behavior_protection.message = - DefaultPolicyRuleNotificationMessage; - } - if ( - policyItem.inputs[0].config.policy.value.linux.popup.behavior_protection.message === '' - ) { - policyItem.inputs[0].config.policy.value.linux.popup.behavior_protection.message = - DefaultPolicyRuleNotificationMessage; - } - } catch (error) { - dispatch({ - type: 'serverFailedToReturnPolicyDetailsData', - payload: error.body || error, - }); - return; - } - - dispatch({ - type: 'serverReturnedPolicyDetailsData', - payload: { - policyItem, - }, - }); - - // Agent summary is secondary data, so its ok for it to come after the details - // page is populated with the main content - if (policyItem.policy_id) { - const { results } = await sendGetFleetAgentStatusForPolicy(http, policyItem.policy_id); - dispatch({ - type: 'serverReturnedPolicyDetailsAgentSummaryData', - payload: { - agentStatusSummary: results, - }, - }); - } - } else if (action.type === 'userClickedPolicyDetailsSaveButton') { - const { id } = policyDetails(state) as PolicyData; - const updatedPolicyItem = policyDetailsForUpdate(state) as NewPolicyData; - - let apiResponse: UpdatePolicyResponse; - try { - apiResponse = await sendPutPackagePolicy(http, id, updatedPolicyItem).catch( - (error: IHttpFetchError) => { - if (!error.response || error.response.status !== 409) { - return Promise.reject(error); - } - // Handle 409 error (version conflict) here, by using the latest document - // for the package policy and adding the updated policy to it, ensuring that - // any recent updates to `manifest_artifacts` are retained. - return sendGetPackagePolicy(http, id).then((packagePolicy) => { - const latestUpdatedPolicyItem = packagePolicy.item; - latestUpdatedPolicyItem.inputs[0].config.policy = - updatedPolicyItem.inputs[0].config.policy; - - return sendPutPackagePolicy( - http, - id, - getPolicyDataForUpdate(latestUpdatedPolicyItem) - ); - }); - } - ); - } catch (error) { - dispatch({ - type: 'serverReturnedPolicyDetailsUpdateFailure', - payload: { - success: false, - error: error.body || error, - }, - }); - return; - } - - dispatch({ - type: 'serverReturnedUpdatedPolicyDetailsData', - payload: { - policyItem: apiResponse.item, - updateStatus: { - success: true, - }, - }, - }); - } - }; -}; diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_details/middleware/index.ts b/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_details/middleware/index.ts new file mode 100644 index 0000000000000..6b7e4e7d541c8 --- /dev/null +++ b/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_details/middleware/index.ts @@ -0,0 +1,22 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { ImmutableMiddlewareFactory } from '../../../../../../common/store'; +import { PolicyDetailsState } from '../../../types'; +import { policyTrustedAppsMiddlewareRunner } from './policy_trusted_apps_middleware'; +import { policySettingsMiddlewareRunner } from './policy_settings_middleware'; + +export const policyDetailsMiddlewareFactory: ImmutableMiddlewareFactory = ( + coreStart +) => { + return (store) => (next) => async (action) => { + next(action); + + policySettingsMiddlewareRunner(coreStart, store, action); + policyTrustedAppsMiddlewareRunner(coreStart, store, action); + }; +}; diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_details/middleware/policy_settings_middleware.ts b/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_details/middleware/policy_settings_middleware.ts new file mode 100644 index 0000000000000..73b244944e502 --- /dev/null +++ b/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_details/middleware/policy_settings_middleware.ts @@ -0,0 +1,144 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { IHttpFetchError } from 'kibana/public'; +import { + DefaultPolicyNotificationMessage, + DefaultPolicyRuleNotificationMessage, +} from '../../../../../../../common/endpoint/models/policy_config'; +import { MiddlewareRunner, UpdatePolicyResponse } from '../../../types'; +import { + policyIdFromParams, + isOnPolicyDetailsPage, + policyDetails, + policyDetailsForUpdate, + needsToRefresh, +} from '../selectors/policy_settings_selectors'; +import { + sendGetPackagePolicy, + sendGetFleetAgentStatusForPolicy, + sendPutPackagePolicy, +} from '../../services/ingest'; +import { NewPolicyData, PolicyData } from '../../../../../../../common/endpoint/types'; +import { getPolicyDataForUpdate } from '../../../../../../../common/endpoint/service/policy'; + +export const policySettingsMiddlewareRunner: MiddlewareRunner = async ( + coreStart, + { dispatch, getState }, + action +) => { + const http = coreStart.http; + const state = getState(); + + if (action.type === 'userChangedUrl' && needsToRefresh(state) && isOnPolicyDetailsPage(state)) { + const id = policyIdFromParams(state); + let policyItem: PolicyData; + + try { + policyItem = (await sendGetPackagePolicy(http, id)).item; + // sets default user notification message if policy config message is empty + if (policyItem.inputs[0].config.policy.value.windows.popup.malware.message === '') { + policyItem.inputs[0].config.policy.value.windows.popup.malware.message = + DefaultPolicyNotificationMessage; + policyItem.inputs[0].config.policy.value.mac.popup.malware.message = + DefaultPolicyNotificationMessage; + policyItem.inputs[0].config.policy.value.linux.popup.malware.message = + DefaultPolicyNotificationMessage; + } + if (policyItem.inputs[0].config.policy.value.windows.popup.ransomware.message === '') { + policyItem.inputs[0].config.policy.value.windows.popup.ransomware.message = + DefaultPolicyNotificationMessage; + } + if (policyItem.inputs[0].config.policy.value.windows.popup.memory_protection.message === '') { + policyItem.inputs[0].config.policy.value.windows.popup.memory_protection.message = + DefaultPolicyRuleNotificationMessage; + } + if ( + policyItem.inputs[0].config.policy.value.windows.popup.behavior_protection.message === '' + ) { + policyItem.inputs[0].config.policy.value.windows.popup.behavior_protection.message = + DefaultPolicyRuleNotificationMessage; + } + if (policyItem.inputs[0].config.policy.value.mac.popup.behavior_protection.message === '') { + policyItem.inputs[0].config.policy.value.mac.popup.behavior_protection.message = + DefaultPolicyRuleNotificationMessage; + } + if (policyItem.inputs[0].config.policy.value.linux.popup.behavior_protection.message === '') { + policyItem.inputs[0].config.policy.value.linux.popup.behavior_protection.message = + DefaultPolicyRuleNotificationMessage; + } + } catch (error) { + dispatch({ + type: 'serverFailedToReturnPolicyDetailsData', + payload: error.body || error, + }); + return; + } + + dispatch({ + type: 'serverReturnedPolicyDetailsData', + payload: { + policyItem, + }, + }); + + // Agent summary is secondary data, so its ok for it to come after the details + // page is populated with the main content + if (policyItem.policy_id) { + const { results } = await sendGetFleetAgentStatusForPolicy(http, policyItem.policy_id); + dispatch({ + type: 'serverReturnedPolicyDetailsAgentSummaryData', + payload: { + agentStatusSummary: results, + }, + }); + } + } else if (action.type === 'userClickedPolicyDetailsSaveButton') { + const { id } = policyDetails(state) as PolicyData; + const updatedPolicyItem = policyDetailsForUpdate(state) as NewPolicyData; + + let apiResponse: UpdatePolicyResponse; + try { + apiResponse = await sendPutPackagePolicy(http, id, updatedPolicyItem).catch( + (error: IHttpFetchError) => { + if (!error.response || error.response.status !== 409) { + return Promise.reject(error); + } + // Handle 409 error (version conflict) here, by using the latest document + // for the package policy and adding the updated policy to it, ensuring that + // any recent updates to `manifest_artifacts` are retained. + return sendGetPackagePolicy(http, id).then((packagePolicy) => { + const latestUpdatedPolicyItem = packagePolicy.item; + latestUpdatedPolicyItem.inputs[0].config.policy = + updatedPolicyItem.inputs[0].config.policy; + + return sendPutPackagePolicy(http, id, getPolicyDataForUpdate(latestUpdatedPolicyItem)); + }); + } + ); + } catch (error) { + dispatch({ + type: 'serverReturnedPolicyDetailsUpdateFailure', + payload: { + success: false, + error: error.body || error, + }, + }); + return; + } + + dispatch({ + type: 'serverReturnedUpdatedPolicyDetailsData', + payload: { + policyItem: apiResponse.item, + updateStatus: { + success: true, + }, + }, + }); + } +}; diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_details/middleware/policy_trusted_apps_middleware.ts b/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_details/middleware/policy_trusted_apps_middleware.ts new file mode 100644 index 0000000000000..171bbd881302e --- /dev/null +++ b/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_details/middleware/policy_trusted_apps_middleware.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 { MiddlewareRunner } from '../../../types'; + +export const policyTrustedAppsMiddlewareRunner: MiddlewareRunner = async ( + coreStart, + store, + action +) => { + // FIXME: implement middlware for trusted apps +}; diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_details/reducer/index.ts b/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_details/reducer/index.ts new file mode 100644 index 0000000000000..a577c1ca85ef0 --- /dev/null +++ b/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_details/reducer/index.ts @@ -0,0 +1,25 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { ImmutableReducer } from '../../../../../../common/store'; +import { PolicyDetailsState } from '../../../types'; +import { AppAction } from '../../../../../../common/store/actions'; +import { policySettingsReducer } from './policy_settings_reducer'; +import { initialPolicyDetailsState } from './initial_policy_details_state'; +import { policyTrustedAppsReducer } from './trusted_apps_reducer'; + +export * from './initial_policy_details_state'; + +export const policyDetailsReducer: ImmutableReducer = ( + state = initialPolicyDetailsState(), + action +) => { + return [policySettingsReducer, policyTrustedAppsReducer].reduce( + (updatedState, runReducer) => runReducer(updatedState, action), + state + ); +}; diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_details/reducer/initial_policy_details_state.ts b/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_details/reducer/initial_policy_details_state.ts new file mode 100644 index 0000000000000..723f8fe31bd2a --- /dev/null +++ b/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_details/reducer/initial_policy_details_state.ts @@ -0,0 +1,39 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { Immutable } from '../../../../../../../common/endpoint/types'; +import { PolicyDetailsState } from '../../../types'; +import { + MANAGEMENT_DEFAULT_PAGE, + MANAGEMENT_DEFAULT_PAGE_SIZE, +} from '../../../../../common/constants'; +import { createUninitialisedResourceState } from '../../../../../state'; + +/** + * Return a fresh copy of initial state, since we mutate state in the reducer. + */ +export const initialPolicyDetailsState: () => Immutable = () => ({ + policyItem: undefined, + isLoading: false, + agentStatusSummary: { + error: 0, + events: 0, + offline: 0, + online: 0, + total: 0, + other: 0, + }, + artifacts: { + location: { + page_index: MANAGEMENT_DEFAULT_PAGE, + page_size: MANAGEMENT_DEFAULT_PAGE_SIZE, + show: undefined, + filter: '', + }, + availableList: createUninitialisedResourceState(), + }, +}); diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_details/reducer.ts b/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_details/reducer/policy_settings_reducer.ts similarity index 81% rename from x-pack/plugins/security_solution/public/management/pages/policy/store/policy_details/reducer.ts rename to x-pack/plugins/security_solution/public/management/pages/policy/store/policy_details/reducer/policy_settings_reducer.ts index 512059e9c3aab..9997e547e8148 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_details/reducer.ts +++ b/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_details/reducer/policy_settings_reducer.ts @@ -5,16 +5,20 @@ * 2.0. */ -import { fullPolicy, isOnPolicyDetailsPage, license } from './selectors'; +// eslint-disable-next-line import/no-nodejs-modules +import { parse } from 'querystring'; +import { fullPolicy, isOnPolicyDetailsPage, license } from '../selectors/policy_settings_selectors'; import { Immutable, PolicyConfig, - UIPolicyConfig, PolicyData, -} from '../../../../../../common/endpoint/types'; -import { ImmutableReducer } from '../../../../../common/store'; -import { AppAction } from '../../../../../common/store/actions'; -import { PolicyDetailsState } from '../../types'; + UIPolicyConfig, +} from '../../../../../../../common/endpoint/types'; +import { ImmutableReducer } from '../../../../../../common/store'; +import { AppAction } from '../../../../../../common/store/actions'; +import { PolicyDetailsState } from '../../../types'; +import { extractPolicyDetailsArtifactsListPageLocation } from '../../../../../common/routing'; +import { initialPolicyDetailsState } from './initial_policy_details_state'; const updatePolicyConfigInPolicyData = ( policyData: Immutable, @@ -33,23 +37,7 @@ const updatePolicyConfigInPolicyData = ( })), }); -/** - * Return a fresh copy of initial state, since we mutate state in the reducer. - */ -export const initialPolicyDetailsState: () => Immutable = () => ({ - policyItem: undefined, - isLoading: false, - agentStatusSummary: { - error: 0, - events: 0, - offline: 0, - online: 0, - total: 0, - other: 0, - }, -}); - -export const policyDetailsReducer: ImmutableReducer = ( +export const policySettingsReducer: ImmutableReducer = ( state = initialPolicyDetailsState(), action ) => { @@ -106,6 +94,12 @@ export const policyDetailsReducer: ImmutableReducer = { ...state, location: action.payload, + artifacts: { + ...state.artifacts, + location: extractPolicyDetailsArtifactsListPageLocation( + parse(action.payload.search.slice(1)) + ), + }, }; const isCurrentlyOnDetailsPage = isOnPolicyDetailsPage(newState); const wasPreviouslyOnDetailsPage = isOnPolicyDetailsPage(state); diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_details/reducer/trusted_apps_reducer.ts b/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_details/reducer/trusted_apps_reducer.ts new file mode 100644 index 0000000000000..7f2f9e437ca06 --- /dev/null +++ b/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_details/reducer/trusted_apps_reducer.ts @@ -0,0 +1,19 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { ImmutableReducer } from '../../../../../../common/store'; +import { PolicyDetailsState } from '../../../types'; +import { AppAction } from '../../../../../../common/store/actions'; +import { initialPolicyDetailsState } from './initial_policy_details_state'; + +export const policyTrustedAppsReducer: ImmutableReducer = ( + state = initialPolicyDetailsState(), + action +) => { + // FIXME: implement trusted apps reducer + return state; +}; diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_details/selectors/index.ts b/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_details/selectors/index.ts new file mode 100644 index 0000000000000..af5be5c39480e --- /dev/null +++ b/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_details/selectors/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 * from './policy_settings_selectors'; +export * from './trusted_apps_selectors'; diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_details/selectors.ts b/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_details/selectors/policy_settings_selectors.ts similarity index 92% rename from x-pack/plugins/security_solution/public/management/pages/policy/store/policy_details/selectors.ts rename to x-pack/plugins/security_solution/public/management/pages/policy/store/policy_details/selectors/policy_settings_selectors.ts index 017111da8d884..23ab0fd73c9e1 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_details/selectors.ts +++ b/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_details/selectors/policy_settings_selectors.ts @@ -7,23 +7,23 @@ import { createSelector } from 'reselect'; import { matchPath } from 'react-router-dom'; -import { ILicense } from '../../../../../../../licensing/common/types'; -import { unsetPolicyFeaturesAccordingToLicenseLevel } from '../../../../../../common/license/policy_config'; -import { PolicyDetailsState } from '../../types'; +import { ILicense } from '../../../../../../../../licensing/common/types'; +import { unsetPolicyFeaturesAccordingToLicenseLevel } from '../../../../../../../common/license/policy_config'; +import { PolicyDetailsArtifactsPageLocation, PolicyDetailsState } from '../../../types'; import { Immutable, NewPolicyData, PolicyConfig, PolicyData, UIPolicyConfig, -} from '../../../../../../common/endpoint/types'; -import { policyFactory as policyConfigFactory } from '../../../../../../common/endpoint/models/policy_config'; +} from '../../../../../../../common/endpoint/types'; +import { policyFactory as policyConfigFactory } from '../../../../../../../common/endpoint/models/policy_config'; import { MANAGEMENT_ROUTING_POLICY_DETAILS_FORM_PATH, MANAGEMENT_ROUTING_POLICY_DETAILS_TRUSTED_APPS_PATH, -} from '../../../../common/constants'; -import { ManagementRoutePolicyDetailsParams } from '../../../../types'; -import { getPolicyDataForUpdate } from '../../../../../../common/endpoint/service/policy/get_policy_data_for_update'; +} from '../../../../../common/constants'; +import { ManagementRoutePolicyDetailsParams } from '../../../../../types'; +import { getPolicyDataForUpdate } from '../../../../../../../common/endpoint/service/policy/get_policy_data_for_update'; /** Returns the policy details */ export const policyDetails = (state: Immutable) => state.policyItem; @@ -80,6 +80,13 @@ export const needsToRefresh = (state: Immutable): boolean => return !state.policyItem && !state.apiError; }; +/** + * Returns current artifacts location + */ +export const getCurrentArtifactsLocation = ( + state: Immutable +): Immutable => state.artifacts.location; + /** Returns a boolean of whether the user is on the policy form page or not */ export const isOnPolicyFormPage = (state: Immutable) => { return ( diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_details/selectors/trusted_apps_selectors.ts b/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_details/selectors/trusted_apps_selectors.ts new file mode 100644 index 0000000000000..f7a568b5ade0e --- /dev/null +++ b/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_details/selectors/trusted_apps_selectors.ts @@ -0,0 +1,10 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export const isOnTrustedAppsView = () => { + return true; +}; diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/types.ts b/x-pack/plugins/security_solution/public/management/pages/policy/types.ts index 6d767df73cd1c..9000fb469afd3 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/types.ts +++ b/x-pack/plugins/security_solution/public/management/pages/policy/types.ts @@ -5,6 +5,7 @@ * 2.0. */ +import { CoreStart } from 'kibana/public'; import { ILicense } from '../../../../../licensing/common/types'; import { AppLocation, @@ -12,6 +13,7 @@ import { ProtectionFields, PolicyData, UIPolicyConfig, + MaybeImmutable, } from '../../../../common/endpoint/types'; import { ServerApiError } from '../../../common/types'; import { @@ -21,6 +23,19 @@ import { GetPackagesResponse, UpdatePackagePolicyResponse, } from '../../../../../fleet/common'; +import { AsyncResourceState } from '../../state'; +import { TrustedAppsListData } from '../trusted_apps/state'; +import { ImmutableMiddlewareAPI } from '../../../common/store'; +import { AppAction } from '../../../common/store/actions'; + +/** + * Function that runs Policy Details middleware + */ +export type MiddlewareRunner = ( + coreStart: CoreStart, + store: ImmutableMiddlewareAPI, + action: MaybeImmutable +) => Promise; /** * Policy list store state @@ -61,6 +76,8 @@ export interface PolicyDetailsState { isLoading: boolean; /** current location of the application */ location?: Immutable; + /** artifacts namespace inside policy details page */ + artifacts: PolicyArtifactsState; /** A summary of stats for the agents associated with a given Fleet Agent Policy */ agentStatusSummary?: Omit; /** Status of an update to the policy */ @@ -72,12 +89,29 @@ export interface PolicyDetailsState { license?: ILicense; } +/** + * Policy artifacts store state + */ +export interface PolicyArtifactsState { + /** artifacts location params */ + location: PolicyDetailsArtifactsPageLocation; + /** A list of artifacts can be linked to the policy */ + availableList: AsyncResourceState; +} + export enum OS { windows = 'windows', mac = 'mac', linux = 'linux', } +export interface PolicyDetailsArtifactsPageLocation { + page_index: number; + page_size: number; + show?: 'list'; + filter: string; +} + /** * Returns the keys of an object whose values meet a criteria. * Ex) interface largeNestedObject = { diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/components/antivirus_registration_form/index.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/components/antivirus_registration_form/index.tsx index 51edf1a200c53..45aad6c3d1432 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/components/antivirus_registration_form/index.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/components/antivirus_registration_form/index.tsx @@ -13,7 +13,7 @@ import { EuiSpacer, EuiSwitch, EuiText } from '@elastic/eui'; import { OperatingSystem } from '../../../../../../../common/endpoint/types'; import { isAntivirusRegistrationEnabled } from '../../../store/policy_details/selectors'; import { usePolicyDetailsSelector } from '../../policy_hooks'; -import { ConfigForm } from '../../components/config_form'; +import { ConfigForm } from '../config_form'; const TRANSLATIONS: Readonly<{ [K in 'title' | 'description' | 'label']: string }> = { title: i18n.translate( diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/endpoint_package_custom_extension/components/exception_items_summary.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/endpoint_package_custom_extension/components/exception_items_summary.tsx index ed3d9967f318e..5f0c5cca0ad2c 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/endpoint_package_custom_extension/components/exception_items_summary.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/endpoint_package_custom_extension/components/exception_items_summary.tsx @@ -5,9 +5,10 @@ * 2.0. */ +import React, { FC, memo, useCallback } from 'react'; import { EuiBadge, EuiBadgeProps, EuiFlexGroup, EuiFlexItem, EuiText } from '@elastic/eui'; -import React, { FC, memo } from 'react'; import { i18n } from '@kbn/i18n'; +import styled from 'styled-components'; import { GetExceptionSummaryResponse } from '../../../../../../../../common/endpoint/types'; const SUMMARY_KEYS: Readonly> = [ @@ -36,46 +37,76 @@ const SUMMARY_LABELS: Readonly<{ [key in keyof GetExceptionSummaryResponse]: str ), }; +export const StyledEuiFlexGridGroup = styled(EuiFlexGroup)` + display: grid; + min-width: 240px; + grid-template-columns: 50% 50%; +`; + +const StyledEuiFlexGroup = styled(EuiFlexGroup)<{ + isSmall: boolean; +}>` + font-size: ${({ isSmall, theme }) => (isSmall ? theme.eui.euiFontSizeXS : 'innherit')}; + font-weight: ${({ isSmall }) => (isSmall ? '1px' : 'innherit')}; +`; + const CSS_BOLD: Readonly = { fontWeight: 'bold' }; interface ExceptionItemsSummaryProps { stats: GetExceptionSummaryResponse | undefined; + isSmall?: boolean; } -export const ExceptionItemsSummary = memo(({ stats }) => { - return ( - - {SUMMARY_KEYS.map((stat) => { - return ( - - - {SUMMARY_LABELS[stat]} - - - ); - })} - - ); -}); +export const ExceptionItemsSummary = memo( + ({ stats, isSmall = false }) => { + const getItem = useCallback( + (stat: keyof GetExceptionSummaryResponse) => ( + + + {SUMMARY_LABELS[stat]} + + + ), + [stats, isSmall] + ); + + return ( + + {SUMMARY_KEYS.map((stat) => getItem(stat))} + + ); + } +); ExceptionItemsSummary.displayName = 'ExceptionItemsSummary'; -const SummaryStat: FC<{ value: number; color?: EuiBadgeProps['color'] }> = memo( - ({ children, value, color, ...commonProps }) => { +const SummaryStat: FC<{ value: number; color?: EuiBadgeProps['color']; isSmall?: boolean }> = memo( + ({ children, value, color, isSmall = false, ...commonProps }) => { return ( - - + + {children} {value} - + ); } diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/endpoint_package_custom_extension/components/fleet_event_filters_card.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/endpoint_package_custom_extension/components/fleet_event_filters_card.tsx index 22e1c3a612eb7..41768f4be7d2e 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/endpoint_package_custom_extension/components/fleet_event_filters_card.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/endpoint_package_custom_extension/components/fleet_event_filters_card.tsx @@ -46,15 +46,17 @@ export const FleetEventFiltersCard = memo( setStats(summary); } } catch (error) { - toasts.addDanger( - i18n.translate( - 'xpack.securitySolution.endpoint.fleetCustomExtension.eventFiltersSummaryError', - { - defaultMessage: 'There was an error trying to fetch event filters stats: "{error}"', - values: { error }, - } - ) - ); + if (isMounted.current) { + toasts.addDanger( + i18n.translate( + 'xpack.securitySolution.endpoint.fleetCustomExtension.eventFiltersSummaryError', + { + defaultMessage: 'There was an error trying to fetch event filters stats: "{error}"', + values: { error }, + } + ) + ); + } } }; fetchStats(); @@ -78,12 +80,15 @@ export const FleetEventFiltersCard = memo( path: fleetPackageCustomUrlPath, }, ], - backButtonUrl: getAppUrl({ appId: INTEGRATIONS_PLUGIN_ID, path: fleetPackageCustomUrlPath }), + backButtonUrl: getAppUrl({ + appId: INTEGRATIONS_PLUGIN_ID, + path: fleetPackageCustomUrlPath, + }), }; }, [getAppUrl, pkgkey]); return ( - + diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/endpoint_package_custom_extension/components/fleet_trusted_apps_card.test.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/endpoint_package_custom_extension/components/fleet_trusted_apps_card.test.tsx index 22a7072caea02..aa4b36d548604 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/endpoint_package_custom_extension/components/fleet_trusted_apps_card.test.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/endpoint_package_custom_extension/components/fleet_trusted_apps_card.test.tsx @@ -8,7 +8,7 @@ import React from 'react'; import { ThemeProvider } from 'styled-components'; import { I18nProvider } from '@kbn/i18n/react'; -import { FleetTrustedAppsCard } from './fleet_trusted_apps_card'; +import { FleetTrustedAppsCardWrapper } from './fleet_trusted_apps_card_wrapper'; import * as reactTestingLibrary from '@testing-library/react'; import { TrustedAppsHttpService } from '../../../../../trusted_apps/service'; import { useToasts } from '../../../../../../../common/lib/kibana'; @@ -67,7 +67,9 @@ describe('Fleet trusted apps card', () => { ); // @ts-ignore - const component = reactTestingLibrary.render(, { wrapper: Wrapper }); + const component = reactTestingLibrary.render(, { + wrapper: Wrapper, + }); try { // @ts-ignore await reactTestingLibrary.act(() => promise); diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/endpoint_package_custom_extension/components/fleet_trusted_apps_card.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/endpoint_package_custom_extension/components/fleet_trusted_apps_card.tsx index 4f10eceb6781c..08e8ec39dbaa8 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/endpoint_package_custom_extension/components/fleet_trusted_apps_card.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/endpoint_package_custom_extension/components/fleet_trusted_apps_card.tsx @@ -9,116 +9,87 @@ import React, { memo, useMemo, useState, useEffect, useRef } from 'react'; import { EuiPanel, EuiText } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; -import { - PackageCustomExtensionComponentProps, - pagePathGetters, -} from '../../../../../../../../../fleet/public'; -import { getTrustedAppsListPath } from '../../../../../../common/routing'; -import { - ListPageRouteState, - GetExceptionSummaryResponse, -} from '../../../../../../../../common/endpoint/types'; -import { INTEGRATIONS_PLUGIN_ID } from '../../../../../../../../../fleet/common'; +import { GetExceptionSummaryResponse } from '../../../../../../../../common/endpoint/types'; -import { useAppUrl } from '../../../../../../../common/lib/kibana/hooks'; import { useKibana, useToasts } from '../../../../../../../common/lib/kibana'; -import { LinkWithIcon } from './link_with_icon'; import { ExceptionItemsSummary } from './exception_items_summary'; import { TrustedAppsHttpService } from '../../../../../trusted_apps/service'; import { StyledEuiFlexGridGroup, StyledEuiFlexGridItem } from './styled_components'; -export const FleetTrustedAppsCard = memo(({ pkgkey }) => { - const { getAppUrl } = useAppUrl(); - const { - services: { http }, - } = useKibana(); - const toasts = useToasts(); - const [stats, setStats] = useState(); - const trustedAppsApi = useMemo(() => new TrustedAppsHttpService(http), [http]); - const isMounted = useRef(); +interface FleetTrustedAppsCardProps { + customLink: React.ReactNode; + policyId?: string; + cardSize?: 'm' | 'l'; +} - useEffect(() => { - isMounted.current = true; - const fetchStats = async () => { - try { - const response = await trustedAppsApi.getTrustedAppsSummary(); - if (isMounted) { - setStats(response); +export const FleetTrustedAppsCard = memo( + ({ customLink, policyId, cardSize = 'l' }) => { + const { + services: { http }, + } = useKibana(); + const toasts = useToasts(); + const [stats, setStats] = useState(); + const trustedAppsApi = useMemo(() => new TrustedAppsHttpService(http), [http]); + const isMounted = useRef(); + + useEffect(() => { + isMounted.current = true; + const fetchStats = async () => { + try { + const response = await trustedAppsApi.getTrustedAppsSummary({ + kuery: policyId + ? `exception-list-agnostic.attributes.tags:"policy:${policyId}" OR exception-list-agnostic.attributes.tags:"policy:all"` + : undefined, + }); + if (isMounted) { + setStats(response); + } + } catch (error) { + if (isMounted.current) { + toasts.addDanger( + i18n.translate( + 'xpack.securitySolution.endpoint.fleetCustomExtension.trustedAppsSummaryError', + { + defaultMessage: + 'There was an error trying to fetch trusted apps stats: "{error}"', + values: { error }, + } + ) + ); + } } - } catch (error) { - toasts.addDanger( - i18n.translate( - 'xpack.securitySolution.endpoint.fleetCustomExtension.trustedAppsSummaryError', - { - defaultMessage: 'There was an error trying to fetch trusted apps stats: "{error}"', - values: { error }, - } - ) - ); - } - }; - fetchStats(); - return () => { - isMounted.current = false; - }; - }, [toasts, trustedAppsApi]); - const trustedAppsListUrlPath = getTrustedAppsListPath(); + }; + fetchStats(); + return () => { + isMounted.current = false; + }; + }, [toasts, trustedAppsApi, policyId]); - const trustedAppRouteState = useMemo(() => { - const fleetPackageCustomUrlPath = `#${ - pagePathGetters.integration_details_custom({ pkgkey })[1] - }`; + const getTitleMessage = () => ( + + ); - return { - backButtonLabel: i18n.translate( - 'xpack.securitySolution.endpoint.fleetCustomExtension.backButtonLabel', - { defaultMessage: 'Back to Endpoint Integration' } - ), - onBackButtonNavigateTo: [ - INTEGRATIONS_PLUGIN_ID, - { - path: fleetPackageCustomUrlPath, - }, - ], - backButtonUrl: getAppUrl({ appId: INTEGRATIONS_PLUGIN_ID, path: fleetPackageCustomUrlPath }), - }; - }, [getAppUrl, pkgkey]); - return ( - - - - -

    - -

    -
    -
    - - - - - <> - - - - - -
    -
    - ); -}); + return ( + + + + + {cardSize === 'l' ?

    {getTitleMessage()}

    :
    {getTitleMessage()}
    } +
    +
    + + + + + {customLink} + +
    +
    + ); + } +); FleetTrustedAppsCard.displayName = 'FleetTrustedAppsCard'; diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/endpoint_package_custom_extension/components/fleet_trusted_apps_card_wrapper.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/endpoint_package_custom_extension/components/fleet_trusted_apps_card_wrapper.tsx new file mode 100644 index 0000000000000..5ac79a5dd5d5a --- /dev/null +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/endpoint_package_custom_extension/components/fleet_trusted_apps_card_wrapper.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, { memo, useMemo } from 'react'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { + PackageCustomExtensionComponentProps, + pagePathGetters, +} from '../../../../../../../../../fleet/public'; +import { getTrustedAppsListPath } from '../../../../../../common/routing'; +import { ListPageRouteState } from '../../../../../../../../common/endpoint/types'; +import { INTEGRATIONS_PLUGIN_ID } from '../../../../../../../../../fleet/common'; + +import { useAppUrl } from '../../../../../../../common/lib/kibana/hooks'; +import { LinkWithIcon } from './link_with_icon'; +import { FleetTrustedAppsCard } from './fleet_trusted_apps_card'; + +export const FleetTrustedAppsCardWrapper = memo( + ({ pkgkey }) => { + const { getAppUrl } = useAppUrl(); + const trustedAppsListUrlPath = getTrustedAppsListPath(); + + const trustedAppRouteState = useMemo(() => { + const fleetPackageCustomUrlPath = `#${ + pagePathGetters.integration_details_custom({ pkgkey })[1] + }`; + + return { + backButtonLabel: i18n.translate( + 'xpack.securitySolution.endpoint.fleetCustomExtension.backButtonLabel', + { defaultMessage: 'Back to Endpoint Integration' } + ), + onBackButtonNavigateTo: [ + INTEGRATIONS_PLUGIN_ID, + { + path: fleetPackageCustomUrlPath, + }, + ], + backButtonUrl: getAppUrl({ + appId: INTEGRATIONS_PLUGIN_ID, + path: fleetPackageCustomUrlPath, + }), + }; + }, [getAppUrl, pkgkey]); + + const customLink = useMemo( + () => ( + + + + ), + [getAppUrl, trustedAppRouteState, trustedAppsListUrlPath] + ); + return ; + } +); + +FleetTrustedAppsCardWrapper.displayName = 'FleetTrustedAppsCardWrapper'; diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/endpoint_package_custom_extension/components/link_with_icon.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/endpoint_package_custom_extension/components/link_with_icon.tsx index 6600fcfddde0c..6aebb130eb896 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/endpoint_package_custom_extension/components/link_with_icon.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/endpoint_package_custom_extension/components/link_with_icon.tsx @@ -13,16 +13,23 @@ import { LinkToAppProps, } from '../../../../../../../common/components/endpoint/link_to_app'; -const LinkLabel = styled.span` +const LinkLabel = styled.span<{ + size?: 'm' | 'l'; +}>` display: inline-block; padding-right: ${(props) => props.theme.eui.paddingSizes.s}; + font-size: ${({ size, theme }) => (size === 'm' ? theme.eui.euiFontSizeXS : 'innherit')}; `; -export const LinkWithIcon: FC = memo(({ children, ...props }) => { +type ComponentProps = LinkToAppProps & { + size?: 'm' | 'l'; +}; + +export const LinkWithIcon: FC = memo(({ children, size = 'l', ...props }) => { return ( - {children} - + {children} + ); }); diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/endpoint_package_custom_extension/components/styled_components.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/endpoint_package_custom_extension/components/styled_components.tsx index cb128946d8efa..d2d5de5d43a3f 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/endpoint_package_custom_extension/components/styled_components.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/endpoint_package_custom_extension/components/styled_components.tsx @@ -7,9 +7,12 @@ import styled from 'styled-components'; import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; -export const StyledEuiFlexGridGroup = styled(EuiFlexGroup)` +export const StyledEuiFlexGridGroup = styled(EuiFlexGroup)<{ + cardSize?: 'm' | 'l'; +}>` display: grid; - grid-template-columns: 25% 45% 30%; + grid-template-columns: ${({ cardSize = 'l' }) => + cardSize === 'l' ? '25% 45% 30%' : '30% 35% 35%'}; grid-template-areas: 'title summary link'; `; diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/endpoint_package_custom_extension/index.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/endpoint_package_custom_extension/index.tsx index 094f1131d7034..0748a95f63c9f 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/endpoint_package_custom_extension/index.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/endpoint_package_custom_extension/index.tsx @@ -8,14 +8,14 @@ import { EuiSpacer } from '@elastic/eui'; import React, { memo } from 'react'; import { PackageCustomExtensionComponentProps } from '../../../../../../../../fleet/public'; -import { FleetTrustedAppsCard } from './components/fleet_trusted_apps_card'; +import { FleetTrustedAppsCardWrapper } from './components/fleet_trusted_apps_card_wrapper'; import { FleetEventFiltersCard } from './components/fleet_event_filters_card'; export const EndpointPackageCustomExtension = memo( (props) => { return (
    - +
    diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/endpoint_policy_edit_extension.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/endpoint_policy_edit_extension.tsx index fe321e6a321c2..0a912598c5722 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/endpoint_policy_edit_extension.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/endpoint_policy_edit_extension.tsx @@ -5,19 +5,27 @@ * 2.0. */ -import React, { memo, useEffect, useState } from 'react'; -import { EuiSpacer } from '@elastic/eui'; +import React, { memo, useEffect, useState, useMemo } from 'react'; +import { EuiSpacer, EuiText } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; import { useDispatch } from 'react-redux'; import { PackagePolicyEditExtensionComponentProps, NewPackagePolicy, + pagePathGetters, } from '../../../../../../../fleet/public'; -import { getPolicyDetailPath } from '../../../../common/routing'; +import { useIsExperimentalFeatureEnabled } from '../../../../../common/hooks/use_experimental_features'; +import { INTEGRATIONS_PLUGIN_ID } from '../../../../../../../fleet/common'; +import { useAppUrl } from '../../../../../common/lib/kibana/hooks'; +import { PolicyDetailsRouteState } from '../../../../../../common/endpoint/types'; +import { getPolicyDetailPath, getPolicyTrustedAppsPath } from '../../../../common/routing'; import { PolicyDetailsForm } from '../policy_details_form'; import { AppAction } from '../../../../../common/store/actions'; import { usePolicyDetailsSelector } from '../policy_hooks'; import { policyDetailsForUpdate } from '../../store/policy_details/selectors'; - +import { FleetTrustedAppsCard } from './endpoint_package_custom_extension/components/fleet_trusted_apps_card'; +import { LinkWithIcon } from './endpoint_package_custom_extension/components/link_with_icon'; /** * Exports Endpoint-specific package policy instructions * for use in the Ingest app create / edit package policy @@ -40,7 +48,12 @@ const WrappedPolicyDetailsForm = memo<{ }>(({ policyId, onChange }) => { const dispatch = useDispatch<(a: AppAction) => void>(); const updatedPolicy = usePolicyDetailsSelector(policyDetailsForUpdate); + const { getAppUrl } = useAppUrl(); const [, setLastUpdatedPolicy] = useState(updatedPolicy); + // TODO: Remove this and related code when removing FF + const isTrustedAppsByPolicyEnabled = useIsExperimentalFeatureEnabled( + 'trustedAppsByPolicyEnabled' + ); // When the form is initially displayed, trigger the Redux middleware which is based on // the location information stored via the `userChangedUrl` action. @@ -93,9 +106,91 @@ const WrappedPolicyDetailsForm = memo<{ }); }, [onChange, updatedPolicy]); + const policyTrustedAppsPath = useMemo(() => getPolicyTrustedAppsPath(policyId), [policyId]); + const policyTrustedAppRouteState = useMemo(() => { + const fleetPackageIntegrationCustomUrlPath = `#${ + pagePathGetters.integration_policy_edit({ packagePolicyId: policyId })[1] + }`; + + return { + backLink: { + label: i18n.translate( + 'xpack.securitySolution.endpoint.fleetCustomExtension.artifacts.backButtonLabel', + { + defaultMessage: `Back to Fleet integration policy`, + } + ), + navigateTo: [ + INTEGRATIONS_PLUGIN_ID, + { + path: fleetPackageIntegrationCustomUrlPath, + }, + ], + href: getAppUrl({ + appId: INTEGRATIONS_PLUGIN_ID, + path: fleetPackageIntegrationCustomUrlPath, + }), + }, + }; + }, [getAppUrl, policyId]); + + const policyTrustedAppsLink = useMemo( + () => ( + + + + ), + [getAppUrl, policyTrustedAppsPath, policyTrustedAppRouteState] + ); + return (
    - + {isTrustedAppsByPolicyEnabled ? ( + <> +
    + +
    + +
    +
    + + +
    + +
    + +
    + +
    +
    + + +
    + + ) : ( + + )}
    ); }); diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/with_security_context.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/with_security_context.tsx index b657dfc74bdbc..1135a29759315 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/with_security_context.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/with_security_context.tsx @@ -13,6 +13,8 @@ import { CurrentLicense } from '../../../../../common/components/current_license import { StartPlugins } from '../../../../../types'; import { managementReducer } from '../../../../store/reducer'; import { managementMiddlewareFactory } from '../../../../store/middleware'; +import { appReducer } from '../../../../../common/store/app'; +import { ExperimentalFeaturesService } from '../../../../../common/experimental_features_service'; type ComposeType = typeof compose; declare global { @@ -51,8 +53,15 @@ export const withSecurityContext =

    ({ store = createStore( combineReducers({ management: managementReducer, + app: appReducer, }), - { management: undefined }, + { + management: undefined, + // @ts-ignore ignore this error as we just need the enableExperimental and it's temporary + app: { + enableExperimental: ExperimentalFeaturesService.get(), + }, + }, composeEnhancers(applyMiddleware(...managementMiddlewareFactory(coreStart, depsStart))) ); } diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_details_form.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_details_form.tsx index c9d1b3b7882a0..ed6a33166ff59 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_details_form.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_details_form.tsx @@ -29,14 +29,14 @@ const LOCKED_CARD_RAMSOMWARE_TITLE = i18n.translate( const LOCKED_CARD_MEMORY_TITLE = i18n.translate( 'xpack.securitySolution.endpoint.policy.details.memory', { - defaultMessage: 'Memory', + defaultMessage: 'Memory Threat', } ); const LOCKED_CARD_BEHAVIOR_TITLE = i18n.translate( 'xpack.securitySolution.endpoint.policy.details.behavior', { - defaultMessage: 'Behavior', + defaultMessage: 'Malicious Behavior', } ); diff --git a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/service/index.ts b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/service/index.ts index 9d39ecd05ad8a..c643094e61126 100644 --- a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/service/index.ts +++ b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/service/index.ts @@ -28,6 +28,7 @@ import { PutTrustedAppsRequestParams, GetOneTrustedAppRequestParams, GetOneTrustedAppResponse, + GetTrustedAppsSummaryRequest, } from '../../../../../common/endpoint/types/trusted_apps'; import { resolvePathVariables } from '../../../../common/utils/resolve_path_variables'; @@ -82,8 +83,10 @@ export class TrustedAppsHttpService implements TrustedAppsService { ); } - async getTrustedAppsSummary() { - return this.http.get(TRUSTED_APPS_SUMMARY_API); + async getTrustedAppsSummary(request: GetTrustedAppsSummaryRequest) { + return this.http.get(TRUSTED_APPS_SUMMARY_API, { + query: request, + }); } getPolicyList(options?: Parameters[1]) { diff --git a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/create_trusted_app_flyout.tsx b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/create_trusted_app_flyout.tsx index 62bdc446ddb9e..049ab5884b179 100644 --- a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/create_trusted_app_flyout.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/create_trusted_app_flyout.tsx @@ -171,11 +171,21 @@ export const CreateTrustedAppFlyout = memo( + +

    + {i18n.translate('xpack.securitySolution.trustedApps.detailsSectionTitle', { + defaultMessage: 'Details', + })} +

    +
    + {!isEditMode && ( - -

    {ABOUT_TRUSTED_APPS}

    + <> + +

    {ABOUT_TRUSTED_APPS}

    +
    -
    + )} { const getOsField = (dataTestSub: string = dataTestSubjForForm): HTMLButtonElement => { return renderResult.getByTestId(`${dataTestSub}-osSelectField`) as HTMLButtonElement; }; - const getGlobalSwitchField = (dataTestSub: string = dataTestSubjForForm): HTMLButtonElement => { - return renderResult.getByTestId( - `${dataTestSub}-effectedPolicies-globalSwitch` - ) as HTMLButtonElement; - }; const getDescriptionField = (dataTestSub: string = dataTestSubjForForm): HTMLTextAreaElement => { return renderResult.getByTestId(`${dataTestSub}-descriptionField`) as HTMLTextAreaElement; }; @@ -252,55 +247,50 @@ describe('When using the Trusted App Form', () => { }); describe('the Policy Selection area', () => { - it('should show loader when setting `policies.isLoading` to true', () => { + beforeEach(() => { + const policy = generator.generatePolicyPackagePolicy(); + policy.name = 'test policy A'; + policy.id = '123'; + + formProps.policies.options = [policy]; + }); + + it('should have `global` switch on if effective scope is global and policy options hidden', () => { + render(); + const globalButton = renderResult.getByTestId( + `${dataTestSubjForForm}-effectedPolicies-global` + ) as HTMLButtonElement; + + expect(globalButton.classList.contains('euiButtonGroupButton-isSelected')).toEqual(true); + expect(renderResult.queryByTestId('policy-123')).toBeNull(); + }); + + it('should have policy options visible and specific policies checked if scope is per-policy', () => { + (formProps.trustedApp as NewTrustedApp).effectScope = { + type: 'policy', + policies: ['123'], + }; + render(); + const perPolicyButton = renderResult.getByTestId( + `${dataTestSubjForForm}-effectedPolicies-perPolicy` + ) as HTMLButtonElement; + + expect(perPolicyButton.classList.contains('euiButtonGroupButton-isSelected')).toEqual(true); + expect(renderResult.getByTestId('policy-123').getAttribute('aria-disabled')).toEqual('false'); + expect(renderResult.getByTestId('policy-123').getAttribute('aria-selected')).toEqual('true'); + }); + it('should show loader when setting `policies.isLoading` to true and scope is per-policy', () => { formProps.policies.isLoading = true; + (formProps.trustedApp as NewTrustedApp).effectScope = { + type: 'policy', + policies: ['123'], + }; render(); expect( renderResult.getByTestId(`${dataTestSubjForForm}-effectedPolicies-policiesSelectable`) .textContent ).toEqual('Loading options'); }); - - describe('and policies exist', () => { - beforeEach(() => { - const policy = generator.generatePolicyPackagePolicy(); - policy.name = 'test policy A'; - policy.id = '123'; - - formProps.policies.options = [policy]; - }); - - it('should display the policies available, but disabled if ', () => { - render(); - expect(renderResult.getByTestId('policy-123')); - }); - - it('should have `global` switch on if effective scope is global and policy options disabled', () => { - render(); - expect(getGlobalSwitchField().getAttribute('aria-checked')).toEqual('true'); - expect(renderResult.getByTestId('policy-123').getAttribute('aria-disabled')).toEqual( - 'true' - ); - expect(renderResult.getByTestId('policy-123').getAttribute('aria-selected')).toEqual( - 'false' - ); - }); - - it('should have specific policies checked if scope is per-policy', () => { - (formProps.trustedApp as NewTrustedApp).effectScope = { - type: 'policy', - policies: ['123'], - }; - render(); - expect(getGlobalSwitchField().getAttribute('aria-checked')).toEqual('false'); - expect(renderResult.getByTestId('policy-123').getAttribute('aria-disabled')).toEqual( - 'false' - ); - expect(renderResult.getByTestId('policy-123').getAttribute('aria-selected')).toEqual( - 'true' - ); - }); - }); }); describe('the Policy Selection area under feature flag', () => { diff --git a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/create_trusted_app_form.tsx b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/create_trusted_app_form.tsx index f9b83fd69a75e..5db9a8557fa10 100644 --- a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/create_trusted_app_form.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/create_trusted_app_form.tsx @@ -14,6 +14,8 @@ import { EuiSuperSelect, EuiSuperSelectOption, EuiTextArea, + EuiText, + EuiSpacer, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { EuiFormProps } from '@elastic/eui/src/components/form/form'; @@ -458,6 +460,41 @@ export const CreateTrustedAppForm = memo( data-test-subj={getTestId('nameTextField')} /> + + + + + +

    + {i18n.translate('xpack.securitySolution.trustedApps.conditionsSectionTitle', { + defaultMessage: 'Conditions', + })} +

    +
    + + +

    + {i18n.translate('xpack.securitySolution.trustedApps.conditionsSectionDescription', { + defaultMessage: + 'Select an operating system and add conditions. Availability of conditions may depend on your chosen OS.', + })} +

    +
    + ( data-test-subj={getTestId('conditionsBuilder')} /> - - - - {isTrustedAppsByPolicyEnabled ? ( <> diff --git a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/effected_policy_select/effected_policy_select.test.tsx b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/effected_policy_select/effected_policy_select.test.tsx index 427d880444d39..4837a816d0ed8 100644 --- a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/effected_policy_select/effected_policy_select.test.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/effected_policy_select/effected_policy_select.test.tsx @@ -56,7 +56,7 @@ describe('when using EffectedPolicySelect component', () => { describe('and no policy entries exist', () => { it('should display no options available message', () => { - const { getByTestId } = render(); + const { getByTestId } = render({ isGlobal: false }); expect(getByTestId('test-policiesSelectable').textContent).toEqual('No options available'); }); }); @@ -65,9 +65,15 @@ describe('when using EffectedPolicySelect component', () => { const policyId = 'abc123'; const policyTestSubj = `policy-${policyId}`; - const toggleGlobalSwitch = () => { + const selectGlobalPolicy = () => { act(() => { - fireEvent.click(renderResult.getByTestId('test-globalSwitch')); + fireEvent.click(renderResult.getByTestId('globalPolicy')); + }); + }; + + const selectPerPolicy = () => { + act(() => { + fireEvent.click(renderResult.getByTestId('perPolicy')); }); }; @@ -97,59 +103,41 @@ describe('when using EffectedPolicySelect component', () => { }); it('should display policies', () => { - const { getByTestId } = render(); + const { getByTestId } = render({ isGlobal: false }); expect(getByTestId(policyTestSubj)); }); - it('should disable policy items if global is checked', () => { - const { getByTestId } = render(); - expect(getByTestId(policyTestSubj).getAttribute('aria-disabled')).toEqual('true'); + it('should hide policy items if global is checked', () => { + const { queryByTestId } = render({ isGlobal: true }); + expect(queryByTestId(policyTestSubj)).toBeNull(); }); it('should enable policy items if global is unchecked', async () => { - const { getByTestId } = render(); - toggleGlobalSwitch(); + const { getByTestId } = render({ isGlobal: false }); + selectPerPolicy(); expect(getByTestId(policyTestSubj).getAttribute('aria-disabled')).toEqual('false'); }); it('should call onChange with selection when global is toggled', () => { render(); - toggleGlobalSwitch(); + selectPerPolicy(); expect(handleOnChange.mock.calls[0][0]).toEqual({ isGlobal: false, selected: [], }); - toggleGlobalSwitch(); + selectGlobalPolicy(); expect(handleOnChange.mock.calls[1][0]).toEqual({ isGlobal: true, selected: [], }); }); - it('should not allow clicking on policies when global is true', () => { - render(); - - clickOnPolicy(); - expect(handleOnChange.mock.calls.length).toBe(0); - - // Select a Policy, then switch back to global and try to click the policy again (should be disabled and trigger onChange()) - toggleGlobalSwitch(); - clickOnPolicy(); - toggleGlobalSwitch(); - clickOnPolicy(); - expect(handleOnChange.mock.calls.length).toBe(3); - expect(handleOnChange.mock.calls[2][0]).toEqual({ - isGlobal: true, - selected: [componentProps.options[0]], - }); - }); - - it('should maintain policies selection even if global was checked', () => { + it('should maintain policies selection even if global was checked, and user switched back to per policy', () => { render(); - toggleGlobalSwitch(); + selectPerPolicy(); clickOnPolicy(); expect(handleOnChange.mock.calls[1][0]).toEqual({ isGlobal: false, @@ -157,7 +145,7 @@ describe('when using EffectedPolicySelect component', () => { }); // Toggle isGlobal back to True - toggleGlobalSwitch(); + selectGlobalPolicy(); expect(handleOnChange.mock.calls[2][0]).toEqual({ isGlobal: true, selected: [componentProps.options[0]], diff --git a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/effected_policy_select/effected_policy_select.tsx b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/effected_policy_select/effected_policy_select.tsx index 99db45c0e4b84..bb620ee5e7c01 100644 --- a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/effected_policy_select/effected_policy_select.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/effected_policy_select/effected_policy_select.tsx @@ -7,12 +7,15 @@ import React, { memo, useCallback, useMemo } from 'react'; import { + EuiButtonGroup, + EuiButtonGroupOptionProps, EuiCheckbox, + EuiFlexGroup, + EuiFlexItem, EuiFormRow, EuiSelectable, EuiSelectableProps, - EuiSwitch, - EuiSwitchProps, + EuiSpacer, EuiText, htmlIdGenerator, } from '@elastic/eui'; @@ -70,6 +73,28 @@ export const EffectedPolicySelect = memo( const getTestId = useTestIdGenerator(dataTestSubj); + const toggleGlobal: EuiButtonGroupOptionProps[] = useMemo( + () => [ + { + id: 'globalPolicy', + label: i18n.translate('xpack.securitySolution.endpoint.trustedAppsByPolicy.global', { + defaultMessage: 'Global', + }), + iconType: isGlobal ? 'checkInCircleFilled' : '', + 'data-test-subj': getTestId('global'), + }, + { + id: 'perPolicy', + label: i18n.translate('xpack.securitySolution.endpoint.trustedAppsByPolicy.perPolicy', { + defaultMessage: 'Per Policy', + }), + iconType: !isGlobal ? 'checkInCircleFilled' : '', + 'data-test-subj': getTestId('perPolicy'), + }, + ], + [getTestId, isGlobal] + ); + const selectableOptions: EffectedPolicyOption[] = useMemo(() => { const isPolicySelected = new Set(selected.map((policy) => policy.id)); @@ -117,10 +142,10 @@ export const EffectedPolicySelect = memo( [isGlobal, onChange] )!; - const handleGlobalSwitchChange: EuiSwitchProps['onChange'] = useCallback( - ({ target: { checked } }) => { + const handleGlobalButtonChange = useCallback( + (selectedId) => { onChange({ - isGlobal: checked, + isGlobal: selectedId === 'globalPolicy', selected, }); }, @@ -138,48 +163,54 @@ export const EffectedPolicySelect = memo( return ( - +

    + +

    + + + + -

    - -

    +

    + {i18n.translate('xpack.securitySolution.trustedApps.assignmentSectionDescription', { + defaultMessage: + 'You can assign this trusted application globally across all policies or assign it to specific policies.', + })} +

    - } - > - -
    - - - {...otherSelectableProps} - options={selectableOptions} - listProps={listProps || DEFAULT_LIST_PROPS} - onChange={handleOnPolicySelectChange} - searchable={true} - data-test-subj={getTestId('policiesSelectable')} - > - {listBuilderCallback} - - + + + + + + + + + {!isGlobal && ( + + + {...otherSelectableProps} + options={selectableOptions} + listProps={listProps || DEFAULT_LIST_PROPS} + onChange={handleOnPolicySelectChange} + searchable={true} + data-test-subj={getTestId('policiesSelectable')} + > + {listBuilderCallback} + + + )}
    ); } diff --git a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/trusted_apps_page.test.tsx b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/trusted_apps_page.test.tsx index c696c4705912e..b9609fb43ada5 100644 --- a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/trusted_apps_page.test.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/trusted_apps_page.test.tsx @@ -430,8 +430,11 @@ describe('When on the Trusted Apps Page', () => { it('should have list of policies populated', async () => { useIsExperimentalFeatureEnabledMock.mockReturnValue(true); const resetEnv = forceHTMLElementOffsetWidth(); - const { getByTestId } = await renderAndClickAddButton(); - expect(getByTestId('policy-abc123')); + const renderResult = await renderAndClickAddButton(); + act(() => { + fireEvent.click(renderResult.getByTestId('perPolicy')); + }); + expect(renderResult.getByTestId('policy-abc123')); resetEnv(); }); diff --git a/x-pack/plugins/security_solution/public/management/store/reducer.ts b/x-pack/plugins/security_solution/public/management/store/reducer.ts index 25c7c87c6f5c9..662d2b4322bcb 100644 --- a/x-pack/plugins/security_solution/public/management/store/reducer.ts +++ b/x-pack/plugins/security_solution/public/management/store/reducer.ts @@ -9,7 +9,7 @@ import { combineReducers } from 'redux'; import { policyDetailsReducer, initialPolicyDetailsState, -} from '../pages/policy/store/policy_details/reducer'; +} from '../pages/policy/store/policy_details'; import { MANAGEMENT_STORE_ENDPOINTS_NAMESPACE, MANAGEMENT_STORE_POLICY_DETAILS_NAMESPACE, diff --git a/x-pack/plugins/security_solution/public/plugin.tsx b/x-pack/plugins/security_solution/public/plugin.tsx index f8a2de61f5d6f..cd65808f28bce 100644 --- a/x-pack/plugins/security_solution/public/plugin.tsx +++ b/x-pack/plugins/security_solution/public/plugin.tsx @@ -53,6 +53,7 @@ import { import { SecurityAppStore } from './common/store/store'; import { licenseService } from './common/hooks/use_license'; import { SecuritySolutionUiConfigType } from './common/types'; +import { ExperimentalFeaturesService } from './common/experimental_features_service'; import { getLazyEndpointPolicyEditExtension } from './management/pages/policy/view/ingest_manager_integration/lazy_endpoint_policy_edit_extension'; import { LazyEndpointPolicyCreateExtension } from './management/pages/policy/view/ingest_manager_integration/lazy_endpoint_policy_create_extension'; @@ -184,6 +185,7 @@ export class Plugin implements IPlugin = { enableExperimental: true, }, schema: configSchema, - deprecations: ({ renameFromRoot }) => [ + deprecations: ({ deprecate, renameFromRoot }) => [ + deprecate('enabled', '8.0.0'), renameFromRoot('xpack.siem.enabled', 'xpack.securitySolution.enabled'), renameFromRoot( 'xpack.siem.maxRuleImportExportSize', diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/__mocks__/threshold.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/__mocks__/threshold.ts index 40d2ed37a5576..554672806c12e 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/__mocks__/threshold.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/__mocks__/threshold.ts @@ -5,7 +5,35 @@ * 2.0. */ +import { + SPACE_IDS, + ALERT_RULE_CONSUMER, + ALERT_REASON, + ALERT_STATUS, + ALERT_STATUS_ACTIVE, + ALERT_WORKFLOW_STATUS, + ALERT_RULE_NAMESPACE, + ALERT_INSTANCE_ID, + ALERT_UUID, + ALERT_RULE_TYPE_ID, + ALERT_RULE_PRODUCER, + ALERT_RULE_CATEGORY, + ALERT_RULE_UUID, + ALERT_RULE_NAME, +} from '@kbn/rule-data-utils'; +import { TypeOfFieldMap } from '../../../../../../rule_registry/common/field_map'; +import { SERVER_APP_ID } from '../../../../../common/constants'; +import { ANCHOR_DATE } from '../../../../../common/detection_engine/schemas/response/rules_schema.mocks'; +import { getListArrayMock } from '../../../../../common/detection_engine/schemas/types/lists.mock'; import { sampleDocNoSortId } from '../../signals/__mocks__/es_results'; +import { flattenWithPrefix } from '../factories/utils/flatten_with_prefix'; +import { RulesFieldMap } from '../field_maps'; +import { + ALERT_ANCESTORS, + ALERT_ORIGINAL_TIME, + ALERT_ORIGINAL_EVENT, +} from '../field_maps/field_names'; +import { WrappedRACAlert } from '../types'; export const mockThresholdResults = { rawResponse: { @@ -59,3 +87,79 @@ export const mockThresholdResults = { }, }, }; + +export const sampleThresholdAlert: WrappedRACAlert = { + _id: 'b3ad77a4-65bd-4c4e-89cf-13c46f54bc4d', + _index: 'some-index', + _source: { + '@timestamp': '2020-04-20T21:26:30.000Z', + [SPACE_IDS]: ['default'], + [ALERT_INSTANCE_ID]: 'b3ad77a4-65bd-4c4e-89cf-13c46f54bc4d', + [ALERT_UUID]: '310158f7-994d-4a38-8cdc-152139ac4d29', + [ALERT_RULE_CONSUMER]: SERVER_APP_ID, + [ALERT_ANCESTORS]: [ + { + id: 'd5e8eb51-a6a0-456d-8a15-4b79bfec3d71', + type: 'event', + index: 'myFakeSignalIndex', + depth: 0, + }, + ], + [ALERT_ORIGINAL_TIME]: '2020-04-20T21:27:45.000Z', + [ALERT_ORIGINAL_EVENT]: { + action: 'socket_opened', + dataset: 'socket', + kind: 'event', + module: 'system', + }, + [ALERT_REASON]: 'alert reasonable reason', + [ALERT_STATUS]: ALERT_STATUS_ACTIVE, + [ALERT_WORKFLOW_STATUS]: 'open', + 'source.ip': '127.0.0.1', + 'host.name': 'garden-gnomes', + [ALERT_RULE_CATEGORY]: 'security', + [ALERT_RULE_NAME]: 'a threshold rule', + [ALERT_RULE_PRODUCER]: 'siem', + [ALERT_RULE_TYPE_ID]: 'query-rule-id', + [ALERT_RULE_UUID]: '151af49f-2e82-4b6f-831b-7f8cb341a5ff', + ...(flattenWithPrefix(ALERT_RULE_NAMESPACE, { + author: [], + uuid: '7a7065d7-6e8b-4aae-8d20-c93613dec9f9', + created_at: new Date(ANCHOR_DATE).toISOString(), + updated_at: new Date(ANCHOR_DATE).toISOString(), + created_by: 'elastic', + description: 'some description', + enabled: true, + false_positives: ['false positive 1', 'false positive 2'], + from: 'now-6m', + immutable: false, + name: 'Query with a rule id', + query: 'user.name: root or user.name: admin', + references: ['test 1', 'test 2'], + severity: 'high', + severity_mapping: [], + updated_by: 'elastic_kibana', + tags: ['some fake tag 1', 'some fake tag 2'], + to: 'now', + type: 'query', + threat: [], + threshold: { + field: ['source.ip', 'host.name'], + value: 1, + }, + version: 1, + status: 'succeeded', + status_date: '2020-02-22T16:47:50.047Z', + last_success_at: '2020-02-22T16:47:50.047Z', + last_success_message: 'succeeded', + max_signals: 100, + risk_score: 55, + risk_score_mapping: [], + language: 'kuery', + rule_id: 'f88a544c-1d4e-4652-ae2a-c953b38da5d0', + interval: '5m', + exceptions_list: getListArrayMock(), + }) as TypeOfFieldMap), + 'kibana.alert.depth': 1, + }, +}; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/factories/bulk_create_factory.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/factories/bulk_create_factory.ts index 5d6bc698adc56..af0a8a27f2b25 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/factories/bulk_create_factory.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/factories/bulk_create_factory.ts @@ -5,6 +5,8 @@ * 2.0. */ +import { ALERT_INSTANCE_ID } from '@kbn/rule-data-utils'; + import { performance } from 'perf_hooks'; import { countBy, isEmpty } from 'lodash'; @@ -64,11 +66,15 @@ export const bulkCreateFactory = ); const createdItems = wrappedDocs - .map((doc, index) => ({ - _id: response.body.items[index].index?._id ?? '', - _index: response.body.items[index].index?._index ?? '', - ...doc._source, - })) + .map((doc, index) => { + const responseIndex = response.body.items[index].index; + return { + _id: responseIndex?._id ?? '', + _index: responseIndex?._index ?? '', + [ALERT_INSTANCE_ID]: responseIndex?._id ?? '', + ...doc._source, + }; + }) .filter((_, index) => response.body.items[index].index?.status === 201); const createdItemsCount = createdItems.length; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/factories/utils/build_alert.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/factories/utils/build_alert.test.ts index d75458630dc75..70b17ab96ab00 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/factories/utils/build_alert.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/factories/utils/build_alert.test.ts @@ -102,7 +102,6 @@ describe('buildAlert', () => { status_date: '2020-02-22T16:47:50.047Z', last_success_at: '2020-02-22T16:47:50.047Z', last_success_message: 'succeeded', - output_index: '.siem-signals-default', max_signals: 100, risk_score: 55, risk_score_mapping: [], @@ -179,7 +178,6 @@ describe('buildAlert', () => { status_date: '2020-02-22T16:47:50.047Z', last_success_at: '2020-02-22T16:47:50.047Z', last_success_message: 'succeeded', - output_index: '.siem-signals-default', max_signals: 100, risk_score: 55, risk_score_mapping: [], diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/factories/utils/build_alert.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/factories/utils/build_alert.ts index 316c1365102d0..6bb14df48eac0 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/factories/utils/build_alert.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/factories/utils/build_alert.ts @@ -120,7 +120,7 @@ export const buildAlert = ( [] ); - const { id, ...mappedRule } = rule; + const { id, output_index: outputIndex, ...mappedRule } = rule; mappedRule.uuid = id; return { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/factories/utils/flatten_with_prefix.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/factories/utils/flatten_with_prefix.ts index d472dc5885e57..02f418a151888 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/factories/utils/flatten_with_prefix.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/factories/utils/flatten_with_prefix.ts @@ -5,16 +5,26 @@ * 2.0. */ +import { isPlainObject } from 'lodash'; import { SearchTypes } from '../../../../../../common/detection_engine/types'; export const flattenWithPrefix = ( prefix: string, - obj: Record + maybeObj: unknown ): Record => { - return Object.keys(obj).reduce((acc: Record, key) => { + if (maybeObj != null && isPlainObject(maybeObj)) { + return Object.keys(maybeObj as Record).reduce( + (acc: Record, key) => { + return { + ...acc, + ...flattenWithPrefix(`${prefix}.${key}`, (maybeObj as Record)[key]), + }; + }, + {} + ); + } else { return { - ...acc, - [`${prefix}.${key}`]: obj[key], + [prefix]: maybeObj as SearchTypes, }; - }, {}); + } }; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/field_maps/alerts.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/field_maps/alerts.ts index 1c4b7f03fd73f..f21fc5b6ad393 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/field_maps/alerts.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/field_maps/alerts.ts @@ -43,11 +43,6 @@ export const alertsFieldMap: FieldMap = { array: false, required: true, }, - 'kibana.alert.group': { - type: 'object', - array: false, - required: false, - }, 'kibana.alert.group.id': { type: 'keyword', array: false, @@ -58,11 +53,6 @@ export const alertsFieldMap: FieldMap = { array: false, required: false, }, - 'kibana.alert.original_event': { - type: 'object', - array: false, - required: false, - }, 'kibana.alert.original_event.action': { type: 'keyword', array: false, @@ -198,81 +188,6 @@ export const alertsFieldMap: FieldMap = { array: false, required: false, }, - 'kibana.alert.threat': { - type: 'object', - array: false, - required: false, - }, - 'kibana.alert.threat.framework': { - type: 'keyword', - array: false, - required: true, - }, - 'kibana.alert.threat.tactic': { - type: 'object', - array: false, - required: true, - }, - 'kibana.alert.threat.tactic.id': { - type: 'keyword', - array: false, - required: true, - }, - 'kibana.alert.threat.tactic.name': { - type: 'keyword', - array: false, - required: true, - }, - 'kibana.alert.threat.tactic.reference': { - type: 'keyword', - array: false, - required: true, - }, - 'kibana.alert.threat.technique': { - type: 'object', - array: false, - required: true, - }, - 'kibana.alert.threat.technique.id': { - type: 'keyword', - array: false, - required: true, - }, - 'kibana.alert.threat.technique.name': { - type: 'keyword', - array: false, - required: true, - }, - 'kibana.alert.threat.technique.reference': { - type: 'keyword', - array: false, - required: true, - }, - 'kibana.alert.threat.technique.subtechnique': { - type: 'object', - array: false, - required: true, - }, - 'kibana.alert.threat.technique.subtechnique.id': { - type: 'keyword', - array: false, - required: true, - }, - 'kibana.alert.threat.technique.subtechnique.name': { - type: 'keyword', - array: false, - required: true, - }, - 'kibana.alert.threat.technique.subtechnique.reference': { - type: 'keyword', - array: false, - required: true, - }, - 'kibana.alert.threshold_result': { - type: 'object', - array: false, - required: false, - }, 'kibana.alert.threshold_result.cardinality': { type: 'object', array: false, @@ -300,7 +215,7 @@ export const alertsFieldMap: FieldMap = { }, 'kibana.alert.threshold_result.terms': { type: 'object', - array: false, + array: true, required: false, }, 'kibana.alert.threshold_result.terms.field': { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/field_maps/field_names.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/field_maps/field_names.ts index fb9e597a30448..68d08e08086a0 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/field_maps/field_names.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/field_maps/field_names.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { ALERT_NAMESPACE } from '@kbn/rule-data-utils'; +import { ALERT_NAMESPACE, ALERT_RULE_NAMESPACE } from '@kbn/rule-data-utils'; export const ALERT_ANCESTORS = `${ALERT_NAMESPACE}.ancestors` as const; export const ALERT_BUILDING_BLOCK_TYPE = `${ALERT_NAMESPACE}.building_block_type` as const; @@ -14,3 +14,6 @@ export const ALERT_GROUP_ID = `${ALERT_NAMESPACE}.group.id` as const; export const ALERT_GROUP_INDEX = `${ALERT_NAMESPACE}.group.index` as const; export const ALERT_ORIGINAL_EVENT = `${ALERT_NAMESPACE}.original_event` as const; export const ALERT_ORIGINAL_TIME = `${ALERT_NAMESPACE}.original_time` as const; + +const ALERT_RULE_THRESHOLD = `${ALERT_RULE_NAMESPACE}.threshold` as const; +export const ALERT_RULE_THRESHOLD_FIELD = `${ALERT_RULE_THRESHOLD}.field` as const; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/field_maps/rules.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/field_maps/rules.ts index 21405672fdf7f..87b55e092ec5d 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/field_maps/rules.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/field_maps/rules.ts @@ -11,6 +11,11 @@ export const rulesFieldMap = { array: false, required: false, }, + 'kibana.alert.rule.exceptions_list': { + type: 'object', + array: true, + required: false, + }, 'kibana.alert.rule.false_positives': { type: 'keyword', array: true, @@ -46,6 +51,56 @@ export const rulesFieldMap = { array: true, required: true, }, + 'kibana.alert.rule.threat.framework': { + type: 'keyword', + array: false, + required: true, + }, + 'kibana.alert.rule.threat.tactic.id': { + type: 'keyword', + array: false, + required: true, + }, + 'kibana.alert.rule.threat.tactic.name': { + type: 'keyword', + array: false, + required: true, + }, + 'kibana.alert.rule.threat.tactic.reference': { + type: 'keyword', + array: false, + required: true, + }, + 'kibana.alert.rule.threat.technique.id': { + type: 'keyword', + array: false, + required: true, + }, + 'kibana.alert.rule.threat.technique.name': { + type: 'keyword', + array: false, + required: true, + }, + 'kibana.alert.rule.threat.technique.reference': { + type: 'keyword', + array: false, + required: true, + }, + 'kibana.alert.rule.threat.technique.subtechnique.id': { + type: 'keyword', + array: false, + required: true, + }, + 'kibana.alert.rule.threat.technique.subtechnique.name': { + type: 'keyword', + array: false, + required: true, + }, + 'kibana.alert.rule.threat.technique.subtechnique.reference': { + type: 'keyword', + array: false, + required: true, + }, 'kibana.alert.rule.threat_filters': { type: 'keyword', array: true, @@ -91,11 +146,6 @@ export const rulesFieldMap = { array: true, required: false, }, - 'kibana.alert.rule.threshold': { - type: 'object', - array: true, - required: false, - }, 'kibana.alert.rule.threshold.field': { type: 'keyword', array: true, @@ -103,7 +153,7 @@ export const rulesFieldMap = { }, 'kibana.alert.rule.threshold.value': { type: 'float', // TODO: should be 'long' (eventually, after we stabilize) - array: true, + array: false, required: false, }, 'kibana.alert.rule.threshold.cardinality': { @@ -113,12 +163,12 @@ export const rulesFieldMap = { }, 'kibana.alert.rule.threshold.cardinality.field': { type: 'keyword', - array: true, + array: false, required: false, }, 'kibana.alert.rule.threshold.cardinality.value': { type: 'long', - array: true, + array: false, required: false, }, 'kibana.alert.rule.timeline_id': { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/index.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/index.ts index 39325cab2c762..1787a15588b51 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/index.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/index.ts @@ -7,5 +7,6 @@ export { createEqlAlertType } from './eql/create_eql_alert_type'; export { createIndicatorMatchAlertType } from './indicator_match/create_indicator_match_alert_type'; -export { createQueryAlertType } from './query/create_query_alert_type'; export { createMlAlertType } from './ml/create_ml_alert_type'; +export { createQueryAlertType } from './query/create_query_alert_type'; +export { createThresholdAlertType } from './threshold/create_threshold_alert_type'; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/query/create_query_alert_type.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/query/create_query_alert_type.test.ts index ed791af08890c..e45d8440386fe 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/query/create_query_alert_type.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/query/create_query_alert_type.test.ts @@ -24,7 +24,7 @@ jest.mock('../utils/get_list_client', () => ({ jest.mock('../../rule_execution_log/rule_execution_log_client'); -describe('Custom query alerts', () => { +describe('Custom Query Alerts', () => { it('does not send an alert when no events found', async () => { const { services, dependencies, executor } = createRuleTypeMocks(); const queryAlertType = createQueryAlertType({ diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/scripts/create_rule_threshold.sh b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/scripts/create_rule_threshold.sh new file mode 100644 index 0000000000000..47c5cb4eda2e9 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/scripts/create_rule_threshold.sh @@ -0,0 +1,63 @@ +#!/bin/sh +# +# Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one +# or more contributor license agreements. Licensed under the Elastic License +# 2.0; you may not use this file except in compliance with the Elastic License +# 2.0. +# + +curl -X POST ${KIBANA_URL}${SPACE_URL}/api/alerts/alert \ + -u ${ELASTICSEARCH_USERNAME}:${ELASTICSEARCH_PASSWORD} \ + -H 'kbn-xsrf: true' \ + -H 'Content-Type: application/json' \ + --verbose \ + -d ' +{ + "params":{ + "author": [], + "description": "Basic threshold rule", + "exceptionsList": [], + "falsePositives": [], + "from": "now-300s", + "query": "*:*", + "immutable": false, + "index": ["*"], + "language": "kuery", + "maxSignals": 10, + "outputIndex": "", + "references": [], + "riskScore": 21, + "riskScoreMapping": [], + "ruleId": "52dec1ba-b779-469c-9667-6b0e865fb89a", + "severity": "low", + "severityMapping": [], + "threat": [], + "threshold": { + "field": ["source.ip"], + "value": 2, + "cardinality": [ + { + "field": "source.ip", + "value": 1 + } + ] + }, + "to": "now", + "type": "threshold", + "version": 1 + }, + "consumer":"alerts", + "alertTypeId":"siem.thresholdRule", + "schedule":{ + "interval":"1m" + }, + "actions":[], + "tags":[ + "custom", + "persistence" + ], + "notifyWhen":"onActionGroupChange", + "name":"Basic threshold rule" +}' + + diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/threshold/create_threshold_alert_type.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/threshold/create_threshold_alert_type.test.ts new file mode 100644 index 0000000000000..74435cb300472 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/threshold/create_threshold_alert_type.test.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 { allowedExperimentalValues } from '../../../../../common/experimental_features'; +import { createThresholdAlertType } from './create_threshold_alert_type'; +import { createRuleTypeMocks } from '../__mocks__/rule_type'; +import { getThresholdRuleParams } from '../../schemas/rule_schemas.mock'; + +jest.mock('../../rule_execution_log/rule_execution_log_client'); + +describe('Threshold Alerts', () => { + it('does not send an alert when no events found', async () => { + const params = getThresholdRuleParams(); + const { dependencies, executor } = createRuleTypeMocks('threshold', params); + const thresholdAlertTpe = createThresholdAlertType({ + experimentalFeatures: allowedExperimentalValues, + lists: dependencies.lists, + logger: dependencies.logger, + mergeStrategy: 'allFields', + ignoreFields: [], + ruleDataClient: dependencies.ruleDataClient, + ruleDataService: dependencies.ruleDataService, + version: '1.0.0', + }); + dependencies.alerting.registerType(thresholdAlertTpe); + + await executor({ params }); + expect(dependencies.ruleDataClient.getWriter).not.toBeCalled(); + }); +}); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/threshold/create_threshold_alert_type.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/threshold/create_threshold_alert_type.ts new file mode 100644 index 0000000000000..a503cf5aedbea --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/threshold/create_threshold_alert_type.ts @@ -0,0 +1,95 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { validateNonExact } from '@kbn/securitysolution-io-ts-utils'; + +import { PersistenceServices } from '../../../../../../rule_registry/server'; +import { THRESHOLD_RULE_TYPE_ID } from '../../../../../common/constants'; +import { thresholdRuleParams, ThresholdRuleParams } from '../../schemas/rule_schemas'; +import { thresholdExecutor } from '../../signals/executors/threshold'; +import { ThresholdAlertState } from '../../signals/types'; +import { createSecurityRuleTypeFactory } from '../create_security_rule_type_factory'; +import { CreateRuleOptions } from '../types'; + +export const createThresholdAlertType = (createOptions: CreateRuleOptions) => { + const { + experimentalFeatures, + lists, + logger, + mergeStrategy, + ignoreFields, + ruleDataClient, + version, + ruleDataService, + } = createOptions; + const createSecurityRuleType = createSecurityRuleTypeFactory({ + lists, + logger, + mergeStrategy, + ignoreFields, + ruleDataClient, + ruleDataService, + }); + return createSecurityRuleType({ + id: THRESHOLD_RULE_TYPE_ID, + name: 'Threshold Rule', + validate: { + params: { + validate: (object: unknown): ThresholdRuleParams => { + const [validated, errors] = validateNonExact(object, thresholdRuleParams); + if (errors != null) { + throw new Error(errors); + } + if (validated == null) { + throw new Error('Validation of rule params failed'); + } + return validated; + }, + }, + }, + actionGroups: [ + { + id: 'default', + name: 'Default', + }, + ], + defaultActionGroupId: 'default', + actionVariables: { + context: [{ name: 'server', description: 'the server' }], + }, + minimumLicenseRequired: 'basic', + isExportable: false, + producer: 'security-solution', + async executor(execOptions) { + const { + runOpts: { buildRuleMessage, bulkCreate, exceptionItems, rule, tuple, wrapHits }, + services, + startedAt, + state, + } = execOptions; + + // console.log(JSON.stringify(state)); + + const result = await thresholdExecutor({ + buildRuleMessage, + bulkCreate, + exceptionItems, + experimentalFeatures, + logger, + rule, + services, + startedAt, + state, + tuple, + version, + wrapHits, + }); + + return result; + }, + }); +}; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/find_rules.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/find_rules.test.ts index 0f7545c4df936..ebde1d0ad6df8 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/find_rules.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/find_rules.test.ts @@ -7,15 +7,19 @@ import { getFilter } from './find_rules'; import { + EQL_RULE_TYPE_ID, INDICATOR_RULE_TYPE_ID, ML_RULE_TYPE_ID, QUERY_RULE_TYPE_ID, + THRESHOLD_RULE_TYPE_ID, SIGNALS_ID, } from '../../../../common/constants'; -const allAlertTypeIds = `(alert.attributes.alertTypeId: ${ML_RULE_TYPE_ID} +const allAlertTypeIds = `(alert.attributes.alertTypeId: ${EQL_RULE_TYPE_ID} + OR alert.attributes.alertTypeId: ${ML_RULE_TYPE_ID} OR alert.attributes.alertTypeId: ${QUERY_RULE_TYPE_ID} - OR alert.attributes.alertTypeId: ${INDICATOR_RULE_TYPE_ID})`.replace(/[\n\r]/g, ''); + OR alert.attributes.alertTypeId: ${INDICATOR_RULE_TYPE_ID} + OR alert.attributes.alertTypeId: ${THRESHOLD_RULE_TYPE_ID})`.replace(/[\n\r]/g, ''); describe('find_rules', () => { const fullFilterTestCases: Array<[boolean, string]> = [ diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/schemas/rule_schemas.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/schemas/rule_schemas.ts index e9215084614c0..578d8c4926b69 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/schemas/rule_schemas.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/schemas/rule_schemas.ts @@ -69,6 +69,8 @@ import { INDICATOR_RULE_TYPE_ID, ML_RULE_TYPE_ID, QUERY_RULE_TYPE_ID, + EQL_RULE_TYPE_ID, + THRESHOLD_RULE_TYPE_ID, } from '../../../../common/constants'; const nonEqlLanguages = t.keyof({ kuery: null, lucene: null }); @@ -206,12 +208,11 @@ export const notifyWhen = t.union([ export const allRuleTypes = t.union([ t.literal(SIGNALS_ID), - // t.literal(EQL_RULE_TYPE_ID), + t.literal(EQL_RULE_TYPE_ID), t.literal(ML_RULE_TYPE_ID), t.literal(QUERY_RULE_TYPE_ID), - // t.literal(SAVED_QUERY_RULE_TYPE_ID), t.literal(INDICATOR_RULE_TYPE_ID), - // t.literal(THRESHOLD_RULE_TYPE_ID), + t.literal(THRESHOLD_RULE_TYPE_ID), ]); export type AllRuleTypes = t.TypeOf; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/executors/threshold.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/executors/threshold.test.ts index afcb3707591fc..5766390099e29 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/executors/threshold.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/executors/threshold.test.ts @@ -73,6 +73,7 @@ describe('threshold_executor', () => { exceptionItems, experimentalFeatures: allowedExperimentalValues, services: alertServices, + state: { initialized: true, signalHistory: {} }, version, logger, buildRuleMessage, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/executors/threshold.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/executors/threshold.ts index a6ea55797dc53..02cad1e8e508c 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/executors/threshold.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/executors/threshold.ts @@ -5,9 +5,12 @@ * 2.0. */ +import { SearchHit } from '@elastic/elasticsearch/api/types'; +import type { ExceptionListItemSchema } from '@kbn/securitysolution-io-ts-list-types'; + import { Logger } from 'src/core/server'; import { SavedObject } from 'src/core/types'; -import type { ExceptionListItemSchema } from '@kbn/securitysolution-io-ts-list-types'; + import { AlertInstanceContext, AlertInstanceState, @@ -28,6 +31,7 @@ import { BulkCreate, RuleRangeTuple, SearchAfterAndBulkCreateReturnType, + ThresholdAlertState, WrapHits, } from '../types'; import { @@ -37,6 +41,7 @@ import { } from '../utils'; import { BuildRuleMessage } from '../rule_messages'; import { ExperimentalFeatures } from '../../../../../common/experimental_features'; +import { buildThresholdSignalHistory } from '../threshold/build_signal_history'; export const thresholdExecutor = async ({ rule, @@ -48,6 +53,7 @@ export const thresholdExecutor = async ({ logger, buildRuleMessage, startedAt, + state, bulkCreate, wrapHits, }: { @@ -60,17 +66,48 @@ export const thresholdExecutor = async ({ logger: Logger; buildRuleMessage: BuildRuleMessage; startedAt: Date; + state: ThresholdAlertState; bulkCreate: BulkCreate; wrapHits: WrapHits; -}): Promise => { +}): Promise => { let result = createSearchAfterReturnType(); const ruleParams = rule.attributes.params; + + // Get state or build initial state (on upgrade) + const { signalHistory, searchErrors: previousSearchErrors } = state.initialized + ? { signalHistory: state.signalHistory, searchErrors: [] } + : await getThresholdSignalHistory({ + indexPattern: ['*'], // TODO: get outputIndex? + from: tuple.from.toISOString(), + to: tuple.to.toISOString(), + services, + logger, + ruleId: ruleParams.ruleId, + bucketByFields: ruleParams.threshold.field, + timestampOverride: ruleParams.timestampOverride, + buildRuleMessage, + }); + + if (!state.initialized) { + // Clean up any signal history that has fallen outside the window + const toDelete: string[] = []; + for (const [hash, entry] of Object.entries(signalHistory)) { + if (entry.lastSignalTimestamp < tuple.from.valueOf()) { + toDelete.push(hash); + } + } + for (const hash of toDelete) { + delete signalHistory[hash]; + } + } + if (hasLargeValueItem(exceptionItems)) { result.warningMessages.push( 'Exceptions that use "is in list" or "is not in list" operators are not applied to Threshold rules' ); result.warning = true; } + const inputIndex = await getInputIndex({ experimentalFeatures, services, @@ -78,21 +115,8 @@ export const thresholdExecutor = async ({ index: ruleParams.index, }); - const { thresholdSignalHistory, searchErrors: previousSearchErrors } = - await getThresholdSignalHistory({ - indexPattern: [ruleParams.outputIndex], - from: tuple.from.toISOString(), - to: tuple.to.toISOString(), - services, - logger, - ruleId: ruleParams.ruleId, - bucketByFields: ruleParams.threshold.field, - timestampOverride: ruleParams.timestampOverride, - buildRuleMessage, - }); - const bucketFilters = await getThresholdBucketFilters({ - thresholdSignalHistory, + signalHistory, timestampOverride: ruleParams.timestampOverride, }); @@ -134,7 +158,7 @@ export const thresholdExecutor = async ({ signalsIndex: ruleParams.outputIndex, startedAt, from: tuple.from.toDate(), - thresholdSignalHistory, + signalHistory, bulkCreate, wrapHits, }); @@ -154,5 +178,31 @@ export const thresholdExecutor = async ({ searchAfterTimes: [thresholdSearchDuration], }), ]); - return result; + + const createdAlerts = createdItems.map((alert) => { + const { _id, _index, ...source } = alert as { _id: string; _index: string }; + return { + _id, + _index, + _source: { + ...source, + }, + } as SearchHit; + }); + + const newSignalHistory = buildThresholdSignalHistory({ + alerts: createdAlerts, + }); + + return { + ...result, + state: { + ...state, + initialized: true, + signalHistory: { + ...signalHistory, + ...newSignalHistory, + }, + }, + }; }; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_rule_alert_type.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_rule_alert_type.ts index 68d60f7757e4a..9a6c099ed1760 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_rule_alert_type.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_rule_alert_type.ts @@ -29,7 +29,7 @@ import { } from '../../../../common/detection_engine/utils'; import { SetupPlugins } from '../../../plugin'; import { getInputIndex } from './get_input_output_index'; -import { AlertAttributes, SignalRuleAlertTypeDefinition } from './types'; +import { AlertAttributes, SignalRuleAlertTypeDefinition, ThresholdAlertState } from './types'; import { getListsClient, getExceptions, @@ -125,6 +125,7 @@ export const signalRulesAlertType = ({ async executor({ previousStartedAt, startedAt, + state, alertId, services, params, @@ -316,6 +317,7 @@ export const signalRulesAlertType = ({ logger, buildRuleMessage, startedAt, + state: state as ThresholdAlertState, bulkCreate, wrapHits, }); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/single_search_after.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/single_search_after.ts index 8957c5400d854..2b1d27fc2fcd0 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/single_search_after.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/single_search_after.ts @@ -74,9 +74,11 @@ export const singleSearchAfter = async ({ searchAfterQuery as estypes.SearchRequest ); const end = performance.now(); + const searchErrors = createErrorsFromShard({ errors: nextSearchAfterResult._shards.failures ?? [], }); + return { searchResult: nextSearchAfterResult, searchDuration: makeFloatString(end - start), diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threshold/build_signal_history.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threshold/build_signal_history.test.ts new file mode 100644 index 0000000000000..8362942af15b9 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threshold/build_signal_history.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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { ALERT_ORIGINAL_TIME } from '../../rule_types/field_maps/field_names'; +import { sampleThresholdAlert } from '../../rule_types/__mocks__/threshold'; +import { buildThresholdSignalHistory } from './build_signal_history'; + +describe('buildSignalHistory', () => { + it('builds a signal history from an alert', () => { + const signalHistory = buildThresholdSignalHistory({ alerts: [sampleThresholdAlert] }); + expect(signalHistory).toEqual({ + '7a75c5c2db61f57ec166c669cb8244b91f812f0b2f1d4f8afd528d4f8b4e199b': { + lastSignalTimestamp: Date.parse( + sampleThresholdAlert._source[ALERT_ORIGINAL_TIME] as string + ), + terms: [ + { + field: 'host.name', + value: 'garden-gnomes', + }, + { + field: 'source.ip', + value: '127.0.0.1', + }, + ], + }, + }); + }); +}); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threshold/build_signal_history.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threshold/build_signal_history.ts new file mode 100644 index 0000000000000..81b12d2d4f229 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threshold/build_signal_history.ts @@ -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 { SearchHit } from '@elastic/elasticsearch/api/types'; +import { + ALERT_ORIGINAL_TIME, + ALERT_RULE_THRESHOLD_FIELD, +} from '../../rule_types/field_maps/field_names'; + +import { SimpleHit, ThresholdSignalHistory } from '../types'; +import { getThresholdTermsHash, isWrappedRACAlert, isWrappedSignalHit } from '../utils'; + +interface GetThresholdSignalHistoryParams { + alerts: Array>; +} + +const getTerms = (alert: SimpleHit) => { + if (isWrappedRACAlert(alert)) { + return (alert._source[ALERT_RULE_THRESHOLD_FIELD] as string[]).map((field) => ({ + field, + value: alert._source[field] as string, + })); + } else if (isWrappedSignalHit(alert)) { + return alert._source.signal?.threshold_result?.terms ?? []; + } else { + // We shouldn't be here + return []; + } +}; + +const getOriginalTime = (alert: SimpleHit) => { + if (isWrappedRACAlert(alert)) { + const originalTime = alert._source[ALERT_ORIGINAL_TIME]; + return originalTime != null ? new Date(originalTime as string).getTime() : undefined; + } else if (isWrappedSignalHit(alert)) { + const originalTime = alert._source.signal?.original_time; + return originalTime != null ? new Date(originalTime).getTime() : undefined; + } else { + // We shouldn't be here + return undefined; + } +}; + +export const buildThresholdSignalHistory = ({ + alerts, +}: GetThresholdSignalHistoryParams): ThresholdSignalHistory => { + const signalHistory = alerts.reduce((acc, alert) => { + if (!alert._source) { + return acc; + } + + const terms = getTerms(alert as SimpleHit); + const hash = getThresholdTermsHash(terms); + const existing = acc[hash]; + const originalTime = getOriginalTime(alert as SimpleHit); + + if (existing != null) { + if (originalTime && originalTime > existing.lastSignalTimestamp) { + acc[hash].lastSignalTimestamp = originalTime; + } + } else if (originalTime) { + acc[hash] = { + terms, + lastSignalTimestamp: originalTime, + }; + } + return acc; + }, {}); + + return signalHistory; +}; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threshold/bulk_create_threshold_signals.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threshold/bulk_create_threshold_signals.ts index afb0353c4ba03..ce8ee4542d603 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threshold/bulk_create_threshold_signals.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threshold/bulk_create_threshold_signals.ts @@ -46,7 +46,7 @@ interface BulkCreateThresholdSignalsParams { signalsIndex: string; startedAt: Date; from: Date; - thresholdSignalHistory: ThresholdSignalHistory; + signalHistory: ThresholdSignalHistory; bulkCreate: BulkCreate; wrapHits: WrapHits; } @@ -61,7 +61,7 @@ const getTransformedHits = ( ruleId: string, filter: unknown, timestampOverride: TimestampOverrideOrUndefined, - thresholdSignalHistory: ThresholdSignalHistory + signalHistory: ThresholdSignalHistory ) => { const aggParts = threshold.field.length ? results.aggregations && getThresholdAggregationParts(results.aggregations) @@ -148,7 +148,7 @@ const getTransformedHits = ( } const termsHash = getThresholdTermsHash(bucket.terms); - const signalHit = thresholdSignalHistory[termsHash]; + const signalHit = signalHistory[termsHash]; const source = { '@timestamp': timestamp, @@ -202,7 +202,7 @@ export const transformThresholdResultsToEcs = ( threshold: ThresholdNormalized, ruleId: string, timestampOverride: TimestampOverrideOrUndefined, - thresholdSignalHistory: ThresholdSignalHistory + signalHistory: ThresholdSignalHistory ): SignalSearchResponse => { const transformedHits = getTransformedHits( results, @@ -214,7 +214,7 @@ export const transformThresholdResultsToEcs = ( ruleId, filter, timestampOverride, - thresholdSignalHistory + signalHistory ); const thresholdResults = { ...results, @@ -246,7 +246,7 @@ export const bulkCreateThresholdSignals = async ( ruleParams.threshold, ruleParams.ruleId, ruleParams.timestampOverride, - params.thresholdSignalHistory + params.signalHistory ); return params.bulkCreate( diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threshold/find_threshold_signals.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threshold/find_threshold_signals.test.ts index e84b4f31fb15f..41d46925770bd 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threshold/find_threshold_signals.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threshold/find_threshold_signals.test.ts @@ -227,6 +227,7 @@ describe('findThresholdSignals', () => { 'threshold_1:user.name': { terms: { field: 'user.name', + order: { cardinality_count: 'desc' }, min_doc_count: 100, size: 10000, }, @@ -302,6 +303,7 @@ describe('findThresholdSignals', () => { lang: 'painless', }, min_doc_count: 200, + order: { cardinality_count: 'desc' }, }, aggs: { cardinality_count: { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threshold/find_threshold_signals.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threshold/find_threshold_signals.ts index ca7f22e4a7570..740ba281cfcfb 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threshold/find_threshold_signals.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threshold/find_threshold_signals.ts @@ -89,6 +89,13 @@ export const findThresholdSignals = async ({ const thresholdFields = threshold.field; + // order buckets by cardinality (https://github.com/elastic/kibana/issues/95258) + const thresholdFieldCount = thresholdFields.length; + const orderByCardinality = (i: number = 0) => + (thresholdFieldCount === 0 || i === thresholdFieldCount - 1) && threshold.cardinality?.length + ? { order: { cardinality_count: 'desc' } } + : {}; + // Generate a nested terms aggregation for each threshold grouping field provided, appending leaf // aggregations to 1) filter out buckets that don't meet the cardinality threshold, if provided, and // 2) return the latest hit for each bucket so that we can persist the timestamp of the event in the @@ -104,6 +111,7 @@ export const findThresholdSignals = async ({ set(acc, aggPath, { terms: { field, + ...orderByCardinality(i), min_doc_count: threshold.value, // not needed on parent agg, but can help narrow down result set size: 10000, // max 10k buckets }, @@ -121,6 +129,7 @@ export const findThresholdSignals = async ({ source: '""', // Group everything in the same bucket lang: 'painless', }, + ...orderByCardinality(), min_doc_count: threshold.value, }, aggs: leafAggs, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threshold/get_threshold_bucket_filters.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threshold/get_threshold_bucket_filters.test.ts index d621868a0956c..e67a6fa3dfa9c 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threshold/get_threshold_bucket_filters.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threshold/get_threshold_bucket_filters.test.ts @@ -11,7 +11,7 @@ import { getThresholdBucketFilters } from './get_threshold_bucket_filters'; describe('getThresholdBucketFilters', () => { it('should generate filters for threshold signal detection with dupe mitigation', async () => { const result = await getThresholdBucketFilters({ - thresholdSignalHistory: sampleThresholdSignalHistory(), + signalHistory: sampleThresholdSignalHistory(), timestampOverride: undefined, }); expect(result).toEqual([ diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threshold/get_threshold_bucket_filters.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threshold/get_threshold_bucket_filters.ts index a8b2ee31c6210..5cafff24c544b 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threshold/get_threshold_bucket_filters.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threshold/get_threshold_bucket_filters.ts @@ -9,14 +9,18 @@ import { Filter } from 'src/plugins/data/common'; import { ESFilter } from '../../../../../../../../src/core/types/elasticsearch'; import { ThresholdSignalHistory, ThresholdSignalHistoryRecord } from '../types'; +/* + * Returns a filter to exclude events that have already been included in a + * previous threshold signal. Uses the threshold signal history to achieve this. + */ export const getThresholdBucketFilters = async ({ - thresholdSignalHistory, + signalHistory, timestampOverride, }: { - thresholdSignalHistory: ThresholdSignalHistory; + signalHistory: ThresholdSignalHistory; timestampOverride: string | undefined; }): Promise => { - const filters = Object.values(thresholdSignalHistory).reduce( + const filters = Object.values(signalHistory).reduce( (acc: ESFilter[], bucket: ThresholdSignalHistoryRecord): ESFilter[] => { const filter = { bool: { @@ -24,6 +28,7 @@ export const getThresholdBucketFilters = async ({ { range: { [timestampOverride ?? '@timestamp']: { + // Timestamp of last event signaled on for this set of terms. lte: new Date(bucket.lastSignalTimestamp).toISOString(), }, }, @@ -32,6 +37,7 @@ export const getThresholdBucketFilters = async ({ }, } as ESFilter; + // Terms to filter events older than `lastSignalTimestamp`. bucket.terms.forEach((term) => { if (term.field != null) { (filter.bool!.filter as ESFilter[]).push({ diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threshold/get_threshold_signal_history.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threshold/get_threshold_signal_history.ts index ebfad5c7f9bec..276431c3bc929 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threshold/get_threshold_signal_history.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threshold/get_threshold_signal_history.ts @@ -5,7 +5,6 @@ * 2.0. */ -import { RulesSchema } from '../../../../../common/detection_engine/schemas/response/rules_schema'; import { TimestampOverrideOrUndefined } from '../../../../../common/detection_engine/schemas/common/schemas'; import { AlertInstanceContext, @@ -16,7 +15,7 @@ import { Logger } from '../../../../../../../../src/core/server'; import { ThresholdSignalHistory } from '../types'; import { BuildRuleMessage } from '../rule_messages'; import { findPreviousThresholdSignals } from './find_previous_threshold_signals'; -import { getThresholdTermsHash } from '../utils'; +import { buildThresholdSignalHistory } from './build_signal_history'; interface GetThresholdSignalHistoryParams { from: string; @@ -41,7 +40,7 @@ export const getThresholdSignalHistory = async ({ timestampOverride, buildRuleMessage, }: GetThresholdSignalHistoryParams): Promise<{ - thresholdSignalHistory: ThresholdSignalHistory; + signalHistory: ThresholdSignalHistory; searchErrors: string[]; }> => { const { searchResult, searchErrors } = await findPreviousThresholdSignals({ @@ -56,52 +55,10 @@ export const getThresholdSignalHistory = async ({ buildRuleMessage, }); - const thresholdSignalHistory = searchResult.hits.hits.reduce( - (acc, hit) => { - if (!hit._source) { - return acc; - } - - const terms = - hit._source.signal?.threshold_result?.terms != null - ? hit._source.signal.threshold_result.terms - : [ - // Pre-7.12 signals - { - field: - ( - (hit._source.signal?.rule as RulesSchema).threshold as unknown as { - field: string; - } - ).field ?? '', - value: (hit._source.signal?.threshold_result as unknown as { value: string }).value, - }, - ]; - - const hash = getThresholdTermsHash(terms); - const existing = acc[hash]; - const originalTime = - hit._source.signal?.original_time != null - ? new Date(hit._source.signal?.original_time).getTime() - : undefined; - - if (existing != null) { - if (originalTime && originalTime > existing.lastSignalTimestamp) { - acc[hash].lastSignalTimestamp = originalTime; - } - } else if (originalTime) { - acc[hash] = { - terms, - lastSignalTimestamp: originalTime, - }; - } - return acc; - }, - {} - ); - return { - thresholdSignalHistory, + signalHistory: buildThresholdSignalHistory({ + alerts: searchResult.hits.hits, + }), searchErrors, }; }; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/types.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/types.ts index 3751f6f6e98f2..fc6b42c38549e 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/types.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/types.ts @@ -362,3 +362,8 @@ export interface ThresholdQueryBucket extends TermAggregationBucket { value_as_string: string; }; } + +export interface ThresholdAlertState extends AlertTypeState { + initialized: boolean; + signalHistory: ThresholdSignalHistory; +} diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/utils.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/utils.ts index efd7200202b59..5993dd626729f 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/utils.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/utils.ts @@ -63,10 +63,12 @@ import { WrappedRACAlert } from '../rule_types/types'; import { SearchTypes } from '../../../../common/detection_engine/types'; import { IRuleExecutionLogClient } from '../rule_execution_log/types'; import { + EQL_RULE_TYPE_ID, INDICATOR_RULE_TYPE_ID, ML_RULE_TYPE_ID, QUERY_RULE_TYPE_ID, SIGNALS_ID, + THRESHOLD_RULE_TYPE_ID, } from '../../../../common/constants'; interface SortExceptionsReturn { @@ -1013,10 +1015,10 @@ export const getField = (event: SimpleHit, field: string) * Maps legacy rule types to RAC rule type IDs. */ export const ruleTypeMappings = { - eql: SIGNALS_ID, + eql: EQL_RULE_TYPE_ID, machine_learning: ML_RULE_TYPE_ID, query: QUERY_RULE_TYPE_ID, saved_query: SIGNALS_ID, threat_match: INDICATOR_RULE_TYPE_ID, - threshold: SIGNALS_ID, + threshold: THRESHOLD_RULE_TYPE_ID, }; diff --git a/x-pack/plugins/security_solution/server/lib/telemetry/constants.ts b/x-pack/plugins/security_solution/server/lib/telemetry/constants.ts index 1c03e52c67ae7..771e3e059c336 100644 --- a/x-pack/plugins/security_solution/server/lib/telemetry/constants.ts +++ b/x-pack/plugins/security_solution/server/lib/telemetry/constants.ts @@ -11,6 +11,8 @@ export const TELEMETRY_CHANNEL_LISTS = 'security-lists'; export const TELEMETRY_CHANNEL_ENDPOINT_META = 'endpoint-metadata'; +export const LIST_TRUSTED_APPLICATION = 'trusted_application'; + export const LIST_ENDPOINT_EXCEPTION = 'endpoint_exception'; export const LIST_ENDPOINT_EVENT_FILTER = 'endpoint_event_filter'; diff --git a/x-pack/plugins/security_solution/server/lib/telemetry/filters.test.ts b/x-pack/plugins/security_solution/server/lib/telemetry/filters.test.ts new file mode 100644 index 0000000000000..4844a10d99f90 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/telemetry/filters.test.ts @@ -0,0 +1,127 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { copyAllowlistedFields } from './filters'; + +describe('Security Telemetry filters', () => { + describe('allowlistEventFields', () => { + const allowlist = { + a: true, + b: true, + c: { + d: true, + }, + }; + + it('filters top level', () => { + const event = { + a: 'a', + a1: 'a1', + b: 'b', + b1: 'b1', + }; + expect(copyAllowlistedFields(allowlist, event)).toStrictEqual({ + a: 'a', + b: 'b', + }); + }); + + it('filters nested', () => { + const event = { + a: { + a1: 'a1', + }, + a1: 'a1', + b: { + b1: 'b1', + }, + b1: 'b1', + c: { + d: 'd', + e: 'e', + f: 'f', + }, + }; + expect(copyAllowlistedFields(allowlist, event)).toStrictEqual({ + a: { + a1: 'a1', + }, + b: { + b1: 'b1', + }, + c: { + d: 'd', + }, + }); + }); + + it('filters arrays of objects', () => { + const event = { + a: [ + { + a1: 'a1', + }, + ], + b: { + b1: 'b1', + }, + c: [ + { + d: 'd1', + e: 'e1', + f: 'f1', + }, + { + d: 'd2', + e: 'e2', + f: 'f2', + }, + { + d: 'd3', + e: 'e3', + f: 'f3', + }, + ], + }; + expect(copyAllowlistedFields(allowlist, event)).toStrictEqual({ + a: [ + { + a1: 'a1', + }, + ], + b: { + b1: 'b1', + }, + c: [ + { + d: 'd1', + }, + { + d: 'd2', + }, + { + d: 'd3', + }, + ], + }); + }); + + it("doesn't create empty objects", () => { + const event = { + a: 'a', + b: 'b', + c: { + e: 'e', + }, + }; + expect(copyAllowlistedFields(allowlist, event)).toStrictEqual({ + a: 'a', + b: 'b', + }); + }); + }); +}); diff --git a/x-pack/plugins/security_solution/server/lib/telemetry/filters.ts b/x-pack/plugins/security_solution/server/lib/telemetry/filters.ts index 18c3baccf9aa0..61172fac511f7 100644 --- a/x-pack/plugins/security_solution/server/lib/telemetry/filters.ts +++ b/x-pack/plugins/security_solution/server/lib/telemetry/filters.ts @@ -5,6 +5,8 @@ * 2.0. */ +import { TelemetryEvent } from './types'; + export interface AllowlistFields { [key: string]: boolean | AllowlistFields; } @@ -124,3 +126,48 @@ export const allowlistEventFields: AllowlistFields = { }, ...allowlistBaseEventFields, }; + +export const exceptionListEventFields: AllowlistFields = { + created_at: true, + description: true, + effectScope: true, + entries: true, + id: true, + name: true, + os: true, + os_types: true, +}; + +/** + * Filters out information not required for downstream analysis + * + * @param allowlist + * @param event + * @returns + */ +export function copyAllowlistedFields( + allowlist: AllowlistFields, + event: TelemetryEvent +): TelemetryEvent { + return Object.entries(allowlist).reduce((newEvent, [allowKey, allowValue]) => { + const eventValue = event[allowKey]; + if (eventValue !== null && eventValue !== undefined) { + if (allowValue === true) { + return { ...newEvent, [allowKey]: eventValue }; + } else if (typeof allowValue === 'object' && Array.isArray(eventValue)) { + const subValues = eventValue.filter((v) => typeof v === 'object'); + return { + ...newEvent, + [allowKey]: subValues.map((v) => copyAllowlistedFields(allowValue, v as TelemetryEvent)), + }; + } else if (typeof allowValue === 'object' && typeof eventValue === 'object') { + const values = copyAllowlistedFields(allowValue, eventValue as TelemetryEvent); + return { + ...newEvent, + ...(Object.keys(values).length > 0 ? { [allowKey]: values } : {}), + }; + } + } + return newEvent; + }, {}); +} diff --git a/x-pack/plugins/security_solution/server/lib/telemetry/helpers.test.ts b/x-pack/plugins/security_solution/server/lib/telemetry/helpers.test.ts index a4d11b71f2a8e..647219e8c5585 100644 --- a/x-pack/plugins/security_solution/server/lib/telemetry/helpers.test.ts +++ b/x-pack/plugins/security_solution/server/lib/telemetry/helpers.test.ts @@ -7,17 +7,19 @@ import moment from 'moment'; import { createMockPackagePolicy } from './mocks'; -import { TrustedApp } from '../../../common/endpoint/types'; -import { LIST_ENDPOINT_EXCEPTION, LIST_ENDPOINT_EVENT_FILTER } from './constants'; +import { + LIST_ENDPOINT_EXCEPTION, + LIST_ENDPOINT_EVENT_FILTER, + LIST_TRUSTED_APPLICATION, +} from './constants'; import { getPreviousDiagTaskTimestamp, getPreviousEpMetaTaskTimestamp, batchTelemetryRecords, isPackagePolicyList, - templateTrustedApps, - templateEndpointExceptions, + templateExceptionList, } from './helpers'; -import { EndpointExceptionListItem } from './types'; +import { ExceptionListItem } from './types'; describe('test diagnostic telemetry scheduled task timing helper', () => { test('test -5 mins is returned when there is no previous task run', async () => { @@ -133,8 +135,8 @@ describe('test package policy type guard', () => { describe('list telemetry schema', () => { test('trusted apps document is correctly formed', () => { - const data = [{ id: 'test_1' }] as TrustedApp[]; - const templatedItems = templateTrustedApps(data); + const data = [{ id: 'test_1' }] as ExceptionListItem[]; + const templatedItems = templateExceptionList(data, LIST_TRUSTED_APPLICATION); expect(templatedItems[0]?.trusted_application.length).toEqual(1); expect(templatedItems[0]?.endpoint_exception.length).toEqual(0); @@ -142,8 +144,8 @@ describe('list telemetry schema', () => { }); test('trusted apps document is correctly formed with multiple entries', () => { - const data = [{ id: 'test_2' }, { id: 'test_2' }] as TrustedApp[]; - const templatedItems = templateTrustedApps(data); + const data = [{ id: 'test_2' }, { id: 'test_2' }] as ExceptionListItem[]; + const templatedItems = templateExceptionList(data, LIST_TRUSTED_APPLICATION); expect(templatedItems[0]?.trusted_application.length).toEqual(1); expect(templatedItems[1]?.trusted_application.length).toEqual(1); @@ -152,8 +154,8 @@ describe('list telemetry schema', () => { }); test('endpoint exception document is correctly formed', () => { - const data = [{ id: 'test_3' }] as EndpointExceptionListItem[]; - const templatedItems = templateEndpointExceptions(data, LIST_ENDPOINT_EXCEPTION); + const data = [{ id: 'test_3' }] as ExceptionListItem[]; + const templatedItems = templateExceptionList(data, LIST_ENDPOINT_EXCEPTION); expect(templatedItems[0]?.trusted_application.length).toEqual(0); expect(templatedItems[0]?.endpoint_exception.length).toEqual(1); @@ -161,12 +163,8 @@ describe('list telemetry schema', () => { }); test('endpoint exception document is correctly formed with multiple entries', () => { - const data = [ - { id: 'test_4' }, - { id: 'test_4' }, - { id: 'test_4' }, - ] as EndpointExceptionListItem[]; - const templatedItems = templateEndpointExceptions(data, LIST_ENDPOINT_EXCEPTION); + const data = [{ id: 'test_4' }, { id: 'test_4' }, { id: 'test_4' }] as ExceptionListItem[]; + const templatedItems = templateExceptionList(data, LIST_ENDPOINT_EXCEPTION); expect(templatedItems[0]?.trusted_application.length).toEqual(0); expect(templatedItems[0]?.endpoint_exception.length).toEqual(1); @@ -176,8 +174,8 @@ describe('list telemetry schema', () => { }); test('endpoint event filters document is correctly formed', () => { - const data = [{ id: 'test_5' }] as EndpointExceptionListItem[]; - const templatedItems = templateEndpointExceptions(data, LIST_ENDPOINT_EVENT_FILTER); + const data = [{ id: 'test_5' }] as ExceptionListItem[]; + const templatedItems = templateExceptionList(data, LIST_ENDPOINT_EVENT_FILTER); expect(templatedItems[0]?.trusted_application.length).toEqual(0); expect(templatedItems[0]?.endpoint_exception.length).toEqual(0); @@ -185,8 +183,8 @@ describe('list telemetry schema', () => { }); test('endpoint event filters document is correctly formed with multiple entries', () => { - const data = [{ id: 'test_6' }, { id: 'test_6' }] as EndpointExceptionListItem[]; - const templatedItems = templateEndpointExceptions(data, LIST_ENDPOINT_EVENT_FILTER); + const data = [{ id: 'test_6' }, { id: 'test_6' }] as ExceptionListItem[]; + const templatedItems = templateExceptionList(data, LIST_ENDPOINT_EVENT_FILTER); expect(templatedItems[0]?.trusted_application.length).toEqual(0); expect(templatedItems[0]?.endpoint_exception.length).toEqual(0); diff --git a/x-pack/plugins/security_solution/server/lib/telemetry/helpers.ts b/x-pack/plugins/security_solution/server/lib/telemetry/helpers.ts index bb2cc4f42ca90..a9eaef3ce6edc 100644 --- a/x-pack/plugins/security_solution/server/lib/telemetry/helpers.ts +++ b/x-pack/plugins/security_solution/server/lib/telemetry/helpers.ts @@ -7,10 +7,15 @@ import moment from 'moment'; import type { ExceptionListItemSchema } from '@kbn/securitysolution-io-ts-list-types'; -import { TrustedApp } from '../../../common/endpoint/types'; import { PackagePolicy } from '../../../../fleet/common/types/models/package_policy'; -import { EndpointExceptionListItem, ListTemplate } from './types'; -import { LIST_ENDPOINT_EXCEPTION, LIST_ENDPOINT_EVENT_FILTER } from './constants'; +import { copyAllowlistedFields, exceptionListEventFields } from './filters'; +import { ExceptionListItem, ListTemplate, TelemetryEvent } from './types'; +import { + LIST_ENDPOINT_EXCEPTION, + LIST_ENDPOINT_EVENT_FILTER, + LIST_TRUSTED_APPLICATION, +} from './constants'; +import { TrustedApp } from '../../../common/endpoint/types'; /** * Determines the when the last run was in order to execute to. @@ -89,43 +94,41 @@ export function isPackagePolicyList( } /** - * Maps Exception list item to parsable object + * Maps trusted application to shared telemetry object * * @param exceptionListItem * @returns collection of endpoint exceptions */ -export const exceptionListItemToEndpointEntry = (exceptionListItem: ExceptionListItemSchema) => { +export const trustedApplicationToTelemetryEntry = (trustedApplication: TrustedApp) => { + return { + id: trustedApplication.id, + version: trustedApplication.version || '', + name: trustedApplication.name, + description: trustedApplication.description, + created_at: trustedApplication.created_at, + updated_at: trustedApplication.updated_at, + entries: trustedApplication.entries, + os: trustedApplication.os, + } as ExceptionListItem; +}; + +/** + * Maps endpoint lists to shared telemetry object + * + * @param exceptionListItem + * @returns collection of endpoint exceptions + */ +export const exceptionListItemToTelemetryEntry = (exceptionListItem: ExceptionListItemSchema) => { return { id: exceptionListItem.id, version: exceptionListItem._version || '', name: exceptionListItem.name, description: exceptionListItem.description, created_at: exceptionListItem.created_at, - created_by: exceptionListItem.created_by, updated_at: exceptionListItem.updated_at, - updated_by: exceptionListItem.updated_by, entries: exceptionListItem.entries, os_types: exceptionListItem.os_types, - } as EndpointExceptionListItem; -}; - -/** - * Constructs the lists telemetry schema from a collection of Trusted Apps - * - * @param listData - * @returns lists telemetry schema - */ -export const templateTrustedApps = (listData: TrustedApp[]) => { - return listData.map((item) => { - const template: ListTemplate = { - trusted_application: [], - endpoint_exception: [], - endpoint_event_filter: [], - }; - - template.trusted_application.push(item); - return template; - }); + } as ExceptionListItem; }; /** @@ -135,10 +138,7 @@ export const templateTrustedApps = (listData: TrustedApp[]) => { * @param listType * @returns lists telemetry schema */ -export const templateEndpointExceptions = ( - listData: EndpointExceptionListItem[], - listType: string -) => { +export const templateExceptionList = (listData: ExceptionListItem[], listType: string) => { return listData.map((item) => { const template: ListTemplate = { trusted_application: [], @@ -146,13 +146,24 @@ export const templateEndpointExceptions = ( endpoint_event_filter: [], }; + // cast exception list type to a TelemetryEvent for allowlist filtering + const filteredListItem = copyAllowlistedFields( + exceptionListEventFields, + item as unknown as TelemetryEvent + ); + + if (listType === LIST_TRUSTED_APPLICATION) { + template.trusted_application.push(filteredListItem); + return template; + } + if (listType === LIST_ENDPOINT_EXCEPTION) { - template.endpoint_exception.push(item); + template.endpoint_exception.push(filteredListItem); return template; } if (listType === LIST_ENDPOINT_EVENT_FILTER) { - template.endpoint_event_filter.push(item); + template.endpoint_event_filter.push(filteredListItem); return template; } diff --git a/x-pack/plugins/security_solution/server/lib/telemetry/receiver.ts b/x-pack/plugins/security_solution/server/lib/telemetry/receiver.ts index 06dffdddc49c8..8b715b8e8d585 100644 --- a/x-pack/plugins/security_solution/server/lib/telemetry/receiver.ts +++ b/x-pack/plugins/security_solution/server/lib/telemetry/receiver.ts @@ -17,7 +17,7 @@ import { AgentService, AgentPolicyServiceInterface } from '../../../../fleet/ser import { ExceptionListClient } from '../../../../lists/server'; import { EndpointAppContextService } from '../../endpoint/endpoint_app_context_services'; import { TELEMETRY_MAX_BUFFER_SIZE } from './constants'; -import { exceptionListItemToEndpointEntry } from './helpers'; +import { exceptionListItemToTelemetryEntry, trustedApplicationToTelemetryEntry } from './helpers'; import { TelemetryEvent, ESLicense, ESClusterInfo, GetEndpointListResponse } from './types'; export class TelemetryReceiver { @@ -202,7 +202,16 @@ export class TelemetryReceiver { throw Error('exception list client is unavailable: cannot retrieve trusted applications'); } - return getTrustedAppsList(this.exceptionListClient, { page: 1, per_page: 10_000 }); + const results = await getTrustedAppsList(this.exceptionListClient, { + page: 1, + per_page: 10_000, + }); + return { + data: results?.data.map(trustedApplicationToTelemetryEntry), + total: results?.total ?? 0, + page: results?.page ?? 1, + per_page: results?.per_page ?? this.max_records, + }; } public async fetchEndpointList(listId: string): Promise { @@ -224,7 +233,7 @@ export class TelemetryReceiver { }); return { - data: results?.data.map(exceptionListItemToEndpointEntry) ?? [], + data: results?.data.map(exceptionListItemToTelemetryEntry) ?? [], total: results?.total ?? 0, page: results?.page ?? 1, per_page: results?.per_page ?? this.max_records, diff --git a/x-pack/plugins/security_solution/server/lib/telemetry/sender.test.ts b/x-pack/plugins/security_solution/server/lib/telemetry/sender.test.ts index d04d0ab49afe9..21e6b2cf6d9c4 100644 --- a/x-pack/plugins/security_solution/server/lib/telemetry/sender.test.ts +++ b/x-pack/plugins/security_solution/server/lib/telemetry/sender.test.ts @@ -6,7 +6,7 @@ */ /* eslint-disable dot-notation */ -import { TelemetryEventsSender, copyAllowlistedFields } from './sender'; +import { TelemetryEventsSender } from './sender'; import { loggingSystemMock } from 'src/core/server/mocks'; import { usageCountersServiceMock } from 'src/plugins/usage_collection/server/usage_counters/usage_counters_service.mock'; import { URL } from 'url'; @@ -220,123 +220,6 @@ describe('TelemetryEventsSender', () => { }); }); -describe('allowlistEventFields', () => { - const allowlist = { - a: true, - b: true, - c: { - d: true, - }, - }; - - it('filters top level', () => { - const event = { - a: 'a', - a1: 'a1', - b: 'b', - b1: 'b1', - }; - expect(copyAllowlistedFields(allowlist, event)).toStrictEqual({ - a: 'a', - b: 'b', - }); - }); - - it('filters nested', () => { - const event = { - a: { - a1: 'a1', - }, - a1: 'a1', - b: { - b1: 'b1', - }, - b1: 'b1', - c: { - d: 'd', - e: 'e', - f: 'f', - }, - }; - expect(copyAllowlistedFields(allowlist, event)).toStrictEqual({ - a: { - a1: 'a1', - }, - b: { - b1: 'b1', - }, - c: { - d: 'd', - }, - }); - }); - - it('filters arrays of objects', () => { - const event = { - a: [ - { - a1: 'a1', - }, - ], - b: { - b1: 'b1', - }, - c: [ - { - d: 'd1', - e: 'e1', - f: 'f1', - }, - { - d: 'd2', - e: 'e2', - f: 'f2', - }, - { - d: 'd3', - e: 'e3', - f: 'f3', - }, - ], - }; - expect(copyAllowlistedFields(allowlist, event)).toStrictEqual({ - a: [ - { - a1: 'a1', - }, - ], - b: { - b1: 'b1', - }, - c: [ - { - d: 'd1', - }, - { - d: 'd2', - }, - { - d: 'd3', - }, - ], - }); - }); - - it("doesn't create empty objects", () => { - const event = { - a: 'a', - b: 'b', - c: { - e: 'e', - }, - }; - expect(copyAllowlistedFields(allowlist, event)).toStrictEqual({ - a: 'a', - b: 'b', - }); - }); -}); - describe('getV3UrlFromV2', () => { let logger: ReturnType; diff --git a/x-pack/plugins/security_solution/server/lib/telemetry/sender.ts b/x-pack/plugins/security_solution/server/lib/telemetry/sender.ts index 2e615a2681174..0037aaa28fee3 100644 --- a/x-pack/plugins/security_solution/server/lib/telemetry/sender.ts +++ b/x-pack/plugins/security_solution/server/lib/telemetry/sender.ts @@ -17,7 +17,7 @@ import { TaskManagerStartContract, } from '../../../../task_manager/server'; import { TelemetryReceiver } from './receiver'; -import { AllowlistFields, allowlistEventFields } from './filters'; +import { allowlistEventFields, copyAllowlistedFields } from './filters'; import { DiagnosticTask, EndpointTask, ExceptionListsTask } from './tasks'; import { createUsageCounterLabel } from './helpers'; import { TelemetryEvent } from './types'; @@ -194,8 +194,8 @@ export class TelemetryEventsSender { /** * This function sends events to the elastic telemetry channel. Caution is required - * because it does no allowlist filtering. The caller is responsible for making sure - * that there is no sensitive material or PII in the records that are sent upstream. + * because it does no allowlist filtering at send time. The function call site is + * responsible for ensuring sure no sensitive material is in telemetry events. * * @param channel the elastic telemetry channel * @param toSend telemetry events @@ -294,30 +294,3 @@ export class TelemetryEventsSender { } } } - -export function copyAllowlistedFields( - allowlist: AllowlistFields, - event: TelemetryEvent -): TelemetryEvent { - return Object.entries(allowlist).reduce((newEvent, [allowKey, allowValue]) => { - const eventValue = event[allowKey]; - if (eventValue !== null && eventValue !== undefined) { - if (allowValue === true) { - return { ...newEvent, [allowKey]: eventValue }; - } else if (typeof allowValue === 'object' && Array.isArray(eventValue)) { - const subValues = eventValue.filter((v) => typeof v === 'object'); - return { - ...newEvent, - [allowKey]: subValues.map((v) => copyAllowlistedFields(allowValue, v as TelemetryEvent)), - }; - } else if (typeof allowValue === 'object' && typeof eventValue === 'object') { - const values = copyAllowlistedFields(allowValue, eventValue as TelemetryEvent); - return { - ...newEvent, - ...(Object.keys(values).length > 0 ? { [allowKey]: values } : {}), - }; - } - } - return newEvent; - }, {}); -} diff --git a/x-pack/plugins/security_solution/server/lib/telemetry/tasks/security_lists.ts b/x-pack/plugins/security_solution/server/lib/telemetry/tasks/security_lists.ts index b54858e1f5f42..fe2039419b02d 100644 --- a/x-pack/plugins/security_solution/server/lib/telemetry/tasks/security_lists.ts +++ b/x-pack/plugins/security_solution/server/lib/telemetry/tasks/security_lists.ts @@ -19,9 +19,10 @@ import { import { LIST_ENDPOINT_EXCEPTION, LIST_ENDPOINT_EVENT_FILTER, + LIST_TRUSTED_APPLICATION, TELEMETRY_CHANNEL_LISTS, } from '../constants'; -import { batchTelemetryRecords, templateEndpointExceptions, templateTrustedApps } from '../helpers'; +import { batchTelemetryRecords, templateExceptionList } from '../helpers'; import { TelemetryEventsSender } from '../sender'; import { TelemetryReceiver } from '../receiver'; @@ -110,7 +111,7 @@ export class TelemetryExceptionListsTask { // Lists Telemetry: Trusted Applications const trustedApps = await this.receiver.fetchTrustedApplications(); - const trustedAppsJson = templateTrustedApps(trustedApps.data); + const trustedAppsJson = templateExceptionList(trustedApps.data, LIST_TRUSTED_APPLICATION); this.logger.debug(`Trusted Apps: ${trustedAppsJson}`); batchTelemetryRecords(trustedAppsJson, MAX_TELEMETRY_BATCH).forEach((batch) => @@ -120,7 +121,7 @@ export class TelemetryExceptionListsTask { // Lists Telemetry: Endpoint Exceptions const epExceptions = await this.receiver.fetchEndpointList(ENDPOINT_LIST_ID); - const epExceptionsJson = templateEndpointExceptions(epExceptions.data, LIST_ENDPOINT_EXCEPTION); + const epExceptionsJson = templateExceptionList(epExceptions.data, LIST_ENDPOINT_EXCEPTION); this.logger.debug(`EP Exceptions: ${epExceptionsJson}`); batchTelemetryRecords(epExceptionsJson, MAX_TELEMETRY_BATCH).forEach((batch) => @@ -130,7 +131,7 @@ export class TelemetryExceptionListsTask { // Lists Telemetry: Endpoint Event Filters const epFilters = await this.receiver.fetchEndpointList(ENDPOINT_EVENT_FILTERS_LIST_ID); - const epFiltersJson = templateEndpointExceptions(epFilters.data, LIST_ENDPOINT_EVENT_FILTER); + const epFiltersJson = templateExceptionList(epFilters.data, LIST_ENDPOINT_EVENT_FILTER); this.logger.debug(`EP Event Filters: ${epFiltersJson}`); batchTelemetryRecords(epFiltersJson, MAX_TELEMETRY_BATCH).forEach((batch) => diff --git a/x-pack/plugins/security_solution/server/lib/telemetry/types.ts b/x-pack/plugins/security_solution/server/lib/telemetry/types.ts index b78017314a982..abcad26ed000c 100644 --- a/x-pack/plugins/security_solution/server/lib/telemetry/types.ts +++ b/x-pack/plugins/security_solution/server/lib/telemetry/types.ts @@ -6,7 +6,6 @@ */ import { schema, TypeOf } from '@kbn/config-schema'; -import { TrustedApp } from '../../../common/endpoint/types'; type BaseSearchTypes = string | number | boolean | object; export type SearchTypes = BaseSearchTypes | BaseSearchTypes[] | undefined; @@ -211,26 +210,25 @@ export interface GetEndpointListResponse { per_page: number; page: number; total: number; - data: EndpointExceptionListItem[]; + data: ExceptionListItem[]; } // Telemetry List types -export interface EndpointExceptionListItem { +export interface ExceptionListItem { id: string; version: string; name: string; description: string; created_at: string; - created_by: string; updated_at: string; - updated_by: string; entries: object; + os: string; os_types: object; } export interface ListTemplate { - trusted_application: TrustedApp[]; - endpoint_exception: EndpointExceptionListItem[]; - endpoint_event_filter: EndpointExceptionListItem[]; + trusted_application: TelemetryEvent[]; + endpoint_exception: TelemetryEvent[]; + endpoint_event_filter: TelemetryEvent[]; } diff --git a/x-pack/plugins/security_solution/server/lib/timeline/saved_object/pinned_events/field_migrator.ts b/x-pack/plugins/security_solution/server/lib/timeline/saved_object/pinned_events/field_migrator.ts new file mode 100644 index 0000000000000..5939676c2a924 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/timeline/saved_object/pinned_events/field_migrator.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. + */ + +import { TIMELINE_ID_REF_NAME } from '../../constants'; +import { timelineSavedObjectType } from '../../saved_object_mappings'; +import { FieldMigrator } from '../../utils/migrator'; + +/** + * A migrator to handle moving specific fields that reference the timeline saved object to the references field within a note saved + * object. + */ +export const pinnedEventFieldsMigrator = new FieldMigrator([ + { path: 'timelineId', type: timelineSavedObjectType, name: TIMELINE_ID_REF_NAME }, +]); diff --git a/x-pack/plugins/security_solution/server/lib/timeline/saved_object/pinned_events/index.ts b/x-pack/plugins/security_solution/server/lib/timeline/saved_object/pinned_events/index.ts index b3d262b13cbf3..260531e1106bf 100644 --- a/x-pack/plugins/security_solution/server/lib/timeline/saved_object/pinned_events/index.ts +++ b/x-pack/plugins/security_solution/server/lib/timeline/saved_object/pinned_events/index.ts @@ -19,13 +19,13 @@ import { PinnedEventSavedObjectRuntimeType, SavedPinnedEvent, PinnedEvent as PinnedEventResponse, + PinnedEventWithoutExternalRefs, } from '../../../../../common/types/timeline/pinned_event'; -import { PageInfoNote, SortNote } from '../../../../../common/types/timeline/note'; import { FrameworkRequest } from '../../../framework'; -import { pickSavedTimeline } from '../../saved_object/timelines'; -import { convertSavedObjectToSavedTimeline } from '../timelines'; +import { createTimeline } from '../../saved_object/timelines'; import { pinnedEventSavedObjectType } from '../../saved_object_mappings/pinned_events'; +import { pinnedEventFieldsMigrator } from './field_migrator'; import { timelineSavedObjectType } from '../../saved_object_mappings'; export interface PinnedEvent { @@ -46,13 +46,6 @@ export interface PinnedEvent { timelineId: string ) => Promise; - getAllPinnedEvents: ( - request: FrameworkRequest, - pageInfo: PageInfoNote | null, - search: string | null, - sort: SortNote | null - ) => Promise; - persistPinnedEventOnTimeline: ( request: FrameworkRequest, pinnedEventId: string | null, // pinned event saved object id @@ -117,26 +110,7 @@ export const getAllPinnedEventsByTimelineId = async ( ): Promise => { const options: SavedObjectsFindOptions = { type: pinnedEventSavedObjectType, - search: timelineId, - searchFields: ['timelineId'], - }; - return getAllSavedPinnedEvents(request, options); -}; - -export const getAllPinnedEvents = async ( - request: FrameworkRequest, - pageInfo: PageInfoNote | null, - search: string | null, - sort: SortNote | null -): Promise => { - const options: SavedObjectsFindOptions = { - type: pinnedEventSavedObjectType, - perPage: pageInfo != null ? pageInfo.pageSize : undefined, - page: pageInfo != null ? pageInfo.pageIndex : undefined, - search: search != null ? search : undefined, - searchFields: ['timelineId', 'eventId'], - sortField: sort != null ? sort.sortField : undefined, - sortOrder: sort != null ? sort.sortOrder : undefined, + hasReference: { type: timelineSavedObjectType, id: timelineId }, }; return getAllSavedPinnedEvents(request, options); }; @@ -147,51 +121,35 @@ export const persistPinnedEventOnTimeline = async ( eventId: string, timelineId: string | null ): Promise => { - const savedObjectsClient = request.context.core.savedObjects.client; - try { - if (pinnedEventId == null) { - const timelineVersionSavedObject = - timelineId == null - ? await (async () => { - const timelineResult = convertSavedObjectToSavedTimeline( - await savedObjectsClient.create( - timelineSavedObjectType, - pickSavedTimeline(null, {}, request.user || null) - ) - ); - timelineId = timelineResult.savedObjectId; // eslint-disable-line no-param-reassign - return timelineResult.version; - })() - : null; - - if (timelineId != null) { - const allPinnedEventId = await getAllPinnedEventsByTimelineId(request, timelineId); - const isPinnedAlreadyExisting = allPinnedEventId.filter( - (pinnedEvent) => pinnedEvent.eventId === eventId - ); - - if (isPinnedAlreadyExisting.length === 0) { - const savedPinnedEvent: SavedPinnedEvent = { - eventId, - timelineId, - }; - // create Pinned Event on Timeline - return convertSavedObjectToSavedPinnedEvent( - await savedObjectsClient.create( - pinnedEventSavedObjectType, - pickSavedPinnedEvent(pinnedEventId, savedPinnedEvent, request.user || null) - ), - timelineVersionSavedObject != null ? timelineVersionSavedObject : undefined - ); - } - return isPinnedAlreadyExisting[0]; - } - throw new Error('You can NOT pinned event without a timelineID'); + if (pinnedEventId != null) { + // Delete Pinned Event on Timeline + await deletePinnedEventOnTimeline(request, [pinnedEventId]); + return null; } - // Delete Pinned Event on Timeline - await deletePinnedEventOnTimeline(request, [pinnedEventId]); - return null; + + const { timelineId: validatedTimelineId, timelineVersion } = await getValidTimelineIdAndVersion( + request, + timelineId + ); + + const pinnedEvents = await getPinnedEventsInTimelineWithEventId( + request, + validatedTimelineId, + eventId + ); + + // we already had this event pinned so let's just return the one we already had + if (pinnedEvents.length > 0) { + return pinnedEvents[0]; + } + + return await createPinnedEvent({ + request, + eventId, + timelineId: validatedTimelineId, + timelineVersion, + }); } catch (err) { if (getOr(null, 'output.statusCode', err) === 404) { /* @@ -215,11 +173,91 @@ export const persistPinnedEventOnTimeline = async ( } }; +const getValidTimelineIdAndVersion = async ( + request: FrameworkRequest, + timelineId: string | null +): Promise<{ timelineId: string; timelineVersion?: string }> => { + if (timelineId != null) { + return { + timelineId, + }; + } + + const savedObjectsClient = request.context.core.savedObjects.client; + + // create timeline because it didn't exist + const { timeline: timelineResult } = await createTimeline({ + timelineId: null, + timeline: {}, + savedObjectsClient, + userInfo: request.user, + }); + + return { + timelineId: timelineResult.savedObjectId, + timelineVersion: timelineResult.version, + }; +}; + +const getPinnedEventsInTimelineWithEventId = async ( + request: FrameworkRequest, + timelineId: string, + eventId: string +): Promise => { + const allPinnedEventId = await getAllPinnedEventsByTimelineId(request, timelineId); + const pinnedEvents = allPinnedEventId.filter((pinnedEvent) => pinnedEvent.eventId === eventId); + + return pinnedEvents; +}; + +const createPinnedEvent = async ({ + request, + eventId, + timelineId, + timelineVersion, +}: { + request: FrameworkRequest; + eventId: string; + timelineId: string; + timelineVersion?: string; +}) => { + const savedObjectsClient = request.context.core.savedObjects.client; + + const savedPinnedEvent: SavedPinnedEvent = { + eventId, + timelineId, + }; + + const pinnedEventWithCreator = pickSavedPinnedEvent(null, savedPinnedEvent, request.user); + + const { transformedFields: migratedAttributes, references } = + pinnedEventFieldsMigrator.extractFieldsToReferences({ + data: pinnedEventWithCreator, + }); + + const createdPinnedEvent = await savedObjectsClient.create( + pinnedEventSavedObjectType, + migratedAttributes, + { references } + ); + + const repopulatedSavedObject = + pinnedEventFieldsMigrator.populateFieldsFromReferences(createdPinnedEvent); + + // create Pinned Event on Timeline + return convertSavedObjectToSavedPinnedEvent(repopulatedSavedObject, timelineVersion); +}; + const getSavedPinnedEvent = async (request: FrameworkRequest, pinnedEventId: string) => { const savedObjectsClient = request.context.core.savedObjects.client; - const savedObject = await savedObjectsClient.get(pinnedEventSavedObjectType, pinnedEventId); + const savedObject = await savedObjectsClient.get( + pinnedEventSavedObjectType, + pinnedEventId + ); + + const populatedPinnedEvent = pinnedEventFieldsMigrator.populateFieldsFromReferences(savedObject); - return convertSavedObjectToSavedPinnedEvent(savedObject); + return convertSavedObjectToSavedPinnedEvent(populatedPinnedEvent); }; const getAllSavedPinnedEvents = async ( @@ -227,11 +265,14 @@ const getAllSavedPinnedEvents = async ( options: SavedObjectsFindOptions ) => { const savedObjectsClient = request.context.core.savedObjects.client; - const savedObjects = await savedObjectsClient.find(options); + const savedObjects = await savedObjectsClient.find(options); - return savedObjects.saved_objects.map((savedObject) => - convertSavedObjectToSavedPinnedEvent(savedObject) - ); + return savedObjects.saved_objects.map((savedObject) => { + const populatedPinnedEvent = + pinnedEventFieldsMigrator.populateFieldsFromReferences(savedObject); + + return convertSavedObjectToSavedPinnedEvent(populatedPinnedEvent); + }); }; export const savePinnedEvents = ( @@ -284,11 +325,10 @@ export const pickSavedPinnedEvent = ( if (pinnedEventId == null) { savedPinnedEvent.created = dateNow; savedPinnedEvent.createdBy = userInfo?.username ?? UNAUTHENTICATED_USER; - savedPinnedEvent.updated = dateNow; - savedPinnedEvent.updatedBy = userInfo?.username ?? UNAUTHENTICATED_USER; - } else if (pinnedEventId != null) { - savedPinnedEvent.updated = dateNow; - savedPinnedEvent.updatedBy = userInfo?.username ?? UNAUTHENTICATED_USER; } + + savedPinnedEvent.updated = dateNow; + savedPinnedEvent.updatedBy = userInfo?.username ?? UNAUTHENTICATED_USER; + return savedPinnedEvent; }; diff --git a/x-pack/plugins/security_solution/server/lib/timeline/saved_object_mappings/migrations/notes.test.ts b/x-pack/plugins/security_solution/server/lib/timeline/saved_object_mappings/migrations/notes.test.ts deleted file mode 100644 index b9649896c25a6..0000000000000 --- a/x-pack/plugins/security_solution/server/lib/timeline/saved_object_mappings/migrations/notes.test.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 { TIMELINE_ID_REF_NAME } from '../../constants'; -import { migrateNoteTimelineIdToReferences, TimelineId } from './notes'; - -describe('notes migrations', () => { - describe('7.16.0 timelineId', () => { - it('removes the timelineId from the migrated document', () => { - const migratedDoc = migrateNoteTimelineIdToReferences({ - id: '1', - type: 'awesome', - attributes: { timelineId: '123' }, - }); - - expect(migratedDoc.attributes).toEqual({}); - expect(migratedDoc.references).toEqual([ - // importing the timeline saved object type from the timeline saved object causes a circular import and causes the jest tests to fail - { id: '123', name: TIMELINE_ID_REF_NAME, type: 'siem-ui-timeline' }, - ]); - }); - - it('preserves additional fields when migrating timeline id', () => { - const migratedDoc = migrateNoteTimelineIdToReferences({ - id: '1', - type: 'awesome', - attributes: { awesome: 'yes', timelineId: '123' } as unknown as TimelineId, - }); - - expect(migratedDoc.attributes).toEqual({ awesome: 'yes' }); - expect(migratedDoc.references).toEqual([ - { id: '123', name: TIMELINE_ID_REF_NAME, type: 'siem-ui-timeline' }, - ]); - }); - }); -}); diff --git a/x-pack/plugins/security_solution/server/lib/timeline/saved_object_mappings/migrations/notes.ts b/x-pack/plugins/security_solution/server/lib/timeline/saved_object_mappings/migrations/notes.ts index a8d753e916afb..76773b7fcd518 100644 --- a/x-pack/plugins/security_solution/server/lib/timeline/saved_object_mappings/migrations/notes.ts +++ b/x-pack/plugins/security_solution/server/lib/timeline/saved_object_mappings/migrations/notes.ts @@ -5,39 +5,9 @@ * 2.0. */ -import { - SavedObjectMigrationMap, - SavedObjectSanitizedDoc, - SavedObjectUnsanitizedDoc, -} from 'kibana/server'; -import { timelineSavedObjectType } from '..'; -import { TIMELINE_ID_REF_NAME } from '../../constants'; -import { createMigratedDoc, createReference } from './utils'; - -export interface TimelineId { - timelineId?: string | null; -} - -export const migrateNoteTimelineIdToReferences = ( - doc: SavedObjectUnsanitizedDoc -): SavedObjectSanitizedDoc => { - const { timelineId, ...restAttributes } = doc.attributes; - - const { references: docReferences = [] } = doc; - const timelineIdReferences = createReference( - timelineId, - TIMELINE_ID_REF_NAME, - timelineSavedObjectType - ); - - return createMigratedDoc({ - doc, - attributes: restAttributes, - docReferences, - migratedReferences: timelineIdReferences, - }); -}; +import { SavedObjectMigrationMap } from 'kibana/server'; +import { migrateTimelineIdToReferences } from './utils'; export const notesMigrations: SavedObjectMigrationMap = { - '7.16.0': migrateNoteTimelineIdToReferences, + '7.16.0': migrateTimelineIdToReferences, }; diff --git a/x-pack/plugins/security_solution/server/lib/timeline/saved_object_mappings/migrations/pinned_events.ts b/x-pack/plugins/security_solution/server/lib/timeline/saved_object_mappings/migrations/pinned_events.ts new file mode 100644 index 0000000000000..4d21190d9381c --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/timeline/saved_object_mappings/migrations/pinned_events.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 { SavedObjectMigrationMap } from 'kibana/server'; +import { migrateTimelineIdToReferences } from './utils'; + +export const pinnedEventsMigrations: SavedObjectMigrationMap = { + '7.16.0': migrateTimelineIdToReferences, +}; diff --git a/x-pack/plugins/security_solution/server/lib/timeline/saved_object_mappings/migrations/types.ts b/x-pack/plugins/security_solution/server/lib/timeline/saved_object_mappings/migrations/types.ts new file mode 100644 index 0000000000000..7c62310a99aa6 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/timeline/saved_object_mappings/migrations/types.ts @@ -0,0 +1,10 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export interface TimelineId { + timelineId?: string | null; +} diff --git a/x-pack/plugins/security_solution/server/lib/timeline/saved_object_mappings/migrations/utils.test.ts b/x-pack/plugins/security_solution/server/lib/timeline/saved_object_mappings/migrations/utils.test.ts index 02e3fca996d5d..329f09e85f3a7 100644 --- a/x-pack/plugins/security_solution/server/lib/timeline/saved_object_mappings/migrations/utils.test.ts +++ b/x-pack/plugins/security_solution/server/lib/timeline/saved_object_mappings/migrations/utils.test.ts @@ -5,9 +5,39 @@ * 2.0. */ -import { createMigratedDoc, createReference } from './utils'; +import { timelineSavedObjectType } from '../timelines'; +import { TIMELINE_ID_REF_NAME } from '../../constants'; +import { TimelineId } from './types'; +import { createMigratedDoc, createReference, migrateTimelineIdToReferences } from './utils'; describe('migration utils', () => { + describe('migrateTimelineIdToReferences', () => { + it('removes the timelineId from the migrated document', () => { + const migratedDoc = migrateTimelineIdToReferences({ + id: '1', + type: 'awesome', + attributes: { timelineId: '123' }, + }); + + expect(migratedDoc.attributes).toEqual({}); + expect(migratedDoc.references).toEqual([ + { id: '123', name: TIMELINE_ID_REF_NAME, type: timelineSavedObjectType }, + ]); + }); + + it('preserves additional fields when migrating timeline id', () => { + const migratedDoc = migrateTimelineIdToReferences({ + id: '1', + type: 'awesome', + attributes: { awesome: 'yes', timelineId: '123' } as unknown as TimelineId, + }); + + expect(migratedDoc.attributes).toEqual({ awesome: 'yes' }); + expect(migratedDoc.references).toEqual([ + { id: '123', name: TIMELINE_ID_REF_NAME, type: timelineSavedObjectType }, + ]); + }); + }); describe('createReference', () => { it('returns an array with a reference when the id is defined', () => { expect(createReference('awesome', 'name', 'type')).toEqual([ diff --git a/x-pack/plugins/security_solution/server/lib/timeline/saved_object_mappings/migrations/utils.ts b/x-pack/plugins/security_solution/server/lib/timeline/saved_object_mappings/migrations/utils.ts index ff9b56e6ae2c9..7bd7bc148c263 100644 --- a/x-pack/plugins/security_solution/server/lib/timeline/saved_object_mappings/migrations/utils.ts +++ b/x-pack/plugins/security_solution/server/lib/timeline/saved_object_mappings/migrations/utils.ts @@ -10,6 +10,9 @@ import { SavedObjectSanitizedDoc, SavedObjectUnsanitizedDoc, } from 'kibana/server'; +import { timelineSavedObjectType } from '../timelines'; +import { TIMELINE_ID_REF_NAME } from '../../constants'; +import { TimelineId } from './types'; export function createReference( id: string | null | undefined, @@ -19,6 +22,26 @@ export function createReference( return id != null ? [{ id, name, type }] : []; } +export const migrateTimelineIdToReferences = ( + doc: SavedObjectUnsanitizedDoc +): SavedObjectSanitizedDoc => { + const { timelineId, ...restAttributes } = doc.attributes; + + const { references: docReferences = [] } = doc; + const timelineIdReferences = createReference( + timelineId, + TIMELINE_ID_REF_NAME, + timelineSavedObjectType + ); + + return createMigratedDoc({ + doc, + attributes: restAttributes, + docReferences, + migratedReferences: timelineIdReferences, + }); +}; + export const createMigratedDoc = ({ doc, attributes, diff --git a/x-pack/plugins/security_solution/server/lib/timeline/saved_object_mappings/notes.ts b/x-pack/plugins/security_solution/server/lib/timeline/saved_object_mappings/notes.ts index 387f78e5059f4..eda2478e7809d 100644 --- a/x-pack/plugins/security_solution/server/lib/timeline/saved_object_mappings/notes.ts +++ b/x-pack/plugins/security_solution/server/lib/timeline/saved_object_mappings/notes.ts @@ -6,7 +6,7 @@ */ import { SavedObjectsType } from '../../../../../../../src/core/server'; -import { notesMigrations } from './migrations'; +import { notesMigrations } from './migrations/notes'; export const noteSavedObjectType = 'siem-ui-timeline-note'; diff --git a/x-pack/plugins/security_solution/server/lib/timeline/saved_object_mappings/pinned_events.ts b/x-pack/plugins/security_solution/server/lib/timeline/saved_object_mappings/pinned_events.ts index fbbffe35a58c0..2f8e72ad763f9 100644 --- a/x-pack/plugins/security_solution/server/lib/timeline/saved_object_mappings/pinned_events.ts +++ b/x-pack/plugins/security_solution/server/lib/timeline/saved_object_mappings/pinned_events.ts @@ -6,14 +6,12 @@ */ import { SavedObjectsType } from '../../../../../../../src/core/server'; +import { pinnedEventsMigrations } from './migrations/pinned_events'; export const pinnedEventSavedObjectType = 'siem-ui-timeline-pinned-event'; export const pinnedEventSavedObjectMappings: SavedObjectsType['mappings'] = { properties: { - timelineId: { - type: 'keyword', - }, eventId: { type: 'keyword', }, @@ -37,4 +35,5 @@ export const pinnedEventType: SavedObjectsType = { hidden: false, namespaceType: 'single', mappings: pinnedEventSavedObjectMappings, + migrations: pinnedEventsMigrations, }; diff --git a/x-pack/plugins/security_solution/server/lib/timeline/saved_object_mappings/timelines.ts b/x-pack/plugins/security_solution/server/lib/timeline/saved_object_mappings/timelines.ts index 8300f72a162ed..e1e3a454087f9 100644 --- a/x-pack/plugins/security_solution/server/lib/timeline/saved_object_mappings/timelines.ts +++ b/x-pack/plugins/security_solution/server/lib/timeline/saved_object_mappings/timelines.ts @@ -6,7 +6,7 @@ */ import { SavedObjectsType } from '../../../../../../../src/core/server'; -import { timelinesMigrations } from './migrations'; +import { timelinesMigrations } from './migrations/timelines'; export const timelineSavedObjectType = 'siem-ui-timeline'; diff --git a/x-pack/plugins/security_solution/server/plugin.ts b/x-pack/plugins/security_solution/server/plugin.ts index f07137a118ab6..14da8ca650960 100644 --- a/x-pack/plugins/security_solution/server/plugin.ts +++ b/x-pack/plugins/security_solution/server/plugin.ts @@ -53,6 +53,7 @@ import { createIndicatorMatchAlertType, createMlAlertType, createQueryAlertType, + createThresholdAlertType, } from './lib/detection_engine/rule_types'; import { initRoutes } from './routes'; import { isAlertExecutor } from './lib/detection_engine/signals/types'; @@ -264,9 +265,10 @@ export class Plugin implements IPlugin) => ({ }, ja3: { terms: { - field: 'tls.server.ja3s', + field: 'tls.client.ja3', }, }, }, diff --git a/x-pack/plugins/snapshot_restore/server/index.ts b/x-pack/plugins/snapshot_restore/server/index.ts index d85d03923df1f..e10bffd6073d2 100644 --- a/x-pack/plugins/snapshot_restore/server/index.ts +++ b/x-pack/plugins/snapshot_restore/server/index.ts @@ -12,6 +12,7 @@ import { configSchema, SnapshotRestoreConfig } from './config'; export const plugin = (ctx: PluginInitializerContext) => new SnapshotRestoreServerPlugin(ctx); export const config: PluginConfigDescriptor = { + deprecations: ({ deprecate }) => [deprecate('enabled', '8.0.0')], schema: configSchema, exposeToBrowser: { slm_ui: true, diff --git a/x-pack/plugins/telemetry_collection_xpack/schema/xpack_plugins.json b/x-pack/plugins/telemetry_collection_xpack/schema/xpack_plugins.json index 9b7e4bd0c9f6b..2dd7b96cfef95 100644 --- a/x-pack/plugins/telemetry_collection_xpack/schema/xpack_plugins.json +++ b/x-pack/plugins/telemetry_collection_xpack/schema/xpack_plugins.json @@ -4086,6 +4086,31 @@ "deprecated": { "type": "long" }, + "sizes": { + "properties": { + "1.0": { + "type": "long" + }, + "5.0": { + "type": "long" + }, + "25.0": { + "type": "long" + }, + "50.0": { + "type": "long" + }, + "75.0": { + "type": "long" + }, + "95.0": { + "type": "long" + }, + "99.0": { + "type": "long" + } + } + }, "app": { "properties": { "search": { @@ -4128,6 +4153,31 @@ "deprecated": { "type": "long" }, + "sizes": { + "properties": { + "1.0": { + "type": "long" + }, + "5.0": { + "type": "long" + }, + "25.0": { + "type": "long" + }, + "50.0": { + "type": "long" + }, + "75.0": { + "type": "long" + }, + "95.0": { + "type": "long" + }, + "99.0": { + "type": "long" + } + } + }, "app": { "properties": { "search": { @@ -4170,6 +4220,31 @@ "deprecated": { "type": "long" }, + "sizes": { + "properties": { + "1.0": { + "type": "long" + }, + "5.0": { + "type": "long" + }, + "25.0": { + "type": "long" + }, + "50.0": { + "type": "long" + }, + "75.0": { + "type": "long" + }, + "95.0": { + "type": "long" + }, + "99.0": { + "type": "long" + } + } + }, "app": { "properties": { "search": { @@ -4212,6 +4287,31 @@ "deprecated": { "type": "long" }, + "sizes": { + "properties": { + "1.0": { + "type": "long" + }, + "5.0": { + "type": "long" + }, + "25.0": { + "type": "long" + }, + "50.0": { + "type": "long" + }, + "75.0": { + "type": "long" + }, + "95.0": { + "type": "long" + }, + "99.0": { + "type": "long" + } + } + }, "app": { "properties": { "search": { @@ -4254,6 +4354,31 @@ "deprecated": { "type": "long" }, + "sizes": { + "properties": { + "1.0": { + "type": "long" + }, + "5.0": { + "type": "long" + }, + "25.0": { + "type": "long" + }, + "50.0": { + "type": "long" + }, + "75.0": { + "type": "long" + }, + "95.0": { + "type": "long" + }, + "99.0": { + "type": "long" + } + } + }, "app": { "properties": { "search": { @@ -4296,6 +4421,31 @@ "deprecated": { "type": "long" }, + "sizes": { + "properties": { + "1.0": { + "type": "long" + }, + "5.0": { + "type": "long" + }, + "25.0": { + "type": "long" + }, + "50.0": { + "type": "long" + }, + "75.0": { + "type": "long" + }, + "95.0": { + "type": "long" + }, + "99.0": { + "type": "long" + } + } + }, "app": { "properties": { "search": { @@ -4338,6 +4488,31 @@ "deprecated": { "type": "long" }, + "sizes": { + "properties": { + "1.0": { + "type": "long" + }, + "5.0": { + "type": "long" + }, + "25.0": { + "type": "long" + }, + "50.0": { + "type": "long" + }, + "75.0": { + "type": "long" + }, + "95.0": { + "type": "long" + }, + "99.0": { + "type": "long" + } + } + }, "app": { "properties": { "search": { @@ -4975,6 +5150,31 @@ } } }, + "output_size": { + "properties": { + "1.0": { + "type": "long" + }, + "5.0": { + "type": "long" + }, + "25.0": { + "type": "long" + }, + "50.0": { + "type": "long" + }, + "75.0": { + "type": "long" + }, + "95.0": { + "type": "long" + }, + "99.0": { + "type": "long" + } + } + }, "available": { "type": "boolean" }, @@ -4997,6 +5197,31 @@ "deprecated": { "type": "long" }, + "sizes": { + "properties": { + "1.0": { + "type": "long" + }, + "5.0": { + "type": "long" + }, + "25.0": { + "type": "long" + }, + "50.0": { + "type": "long" + }, + "75.0": { + "type": "long" + }, + "95.0": { + "type": "long" + }, + "99.0": { + "type": "long" + } + } + }, "app": { "properties": { "search": { @@ -5039,6 +5264,31 @@ "deprecated": { "type": "long" }, + "sizes": { + "properties": { + "1.0": { + "type": "long" + }, + "5.0": { + "type": "long" + }, + "25.0": { + "type": "long" + }, + "50.0": { + "type": "long" + }, + "75.0": { + "type": "long" + }, + "95.0": { + "type": "long" + }, + "99.0": { + "type": "long" + } + } + }, "app": { "properties": { "search": { @@ -5081,6 +5331,31 @@ "deprecated": { "type": "long" }, + "sizes": { + "properties": { + "1.0": { + "type": "long" + }, + "5.0": { + "type": "long" + }, + "25.0": { + "type": "long" + }, + "50.0": { + "type": "long" + }, + "75.0": { + "type": "long" + }, + "95.0": { + "type": "long" + }, + "99.0": { + "type": "long" + } + } + }, "app": { "properties": { "search": { @@ -5123,6 +5398,31 @@ "deprecated": { "type": "long" }, + "sizes": { + "properties": { + "1.0": { + "type": "long" + }, + "5.0": { + "type": "long" + }, + "25.0": { + "type": "long" + }, + "50.0": { + "type": "long" + }, + "75.0": { + "type": "long" + }, + "95.0": { + "type": "long" + }, + "99.0": { + "type": "long" + } + } + }, "app": { "properties": { "search": { @@ -5165,6 +5465,31 @@ "deprecated": { "type": "long" }, + "sizes": { + "properties": { + "1.0": { + "type": "long" + }, + "5.0": { + "type": "long" + }, + "25.0": { + "type": "long" + }, + "50.0": { + "type": "long" + }, + "75.0": { + "type": "long" + }, + "95.0": { + "type": "long" + }, + "99.0": { + "type": "long" + } + } + }, "app": { "properties": { "search": { @@ -5207,6 +5532,31 @@ "deprecated": { "type": "long" }, + "sizes": { + "properties": { + "1.0": { + "type": "long" + }, + "5.0": { + "type": "long" + }, + "25.0": { + "type": "long" + }, + "50.0": { + "type": "long" + }, + "75.0": { + "type": "long" + }, + "95.0": { + "type": "long" + }, + "99.0": { + "type": "long" + } + } + }, "app": { "properties": { "search": { @@ -5249,6 +5599,31 @@ "deprecated": { "type": "long" }, + "sizes": { + "properties": { + "1.0": { + "type": "long" + }, + "5.0": { + "type": "long" + }, + "25.0": { + "type": "long" + }, + "50.0": { + "type": "long" + }, + "75.0": { + "type": "long" + }, + "95.0": { + "type": "long" + }, + "99.0": { + "type": "long" + } + } + }, "app": { "properties": { "search": { @@ -5885,6 +6260,31 @@ } } } + }, + "output_size": { + "properties": { + "1.0": { + "type": "long" + }, + "5.0": { + "type": "long" + }, + "25.0": { + "type": "long" + }, + "50.0": { + "type": "long" + }, + "75.0": { + "type": "long" + }, + "95.0": { + "type": "long" + }, + "99.0": { + "type": "long" + } + } } } } @@ -6924,44 +7324,6 @@ } } } - }, - "ui_open": { - "properties": { - "elasticsearch": { - "type": "long", - "_meta": { - "description": "Number of times a user viewed the list of Elasticsearch deprecations." - } - }, - "overview": { - "type": "long", - "_meta": { - "description": "Number of times a user viewed the Overview page." - } - }, - "kibana": { - "type": "long", - "_meta": { - "description": "Number of times a user viewed the list of Kibana deprecations" - } - } - } - }, - "ui_reindex": { - "properties": { - "close": { - "type": "long" - }, - "open": { - "type": "long" - }, - "start": { - "type": "long" - }, - "stop": { - "type": "long" - } - } } } }, diff --git a/x-pack/plugins/timelines/common/types/timeline/columns/index.tsx b/x-pack/plugins/timelines/common/types/timeline/columns/index.tsx index cc20c856f0e13..644b38d551337 100644 --- a/x-pack/plugins/timelines/common/types/timeline/columns/index.tsx +++ b/x-pack/plugins/timelines/common/types/timeline/columns/index.tsx @@ -8,7 +8,7 @@ import { ReactNode } from 'react'; import { EuiDataGridColumn, EuiDataGridColumnCellActionProps } from '@elastic/eui'; -import { IFieldSubType } from '../../../../../../../src/plugins/data/common'; +import { Filter, IFieldSubType } from '../../../../../../../src/plugins/data/common'; import { BrowserFields } from '../../../search_strategy/index_fields'; import { TimelineNonEcsData } from '../../../search_strategy/timeline'; @@ -45,14 +45,16 @@ export type ColumnId = string; export type TGridCellAction = ({ browserFields, data, - timelineId, + globalFilters, pageSize, + timelineId, }: { browserFields: BrowserFields; /** each row of data is represented as one TimelineNonEcsData[] */ data: TimelineNonEcsData[][]; - timelineId: string; + globalFilters?: Filter[]; pageSize: number; + timelineId: string; }) => (props: EuiDataGridColumnCellActionProps) => ReactNode; /** The specification of a column header */ diff --git a/x-pack/plugins/timelines/public/components/t_grid/body/index.tsx b/x-pack/plugins/timelines/public/components/t_grid/body/index.tsx index f262321846743..d67cc746f352f 100644 --- a/x-pack/plugins/timelines/public/components/t_grid/body/index.tsx +++ b/x-pack/plugins/timelines/public/components/t_grid/body/index.tsx @@ -74,6 +74,7 @@ import type { EuiTheme } from '../../../../../../../src/plugins/kibana_react/com import { ViewSelection } from '../event_rendered_view/selector'; import { EventRenderedView } from '../event_rendered_view'; import { useDataGridHeightHack } from './height_hack'; +import { Filter } from '../../../../../../../src/plugins/data/public'; const StatefulAlertStatusBulkActions = lazy( () => import('../toolbar/bulk_actions/alert_status_bulk_actions') @@ -86,6 +87,7 @@ interface OwnProps { bulkActions?: BulkActionsProp; data: TimelineItem[]; defaultCellActions?: TGridCellAction[]; + filters?: Filter[]; filterQuery: string; filterStatus?: AlertStatus; id: string; @@ -300,15 +302,18 @@ export const BodyComponent = React.memo( data, defaultCellActions, filterQuery, + filters, filterStatus, + hasAlertsCrud, + hasAlertsCrudPermissions, id, indexNames, isEventViewer = false, + isLoading, isSelectAllChecked, itemsPerPageOptions, leadingControlColumns = EMPTY_CONTROL_COLUMNS, loadingEventIds, - isLoading, loadPage, onRuleChange, pageSize, @@ -322,11 +327,9 @@ export const BodyComponent = React.memo( tableView = 'gridView', tabType, totalItems, + totalSelectAllAlerts, trailingControlColumns = EMPTY_CONTROL_COLUMNS, unit = defaultUnit, - hasAlertsCrud, - hasAlertsCrudPermissions, - totalSelectAllAlerts, }) => { const dispatch = useDispatch(); const getManageTimeline = useMemo(() => tGridSelectors.getManageTimelineById(), []); @@ -641,10 +644,11 @@ export const BodyComponent = React.memo( columnHeaders.map((header) => { const buildAction = (tGridCellAction: TGridCellAction) => tGridCellAction({ - data: data.map((row) => row.data), browserFields, - timelineId: id, + data: data.map((row) => row.data), + globalFilters: filters, pageSize, + timelineId: id, }); return { @@ -653,7 +657,7 @@ export const BodyComponent = React.memo( header.tGridCellActions?.map(buildAction) ?? defaultCellActions?.map(buildAction), }; }), - [browserFields, columnHeaders, data, defaultCellActions, id, pageSize] + [browserFields, columnHeaders, data, defaultCellActions, id, pageSize, filters] ); const renderTGridCellValue = useMemo(() => { @@ -717,20 +721,16 @@ export const BodyComponent = React.memo( const onChangeItemsPerPage = useCallback( (itemsChangedPerPage) => { - clearSelected({ id }); - dispatch(tGridActions.setTGridSelectAll({ id, selectAll: false })); dispatch(tGridActions.updateItemsPerPage({ id, itemsPerPage: itemsChangedPerPage })); }, - [id, dispatch, clearSelected] + [id, dispatch] ); const onChangePage = useCallback( (page) => { - clearSelected({ id }); - dispatch(tGridActions.setTGridSelectAll({ id, selectAll: false })); loadPage(page); }, - [id, loadPage, dispatch, clearSelected] + [loadPage] ); const height = useDataGridHeightHack(pageSize, data.length); diff --git a/x-pack/plugins/timelines/public/components/t_grid/integrated/index.tsx b/x-pack/plugins/timelines/public/components/t_grid/integrated/index.tsx index 172efd52c28ba..b649dc36abe0a 100644 --- a/x-pack/plugins/timelines/public/components/t_grid/integrated/index.tsx +++ b/x-pack/plugins/timelines/public/components/t_grid/integrated/index.tsx @@ -293,6 +293,17 @@ const TGridIntegratedComponent: React.FC = ({ }, [inspect, loading, refetch, setQuery]); const timelineContext = useMemo(() => ({ timelineId: id }), [id]); + // Clear checkbox selection when new events are fetched + useEffect(() => { + dispatch(tGridActions.clearSelected({ id })); + dispatch( + tGridActions.setTGridSelectAll({ + id, + selectAll: false, + }) + ); + }, [nonDeletedEvents, dispatch, id]); + return ( = ({ > diff --git a/x-pack/plugins/timelines/public/components/t_grid/standalone/index.tsx b/x-pack/plugins/timelines/public/components/t_grid/standalone/index.tsx index b52fd54f13321..1a374d0c6b87a 100644 --- a/x-pack/plugins/timelines/public/components/t_grid/standalone/index.tsx +++ b/x-pack/plugins/timelines/public/components/t_grid/standalone/index.tsx @@ -337,6 +337,17 @@ const TGridStandaloneComponent: React.FC = ({ }, [loading]); const timelineContext = { timelineId: STANDALONE_ID }; + // Clear checkbox selection when new events are fetched + useEffect(() => { + dispatch(tGridActions.clearSelected({ id: STANDALONE_ID })); + dispatch( + tGridActions.setTGridSelectAll({ + id: STANDALONE_ID, + selectAll: false, + }) + ); + }, [nonDeletedEvents, dispatch]); + return ( diff --git a/x-pack/plugins/timelines/server/index.ts b/x-pack/plugins/timelines/server/index.ts index 8ad2bafdcc13a..229a257d8f549 100644 --- a/x-pack/plugins/timelines/server/index.ts +++ b/x-pack/plugins/timelines/server/index.ts @@ -5,11 +5,12 @@ * 2.0. */ -import { PluginInitializerContext } from '../../../../src/core/server'; +import { PluginInitializerContext, PluginConfigDescriptor } from '../../../../src/core/server'; import { TimelinesPlugin } from './plugin'; -import { ConfigSchema } from './config'; +import { ConfigSchema, ConfigType } from './config'; -export const config = { +export const config: PluginConfigDescriptor = { + deprecations: ({ deprecate }) => [deprecate('enabled', '8.0.0')], schema: ConfigSchema, exposeToBrowser: { enabled: true, diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index f80e793b202f9..3109ab42a452a 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -6399,14 +6399,6 @@ "xpack.apm.correlations.correlationsTable.filterLabel": "フィルター", "xpack.apm.correlations.correlationsTable.loadingText": "読み込み中", "xpack.apm.correlations.correlationsTable.noDataText": "データなし", - "xpack.apm.correlations.customize.buttonLabel": "フィールドのカスタマイズ", - "xpack.apm.correlations.customize.fieldHelpText": "相関関係を分析するフィールドをカスタマイズまたは{reset}します。{docsLink}", - "xpack.apm.correlations.customize.fieldHelpTextDocsLink": "デフォルトフィールドの詳細。", - "xpack.apm.correlations.customize.fieldHelpTextReset": "リセット", - "xpack.apm.correlations.customize.fieldLabel": "フィールド", - "xpack.apm.correlations.customize.fieldPlaceholder": "オプションを選択または作成", - "xpack.apm.correlations.customize.thresholdLabel": "しきい値", - "xpack.apm.correlations.customize.thresholdPercentile": "{percentile}パーセンタイル", "xpack.apm.correlations.failedTransactions.correlationsTable.fieldNameLabel": "フィールド名", "xpack.apm.correlations.failedTransactions.correlationsTable.fieldValueLabel": "フィールド値", "xpack.apm.correlations.failedTransactions.correlationsTable.impactLabel": "インパクト", @@ -6960,7 +6952,6 @@ "xpack.apm.settings.unsupportedConfigs.errorToast.title": "APMサーバー設定を取り込めません", "xpack.apm.settingsLinkLabel": "設定", "xpack.apm.setupInstructionsButtonLabel": "セットアップの手順", - "xpack.apm.significanTerms.license.text": "相関関係APIを使用するには、Elastic Platinumライセンスのサブスクリプションが必要です。", "xpack.apm.stacktraceTab.causedByFramesToogleButtonLabel": "作成元", "xpack.apm.stacktraceTab.localVariablesToogleButtonLabel": "ローカル変数", "xpack.apm.stacktraceTab.noStacktraceAvailableLabel": "利用可能なスタックトレースがありません。", @@ -10352,7 +10343,6 @@ "xpack.enterpriseSearch.workplaceSearch.sources.additionalConfig.heading": "追加の構成が必要", "xpack.enterpriseSearch.workplaceSearch.sources.applicationLinkTitles.github": "GitHub開発者ポータル", "xpack.enterpriseSearch.workplaceSearch.sources.baseUrlTitles.github": "GitHub Enterprise URL", - "xpack.enterpriseSearch.workplaceSearch.sources.config.description": "変更するコンテンツソースコネクター設定を編集します。", "xpack.enterpriseSearch.workplaceSearch.sources.config.link": "コンテンツソースコネクター設定を編集", "xpack.enterpriseSearch.workplaceSearch.sources.config.title": "コンテンツソース構成", "xpack.enterpriseSearch.workplaceSearch.sources.configuration.title": "構成", @@ -17603,7 +17593,6 @@ "xpack.monitoring.alerts.kibanaVersionMismatch.label": "Kibana バージョン不一致", "xpack.monitoring.alerts.kibanaVersionMismatch.shortAction": "すべてのインスタンスのバージョンが同じことを確認してください。", "xpack.monitoring.alerts.kibanaVersionMismatch.ui.firingMessage": "このクラスターでは、複数のバージョンの Kibana({versions})が実行されています。", - "xpack.monitoring.alerts.legacyAlert.expressionText": "構成するものがありません。", "xpack.monitoring.alerts.licenseExpiration.action": "ライセンスを更新してください。", "xpack.monitoring.alerts.licenseExpiration.actionVariables.clusterName": "ライセンスが属しているクラスター。", "xpack.monitoring.alerts.licenseExpiration.actionVariables.expiredDate": "ライセンスの有効期限。", @@ -23098,7 +23087,6 @@ "xpack.securitySolution.paginatedTable.tooManyResultsToastText": "クエリ範囲を縮めて結果をさらにフィルタリングしてください", "xpack.securitySolution.paginatedTable.tooManyResultsToastTitle": " - 結果が多すぎます", "xpack.securitySolution.policiesTab": "ポリシー", - "xpack.securitySolution.policySelect.policySpecificSectionTitle": "特定のエンドポイントポリシーに適用", "xpack.securitySolution.policyStatusText.failure": "失敗", "xpack.securitySolution.policyStatusText.success": "成功", "xpack.securitySolution.policyStatusText.unsupported": "サポートされていない", @@ -23509,8 +23497,6 @@ "xpack.securitySolution.trustedapps.logicalConditionBuilder.group.andOperator": "AND", "xpack.securitySolution.trustedapps.logicalConditionBuilder.noEntries": "条件が定義されていません", "xpack.securitySolution.trustedapps.middleware.editIdMissing": "IDが指定されていません", - "xpack.securitySolution.trustedapps.policySelect.globalSectionTitle": "割り当て", - "xpack.securitySolution.trustedapps.policySelect.globalSwitchTitle": "信頼できるアプリケーションをグローバルに適用", "xpack.securitySolution.trustedapps.trustedapp.entry.field": "フィールド", "xpack.securitySolution.trustedapps.trustedapp.entry.operator": "演算子", "xpack.securitySolution.trustedapps.trustedapp.entry.value": "値", @@ -26220,13 +26206,7 @@ "xpack.uptime.createPackagePolicy.stepConfigure.tlsSettings.label": "TLS設定", "xpack.uptime.durationChart.emptyPrompt.description": "このモニターは選択された時間範囲で一度も{emphasizedText}していません。", "xpack.uptime.durationChart.emptyPrompt.title": "利用可能な期間データがありません", - "xpack.uptime.emptyState.configureHeartbeatIndexSettings": "Heartbeatがすでに設定されている場合は、データがElasticsearchに送信されていることを確認してから、Heartbeat構成に合わせてインデックスパターン設定を更新します。", - "xpack.uptime.emptyState.configureHeartbeatToGetStartedMessage": "サービスの監視を開始するには、Heartbeatを設定します。", "xpack.uptime.emptyState.loadingMessage": "読み込み中…", - "xpack.uptime.emptyState.noDataMessage": "インデックス{indexName}にはアップタイムデータが見つかりません", - "xpack.uptime.emptyState.noIndexTitle": "パターン{indexName}のインデックスが見つかりません", - "xpack.uptime.emptyState.updateIndexPattern": "インデックスパターン設定を更新", - "xpack.uptime.emptyState.viewSetupInstructions": "セットアップの手順を表示", "xpack.uptime.emptyStateError.notAuthorized": "アップタイムデータの表示が承認されていません。システム管理者にお問い合わせください。", "xpack.uptime.emptyStateError.notFoundPage": "ページが見つかりません", "xpack.uptime.emptyStateError.title": "エラー", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index d33ab501138d1..1f4164db5403c 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -6450,14 +6450,6 @@ "xpack.apm.correlations.correlationsTable.filterLabel": "筛选", "xpack.apm.correlations.correlationsTable.loadingText": "正在加载", "xpack.apm.correlations.correlationsTable.noDataText": "无数据", - "xpack.apm.correlations.customize.buttonLabel": "定制字段", - "xpack.apm.correlations.customize.fieldHelpText": "定制或{reset}要针对相关性分析的字段。{docsLink}", - "xpack.apm.correlations.customize.fieldHelpTextDocsLink": "详细了解默认字段。", - "xpack.apm.correlations.customize.fieldHelpTextReset": "重置", - "xpack.apm.correlations.customize.fieldLabel": "字段", - "xpack.apm.correlations.customize.fieldPlaceholder": "选择或创建选项", - "xpack.apm.correlations.customize.thresholdLabel": "阈值", - "xpack.apm.correlations.customize.thresholdPercentile": "第 {percentile} 个百分位数", "xpack.apm.correlations.failedTransactions.correlationsTable.fieldNameLabel": "字段名称", "xpack.apm.correlations.failedTransactions.correlationsTable.fieldValueLabel": "字段值", "xpack.apm.correlations.failedTransactions.correlationsTable.impactLabel": "影响", @@ -7016,7 +7008,6 @@ "xpack.apm.settings.unsupportedConfigs.errorToast.title": "无法获取 APM Server 设置", "xpack.apm.settingsLinkLabel": "设置", "xpack.apm.setupInstructionsButtonLabel": "设置说明", - "xpack.apm.significanTerms.license.text": "要使用相关性 API,必须订阅 Elastic 白金级许可证。", "xpack.apm.stacktraceTab.causedByFramesToogleButtonLabel": "原因", "xpack.apm.stacktraceTab.libraryFramesToogleButtonLabel": "{count, plural, other {# 个库帧}}", "xpack.apm.stacktraceTab.localVariablesToogleButtonLabel": "本地变量", @@ -10454,7 +10445,6 @@ "xpack.enterpriseSearch.workplaceSearch.sources.additionalConfig.heading": "需要其他配置", "xpack.enterpriseSearch.workplaceSearch.sources.applicationLinkTitles.github": "GitHub 开发者门户", "xpack.enterpriseSearch.workplaceSearch.sources.baseUrlTitles.github": "GitHub Enterprise URL", - "xpack.enterpriseSearch.workplaceSearch.sources.config.description": "编辑要更改的内容源连接器设置。", "xpack.enterpriseSearch.workplaceSearch.sources.config.link": "编辑内容源连接器设置", "xpack.enterpriseSearch.workplaceSearch.sources.config.title": "内容源配置", "xpack.enterpriseSearch.workplaceSearch.sources.configuration.title": "配置", @@ -17878,7 +17868,6 @@ "xpack.monitoring.alerts.kibanaVersionMismatch.label": "Kibana 版本不匹配", "xpack.monitoring.alerts.kibanaVersionMismatch.shortAction": "确认所有实例具有相同的版本。", "xpack.monitoring.alerts.kibanaVersionMismatch.ui.firingMessage": "在此集群中正运行着多个 Kibana 版本 ({versions})。", - "xpack.monitoring.alerts.legacyAlert.expressionText": "没有可配置的内容。", "xpack.monitoring.alerts.licenseExpiration.action": "请更新您的许可证。", "xpack.monitoring.alerts.licenseExpiration.actionVariables.clusterName": "许可证所属的集群。", "xpack.monitoring.alerts.licenseExpiration.actionVariables.expiredDate": "许可证过期日期。", @@ -23477,7 +23466,6 @@ "xpack.securitySolution.paginatedTable.tooManyResultsToastText": "缩减您的查询范围,以更好地筛选结果", "xpack.securitySolution.paginatedTable.tooManyResultsToastTitle": " - 结果过多", "xpack.securitySolution.policiesTab": "策略", - "xpack.securitySolution.policySelect.policySpecificSectionTitle": "应用到特定终端策略", "xpack.securitySolution.policyStatusText.failure": "失败", "xpack.securitySolution.policyStatusText.success": "成功", "xpack.securitySolution.policyStatusText.unsupported": "不支持", @@ -23891,8 +23879,6 @@ "xpack.securitySolution.trustedapps.logicalConditionBuilder.group.andOperator": "AND", "xpack.securitySolution.trustedapps.logicalConditionBuilder.noEntries": "未定义条件", "xpack.securitySolution.trustedapps.middleware.editIdMissing": "未提供 ID", - "xpack.securitySolution.trustedapps.policySelect.globalSectionTitle": "分配", - "xpack.securitySolution.trustedapps.policySelect.globalSwitchTitle": "全局应用受信任的应用程序", "xpack.securitySolution.trustedapps.trustedapp.entry.field": "字段", "xpack.securitySolution.trustedapps.trustedapp.entry.operator": "运算符", "xpack.securitySolution.trustedapps.trustedapp.entry.value": "值", @@ -26654,13 +26640,7 @@ "xpack.uptime.createPackagePolicy.stepConfigure.tlsSettings.label": "TLS 设置", "xpack.uptime.durationChart.emptyPrompt.description": "在选定时间范围内此监测从未{emphasizedText}。", "xpack.uptime.durationChart.emptyPrompt.title": "没有持续时间数据", - "xpack.uptime.emptyState.configureHeartbeatIndexSettings": "如果已设置 Heartbeat,请确认其正向 Elasticsearch 发送数据,然后更新索引模式设置以匹配 Heartbeat 配置。", - "xpack.uptime.emptyState.configureHeartbeatToGetStartedMessage": "设置 Heartbeat 以开始监测您的服务。", "xpack.uptime.emptyState.loadingMessage": "正在加载……", - "xpack.uptime.emptyState.noDataMessage": "在索引 {indexName} 中找不到运行时间数据", - "xpack.uptime.emptyState.noIndexTitle": "找不到模式 {indexName} 的索引", - "xpack.uptime.emptyState.updateIndexPattern": "更新索引模式设置", - "xpack.uptime.emptyState.viewSetupInstructions": "查看设置说明", "xpack.uptime.emptyStateError.notAuthorized": "您无权查看 Uptime 数据,请联系系统管理员。", "xpack.uptime.emptyStateError.notFoundPage": "未找到页面", "xpack.uptime.emptyStateError.title": "错误", diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/email/email.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/email/email.test.tsx index ed56ca05538b1..ec86f149e9a43 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/email/email.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/email/email.test.tsx @@ -48,6 +48,7 @@ describe('connector validation', () => { secrets: { user: 'user', password: 'pass', + clientSecret: null, }, id: 'test', actionTypeId: '.email', @@ -70,12 +71,15 @@ describe('connector validation', () => { port: [], host: [], service: [], + clientId: [], + tenantId: [], }, }, secrets: { errors: { user: [], password: [], + clientSecret: [], }, }, }); @@ -86,6 +90,7 @@ describe('connector validation', () => { secrets: { user: null, password: null, + clientSecret: null, }, id: 'test', actionTypeId: '.email', @@ -108,12 +113,15 @@ describe('connector validation', () => { port: [], host: [], service: [], + clientId: [], + tenantId: [], }, }, secrets: { errors: { user: [], password: [], + clientSecret: [], }, }, }); @@ -141,12 +149,15 @@ describe('connector validation', () => { port: ['Port is required.'], host: ['Host is required.'], service: [], + clientId: [], + tenantId: [], }, }, secrets: { errors: { user: [], password: [], + clientSecret: [], }, }, }); @@ -156,6 +167,7 @@ describe('connector validation', () => { secrets: { user: 'user', password: null, + clientSecret: null, }, id: 'test', actionTypeId: '.email', @@ -178,12 +190,15 @@ describe('connector validation', () => { port: [], host: [], service: [], + clientId: [], + tenantId: [], }, }, secrets: { errors: { user: [], password: ['Password is required when username is used.'], + clientSecret: [], }, }, }); @@ -193,6 +208,7 @@ describe('connector validation', () => { secrets: { user: null, password: 'password', + clientSecret: null, }, id: 'test', actionTypeId: '.email', @@ -215,12 +231,15 @@ describe('connector validation', () => { port: [], host: [], service: [], + clientId: [], + tenantId: [], }, }, secrets: { errors: { user: ['Username is required when password is used.'], password: [], + clientSecret: [], }, }, }); @@ -253,12 +272,53 @@ describe('connector validation', () => { port: [], host: [], service: ['Service is required.'], + clientId: [], + tenantId: [], }, }, secrets: { errors: { user: [], password: [], + clientSecret: [], + }, + }, + }); + }); + test('connector validation fails when for exchange service selected, but clientId, tenantId and clientSecrets were not defined', async () => { + const actionConnector = { + secrets: { + user: 'user', + password: 'pass', + clientSecret: null, + }, + id: 'test', + actionTypeId: '.email', + name: 'email', + isPreconfigured: false, + config: { + from: 'test@test.com', + hasAuth: true, + service: 'exchange_server', + }, + } as EmailActionConnector; + + expect(await actionTypeModel.validateConnector(actionConnector)).toEqual({ + config: { + errors: { + from: [], + port: [], + host: [], + service: [], + clientId: ['Client ID is required.'], + tenantId: ['Tenant ID is required.'], + }, + }, + secrets: { + errors: { + clientSecret: ['Client Secret is required.'], + password: [], + user: [], }, }, }); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/email/email.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/email/email.tsx index fe0b18b1b2e61..709101396edf0 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/email/email.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/email/email.tsx @@ -14,6 +14,7 @@ import { GenericValidationResult, } from '../../../../types'; import { EmailActionParams, EmailConfig, EmailSecrets, EmailActionConnector } from '../types'; +import { AdditionalEmailServices } from '../../../../../../actions/common'; const emailServices: EuiSelectOption[] = [ { @@ -106,10 +107,13 @@ export function getActionType(): ActionTypeModel(), host: new Array(), service: new Array(), + clientId: new Array(), + tenantId: new Array(), }; const secretsErrors = { user: new Array(), password: new Array(), + clientSecret: new Array(), }; const validationResult = { @@ -122,17 +126,29 @@ export function getActionType(): ActionTypeModel import('./exchange_form')); export const EmailActionConnectorFields: React.FunctionComponent< ActionConnectorFieldsProps > = ({ action, editActionConfig, editActionSecrets, errors, readOnly }) => { @@ -61,6 +63,88 @@ export const EmailActionConnectorFields: React.FunctionComponent< password !== undefined && errors.password !== undefined && errors.password.length > 0; const isUserInvalid: boolean = user !== undefined && errors.user !== undefined && errors.user.length > 0; + + const authForm = ( + <> + {getEncryptedFieldNotifyLabel( + !action.id, + 2, + action.isMissingSecrets ?? false, + i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.emailAction.reenterValuesLabel', + { + defaultMessage: + 'Username and password are encrypted. Please reenter values for these fields.', + } + ) + )} + + + + { + editActionSecrets('user', nullableString(e.target.value)); + }} + onBlur={() => { + if (!user) { + editActionSecrets('user', ''); + } + }} + /> + + + + + { + editActionSecrets('password', nullableString(e.target.value)); + }} + onBlur={() => { + if (!password) { + editActionSecrets('password', ''); + } + }} + /> + + + + + ); + return ( <> @@ -130,214 +214,149 @@ export const EmailActionConnectorFields: React.FunctionComponent< /> - - - { - editActionConfig('host', e.target.value); - }} - onBlur={() => { - if (!host) { - editActionConfig('host', ''); - } - }} - /> - - - + + {service === AdditionalEmailServices.EXCHANGE ? ( + + ) : ( + <> - { - editActionConfig('port', parseInt(e.target.value, 10)); + editActionConfig('host', e.target.value); }} onBlur={() => { - if (!port) { - editActionConfig('port', 0); + if (!host) { + editActionConfig('host', ''); } }} /> - - - + + { - editActionConfig('secure', e.target.checked); - }} - /> - - + > + { + editActionConfig('port', parseInt(e.target.value, 10)); + }} + onBlur={() => { + if (!port) { + editActionConfig('port', 0); + } + }} + /> + + + + + + { + editActionConfig('secure', e.target.checked); + }} + /> + + + + - - - - - - -

    - -

    -
    - - { - editActionConfig('hasAuth', e.target.checked); - if (!e.target.checked) { - editActionSecrets('user', null); - editActionSecrets('password', null); - } - }} - /> -
    -
    - {hasAuth ? ( - <> - {getEncryptedFieldNotifyLabel( - !action.id, - 2, - action.isMissingSecrets ?? false, - i18n.translate( - 'xpack.triggersActionsUI.components.builtinActionTypes.emailAction.reenterValuesLabel', - { - defaultMessage: - 'Username and password are encrypted. Please reenter values for these fields.', - } - ) - )} - + - + +

    + +

    +
    + + - { - editActionSecrets('user', nullableString(e.target.value)); - }} - onBlur={() => { - if (!user) { - editActionSecrets('user', ''); - } - }} - /> -
    -
    - - { + editActionConfig('hasAuth', e.target.checked); + if (!e.target.checked) { + editActionSecrets('user', null); + editActionSecrets('password', null); } - )} - > - { - editActionSecrets('password', nullableString(e.target.value)); - }} - onBlur={() => { - if (!password) { - editActionSecrets('password', ''); - } - }} - /> - + }} + />
    + {hasAuth ? authForm : null} - ) : null} + )} ); }; // if the string == null or is empty, return null, else return string -function nullableString(str: string | null | undefined) { +export function nullableString(str: string | null | undefined) { if (str == null || str.trim() === '') return null; return str; } diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/email/exchange_form.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/email/exchange_form.test.tsx new file mode 100644 index 0000000000000..2a08c9b19e602 --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/email/exchange_form.test.tsx @@ -0,0 +1,76 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { mountWithIntl } from '@kbn/test/jest'; +import { EmailActionConnector } from '../types'; +import ExchangeFormFields from './exchange_form'; + +jest.mock('../../../../common/lib/kibana'); + +describe('ExchangeFormFields renders', () => { + test('should display exchange form fields', () => { + const actionConnector = { + secrets: { + clientSecret: 'user', + }, + id: 'test', + actionTypeId: '.email', + name: 'exchange email', + config: { + from: 'test@test.com', + hasAuth: true, + service: 'exchange_server', + clientId: '123', + tenantId: '1234', + }, + } as EmailActionConnector; + const wrapper = mountWithIntl( + {}} + editActionSecrets={() => {}} + readOnly={false} + /> + ); + expect(wrapper.find('[data-test-subj="emailClientSecret"]').length > 0).toBeTruthy(); + expect(wrapper.find('[data-test-subj="emailClientId"]').length > 0).toBeTruthy(); + expect(wrapper.find('[data-test-subj="emailTenantId"]').length > 0).toBeTruthy(); + }); + + test('exchange field defaults to empty when not defined', () => { + const actionConnector = { + secrets: {}, + id: 'test', + actionTypeId: '.email', + name: 'email', + config: { + from: 'test@test.com', + hasAuth: true, + service: 'exchange_server', + }, + } as EmailActionConnector; + const wrapper = mountWithIntl( + {}} + editActionSecrets={() => {}} + readOnly={false} + /> + ); + expect(wrapper.find('[data-test-subj="emailClientSecret"]').length > 0).toBeTruthy(); + expect(wrapper.find('input[data-test-subj="emailClientSecret"]').prop('value')).toEqual(''); + + expect(wrapper.find('[data-test-subj="emailClientId"]').length > 0).toBeTruthy(); + expect(wrapper.find('input[data-test-subj="emailClientId"]').prop('value')).toEqual(''); + + expect(wrapper.find('[data-test-subj="emailTenantId"]').length > 0).toBeTruthy(); + expect(wrapper.find('input[data-test-subj="emailTenantId"]').prop('value')).toEqual(''); + }); +}); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/email/exchange_form.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/email/exchange_form.tsx new file mode 100644 index 0000000000000..52fa53da19cd8 --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/email/exchange_form.tsx @@ -0,0 +1,164 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license 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 { + EuiFieldText, + EuiFlexItem, + EuiFlexGroup, + EuiFormRow, + EuiFieldPassword, +} from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { IErrorObject } from '../../../../types'; +import { EmailActionConnector } from '../types'; +import { nullableString } from './email_connector'; +import { getEncryptedFieldNotifyLabel } from '../../get_encrypted_field_notify_label'; + +interface ExchangeFormFieldsProps { + action: EmailActionConnector; + editActionConfig: (property: string, value: unknown) => void; + editActionSecrets: (property: string, value: unknown) => void; + errors: IErrorObject; + readOnly: boolean; +} + +const ExchangeFormFields: React.FunctionComponent = ({ + action, + editActionConfig, + editActionSecrets, + errors, + readOnly, +}) => { + const { tenantId, clientId } = action.config; + const { clientSecret } = action.secrets; + + const isClientIdInvalid: boolean = + clientId !== undefined && errors.clientId !== undefined && errors.clientId.length > 0; + const isTenantIdInvalid: boolean = + tenantId !== undefined && errors.tenantId !== undefined && errors.tenantId.length > 0; + const isClientSecretInvalid: boolean = + clientSecret !== undefined && + errors.clientSecret !== undefined && + errors.clientSecret.length > 0; + + return ( + <> + + + + { + editActionConfig('tenantId', nullableString(e.target.value)); + }} + onBlur={() => { + if (!tenantId) { + editActionConfig('tenantId', ''); + } + }} + /> + + + + + { + editActionConfig('clientId', nullableString(e.target.value)); + }} + onBlur={() => { + if (!clientId) { + editActionConfig('clientId', ''); + } + }} + /> + + + + {getEncryptedFieldNotifyLabel( + !action.id, + 1, + action.isMissingSecrets ?? false, + i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.emailAction.reenterClientSecretLabel', + { + defaultMessage: 'Client Secret is encrypted. Please reenter value for this field.', + } + ) + )} + + + + { + editActionSecrets('clientSecret', nullableString(e.target.value)); + }} + onBlur={() => { + if (!clientSecret) { + editActionSecrets('clientSecret', ''); + } + }} + /> + + + + + ); +}; + +// eslint-disable-next-line import/no-default-export +export { ExchangeFormFields as default }; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/email/translations.ts b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/email/translations.ts index df68d0d1237ed..38e16f6046184 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/email/translations.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/email/translations.ts @@ -21,6 +21,27 @@ export const SENDER_NOT_VALID = i18n.translate( } ); +export const CLIENT_ID_REQUIRED = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.error.requiredClientIdText', + { + defaultMessage: 'Client ID is required.', + } +); + +export const TENANT_ID_REQUIRED = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.error.requiredTenantIdText', + { + defaultMessage: 'Tenant ID is required.', + } +); + +export const CLIENT_SECRET_REQUIRED = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.error.requiredClientSecretText', + { + defaultMessage: 'Client Secret is required.', + } +); + export const PORT_REQUIRED = i18n.translate( 'xpack.triggersActionsUI.components.builtinActionTypes.error.requiredPortText', { diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/email/use_email_config.ts b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/email/use_email_config.ts index fad71cf5d6385..9e6df1d1a1019 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/email/use_email_config.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/email/use_email_config.ts @@ -10,6 +10,7 @@ import { HttpSetup } from 'kibana/public'; import { isEmpty } from 'lodash'; import { EmailConfig } from '../types'; import { getServiceConfig } from './api'; +import { AdditionalEmailServices } from '../../../../../../actions/common'; export function useEmailConfig( http: HttpSetup, @@ -39,9 +40,12 @@ export function useEmailConfig( useEffect(() => { (async () => { if (emailService) { + editActionConfig('service', emailService); + if (emailService === AdditionalEmailServices.EXCHANGE) { + return; + } const serviceConfig = await getEmailServiceConfig(emailService); - editActionConfig('service', emailService); editActionConfig('host', serviceConfig?.host ? serviceConfig.host : ''); editActionConfig('port', serviceConfig?.port ? serviceConfig.port : 0); editActionConfig('secure', null != serviceConfig?.secure ? serviceConfig.secure : false); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/types.ts b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/types.ts index 60e0a0f14b897..abacc5544c712 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/types.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/types.ts @@ -79,11 +79,14 @@ export interface EmailConfig { secure?: boolean; hasAuth: boolean; service: string; + clientId?: string; + tenantId?: string; } export interface EmailSecrets { user: string | null; password: string | null; + clientSecret: string | null; } export type EmailActionConnector = UserConfiguredActionConnector; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/health_check.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/health_check.test.tsx index b998067424edd..ff5992a6542b7 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/health_check.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/health_check.test.tsx @@ -114,7 +114,7 @@ describe('health check', () => { ); expect(action.getAttribute('href')).toMatchInlineSnapshot( - `"https://www.elastic.co/guide/en/kibana/mocked-test-branch/configuring-tls.html"` + `"https://www.elastic.co/guide/en/elasticsearch/reference/mocked-test-branch/security-basic-setup.html#encrypt-internode-communication"` ); }); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_details.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_details.test.tsx index d198c82366fbf..c7c41ac4e8171 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_details.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_details.test.tsx @@ -39,6 +39,10 @@ jest.mock('react-router-dom', () => ({ }), })); +jest.mock('../../../lib/action_connector_api', () => ({ + loadAllActions: jest.fn().mockResolvedValue([]), +})); + jest.mock('../../../lib/capabilities', () => ({ hasAllPrivilege: jest.fn(() => true), hasSaveAlertsCapability: jest.fn(() => true), @@ -60,24 +64,22 @@ const authorizedConsumers = { }; const recoveryActionGroup: ActionGroup<'recovered'> = { id: 'recovered', name: 'Recovered' }; -describe('alert_details', () => { - // mock Api handlers +const alertType: AlertType = { + id: '.noop', + name: 'No Op', + actionGroups: [{ id: 'default', name: 'Default' }], + recoveryActionGroup, + actionVariables: { context: [], state: [], params: [] }, + defaultActionGroupId: 'default', + minimumLicenseRequired: 'basic', + producer: ALERTS_FEATURE_ID, + authorizedConsumers, + enabledInLicense: true, +}; +describe('alert_details', () => { it('renders the alert name as a title', () => { const alert = mockAlert(); - const alertType: AlertType = { - id: '.noop', - name: 'No Op', - actionGroups: [{ id: 'default', name: 'Default' }], - recoveryActionGroup, - actionVariables: { context: [], state: [], params: [] }, - defaultActionGroupId: 'default', - minimumLicenseRequired: 'basic', - producer: ALERTS_FEATURE_ID, - authorizedConsumers, - enabledInLicense: true, - }; - expect( shallow( @@ -87,19 +89,6 @@ describe('alert_details', () => { it('renders the alert type badge', () => { const alert = mockAlert(); - const alertType: AlertType = { - id: '.noop', - name: 'No Op', - actionGroups: [{ id: 'default', name: 'Default' }], - recoveryActionGroup, - actionVariables: { context: [], state: [], params: [] }, - defaultActionGroupId: 'default', - minimumLicenseRequired: 'basic', - producer: ALERTS_FEATURE_ID, - authorizedConsumers, - enabledInLicense: true, - }; - expect( shallow( @@ -118,19 +107,6 @@ describe('alert_details', () => { }, }, }); - const alertType: AlertType = { - id: '.noop', - name: 'No Op', - actionGroups: [{ id: 'default', name: 'Default' }], - recoveryActionGroup, - actionVariables: { context: [], state: [], params: [] }, - defaultActionGroupId: 'default', - minimumLicenseRequired: 'basic', - producer: ALERTS_FEATURE_ID, - authorizedConsumers, - enabledInLicense: true, - }; - expect( shallow( @@ -155,19 +131,6 @@ describe('alert_details', () => { ], }); - const alertType: AlertType = { - id: '.noop', - name: 'No Op', - actionGroups: [{ id: 'default', name: 'Default' }], - recoveryActionGroup, - actionVariables: { context: [], state: [], params: [] }, - defaultActionGroupId: 'default', - minimumLicenseRequired: 'basic', - producer: ALERTS_FEATURE_ID, - authorizedConsumers, - enabledInLicense: true, - }; - const actionTypes: ActionType[] = [ { id: '.server-log', @@ -212,18 +175,6 @@ describe('alert_details', () => { }, ], }); - const alertType: AlertType = { - id: '.noop', - name: 'No Op', - actionGroups: [{ id: 'default', name: 'Default' }], - recoveryActionGroup, - actionVariables: { context: [], state: [], params: [] }, - defaultActionGroupId: 'default', - producer: ALERTS_FEATURE_ID, - minimumLicenseRequired: 'basic', - authorizedConsumers, - enabledInLicense: true, - }; const actionTypes: ActionType[] = [ { id: '.server-log', @@ -273,20 +224,6 @@ describe('alert_details', () => { describe('links', () => { it('links to the app that created the alert', () => { const alert = mockAlert(); - - const alertType: AlertType = { - id: '.noop', - name: 'No Op', - actionGroups: [{ id: 'default', name: 'Default' }], - recoveryActionGroup, - actionVariables: { context: [], state: [], params: [] }, - defaultActionGroupId: 'default', - producer: ALERTS_FEATURE_ID, - authorizedConsumers, - minimumLicenseRequired: 'basic', - enabledInLicense: true, - }; - expect( shallow( @@ -296,19 +233,6 @@ describe('alert_details', () => { it('links to the Edit flyout', () => { const alert = mockAlert(); - - const alertType: AlertType = { - id: '.noop', - name: 'No Op', - actionGroups: [{ id: 'default', name: 'Default' }], - recoveryActionGroup, - actionVariables: { context: [], state: [], params: [] }, - defaultActionGroupId: 'default', - producer: ALERTS_FEATURE_ID, - authorizedConsumers, - minimumLicenseRequired: 'basic', - enabledInLicense: true, - }; const pageHeaderProps = shallow( ) @@ -316,22 +240,22 @@ describe('alert_details', () => { .props() as EuiPageHeaderProps; const rightSideItems = pageHeaderProps.rightSideItems; expect(!!rightSideItems && rightSideItems[2]!).toMatchInlineSnapshot(` - - - - - - `); + + + + + + `); }); }); }); @@ -341,20 +265,6 @@ describe('disable button', () => { const alert = mockAlert({ enabled: true, }); - - const alertType: AlertType = { - id: '.noop', - name: 'No Op', - actionGroups: [{ id: 'default', name: 'Default' }], - recoveryActionGroup, - actionVariables: { context: [], state: [], params: [] }, - defaultActionGroupId: 'default', - producer: ALERTS_FEATURE_ID, - authorizedConsumers, - minimumLicenseRequired: 'basic', - enabledInLicense: true, - }; - const enableButton = shallow( ) @@ -368,55 +278,43 @@ describe('disable button', () => { }); }); - it('should render a enable button when alert is disabled', () => { + it('should render a enable button and empty state when alert is disabled', async () => { const alert = mockAlert({ enabled: false, }); - - const alertType: AlertType = { - id: '.noop', - name: 'No Op', - actionGroups: [{ id: 'default', name: 'Default' }], - recoveryActionGroup, - actionVariables: { context: [], state: [], params: [] }, - defaultActionGroupId: 'default', - producer: ALERTS_FEATURE_ID, - authorizedConsumers, - minimumLicenseRequired: 'basic', - enabledInLicense: true, - }; - - const enableButton = shallow( + const wrapper = mountWithIntl( - ) - .find(EuiSwitch) - .find('[name="enable"]') - .first(); + ); + + await act(async () => { + await nextTick(); + wrapper.update(); + }); + const enableButton = wrapper.find(EuiSwitch).find('[name="enable"]').first(); + const disabledEmptyPrompt = wrapper.find('[data-test-subj="disabledEmptyPrompt"]'); + const disabledEmptyPromptAction = wrapper.find('[data-test-subj="disabledEmptyPromptAction"]'); expect(enableButton.props()).toMatchObject({ checked: false, disabled: false, }); + expect(disabledEmptyPrompt.exists()).toBeTruthy(); + expect(disabledEmptyPromptAction.exists()).toBeTruthy(); + + disabledEmptyPromptAction.first().simulate('click'); + + await act(async () => { + await nextTick(); + wrapper.update(); + }); + + expect(mockAlertApis.enableAlert).toHaveBeenCalledTimes(1); }); - it('should enable the alert when alert is disabled and button is clicked', () => { + it('should disable the alert when alert is enabled and button is clicked', () => { const alert = mockAlert({ enabled: true, }); - - const alertType: AlertType = { - id: '.noop', - name: 'No Op', - actionGroups: [{ id: 'default', name: 'Default' }], - recoveryActionGroup, - actionVariables: { context: [], state: [], params: [] }, - defaultActionGroupId: 'default', - producer: ALERTS_FEATURE_ID, - authorizedConsumers, - minimumLicenseRequired: 'basic', - enabledInLicense: true, - }; - const disableAlert = jest.fn(); const enableButton = shallow( { expect(disableAlert).toHaveBeenCalledTimes(1); }); - it('should disable the alert when alert is enabled and button is clicked', () => { + it('should enable the alert when alert is disabled and button is clicked', () => { const alert = mockAlert({ enabled: false, }); - - const alertType: AlertType = { - id: '.noop', - name: 'No Op', - actionGroups: [{ id: 'default', name: 'Default' }], - recoveryActionGroup, - actionVariables: { context: [], state: [], params: [] }, - defaultActionGroupId: 'default', - producer: ALERTS_FEATURE_ID, - authorizedConsumers, - minimumLicenseRequired: 'basic', - enabledInLicense: true, - }; - const enableAlert = jest.fn(); const enableButton = shallow( { }, }); - const alertType: AlertType = { - id: '.noop', - name: 'No Op', - actionGroups: [{ id: 'default', name: 'Default' }], - recoveryActionGroup, - actionVariables: { context: [], state: [], params: [] }, - defaultActionGroupId: 'default', - producer: ALERTS_FEATURE_ID, - authorizedConsumers, - minimumLicenseRequired: 'basic', - enabledInLicense: true, - }; - const disableAlert = jest.fn(); const enableAlert = jest.fn(); const wrapper = mountWithIntl( @@ -565,19 +436,6 @@ describe('disable button', () => { }, }); - const alertType: AlertType = { - id: '.noop', - name: 'No Op', - actionGroups: [{ id: 'default', name: 'Default' }], - recoveryActionGroup, - actionVariables: { context: [], state: [], params: [] }, - defaultActionGroupId: 'default', - producer: ALERTS_FEATURE_ID, - authorizedConsumers, - minimumLicenseRequired: 'basic', - enabledInLicense: true, - }; - const disableAlert = jest.fn(async () => { await new Promise((resolve) => setTimeout(resolve, 6000)); }); @@ -630,27 +488,12 @@ describe('mute button', () => { enabled: true, muteAll: false, }); - - const alertType: AlertType = { - id: '.noop', - name: 'No Op', - actionGroups: [{ id: 'default', name: 'Default' }], - recoveryActionGroup, - actionVariables: { context: [], state: [], params: [] }, - defaultActionGroupId: 'default', - producer: ALERTS_FEATURE_ID, - authorizedConsumers, - minimumLicenseRequired: 'basic', - enabledInLicense: true, - }; - const enableButton = shallow( ) .find(EuiSwitch) .find('[name="mute"]') .first(); - expect(enableButton.props()).toMatchObject({ checked: false, disabled: false, @@ -662,27 +505,12 @@ describe('mute button', () => { enabled: true, muteAll: true, }); - - const alertType: AlertType = { - id: '.noop', - name: 'No Op', - actionGroups: [{ id: 'default', name: 'Default' }], - recoveryActionGroup, - actionVariables: { context: [], state: [], params: [] }, - defaultActionGroupId: 'default', - producer: ALERTS_FEATURE_ID, - authorizedConsumers, - minimumLicenseRequired: 'basic', - enabledInLicense: true, - }; - const enableButton = shallow( ) .find(EuiSwitch) .find('[name="mute"]') .first(); - expect(enableButton.props()).toMatchObject({ checked: true, disabled: false, @@ -694,20 +522,6 @@ describe('mute button', () => { enabled: true, muteAll: false, }); - - const alertType: AlertType = { - id: '.noop', - name: 'No Op', - actionGroups: [{ id: 'default', name: 'Default' }], - recoveryActionGroup, - actionVariables: { context: [], state: [], params: [] }, - defaultActionGroupId: 'default', - producer: ALERTS_FEATURE_ID, - authorizedConsumers, - minimumLicenseRequired: 'basic', - enabledInLicense: true, - }; - const muteAlert = jest.fn(); const enableButton = shallow( { .find(EuiSwitch) .find('[name="mute"]') .first(); - enableButton.simulate('click'); const handler = enableButton.prop('onChange'); expect(typeof handler).toEqual('function'); @@ -735,20 +548,6 @@ describe('mute button', () => { enabled: true, muteAll: true, }); - - const alertType: AlertType = { - id: '.noop', - name: 'No Op', - actionGroups: [{ id: 'default', name: 'Default' }], - recoveryActionGroup, - actionVariables: { context: [], state: [], params: [] }, - defaultActionGroupId: 'default', - producer: ALERTS_FEATURE_ID, - authorizedConsumers, - minimumLicenseRequired: 'basic', - enabledInLicense: true, - }; - const unmuteAlert = jest.fn(); const enableButton = shallow( { .find(EuiSwitch) .find('[name="mute"]') .first(); - enableButton.simulate('click'); const handler = enableButton.prop('onChange'); expect(typeof handler).toEqual('function'); @@ -776,27 +574,12 @@ describe('mute button', () => { enabled: false, muteAll: false, }); - - const alertType: AlertType = { - id: '.noop', - name: 'No Op', - actionGroups: [{ id: 'default', name: 'Default' }], - recoveryActionGroup, - actionVariables: { context: [], state: [], params: [] }, - defaultActionGroupId: 'default', - producer: ALERTS_FEATURE_ID, - authorizedConsumers, - minimumLicenseRequired: 'basic', - enabledInLicense: true, - }; - const enableButton = shallow( ) .find(EuiSwitch) .find('[name="mute"]') .first(); - expect(enableButton.props()).toMatchObject({ checked: false, disabled: true, @@ -843,20 +626,6 @@ describe('edit button', () => { }, ], }); - - const alertType: AlertType = { - id: '.noop', - name: 'No Op', - actionGroups: [{ id: 'default', name: 'Default' }], - recoveryActionGroup, - actionVariables: { context: [], state: [], params: [] }, - defaultActionGroupId: 'default', - producer: 'alerting', - authorizedConsumers, - minimumLicenseRequired: 'basic', - enabledInLicense: true, - }; - const pageHeaderProps = shallow( { .props() as EuiPageHeaderProps; const rightSideItems = pageHeaderProps.rightSideItems; expect(!!rightSideItems && rightSideItems[2]!).toMatchInlineSnapshot(` - - - - - - `); + + + + + + `); }); it('should not render an edit button when alert editable but actions arent', () => { const { hasExecuteActionsCapability } = jest.requireMock('../../../lib/capabilities'); - hasExecuteActionsCapability.mockReturnValue(false); + hasExecuteActionsCapability.mockReturnValueOnce(false); const alert = mockAlert({ enabled: true, muteAll: false, @@ -902,20 +671,6 @@ describe('edit button', () => { }, ], }); - - const alertType: AlertType = { - id: '.noop', - name: 'No Op', - actionGroups: [{ id: 'default', name: 'Default' }], - recoveryActionGroup, - actionVariables: { context: [], state: [], params: [] }, - defaultActionGroupId: 'default', - producer: 'alerting', - authorizedConsumers, - minimumLicenseRequired: 'basic', - enabledInLicense: true, - }; - expect( shallow( { it('should render an edit button when alert editable but actions arent when there are no actions on the alert', async () => { const { hasExecuteActionsCapability } = jest.requireMock('../../../lib/capabilities'); - hasExecuteActionsCapability.mockReturnValue(false); + hasExecuteActionsCapability.mockReturnValueOnce(false); const alert = mockAlert({ enabled: true, muteAll: false, actions: [], }); - - const alertType: AlertType = { - id: '.noop', - name: 'No Op', - actionGroups: [{ id: 'default', name: 'Default' }], - recoveryActionGroup, - actionVariables: { context: [], state: [], params: [] }, - defaultActionGroupId: 'default', - producer: 'alerting', - authorizedConsumers, - minimumLicenseRequired: 'basic', - enabledInLicense: true, - }; - const pageHeaderProps = shallow( { .props() as EuiPageHeaderProps; const rightSideItems = pageHeaderProps.rightSideItems; expect(!!rightSideItems && rightSideItems[2]!).toMatchInlineSnapshot(` - - - - - - `); + + + + + + `); }); }); -describe('refresh button', () => { - it('should call requestRefresh when clicked', () => { - const alert = mockAlert(); - const alertType: AlertType = { - id: '.noop', - name: 'No Op', - actionGroups: [{ id: 'default', name: 'Default' }], - recoveryActionGroup, - actionVariables: { context: [], state: [], params: [] }, - defaultActionGroupId: 'default', - minimumLicenseRequired: 'basic', - producer: ALERTS_FEATURE_ID, - authorizedConsumers, +describe('broken connector indicator', () => { + const actionTypes: ActionType[] = [ + { + id: '.server-log', + name: 'Server log', + enabled: true, + enabledInConfig: true, enabledInLicense: true, - }; + minimumLicenseRequired: 'basic', + }, + ]; + ruleTypeRegistry.has.mockReturnValue(true); + const alertTypeR: AlertTypeModel = { + id: 'my-alert-type', + iconClass: 'test', + description: 'Alert when testing', + documentationUrl: 'https://localhost.local/docs', + validate: () => { + return { errors: {} }; + }, + alertParamsExpression: jest.fn(), + requiresAppContext: false, + }; + ruleTypeRegistry.get.mockReturnValue(alertTypeR); + useKibanaMock().services.ruleTypeRegistry = ruleTypeRegistry; + const { loadAllActions } = jest.requireMock('../../../lib/action_connector_api'); + loadAllActions.mockResolvedValue([ + { + secrets: {}, + isMissingSecrets: false, + id: 'connector-id-1', + actionTypeId: '.server-log', + name: 'Test connector', + config: {}, + isPreconfigured: false, + }, + { + secrets: {}, + isMissingSecrets: false, + id: 'connector-id-2', + actionTypeId: '.server-log', + name: 'Test connector 2', + config: {}, + isPreconfigured: false, + }, + ]); + it('should not render broken connector indicator or warning if all rule actions connectors exist', async () => { + const alert = mockAlert({ + enabled: true, + muteAll: false, + actions: [ + { + group: 'default', + id: 'connector-id-1', + params: {}, + actionTypeId: '.server-log', + }, + { + group: 'default', + id: 'connector-id-2', + params: {}, + actionTypeId: '.server-log', + }, + ], + }); + const wrapper = mountWithIntl( + + ); + await act(async () => { + await nextTick(); + wrapper.update(); + }); + const brokenConnectorIndicator = wrapper + .find('[data-test-subj="actionWithBrokenConnector"]') + .first(); + const brokenConnectorWarningBanner = wrapper + .find('[data-test-subj="actionWithBrokenConnectorWarningBanner"]') + .first(); + expect(brokenConnectorIndicator.exists()).toBeFalsy(); + expect(brokenConnectorWarningBanner.exists()).toBeFalsy(); + }); + + it('should render broken connector indicator and warning if any rule actions connector does not exist', async () => { + const alert = mockAlert({ + enabled: true, + muteAll: false, + actions: [ + { + group: 'default', + id: 'connector-id-1', + params: {}, + actionTypeId: '.server-log', + }, + { + group: 'default', + id: 'connector-id-2', + params: {}, + actionTypeId: '.server-log', + }, + { + group: 'default', + id: 'connector-id-doesnt-exist', + params: {}, + actionTypeId: '.server-log', + }, + ], + }); + const wrapper = mountWithIntl( + + ); + await act(async () => { + await nextTick(); + wrapper.update(); + }); + const brokenConnectorIndicator = wrapper + .find('[data-test-subj="actionWithBrokenConnector"]') + .first(); + const brokenConnectorWarningBanner = wrapper + .find('[data-test-subj="actionWithBrokenConnectorWarningBanner"]') + .first(); + const brokenConnectorWarningBannerAction = wrapper + .find('[data-test-subj="actionWithBrokenConnectorWarningBannerEdit"]') + .first(); + expect(brokenConnectorIndicator.exists()).toBeTruthy(); + expect(brokenConnectorWarningBanner.exists()).toBeTruthy(); + expect(brokenConnectorWarningBannerAction.exists()).toBeTruthy(); + }); + + it('should render broken connector indicator and warning with no edit button if any rule actions connector does not exist and user has no edit access', async () => { + const alert = mockAlert({ + enabled: true, + muteAll: false, + actions: [ + { + group: 'default', + id: 'connector-id-1', + params: {}, + actionTypeId: '.server-log', + }, + { + group: 'default', + id: 'connector-id-2', + params: {}, + actionTypeId: '.server-log', + }, + { + group: 'default', + id: 'connector-id-doesnt-exist', + params: {}, + actionTypeId: '.server-log', + }, + ], + }); + const { hasExecuteActionsCapability } = jest.requireMock('../../../lib/capabilities'); + hasExecuteActionsCapability.mockReturnValue(false); + const wrapper = mountWithIntl( + + ); + await act(async () => { + await nextTick(); + wrapper.update(); + }); + const brokenConnectorIndicator = wrapper + .find('[data-test-subj="actionWithBrokenConnector"]') + .first(); + const brokenConnectorWarningBanner = wrapper + .find('[data-test-subj="actionWithBrokenConnectorWarningBanner"]') + .first(); + const brokenConnectorWarningBannerAction = wrapper + .find('[data-test-subj="actionWithBrokenConnectorWarningBannerEdit"]') + .first(); + expect(brokenConnectorIndicator.exists()).toBeTruthy(); + expect(brokenConnectorWarningBanner.exists()).toBeTruthy(); + expect(brokenConnectorWarningBannerAction.exists()).toBeFalsy(); + }); +}); + +describe('refresh button', () => { + it('should call requestRefresh when clicked', async () => { + const alert = mockAlert(); const requestRefresh = jest.fn(); const wrapper = mountWithIntl( { /> ); + await act(async () => { + await nextTick(); + wrapper.update(); + }); const refreshButton = wrapper.find('[data-test-subj="refreshAlertsButton"]').first(); expect(refreshButton.exists()).toBeTruthy(); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_details.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_details.tsx index 2558993a13fe6..2b13bdf613d96 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_details.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_details.tsx @@ -22,13 +22,16 @@ import { EuiButtonEmpty, EuiButton, EuiLoadingSpinner, + EuiIconTip, + EuiEmptyPrompt, + EuiPageTemplate, } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; import { AlertExecutionStatusErrorReasons } from '../../../../../../alerting/common'; import { hasAllPrivilege, hasExecuteActionsCapability } from '../../../lib/capabilities'; import { getAlertingSectionBreadcrumb, getAlertDetailsBreadcrumb } from '../../../lib/breadcrumb'; import { getCurrentDocTitle } from '../../../lib/doc_title'; -import { Alert, AlertType, ActionType } from '../../../../types'; +import { Alert, AlertType, ActionType, ActionConnector } from '../../../../types'; import { ComponentOpts as BulkOperationsComponentOpts, withBulkAlertOperations, @@ -40,6 +43,7 @@ import { routeToRuleDetails } from '../../../constants'; import { alertsErrorReasonTranslationsMapping } from '../../alerts_list/translations'; import { useKibana } from '../../../../common/lib/kibana'; import { alertReducer } from '../../alert_form/alert_reducer'; +import { loadAllActions as loadConnectors } from '../../../lib/action_connector_api'; export type AlertDetailsProps = { alert: Alert; @@ -72,6 +76,9 @@ export const AlertDetails: React.FunctionComponent = ({ dispatch({ command: { type: 'setAlert' }, payload: { key: 'alert', value } }); }; + const [hasActionsWithBrokenConnector, setHasActionsWithBrokenConnector] = + useState(false); + // Set breadcrumb and page title useEffect(() => { setBreadcrumbs([ @@ -82,6 +89,28 @@ export const AlertDetails: React.FunctionComponent = ({ // eslint-disable-next-line react-hooks/exhaustive-deps }, []); + // Determine if any attached action has an issue with its connector + useEffect(() => { + (async () => { + let loadedConnectors: ActionConnector[] = []; + try { + loadedConnectors = await loadConnectors({ http }); + } catch (err) { + loadedConnectors = []; + } + + if (loadedConnectors.length > 0) { + const hasActionWithBrokenConnector = alert.actions.some( + (action) => !loadedConnectors.find((connector) => connector.id === action.id) + ); + if (setHasActionsWithBrokenConnector) { + setHasActionsWithBrokenConnector(hasActionWithBrokenConnector); + } + } + })(); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + const canExecuteActions = hasExecuteActionsCapability(capabilities); const canSaveAlert = hasAllPrivilege(alert, alertType) && @@ -197,13 +226,27 @@ export const AlertDetails: React.FunctionComponent = ({ {uniqueActions && uniqueActions.length ? ( <> -

    - {' '} + {hasActionsWithBrokenConnector && ( + -

    + )}
    + {uniqueActions.map((action, index) => ( @@ -358,6 +401,42 @@ export const AlertDetails: React.FunctionComponent = ({ ) : null} + {hasActionsWithBrokenConnector && ( + + + + + {hasEditButton && ( + + + setEditFlyoutVisibility(true)} + > + + + + + )} + + + + )} {alert.enabled ? ( @@ -370,23 +449,46 @@ export const AlertDetails: React.FunctionComponent = ({ ) : ( <> - + + + } - )} - color="warning" - iconType="help" - > -

    - -

    -
    + body={ + <> +

    + +

    + + } + actions={[ + { + setIsEnabledUpdating(true); + setIsEnabled(true); + await enableAlert(alert); + requestRefresh(); + setIsEnabledUpdating(false); + }} + > + Enable + , + ]} + /> + )}
    diff --git a/x-pack/plugins/upgrade_assistant/README.md b/x-pack/plugins/upgrade_assistant/README.md index 255eb94a0318c..6570e7f8d7617 100644 --- a/x-pack/plugins/upgrade_assistant/README.md +++ b/x-pack/plugins/upgrade_assistant/README.md @@ -226,4 +226,29 @@ This is a non-exhaustive list of different error scenarios in Upgrade Assistant. - **Error updating deprecation logging status.** Mock a `404` status code to `PUT /api/upgrade_assistant/deprecation_logging`. Alternatively, edit [this line](https://github.com/elastic/kibana/blob/545c1420c285af8f5eee56f414bd6eca735aea11/x-pack/plugins/upgrade_assistant/public/application/lib/api.ts#L77) locally and replace `deprecation_logging` with `fake_deprecation_logging`. - **Unauthorized error fetching ES deprecations.** Mock a `403` status code to `GET /api/upgrade_assistant/es_deprecations` with the response payload: `{ "statusCode": 403 }` - **Partially upgraded error fetching ES deprecations.** Mock a `426` status code to `GET /api/upgrade_assistant/es_deprecations` with the response payload: `{ "statusCode": 426, "attributes": { "allNodesUpgraded": false } }` -- **Upgraded error fetching ES deprecations.** Mock a `426` status code to `GET /api/upgrade_assistant/es_deprecations` with the response payload: `{ "statusCode": 426, "attributes": { "allNodesUpgraded": true } }` \ No newline at end of file +- **Upgraded error fetching ES deprecations.** Mock a `426` status code to `GET /api/upgrade_assistant/es_deprecations` with the response payload: `{ "statusCode": 426, "attributes": { "allNodesUpgraded": true } }` + +### Telemetry + +The Upgrade Assistant tracks several triggered events in the UI, using Kibana Usage Collection service's [UI counters](https://github.com/elastic/kibana/blob/master/src/plugins/usage_collection/README.mdx#ui-counters). + +**Overview page** +- Component loaded +- Click event for "Create snapshot" button +- Click event for "View deprecation logs in Observability" link +- Click event for "Analyze logs in Discover" link +- Click event for "Reset counter" button + +**ES deprecations page** +- Component loaded +- Click events for starting and stopping reindex tasks +- Click events for upgrading or deleting a Machine Learning snapshot +- Click event for deleting a deprecated index setting + +**Kibana deprecations page** +- Component loaded +- Click event for "Quick resolve" button + +In addition to UI counters, the Upgrade Assistant has a [custom usage collector](https://github.com/elastic/kibana/blob/master/src/plugins/usage_collection/README.mdx#custom-collector). It currently is only responsible for tracking whether the user has deprecation logging enabled or not. + +For testing instructions, refer to the [Kibana Usage Collection service README](https://github.com/elastic/kibana/blob/master/src/plugins/usage_collection/README.mdx#testing). \ No newline at end of file diff --git a/x-pack/plugins/upgrade_assistant/__jest__/client_integration/es_deprecations/error_handling.test.ts b/x-pack/plugins/upgrade_assistant/__jest__/client_integration/es_deprecations/error_handling.test.ts index 6314b4630f850..2f0c8f0597ec3 100644 --- a/x-pack/plugins/upgrade_assistant/__jest__/client_integration/es_deprecations/error_handling.test.ts +++ b/x-pack/plugins/upgrade_assistant/__jest__/client_integration/es_deprecations/error_handling.test.ts @@ -31,13 +31,10 @@ describe('Error handling', () => { testBed = await setupElasticsearchPage({ isReadOnlyMode: false }); }); - const { component, exists, find } = testBed; - + const { component, find } = testBed; component.update(); - - expect(exists('permissionsError')).toBe(true); - expect(find('permissionsError').text()).toContain( - 'You are not authorized to view Elasticsearch deprecations.' + expect(find('deprecationsPageLoadingError').text()).toContain( + 'You are not authorized to view Elasticsearch deprecation issues.' ); }); @@ -59,12 +56,11 @@ describe('Error handling', () => { testBed = await setupElasticsearchPage({ isReadOnlyMode: false }); }); - const { component, exists, find } = testBed; - + const { component, find } = testBed; component.update(); - - expect(exists('upgradedCallout')).toBe(true); - expect(find('upgradedCallout').text()).toContain('All Elasticsearch nodes have been upgraded.'); + expect(find('deprecationsPageLoadingError').text()).toContain( + 'All Elasticsearch nodes have been upgraded.' + ); }); it('shows partially upgrade error when nodes are running different versions', async () => { @@ -83,12 +79,9 @@ describe('Error handling', () => { testBed = await setupElasticsearchPage({ isReadOnlyMode: false }); }); - const { component, exists, find } = testBed; - + const { component, find } = testBed; component.update(); - - expect(exists('partiallyUpgradedWarning')).toBe(true); - expect(find('partiallyUpgradedWarning').text()).toContain( + expect(find('deprecationsPageLoadingError').text()).toContain( 'Upgrade Kibana to the same version as your Elasticsearch cluster. One or more nodes in the cluster is running a different version than Kibana.' ); }); @@ -106,11 +99,10 @@ describe('Error handling', () => { testBed = await setupElasticsearchPage({ isReadOnlyMode: false }); }); - const { component, exists, find } = testBed; - + const { component, find } = testBed; component.update(); - - expect(exists('requestError')).toBe(true); - expect(find('requestError').text()).toContain('Could not retrieve Elasticsearch deprecations.'); + expect(find('deprecationsPageLoadingError').text()).toContain( + 'Could not retrieve Elasticsearch deprecation issues.' + ); }); }); diff --git a/x-pack/plugins/upgrade_assistant/__jest__/client_integration/helpers/setup_environment.tsx b/x-pack/plugins/upgrade_assistant/__jest__/client_integration/helpers/setup_environment.tsx index 8c164c3d66144..1e49bf2927fc1 100644 --- a/x-pack/plugins/upgrade_assistant/__jest__/client_integration/helpers/setup_environment.tsx +++ b/x-pack/plugins/upgrade_assistant/__jest__/client_integration/helpers/setup_environment.tsx @@ -13,6 +13,7 @@ import axiosXhrAdapter from 'axios/lib/adapters/xhr'; import { HttpSetup } from 'src/core/public'; +import { AuthorizationContext, Authorization, Privileges } from '../../../public/shared_imports'; import { AppContextProvider } from '../../../public/application/app_context'; import { apiService } from '../../../public/application/lib/api'; import { breadcrumbService } from '../../../public/application/lib/breadcrumbs'; @@ -25,8 +26,15 @@ const { GlobalFlyoutProvider } = GlobalFlyout; const mockHttpClient = axios.create({ adapter: axiosXhrAdapter }); +const createAuthorizationContextValue = (privileges: Privileges) => { + return { + isLoading: false, + privileges: privileges ?? { hasAllPrivileges: false, missingPrivileges: {} }, + } as Authorization; +}; + export const WithAppDependencies = - (Comp: any, overrides: Record = {}) => + (Comp: any, { privileges, ...overrides }: Record = {}) => (props: Record) => { apiService.setup(mockHttpClient as unknown as HttpSetup); breadcrumbService.setup(() => ''); @@ -34,11 +42,15 @@ export const WithAppDependencies = const appContextMock = getAppContextMock() as unknown as AppDependencies; return ( - - - - - + + + + + + + ); }; diff --git a/x-pack/plugins/upgrade_assistant/__jest__/client_integration/kibana_deprecations/error_handling.test.ts b/x-pack/plugins/upgrade_assistant/__jest__/client_integration/kibana_deprecations/error_handling.test.ts index 83e0a884a3119..650af2025ee2b 100644 --- a/x-pack/plugins/upgrade_assistant/__jest__/client_integration/kibana_deprecations/error_handling.test.ts +++ b/x-pack/plugins/upgrade_assistant/__jest__/client_integration/kibana_deprecations/error_handling.test.ts @@ -91,12 +91,9 @@ describe('Error handling', () => { }); }); - const { component, exists, find } = testBed; - + const { component, find } = testBed; component.update(); - - expect(exists('kibanaRequestError')).toBe(true); - expect(find('kibanaRequestError').text()).toContain( + expect(find('deprecationsPageLoadingError').text()).toContain( 'Could not retrieve Kibana deprecation issues' ); }); diff --git a/x-pack/plugins/upgrade_assistant/__jest__/client_integration/overview/fix_issues_step/elasticsearch_deprecation_issues.test.tsx b/x-pack/plugins/upgrade_assistant/__jest__/client_integration/overview/fix_issues_step/elasticsearch_deprecation_issues.test.tsx index d3c92625c7a34..bbb8b049ec2b8 100644 --- a/x-pack/plugins/upgrade_assistant/__jest__/client_integration/overview/fix_issues_step/elasticsearch_deprecation_issues.test.tsx +++ b/x-pack/plugins/upgrade_assistant/__jest__/client_integration/overview/fix_issues_step/elasticsearch_deprecation_issues.test.tsx @@ -118,9 +118,11 @@ describe('Overview - Fix deprecation issues step - Elasticsearch deprecations', testBed = await setupOverviewPage(); }); - const { component, exists } = testBed; + const { component, find } = testBed; component.update(); - expect(exists('esRequestErrorIconTip')).toBe(true); + expect(find('loadingIssuesError').text()).toBe( + 'Could not retrieve Elasticsearch deprecation issues.' + ); }); test('handles unauthorized error', async () => { @@ -136,9 +138,11 @@ describe('Overview - Fix deprecation issues step - Elasticsearch deprecations', testBed = await setupOverviewPage(); }); - const { component, exists } = testBed; + const { component, find } = testBed; component.update(); - expect(exists('unauthorizedErrorIconTip')).toBe(true); + expect(find('loadingIssuesError').text()).toBe( + 'You are not authorized to view Elasticsearch deprecation issues.' + ); }); test('handles partially upgraded error', async () => { @@ -157,9 +161,11 @@ describe('Overview - Fix deprecation issues step - Elasticsearch deprecations', testBed = await setupOverviewPage({ isReadOnlyMode: false }); }); - const { component, exists } = testBed; + const { component, find } = testBed; component.update(); - expect(exists('partiallyUpgradedErrorIconTip')).toBe(true); + expect(find('loadingIssuesError').text()).toBe( + 'Upgrade Kibana to the same version as your Elasticsearch cluster. One or more nodes in the cluster is running a different version than Kibana.' + ); }); test('handles upgrade error', async () => { @@ -178,9 +184,9 @@ describe('Overview - Fix deprecation issues step - Elasticsearch deprecations', testBed = await setupOverviewPage({ isReadOnlyMode: false }); }); - const { component, exists } = testBed; + const { component, find } = testBed; component.update(); - expect(exists('upgradedErrorIconTip')).toBe(true); + expect(find('loadingIssuesError').text()).toBe('All Elasticsearch nodes have been upgraded.'); }); }); }); diff --git a/x-pack/plugins/upgrade_assistant/__jest__/client_integration/overview/fix_issues_step/kibana_deprecation_issues.test.tsx b/x-pack/plugins/upgrade_assistant/__jest__/client_integration/overview/fix_issues_step/kibana_deprecation_issues.test.tsx index a6d4e01204781..65d413d5ec433 100644 --- a/x-pack/plugins/upgrade_assistant/__jest__/client_integration/overview/fix_issues_step/kibana_deprecation_issues.test.tsx +++ b/x-pack/plugins/upgrade_assistant/__jest__/client_integration/overview/fix_issues_step/kibana_deprecation_issues.test.tsx @@ -122,9 +122,11 @@ describe('Overview - Fix deprecation issues step - Kibana deprecations', () => { }); }); - const { component, exists } = testBed; + const { component, find } = testBed; component.update(); - expect(exists('kibanaRequestErrorIconTip')).toBe(true); + expect(find('loadingIssuesError').text()).toBe( + 'Could not retrieve Kibana deprecation issues.' + ); }); }); }); diff --git a/x-pack/plugins/upgrade_assistant/__jest__/client_integration/overview/fix_logs_step/fix_logs_step.test.tsx b/x-pack/plugins/upgrade_assistant/__jest__/client_integration/overview/fix_logs_step/fix_logs_step.test.tsx index 96c0a874419a9..7f58b36d29c36 100644 --- a/x-pack/plugins/upgrade_assistant/__jest__/client_integration/overview/fix_logs_step/fix_logs_step.test.tsx +++ b/x-pack/plugins/upgrade_assistant/__jest__/client_integration/overview/fix_logs_step/fix_logs_step.test.tsx @@ -22,10 +22,13 @@ jest.mock('../../../../public/application/lib/logs_checkpoint', () => { }); import { DeprecationLoggingStatus } from '../../../../common/types'; -import { DEPRECATION_LOGS_SOURCE_ID } from '../../../../common/constants'; import { OverviewTestBed, setupOverviewPage } from '../overview.helpers'; import { setupEnvironment, advanceTime } from '../../helpers'; -import { DEPRECATION_LOGS_COUNT_POLL_INTERVAL_MS } from '../../../../common/constants'; +import { + DEPRECATION_LOGS_INDEX, + DEPRECATION_LOGS_SOURCE_ID, + DEPRECATION_LOGS_COUNT_POLL_INTERVAL_MS, +} from '../../../../common/constants'; const getLoggingResponse = (toggle: boolean): DeprecationLoggingStatus => ({ isDeprecationLogIndexingEnabled: toggle, @@ -182,6 +185,7 @@ describe('Overview - Fix deprecation logs step', () => { expect(exists('externalLinksTitle')).toBe(false); expect(exists('deprecationsCountTitle')).toBe(false); + expect(exists('apiCompatibilityNoteTitle')).toBe(false); }); }); @@ -370,4 +374,57 @@ describe('Overview - Fix deprecation logs step', () => { }); }); }); + + describe('Step 4 - API compatibility header', () => { + beforeEach(async () => { + httpRequestsMockHelpers.setLoadDeprecationLoggingResponse(getLoggingResponse(true)); + }); + + test('It shows copy with compatibility api header advice', async () => { + await act(async () => { + testBed = await setupOverviewPage(); + }); + + const { exists, component } = testBed; + + component.update(); + + expect(exists('apiCompatibilityNoteTitle')).toBe(true); + }); + }); + + describe('Privileges check', () => { + test(`permissions warning callout is hidden if user has the right privileges`, async () => { + const { exists } = testBed; + + // Index privileges warning callout should not be shown + expect(exists('noIndexPermissionsCallout')).toBe(false); + // Analyze logs and Resolve logs sections should be shown + expect(exists('externalLinksTitle')).toBe(true); + expect(exists('deprecationsCountTitle')).toBe(true); + }); + + test(`doesn't show analyze and resolve logs if it doesn't have the right privileges`, async () => { + await act(async () => { + testBed = await setupOverviewPage({ + privileges: { + hasAllPrivileges: false, + missingPrivileges: { + index: [DEPRECATION_LOGS_INDEX], + }, + }, + }); + }); + + const { exists, component } = testBed; + + component.update(); + + // No index privileges warning callout should be shown + expect(exists('noIndexPermissionsCallout')).toBe(true); + // Analyze logs and Resolve logs sections should be hidden + expect(exists('externalLinksTitle')).toBe(false); + expect(exists('deprecationsCountTitle')).toBe(false); + }); + }); }); diff --git a/x-pack/plugins/upgrade_assistant/common/constants.ts b/x-pack/plugins/upgrade_assistant/common/constants.ts index 23436a06ae92f..629e7a725efaa 100644 --- a/x-pack/plugins/upgrade_assistant/common/constants.ts +++ b/x-pack/plugins/upgrade_assistant/common/constants.ts @@ -39,4 +39,4 @@ export const DEPRECATION_LOGS_INDEX = '.logs-deprecation.elasticsearch-default'; export const DEPRECATION_LOGS_INDEX_PATTERN = '.logs-deprecation.elasticsearch-default'; export const CLOUD_BACKUP_STATUS_POLL_INTERVAL_MS = 60000; -export const DEPRECATION_LOGS_COUNT_POLL_INTERVAL_MS = 60000; +export const DEPRECATION_LOGS_COUNT_POLL_INTERVAL_MS = 15000; diff --git a/x-pack/plugins/upgrade_assistant/common/types.ts b/x-pack/plugins/upgrade_assistant/common/types.ts index 6fd298dc9e003..6368e3b1e717c 100644 --- a/x-pack/plugins/upgrade_assistant/common/types.ts +++ b/x-pack/plugins/upgrade_assistant/common/types.ts @@ -11,6 +11,8 @@ import { } from '@elastic/elasticsearch/api/types'; import { SavedObject, SavedObjectAttributes } from 'src/core/public'; +export type DeprecationSource = 'Kibana' | 'Elasticsearch'; + export enum ReindexStep { // Enum values are spaced out by 10 to give us room to insert steps in between. created = 0, @@ -136,32 +138,7 @@ export interface UIReindex { stop: boolean; } -export interface UpgradeAssistantTelemetrySavedObject { - ui_open: { - overview: number; - elasticsearch: number; - kibana: number; - }; - ui_reindex: { - close: number; - open: number; - start: number; - stop: number; - }; -} - export interface UpgradeAssistantTelemetry { - ui_open: { - overview: number; - elasticsearch: number; - kibana: number; - }; - ui_reindex: { - close: number; - open: number; - start: number; - stop: number; - }; features: { deprecation_logging: { enabled: boolean; @@ -169,10 +146,6 @@ export interface UpgradeAssistantTelemetry { }; } -export interface UpgradeAssistantTelemetrySavedObjectAttributes { - [key: string]: any; -} - export type MIGRATION_DEPRECATION_LEVEL = 'none' | 'info' | 'warning' | 'critical'; export interface DeprecationInfo { level: MIGRATION_DEPRECATION_LEVEL; diff --git a/x-pack/plugins/upgrade_assistant/public/application/app.tsx b/x-pack/plugins/upgrade_assistant/public/application/app.tsx index 0d32d1b661b84..be2092b46a79a 100644 --- a/x-pack/plugins/upgrade_assistant/public/application/app.tsx +++ b/x-pack/plugins/upgrade_assistant/public/application/app.tsx @@ -10,8 +10,9 @@ import { Router, Switch, Route, Redirect } from 'react-router-dom'; import { ScopedHistory } from 'src/core/public'; import { RedirectAppLinks } from '../../../../../src/plugins/kibana_react/public'; -import { APP_WRAPPER_CLASS, GlobalFlyout } from '../shared_imports'; +import { APP_WRAPPER_CLASS, GlobalFlyout, AuthorizationProvider } from '../shared_imports'; import { AppDependencies } from '../types'; +import { API_BASE_PATH } from '../../common/constants'; import { AppContextProvider, useAppContext } from './app_context'; import { EsDeprecations, ComingSoonPrompt, KibanaDeprecations, Overview } from './components'; @@ -46,18 +47,20 @@ export const AppWithRouter = ({ history }: { history: ScopedHistory }) => { export const RootComponent = (dependencies: AppDependencies) => { const { history, - core: { i18n, application }, + core: { i18n, application, http }, } = dependencies.services; return ( - - - - - - - + + + + + + + + + ); }; diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/index_settings/flyout.tsx b/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/index_settings/flyout.tsx index 24c1897fbdd02..e00edb4f3b11d 100644 --- a/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/index_settings/flyout.tsx +++ b/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/index_settings/flyout.tsx @@ -5,8 +5,9 @@ * 2.0. */ -import React from 'react'; +import React, { useCallback } from 'react'; import { i18n } from '@kbn/i18n'; +import { METRIC_TYPE } from '@kbn/analytics'; import { EuiButton, EuiButtonEmpty, @@ -25,6 +26,7 @@ import { } from '@elastic/eui'; import { EnrichedDeprecationInfo, IndexSettingAction } from '../../../../../../common/types'; import type { ResponseError } from '../../../../lib/api'; +import { uiMetricService, UIM_INDEX_SETTINGS_DELETE_CLICK } from '../../../../lib/ui_metric'; import type { Status } from '../../../types'; import { DeprecationBadge } from '../../../shared'; @@ -107,6 +109,11 @@ export const RemoveIndexSettingsFlyout = ({ // Flag used to hide certain parts of the UI if the deprecation has been resolved or is in progress const isResolvable = ['idle', 'error'].includes(statusType); + const onRemoveSettings = useCallback(() => { + uiMetricService.trackUiMetric(METRIC_TYPE.CLICK, UIM_INDEX_SETTINGS_DELETE_CLICK); + removeIndexSettings(index!, (correctiveAction as IndexSettingAction).deprecatedSettings); + }, [correctiveAction, index, removeIndexSettings]); + return ( <> @@ -190,12 +197,7 @@ export const RemoveIndexSettingsFlyout = ({ fill data-test-subj="deleteSettingsButton" color="danger" - onClick={() => - removeIndexSettings( - index!, - (correctiveAction as IndexSettingAction).deprecatedSettings - ) - } + onClick={onRemoveSettings} > {statusType === 'error' ? i18nTexts.retryRemoveButtonLabel diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/ml_snapshots/flyout.tsx b/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/ml_snapshots/flyout.tsx index 4e3d77ba72ae8..2a36f3e33e83c 100644 --- a/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/ml_snapshots/flyout.tsx +++ b/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/ml_snapshots/flyout.tsx @@ -8,6 +8,7 @@ import React from 'react'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; +import { METRIC_TYPE } from '@kbn/analytics'; import { EuiButton, @@ -25,6 +26,11 @@ import { } from '@elastic/eui'; import { EnrichedDeprecationInfo } from '../../../../../../common/types'; +import { + uiMetricService, + UIM_ML_SNAPSHOT_UPGRADE_CLICK, + UIM_ML_SNAPSHOT_DELETE_CLICK, +} from '../../../../lib/ui_metric'; import { DeprecationBadge } from '../../../shared'; import { MlSnapshotContext } from './context'; import { useAppContext } from '../../../../app_context'; @@ -173,11 +179,13 @@ export const FixSnapshotsFlyout = ({ const isResolved = snapshotState.status === 'complete'; const onUpgradeSnapshot = () => { + uiMetricService.trackUiMetric(METRIC_TYPE.CLICK, UIM_ML_SNAPSHOT_UPGRADE_CLICK); upgradeSnapshot(); closeFlyout(); }; const onDeleteSnapshot = () => { + uiMetricService.trackUiMetric(METRIC_TYPE.CLICK, UIM_ML_SNAPSHOT_DELETE_CLICK); deleteSnapshot(); closeFlyout(); }; diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/reindex/flyout/__snapshots__/checklist_step.test.tsx.snap b/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/reindex/flyout/__snapshots__/checklist_step.test.tsx.snap index 9357e7d2d9b6c..ae03647042dbc 100644 --- a/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/reindex/flyout/__snapshots__/checklist_step.test.tsx.snap +++ b/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/reindex/flyout/__snapshots__/checklist_step.test.tsx.snap @@ -42,7 +42,7 @@ exports[`ChecklistFlyout renders 1`] = ` { + uiMetricService.trackUiMetric(METRIC_TYPE.CLICK, UIM_REINDEX_START_CLICK); + startReindex(); + }, [startReindex]); + + const onStopReindex = useCallback(() => { + uiMetricService.trackUiMetric(METRIC_TYPE.CLICK, UIM_REINDEX_STOP_CLICK); + cancelReindex(); + }, [cancelReindex]); + return ( @@ -124,7 +140,7 @@ export const ChecklistFlyoutStep: React.FunctionComponent<{ /> - + @@ -142,7 +158,7 @@ export const ChecklistFlyoutStep: React.FunctionComponent<{ fill color={status === ReindexStatus.paused ? 'warning' : 'primary'} iconType={status === ReindexStatus.paused ? 'play' : undefined} - onClick={startReindex} + onClick={onStartReindex} isLoading={loading} disabled={loading || !hasRequiredPrivileges} > diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/reindex/table_row.tsx b/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/reindex/table_row.tsx index c2a14ca5be858..1059720e66a59 100644 --- a/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/reindex/table_row.tsx +++ b/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/reindex/table_row.tsx @@ -7,9 +7,15 @@ import React, { useState, useEffect, useCallback } from 'react'; import { EuiTableRowCell } from '@elastic/eui'; +import { METRIC_TYPE } from '@kbn/analytics'; import { EnrichedDeprecationInfo } from '../../../../../../common/types'; import { GlobalFlyout } from '../../../../../shared_imports'; import { useAppContext } from '../../../../app_context'; +import { + uiMetricService, + UIM_REINDEX_CLOSE_FLYOUT_CLICK, + UIM_REINDEX_OPEN_FLYOUT_CLICK, +} from '../../../../lib/ui_metric'; import { DeprecationTableColumns } from '../../../types'; import { EsDeprecationsTableCells } from '../../es_deprecations_table_cells'; import { ReindexResolutionCell } from './resolution_table_cell'; @@ -29,9 +35,6 @@ const ReindexTableRowCells: React.FunctionComponent = ({ }) => { const [showFlyout, setShowFlyout] = useState(false); const reindexState = useReindexContext(); - const { - services: { api }, - } = useAppContext(); const { addContent: addContentToGlobalFlyout, removeContent: removeContentFromGlobalFlyout } = useGlobalFlyout(); @@ -39,8 +42,8 @@ const ReindexTableRowCells: React.FunctionComponent = ({ const closeFlyout = useCallback(async () => { removeContentFromGlobalFlyout('reindexFlyout'); setShowFlyout(false); - await api.sendReindexTelemetryData({ close: true }); - }, [api, removeContentFromGlobalFlyout]); + uiMetricService.trackUiMetric(METRIC_TYPE.CLICK, UIM_REINDEX_CLOSE_FLYOUT_CLICK); + }, [removeContentFromGlobalFlyout]); useEffect(() => { if (showFlyout) { @@ -64,13 +67,9 @@ const ReindexTableRowCells: React.FunctionComponent = ({ useEffect(() => { if (showFlyout) { - async function sendTelemetry() { - await api.sendReindexTelemetryData({ open: true }); - } - - sendTelemetry(); + uiMetricService.trackUiMetric(METRIC_TYPE.CLICK, UIM_REINDEX_OPEN_FLYOUT_CLICK); } - }, [showFlyout, api]); + }, [showFlyout]); return ( <> diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/reindex/use_reindex_state.tsx b/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/reindex/use_reindex_state.tsx index b87a509d25a55..e1f01be2e0174 100644 --- a/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/reindex/use_reindex_state.tsx +++ b/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/reindex/use_reindex_state.tsx @@ -131,8 +131,6 @@ export const useReindexStatus = ({ indexName, api }: { indexName: string; api: A cancelLoadingState: undefined, }); - api.sendReindexTelemetryData({ start: true }); - const { data, error } = await api.startReindexTask(indexName); if (error) { @@ -149,8 +147,6 @@ export const useReindexStatus = ({ indexName, api }: { indexName: string; api: A }, [api, indexName, reindexState, updateStatus]); const cancelReindex = useCallback(async () => { - api.sendReindexTelemetryData({ stop: true }); - const { error } = await api.cancelReindexTask(indexName); setReindexState({ diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/es_deprecation_errors.tsx b/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/es_deprecation_errors.tsx deleted file mode 100644 index 5e3c7a5fe6cef..0000000000000 --- a/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/es_deprecation_errors.tsx +++ /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 React from 'react'; - -import { EuiCallOut } from '@elastic/eui'; - -import { ResponseError } from '../../lib/api'; -import { getEsDeprecationError } from '../../lib/get_es_deprecation_error'; -interface Props { - error: ResponseError; -} - -export const EsDeprecationErrors: React.FunctionComponent = ({ error }) => { - const { code: errorType, message } = getEsDeprecationError(error); - - switch (errorType) { - case 'unauthorized_error': - return ( - - ); - case 'partially_upgraded_error': - return ( - - ); - case 'upgraded_error': - return ; - case 'request_error': - default: - return ( - - {error.message} - - ); - } -}; diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/es_deprecations.tsx b/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/es_deprecations.tsx index 27908bd07b951..98611e3f4f781 100644 --- a/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/es_deprecations.tsx +++ b/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/es_deprecations.tsx @@ -10,13 +10,15 @@ import { withRouter, RouteComponentProps } from 'react-router-dom'; import { EuiPageHeader, EuiSpacer, EuiPageContent } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; +import { METRIC_TYPE } from '@kbn/analytics'; import { EnrichedDeprecationInfo } from '../../../../common/types'; import { SectionLoading } from '../../../shared_imports'; import { useAppContext } from '../../app_context'; +import { uiMetricService, UIM_ES_DEPRECATIONS_PAGE_LOAD } from '../../lib/ui_metric'; +import { getEsDeprecationError } from '../../lib/get_es_deprecation_error'; +import { DeprecationsPageLoadingError, NoDeprecationsPrompt, DeprecationCount } from '../shared'; import { EsDeprecationsTable } from './es_deprecations_table'; -import { EsDeprecationErrors } from './es_deprecation_errors'; -import { NoDeprecationsPrompt, DeprecationCount } from '../shared'; const getDeprecationCountByLevel = (deprecations: EnrichedDeprecationInfo[]) => { const criticalDeprecations: EnrichedDeprecationInfo[] = []; @@ -54,13 +56,7 @@ export const EsDeprecations = withRouter(({ history }: RouteComponentProps) => { services: { api, breadcrumbs }, } = useAppContext(); - const { - data: esDeprecations, - isLoading, - error, - resendRequest, - isInitialRequest, - } = api.useLoadEsDeprecations(); + const { data: esDeprecations, isLoading, error, resendRequest } = api.useLoadEsDeprecations(); const deprecationsCountByLevel: { warningDeprecations: number; @@ -75,19 +71,16 @@ export const EsDeprecations = withRouter(({ history }: RouteComponentProps) => { }, [breadcrumbs]); useEffect(() => { - if (isLoading === false && isInitialRequest) { - async function sendTelemetryData() { - await api.sendPageTelemetryData({ - elasticsearch: true, - }); - } - - sendTelemetryData(); - } - }, [api, isLoading, isInitialRequest]); + uiMetricService.trackUiMetric(METRIC_TYPE.LOADED, UIM_ES_DEPRECATIONS_PAGE_LOAD); + }, []); if (error) { - return ; + return ( + + ); } if (isLoading) { diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/kibana_deprecations/deprecation_details_flyout.tsx b/x-pack/plugins/upgrade_assistant/public/application/components/kibana_deprecations/deprecation_details_flyout.tsx index d41b0a95e7679..6ec9ad175e112 100644 --- a/x-pack/plugins/upgrade_assistant/public/application/components/kibana_deprecations/deprecation_details_flyout.tsx +++ b/x-pack/plugins/upgrade_assistant/public/application/components/kibana_deprecations/deprecation_details_flyout.tsx @@ -5,9 +5,10 @@ * 2.0. */ -import React from 'react'; +import React, { useCallback } from 'react'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; +import { METRIC_TYPE } from '@kbn/analytics'; import { EuiButtonEmpty, @@ -24,6 +25,7 @@ import { EuiSpacer, } from '@elastic/eui'; +import { uiMetricService, UIM_KIBANA_QUICK_RESOLVE_CLICK } from '../../lib/ui_metric'; import type { DeprecationResolutionState, KibanaDeprecationDetails } from './kibana_deprecations'; import { DeprecationBadge } from '../shared'; @@ -134,6 +136,11 @@ export const DeprecationDetailsFlyout = ({ const isCurrent = deprecationResolutionState?.id === deprecation.id; const isResolved = isCurrent && deprecationResolutionState?.resolveDeprecationStatus === 'ok'; + const onResolveDeprecation = useCallback(() => { + uiMetricService.trackUiMetric(METRIC_TYPE.CLICK, UIM_KIBANA_QUICK_RESOLVE_CLICK); + resolveDeprecation(deprecation); + }, [deprecation, resolveDeprecation]); + return ( <> @@ -225,7 +232,7 @@ export const DeprecationDetailsFlyout = ({ resolveDeprecation(deprecation)} + onClick={onResolveDeprecation} isLoading={Boolean( deprecationResolutionState?.resolveDeprecationStatus === 'in_progress' )} diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/kibana_deprecations/kibana_deprecations.tsx b/x-pack/plugins/upgrade_assistant/public/application/components/kibana_deprecations/kibana_deprecations.tsx index 35fc7127dfe0a..b488c84f255cc 100644 --- a/x-pack/plugins/upgrade_assistant/public/application/components/kibana_deprecations/kibana_deprecations.tsx +++ b/x-pack/plugins/upgrade_assistant/public/application/components/kibana_deprecations/kibana_deprecations.tsx @@ -8,14 +8,16 @@ import React, { useEffect, useState, useCallback, useMemo } from 'react'; import uuid from 'uuid'; import { withRouter, RouteComponentProps } from 'react-router-dom'; -import { EuiPageContent, EuiPageHeader, EuiSpacer, EuiCallOut, EuiEmptyPrompt } from '@elastic/eui'; +import { EuiPageContent, EuiPageHeader, EuiSpacer, EuiCallOut } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; +import { METRIC_TYPE } from '@kbn/analytics'; import type { DomainDeprecationDetails } from 'kibana/public'; import { SectionLoading, GlobalFlyout } from '../../../shared_imports'; import { useAppContext } from '../../app_context'; -import { NoDeprecationsPrompt, DeprecationCount } from '../shared'; +import { uiMetricService, UIM_KIBANA_DEPRECATIONS_PAGE_LOAD } from '../../lib/ui_metric'; +import { DeprecationsPageLoadingError, NoDeprecationsPrompt, DeprecationCount } from '../shared'; import { KibanaDeprecationsTable } from './kibana_deprecations_table'; import { DeprecationDetailsFlyout, @@ -67,15 +69,6 @@ const i18nTexts = { pluginIds: pluginIds.join(', '), }, }), - requestErrorTitle: i18n.translate('xpack.upgradeAssistant.kibanaDeprecations.requestErrorTitle', { - defaultMessage: 'Could not retrieve Kibana deprecation issues', - }), - requestErrorDescription: i18n.translate( - 'xpack.upgradeAssistant.kibanaDeprecationErrors.requestErrorDescription', - { - defaultMessage: 'Check the Kibana server logs for errors.', - } - ), }; export interface DeprecationResolutionState { @@ -125,7 +118,6 @@ export const KibanaDeprecations = withRouter(({ history }: RouteComponentProps) services: { core: { deprecations }, breadcrumbs, - api, }, } = useAppContext(); @@ -234,14 +226,8 @@ export const KibanaDeprecations = withRouter(({ history }: RouteComponentProps) ]); useEffect(() => { - async function sendTelemetryData() { - await api.sendPageTelemetryData({ - kibana: true, - }); - } - - sendTelemetryData(); - }, [api]); + uiMetricService.trackUiMetric(METRIC_TYPE.LOADED, UIM_KIBANA_DEPRECATIONS_PAGE_LOAD); + }, []); useEffect(() => { breadcrumbs.setBreadcrumbs('kibanaDeprecations'); @@ -251,15 +237,8 @@ export const KibanaDeprecations = withRouter(({ history }: RouteComponentProps) getAllDeprecations(); }, [deprecations, getAllDeprecations]); - if (kibanaDeprecations && kibanaDeprecations.length === 0) { - return ( - - history.push('/overview')} - /> - - ); + if (error) { + return ; } if (isLoading) { @@ -270,63 +249,53 @@ export const KibanaDeprecations = withRouter(({ history }: RouteComponentProps) ); } - if (kibanaDeprecations?.length) { + if (kibanaDeprecations?.length === 0) { return ( -
    - - - - - - - {kibanaDeprecationErrors.length > 0 && ( - <> - -

    {i18nTexts.getKibanaDeprecationErrorDescription(kibanaDeprecationErrors)}

    -
    - - - - )} - - + history.push('/overview')} /> -
    + ); } - if (error) { - return ( - + - {i18nTexts.requestErrorTitle}} - body={

    {i18nTexts.requestErrorDescription}

    } + -
    - ); - } - - return null; + + + + + {kibanaDeprecationErrors.length > 0 && ( + <> + +

    {i18nTexts.getKibanaDeprecationErrorDescription(kibanaDeprecationErrors)}

    +
    + + + + )} + + +
    + ); }); diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/overview/backup_step/cloud_backup.tsx b/x-pack/plugins/upgrade_assistant/public/application/components/overview/backup_step/cloud_backup.tsx index 55a6ee8e5c73f..4ab860a0bf6a7 100644 --- a/x-pack/plugins/upgrade_assistant/public/application/components/overview/backup_step/cloud_backup.tsx +++ b/x-pack/plugins/upgrade_assistant/public/application/components/overview/backup_step/cloud_backup.tsx @@ -9,6 +9,7 @@ import React, { useEffect } from 'react'; import moment from 'moment-timezone'; import { FormattedDate, FormattedTime, FormattedMessage } from '@kbn/i18n/react'; import { i18n } from '@kbn/i18n'; +import { METRIC_TYPE } from '@kbn/analytics'; import { EuiLoadingContent, EuiFlexGroup, @@ -21,6 +22,7 @@ import { } from '@elastic/eui'; import { useAppContext } from '../../../app_context'; +import { uiMetricService, UIM_BACKUP_DATA_CLOUD_CLICK } from '../../../lib/ui_metric'; interface Props { cloudSnapshotsUrl: string; @@ -128,11 +130,13 @@ export const CloudBackup: React.FunctionComponent = ({ return ( <> {statusMessage} - - + {/* eslint-disable-next-line @elastic/eui/href-or-on-click */} { + uiMetricService.trackUiMetric(METRIC_TYPE.CLICK, UIM_BACKUP_DATA_CLOUD_CLICK); + }} data-test-subj="cloudSnapshotsLink" target="_blank" iconType="popout" diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/overview/backup_step/on_prem_backup.tsx b/x-pack/plugins/upgrade_assistant/public/application/components/overview/backup_step/on_prem_backup.tsx index 2e2e2bd5ce48e..69100b36db7eb 100644 --- a/x-pack/plugins/upgrade_assistant/public/application/components/overview/backup_step/on_prem_backup.tsx +++ b/x-pack/plugins/upgrade_assistant/public/application/components/overview/backup_step/on_prem_backup.tsx @@ -8,9 +8,11 @@ import React from 'react'; import { FormattedMessage } from '@kbn/i18n/react'; import { i18n } from '@kbn/i18n'; +import { METRIC_TYPE } from '@kbn/analytics'; import { EuiText, EuiButton, EuiSpacer } from '@elastic/eui'; import { useAppContext } from '../../../app_context'; +import { uiMetricService, UIM_BACKUP_DATA_ON_PREM_CLICK } from '../../../lib/ui_metric'; const SnapshotRestoreAppLink: React.FunctionComponent = () => { const { @@ -22,7 +24,14 @@ const SnapshotRestoreAppLink: React.FunctionComponent = () => { ?.useUrl({ page: 'snapshots' }); return ( - + // eslint-disable-next-line @elastic/eui/href-or-on-click + { + uiMetricService.trackUiMetric(METRIC_TYPE.CLICK, UIM_BACKUP_DATA_ON_PREM_CLICK); + }} + data-test-subj="snapshotRestoreLink" + > void; } @@ -70,17 +72,14 @@ export const DeprecationIssuesPanel = (props: Props) => { data-test-subj={props['data-test-subj']} className="upgDeprecationIssuesPanel" layout="horizontal" - title={ - <> - {deprecationSource} - {hasError && errorMessage} - - } + title={deprecationSource} {...(!hasNoIssues && reactRouterNavigate(history, linkUrl))} > - {hasNoIssues ? ( + {hasError ? ( + {errorMessage} + ) : hasNoIssues ? ( ) : ( diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/overview/fix_issues_step/components/es_stats.tsx b/x-pack/plugins/upgrade_assistant/public/application/components/overview/fix_issues_step/components/es_deprecation_issues_panel.tsx similarity index 84% rename from x-pack/plugins/upgrade_assistant/public/application/components/overview/fix_issues_step/components/es_stats.tsx rename to x-pack/plugins/upgrade_assistant/public/application/components/overview/fix_issues_step/components/es_deprecation_issues_panel.tsx index 89860d5b58be4..b4258ababc92e 100644 --- a/x-pack/plugins/upgrade_assistant/public/application/components/overview/fix_issues_step/components/es_stats.tsx +++ b/x-pack/plugins/upgrade_assistant/public/application/components/overview/fix_issues_step/components/es_deprecation_issues_panel.tsx @@ -8,14 +8,14 @@ import React, { FunctionComponent } from 'react'; import { useAppContext } from '../../../../app_context'; -import { EsStatsErrors } from './es_stats_error'; +import { getEsDeprecationError } from '../../../../lib/get_es_deprecation_error'; import { DeprecationIssuesPanel } from './deprecation_issues_panel'; interface Props { setIsFixed: (isFixed: boolean) => void; } -export const ElasticsearchDeprecationStats: FunctionComponent = ({ setIsFixed }) => { +export const EsDeprecationIssuesPanel: FunctionComponent = ({ setIsFixed }) => { const { services: { api }, } = useAppContext(); @@ -29,7 +29,7 @@ export const ElasticsearchDeprecationStats: FunctionComponent = ({ setIsF esDeprecations?.deprecations?.filter((deprecation) => deprecation.isCritical === false) ?.length ?? 0; - const errorMessage = error && ; + const errorMessage = error && getEsDeprecationError(error).message; return ( = ({ error }) => { - let iconContent: React.ReactNode; - - const { code: errorType, message } = getEsDeprecationError(error); - - switch (errorType) { - case 'unauthorized_error': - iconContent = ( - - ); - break; - - case 'partially_upgraded_error': - iconContent = ( - - ); - break; - - case 'upgraded_error': - iconContent = ( - - ); - break; - - case 'request_error': - default: - iconContent = ( - - ); - } - - return {iconContent}; -}; diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/overview/fix_issues_step/components/index.ts b/x-pack/plugins/upgrade_assistant/public/application/components/overview/fix_issues_step/components/index.ts index fe3a0dd2ceaff..a2a3219002719 100644 --- a/x-pack/plugins/upgrade_assistant/public/application/components/overview/fix_issues_step/components/index.ts +++ b/x-pack/plugins/upgrade_assistant/public/application/components/overview/fix_issues_step/components/index.ts @@ -5,5 +5,5 @@ * 2.0. */ -export { ElasticsearchDeprecationStats } from './es_stats'; -export { KibanaDeprecationStats } from './kibana_stats'; +export { EsDeprecationIssuesPanel } from './es_deprecation_issues_panel'; +export { KibanaDeprecationIssuesPanel } from './kibana_deprecation_issues_panel'; diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/overview/fix_issues_step/components/kibana_stats.tsx b/x-pack/plugins/upgrade_assistant/public/application/components/overview/fix_issues_step/components/kibana_deprecation_issues_panel.tsx similarity index 77% rename from x-pack/plugins/upgrade_assistant/public/application/components/overview/fix_issues_step/components/kibana_stats.tsx rename to x-pack/plugins/upgrade_assistant/public/application/components/overview/fix_issues_step/components/kibana_deprecation_issues_panel.tsx index 891ccc01dcd40..b0aa7b592e3a1 100644 --- a/x-pack/plugins/upgrade_assistant/public/application/components/overview/fix_issues_step/components/kibana_stats.tsx +++ b/x-pack/plugins/upgrade_assistant/public/application/components/overview/fix_issues_step/components/kibana_deprecation_issues_panel.tsx @@ -6,7 +6,6 @@ */ import React, { FunctionComponent, useEffect, useState } from 'react'; -import { EuiIconTip } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import type { DomainDeprecationDetails } from 'kibana/public'; @@ -17,7 +16,7 @@ interface Props { setIsFixed: (isFixed: boolean) => void; } -export const KibanaDeprecationStats: FunctionComponent = ({ setIsFixed }) => { +export const KibanaDeprecationIssuesPanel: FunctionComponent = ({ setIsFixed }) => { const { services: { core: { deprecations }, @@ -53,20 +52,11 @@ export const KibanaDeprecationStats: FunctionComponent = ({ setIsFixed }) const warningDeprecationsCount = kibanaDeprecations?.filter((deprecation) => deprecation.level === 'warning')?.length ?? 0; - const errorMessage = error && ( - - ); + const errorMessage = + error && + i18n.translate('xpack.upgradeAssistant.deprecationStats.loadingErrorMessage', { + defaultMessage: 'Could not retrieve Kibana deprecation issues.', + }); return ( ( + + + + + + + {children} + + +); diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/overview/fix_issues_step/fix_issues_step.tsx b/x-pack/plugins/upgrade_assistant/public/application/components/overview/fix_issues_step/fix_issues_step.tsx index d4ecccdd5dc15..b061ab5ea2d4d 100644 --- a/x-pack/plugins/upgrade_assistant/public/application/components/overview/fix_issues_step/fix_issues_step.tsx +++ b/x-pack/plugins/upgrade_assistant/public/application/components/overview/fix_issues_step/fix_issues_step.tsx @@ -13,7 +13,7 @@ import { FormattedMessage } from '@kbn/i18n/react'; import type { EuiStepProps } from '@elastic/eui/src/components/steps/step'; import type { OverviewStepProps } from '../../types'; -import { ElasticsearchDeprecationStats, KibanaDeprecationStats } from './components'; +import { EsDeprecationIssuesPanel, KibanaDeprecationIssuesPanel } from './components'; const i18nTexts = { reviewStepTitle: i18n.translate('xpack.upgradeAssistant.overview.fixIssuesStepTitle', { @@ -43,11 +43,11 @@ const FixIssuesStep: FunctionComponent = ({ setIsComplete }) => { return ( - + - + ); diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/overview/fix_logs_step/deprecations_count_checkpoint/deprecations_count_checkpoint.tsx b/x-pack/plugins/upgrade_assistant/public/application/components/overview/fix_logs_step/deprecations_count_checkpoint/deprecations_count_checkpoint.tsx index 8757ce7c35eaf..955d86493d479 100644 --- a/x-pack/plugins/upgrade_assistant/public/application/components/overview/fix_logs_step/deprecations_count_checkpoint/deprecations_count_checkpoint.tsx +++ b/x-pack/plugins/upgrade_assistant/public/application/components/overview/fix_logs_step/deprecations_count_checkpoint/deprecations_count_checkpoint.tsx @@ -8,10 +8,12 @@ import React, { FunctionComponent, useEffect } from 'react'; import moment from 'moment-timezone'; import { FormattedDate, FormattedTime, FormattedMessage } from '@kbn/i18n/react'; - +import { METRIC_TYPE } from '@kbn/analytics'; import { i18n } from '@kbn/i18n'; import { EuiCallOut, EuiButton, EuiLoadingContent } from '@elastic/eui'; + import { useAppContext } from '../../../../app_context'; +import { uiMetricService, UIM_RESET_LOGS_COUNTER_CLICK } from '../../../../lib/ui_metric'; const i18nTexts = { calloutTitle: (warningsCount: number, previousCheck: string) => ( @@ -30,8 +32,7 @@ const i18nTexts = { /> ), calloutBody: i18n.translate('xpack.upgradeAssistant.overview.verifyChanges.calloutBody', { - defaultMessage: - 'Reset the counter after making changes and continue monitoring to verify that you are no longer using deprecated APIs.', + defaultMessage: `After making changes, reset the counter and continue monitoring to verify you're no longer using deprecated features.`, }), loadingError: i18n.translate('xpack.upgradeAssistant.overview.verifyChanges.loadingError', { defaultMessage: 'An error occurred while retrieving the count of deprecation logs', @@ -72,6 +73,7 @@ export const DeprecationsCountCheckpoint: FunctionComponent = ({ const onResetClick = () => { const now = moment().toISOString(); + uiMetricService.trackUiMetric(METRIC_TYPE.CLICK, UIM_RESET_LOGS_COUNTER_CLICK); setCheckpoint(now); }; diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/overview/fix_logs_step/external_links.tsx b/x-pack/plugins/upgrade_assistant/public/application/components/overview/fix_logs_step/external_links.tsx index d027b2f262e9e..69f1f14d4eb58 100644 --- a/x-pack/plugins/upgrade_assistant/public/application/components/overview/fix_logs_step/external_links.tsx +++ b/x-pack/plugins/upgrade_assistant/public/application/components/overview/fix_logs_step/external_links.tsx @@ -9,10 +9,17 @@ import { encode } from 'rison-node'; import React, { FunctionComponent, useState, useEffect } from 'react'; import { FormattedMessage } from '@kbn/i18n/react'; +import { METRIC_TYPE } from '@kbn/analytics'; import { EuiLink, EuiFlexGroup, EuiFlexItem, EuiSpacer, EuiPanel, EuiText } from '@elastic/eui'; -import { useAppContext } from '../../../app_context'; import { DataPublicPluginStart } from '../../../../shared_imports'; +import { useAppContext } from '../../../app_context'; +import { + uiMetricService, + UIM_OBSERVABILITY_CLICK, + UIM_DISCOVER_CLICK, +} from '../../../lib/ui_metric'; + import { DEPRECATION_LOGS_INDEX_PATTERN, DEPRECATION_LOGS_SOURCE_ID, @@ -73,7 +80,14 @@ const DiscoverAppLink: FunctionComponent = ({ checkpoint }) => { }, [dataService, checkpoint, share.url.locators]); return ( - + // eslint-disable-next-line @elastic/eui/href-or-on-click + { + uiMetricService.trackUiMetric(METRIC_TYPE.CLICK, UIM_DISCOVER_CLICK); + }} + data-test-subj="viewDiscoverLogs" + > = ({ checkpoint }) => { ); return ( - + // eslint-disable-next-line @elastic/eui/href-or-on-click + { + uiMetricService.trackUiMetric(METRIC_TYPE.CLICK, UIM_OBSERVABILITY_CLICK); + }} + data-test-subj="viewObserveLogs" + > ( + + + + ), + }} + /> + ), onlyLogWritingEnabledTitle: i18n.translate( 'xpack.upgradeAssistant.overview.deprecationLogs.deprecationWarningTitle', { @@ -47,14 +72,46 @@ const i18nTexts = { 'Go to your logs directory to view the deprecation logs or enable log collecting to see them in the UI.', } ), + deniedPrivilegeTitle: i18n.translate( + 'xpack.upgradeAssistant.overview.deprecationLogs.deniedPrivilegeTitle', + { + defaultMessage: 'You require index privileges to analyze the deprecation logs', + } + ), + deniedPrivilegeDescription: (privilegesMissing: MissingPrivileges) => ( + // NOTE: hardcoding the missing privilege because the WithPrivileges HOC + // doesnt provide a way to retrieve which specific privileges an index + // is missing. + {privilegesMissing?.index?.join(', ')} + ), + privilegesCount: privilegesMissing?.index?.length, + }} + /> + ), }; interface Props { setIsComplete: OverviewStepProps['setIsComplete']; + hasPrivileges: boolean; + privilegesMissing: MissingPrivileges; } -const FixLogsStep: FunctionComponent = ({ setIsComplete }) => { +const FixLogsStep: FunctionComponent = ({ + setIsComplete, + hasPrivileges, + privilegesMissing, +}) => { const state = useDeprecationLogging(); + const { + services: { + core: { docLinks }, + }, + } = useAppContext(); const [checkpoint, setCheckpoint] = useState(loadLogsCheckpoint()); useEffect(() => { @@ -94,7 +151,21 @@ const FixLogsStep: FunctionComponent = ({ setIsComplete }) => { )} - {state.isDeprecationLogIndexingEnabled && ( + {!hasPrivileges && state.isDeprecationLogIndexingEnabled && ( + <> + + +

    {i18nTexts.deniedPrivilegeDescription(privilegesMissing)}

    +
    + + )} + + {hasPrivileges && state.isDeprecationLogIndexingEnabled && ( <> @@ -113,6 +184,19 @@ const FixLogsStep: FunctionComponent = ({ setIsComplete }) => { setCheckpoint={setCheckpoint} setHasNoDeprecationLogs={setIsComplete} /> + + + +

    {i18nTexts.apiCompatibilityNoteTitle}

    +
    + + +

    + {i18nTexts.apiCompatibilityNoteBody( + docLinks.links.elasticsearch.apiCompatibilityHeader + )} +

    +
    )} @@ -126,6 +210,16 @@ export const getFixLogsStep = ({ isComplete, setIsComplete }: OverviewStepProps) status, title: i18nTexts.identifyStepTitle, 'data-test-subj': `fixLogsStep-${status}`, - children: , + children: ( + + {({ hasPrivileges, privilegesMissing, isLoading }) => ( + + )} + + ), }; }; diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/overview/overview.tsx b/x-pack/plugins/upgrade_assistant/public/application/components/overview/overview.tsx index 010c9b7367158..1bd6b0059bc23 100644 --- a/x-pack/plugins/upgrade_assistant/public/application/components/overview/overview.tsx +++ b/x-pack/plugins/upgrade_assistant/public/application/components/overview/overview.tsx @@ -18,9 +18,11 @@ import { EuiPageContent, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; +import { METRIC_TYPE } from '@kbn/analytics'; import { FormattedMessage } from '@kbn/i18n/react'; import { useAppContext } from '../../app_context'; +import { uiMetricService, UIM_OVERVIEW_PAGE_LOAD } from '../../lib/ui_metric'; import { getBackupStep } from './backup_step'; import { getFixIssuesStep } from './fix_issues_step'; import { getFixLogsStep } from './fix_logs_step'; @@ -33,21 +35,14 @@ export const Overview: FunctionComponent = () => { kibanaVersionInfo: { nextMajor }, services: { breadcrumbs, - api, core: { docLinks }, }, plugins: { cloud }, } = useAppContext(); useEffect(() => { - async function sendTelemetryData() { - await api.sendPageTelemetryData({ - overview: true, - }); - } - - sendTelemetryData(); - }, [api]); + uiMetricService.trackUiMetric(METRIC_TYPE.LOADED, UIM_OVERVIEW_PAGE_LOAD); + }, []); useEffect(() => { breadcrumbs.setBreadcrumbs('overview'); diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/shared/deprecations_page_loading_error.tsx b/x-pack/plugins/upgrade_assistant/public/application/components/shared/deprecations_page_loading_error.tsx new file mode 100644 index 0000000000000..01cf950abbc31 --- /dev/null +++ b/x-pack/plugins/upgrade_assistant/public/application/components/shared/deprecations_page_loading_error.tsx @@ -0,0 +1,42 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { FunctionComponent } from 'react'; +import { EuiPageContent, EuiEmptyPrompt } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; + +import { DeprecationSource } from '../../../../common/types'; + +interface Props { + deprecationSource: DeprecationSource; + message?: string; +} + +export const DeprecationsPageLoadingError: FunctionComponent = ({ + deprecationSource, + message, +}) => ( + + + {i18n.translate('xpack.upgradeAssistant.deprecationsPageLoadingError.title', { + defaultMessage: 'Could not retrieve {deprecationSource} deprecation issues', + values: { deprecationSource }, + })} + + } + body={message} + /> + +); diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/shared/index.ts b/x-pack/plugins/upgrade_assistant/public/application/components/shared/index.ts index 9464b5ecb98ca..ef7916f6e8d17 100644 --- a/x-pack/plugins/upgrade_assistant/public/application/components/shared/index.ts +++ b/x-pack/plugins/upgrade_assistant/public/application/components/shared/index.ts @@ -8,3 +8,4 @@ export { NoDeprecationsPrompt } from './no_deprecations'; export { DeprecationCount } from './deprecation_count'; export { DeprecationBadge } from './deprecation_badge'; +export { DeprecationsPageLoadingError } from './deprecations_page_loading_error'; diff --git a/x-pack/plugins/upgrade_assistant/public/application/lib/api.ts b/x-pack/plugins/upgrade_assistant/public/application/lib/api.ts index da4f87f497467..1d51510333ef4 100644 --- a/x-pack/plugins/upgrade_assistant/public/application/lib/api.ts +++ b/x-pack/plugins/upgrade_assistant/public/application/lib/api.ts @@ -65,16 +65,6 @@ export class ApiService { }); } - public async sendPageTelemetryData(telemetryData: { [tabName: string]: boolean }) { - const result = await this.sendRequest({ - path: `${API_BASE_PATH}/stats/ui_open`, - method: 'put', - body: JSON.stringify(telemetryData), - }); - - return result; - } - public useLoadDeprecationLogging() { return this.useRequest<{ isDeprecationLogIndexingEnabled: boolean; @@ -150,16 +140,6 @@ export class ApiService { }); } - public async sendReindexTelemetryData(telemetryData: { [key: string]: boolean }) { - const result = await this.sendRequest({ - path: `${API_BASE_PATH}/stats/ui_reindex`, - method: 'put', - body: JSON.stringify(telemetryData), - }); - - return result; - } - public async getReindexStatus(indexName: string) { return await this.sendRequest({ path: `${API_BASE_PATH}/reindex/${indexName}`, diff --git a/x-pack/plugins/upgrade_assistant/public/application/lib/get_es_deprecation_error.ts b/x-pack/plugins/upgrade_assistant/public/application/lib/get_es_deprecation_error.ts index 85cfd2a3fd16c..889568fb7a412 100644 --- a/x-pack/plugins/upgrade_assistant/public/application/lib/get_es_deprecation_error.ts +++ b/x-pack/plugins/upgrade_assistant/public/application/lib/get_es_deprecation_error.ts @@ -12,7 +12,7 @@ const i18nTexts = { permissionsError: i18n.translate( 'xpack.upgradeAssistant.esDeprecationErrors.permissionsErrorMessage', { - defaultMessage: 'You are not authorized to view Elasticsearch deprecations.', + defaultMessage: 'You are not authorized to view Elasticsearch deprecation issues.', } ), partiallyUpgradedWarning: i18n.translate( @@ -29,7 +29,7 @@ const i18nTexts = { } ), loadingError: i18n.translate('xpack.upgradeAssistant.esDeprecationErrors.loadingErrorMessage', { - defaultMessage: 'Could not retrieve Elasticsearch deprecations.', + defaultMessage: 'Could not retrieve Elasticsearch deprecation issues.', }), }; diff --git a/x-pack/plugins/upgrade_assistant/public/application/lib/ui_metric.ts b/x-pack/plugins/upgrade_assistant/public/application/lib/ui_metric.ts new file mode 100644 index 0000000000000..394f046a8bafe --- /dev/null +++ b/x-pack/plugins/upgrade_assistant/public/application/lib/ui_metric.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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { UiCounterMetricType } from '@kbn/analytics'; +import { UsageCollectionSetup } from 'src/plugins/usage_collection/public'; + +export const UIM_APP_NAME = 'upgrade_assistant'; +export const UIM_ES_DEPRECATIONS_PAGE_LOAD = 'es_deprecations_page_load'; +export const UIM_KIBANA_DEPRECATIONS_PAGE_LOAD = 'kibana_deprecations_page_load'; +export const UIM_OVERVIEW_PAGE_LOAD = 'overview_page_load'; +export const UIM_REINDEX_OPEN_FLYOUT_CLICK = 'reindex_open_flyout_click'; +export const UIM_REINDEX_CLOSE_FLYOUT_CLICK = 'reindex_close_flyout_click'; +export const UIM_REINDEX_START_CLICK = 'reindex_start_click'; +export const UIM_REINDEX_STOP_CLICK = 'reindex_stop_click'; +export const UIM_BACKUP_DATA_CLOUD_CLICK = 'backup_data_cloud_click'; +export const UIM_BACKUP_DATA_ON_PREM_CLICK = 'backup_data_on_prem_click'; +export const UIM_RESET_LOGS_COUNTER_CLICK = 'reset_logs_counter_click'; +export const UIM_OBSERVABILITY_CLICK = 'observability_click'; +export const UIM_DISCOVER_CLICK = 'discover_click'; +export const UIM_ML_SNAPSHOT_UPGRADE_CLICK = 'ml_snapshot_upgrade_click'; +export const UIM_ML_SNAPSHOT_DELETE_CLICK = 'ml_snapshot_delete_click'; +export const UIM_INDEX_SETTINGS_DELETE_CLICK = 'index_settings_delete_click'; +export const UIM_KIBANA_QUICK_RESOLVE_CLICK = 'kibana_quick_resolve_click'; + +export class UiMetricService { + private usageCollection: UsageCollectionSetup | undefined; + + public setup(usageCollection: UsageCollectionSetup) { + this.usageCollection = usageCollection; + } + + private track(metricType: UiCounterMetricType, eventName: string | string[]) { + if (!this.usageCollection) { + // Usage collection might be disabled in Kibana config. + return; + } + return this.usageCollection.reportUiCounter(UIM_APP_NAME, metricType, eventName); + } + + public trackUiMetric(metricType: UiCounterMetricType, eventName: string | string[]) { + return this.track(metricType, eventName); + } +} + +export const uiMetricService = new UiMetricService(); diff --git a/x-pack/plugins/upgrade_assistant/public/plugin.ts b/x-pack/plugins/upgrade_assistant/public/plugin.ts index 974d5e03da2ec..d688ee510ce1f 100644 --- a/x-pack/plugins/upgrade_assistant/public/plugin.ts +++ b/x-pack/plugins/upgrade_assistant/public/plugin.ts @@ -11,6 +11,7 @@ import { Plugin, CoreSetup, PluginInitializerContext } from 'src/core/public'; import { apiService } from './application/lib/api'; import { breadcrumbService } from './application/lib/breadcrumbs'; +import { uiMetricService } from './application/lib/ui_metric'; import { SetupDependencies, StartDependencies, AppDependencies } from './types'; import { Config } from '../common/config'; @@ -18,12 +19,11 @@ export class UpgradeAssistantUIPlugin implements Plugin { constructor(private ctx: PluginInitializerContext) {} - setup(coreSetup: CoreSetup, { management, cloud, share }: SetupDependencies) { - const { enabled, readonly } = this.ctx.config.get(); - - if (!enabled) { - return; - } + setup( + coreSetup: CoreSetup, + { management, cloud, share, usageCollection }: SetupDependencies + ) { + const { readonly } = this.ctx.config.get(); const appRegistrar = management.sections.section.stack; const kibanaVersion = new SemVer(this.ctx.env.packageInfo.version); @@ -38,6 +38,10 @@ export class UpgradeAssistantUIPlugin defaultMessage: 'Upgrade Assistant', }); + if (usageCollection) { + uiMetricService.setup(usageCollection); + } + appRegistrar.registerApp({ id: 'upgrade_assistant', title: pluginName, diff --git a/x-pack/plugins/upgrade_assistant/public/shared_imports.ts b/x-pack/plugins/upgrade_assistant/public/shared_imports.ts index 6fee29cf12938..1d0b1ae51f30f 100644 --- a/x-pack/plugins/upgrade_assistant/public/shared_imports.ts +++ b/x-pack/plugins/upgrade_assistant/public/shared_imports.ts @@ -14,6 +14,12 @@ export { UseRequestResponse, SectionLoading, GlobalFlyout, + WithPrivileges, + Privileges, + MissingPrivileges, + AuthorizationProvider, + AuthorizationContext, + Authorization, } from '../../../../src/plugins/es_ui_shared/public/'; export { Storage } from '../../../../src/plugins/kibana_utils/public'; diff --git a/x-pack/plugins/upgrade_assistant/public/types.ts b/x-pack/plugins/upgrade_assistant/public/types.ts index de5f29593b7c6..e58c90336d856 100644 --- a/x-pack/plugins/upgrade_assistant/public/types.ts +++ b/x-pack/plugins/upgrade_assistant/public/types.ts @@ -10,6 +10,7 @@ import { ManagementSetup } from 'src/plugins/management/public'; import { DataPublicPluginStart } from 'src/plugins/data/public'; import { SharePluginSetup } from 'src/plugins/share/public'; import { CoreStart } from 'src/core/public'; +import { UsageCollectionSetup } from 'src/plugins/usage_collection/public'; import { CloudSetup } from '../../cloud/public'; import { LicensingPluginStart } from '../../licensing/public'; import { BreadcrumbService } from './application/lib/breadcrumbs'; @@ -25,6 +26,7 @@ export interface SetupDependencies { management: ManagementSetup; share: SharePluginSetup; cloud?: CloudSetup; + usageCollection?: UsageCollectionSetup; } export interface StartDependencies { diff --git a/x-pack/plugins/upgrade_assistant/server/index.ts b/x-pack/plugins/upgrade_assistant/server/index.ts index 035a6515de152..5591276b2fa34 100644 --- a/x-pack/plugins/upgrade_assistant/server/index.ts +++ b/x-pack/plugins/upgrade_assistant/server/index.ts @@ -14,9 +14,9 @@ export const plugin = (ctx: PluginInitializerContext) => { }; export const config: PluginConfigDescriptor = { + deprecations: ({ deprecate }) => [deprecate('enabled', '8.0.0')], schema: configSchema, exposeToBrowser: { - enabled: true, readonly: true, }, }; diff --git a/x-pack/plugins/upgrade_assistant/server/lib/telemetry/es_ui_open_apis.test.ts b/x-pack/plugins/upgrade_assistant/server/lib/telemetry/es_ui_open_apis.test.ts deleted file mode 100644 index caff78390b9d1..0000000000000 --- a/x-pack/plugins/upgrade_assistant/server/lib/telemetry/es_ui_open_apis.test.ts +++ /dev/null @@ -1,48 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { savedObjectsRepositoryMock } from 'src/core/server/mocks'; -import { UPGRADE_ASSISTANT_DOC_ID, UPGRADE_ASSISTANT_TYPE } from '../../../common/types'; - -import { upsertUIOpenOption } from './es_ui_open_apis'; - -/** - * Since these route callbacks are so thin, these serve simply as integration tests - * to ensure they're wired up to the lib functions correctly. Business logic is tested - * more thoroughly in the lib/telemetry tests. - */ -describe('Upgrade Assistant Telemetry SavedObject UIOpen', () => { - describe('Upsert UIOpen Option', () => { - it('call saved objects internal repository with the correct info', async () => { - const internalRepo = savedObjectsRepositoryMock.create(); - - await upsertUIOpenOption({ - overview: true, - elasticsearch: true, - kibana: true, - savedObjects: { createInternalRepository: () => internalRepo } as any, - }); - - expect(internalRepo.incrementCounter).toHaveBeenCalledTimes(3); - expect(internalRepo.incrementCounter).toHaveBeenCalledWith( - UPGRADE_ASSISTANT_TYPE, - UPGRADE_ASSISTANT_DOC_ID, - ['ui_open.overview'] - ); - expect(internalRepo.incrementCounter).toHaveBeenCalledWith( - UPGRADE_ASSISTANT_TYPE, - UPGRADE_ASSISTANT_DOC_ID, - ['ui_open.elasticsearch'] - ); - expect(internalRepo.incrementCounter).toHaveBeenCalledWith( - UPGRADE_ASSISTANT_TYPE, - UPGRADE_ASSISTANT_DOC_ID, - ['ui_open.kibana'] - ); - }); - }); -}); diff --git a/x-pack/plugins/upgrade_assistant/server/lib/telemetry/es_ui_open_apis.ts b/x-pack/plugins/upgrade_assistant/server/lib/telemetry/es_ui_open_apis.ts deleted file mode 100644 index 3d463fe4b03ed..0000000000000 --- a/x-pack/plugins/upgrade_assistant/server/lib/telemetry/es_ui_open_apis.ts +++ /dev/null @@ -1,57 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { SavedObjectsServiceStart } from 'src/core/server'; -import { - UIOpen, - UIOpenOption, - UPGRADE_ASSISTANT_DOC_ID, - UPGRADE_ASSISTANT_TYPE, -} from '../../../common/types'; - -interface IncrementUIOpenDependencies { - uiOpenOptionCounter: UIOpenOption; - savedObjects: SavedObjectsServiceStart; -} - -async function incrementUIOpenOptionCounter({ - savedObjects, - uiOpenOptionCounter, -}: IncrementUIOpenDependencies) { - const internalRepository = savedObjects.createInternalRepository(); - - await internalRepository.incrementCounter(UPGRADE_ASSISTANT_TYPE, UPGRADE_ASSISTANT_DOC_ID, [ - `ui_open.${uiOpenOptionCounter}`, - ]); -} - -type UpsertUIOpenOptionDependencies = UIOpen & { savedObjects: SavedObjectsServiceStart }; - -export async function upsertUIOpenOption({ - overview, - elasticsearch, - savedObjects, - kibana, -}: UpsertUIOpenOptionDependencies): Promise { - if (overview) { - await incrementUIOpenOptionCounter({ savedObjects, uiOpenOptionCounter: 'overview' }); - } - - if (elasticsearch) { - await incrementUIOpenOptionCounter({ savedObjects, uiOpenOptionCounter: 'elasticsearch' }); - } - - if (kibana) { - await incrementUIOpenOptionCounter({ savedObjects, uiOpenOptionCounter: 'kibana' }); - } - - return { - overview, - elasticsearch, - kibana, - }; -} diff --git a/x-pack/plugins/upgrade_assistant/server/lib/telemetry/es_ui_reindex_apis.test.ts b/x-pack/plugins/upgrade_assistant/server/lib/telemetry/es_ui_reindex_apis.test.ts deleted file mode 100644 index 6a05e8a697bb8..0000000000000 --- a/x-pack/plugins/upgrade_assistant/server/lib/telemetry/es_ui_reindex_apis.test.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; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { savedObjectsRepositoryMock } from 'src/core/server/mocks'; -import { UPGRADE_ASSISTANT_DOC_ID, UPGRADE_ASSISTANT_TYPE } from '../../../common/types'; -import { upsertUIReindexOption } from './es_ui_reindex_apis'; - -/** - * Since these route callbacks are so thin, these serve simply as integration tests - * to ensure they're wired up to the lib functions correctly. Business logic is tested - * more thoroughly in the lib/telemetry tests. - */ -describe('Upgrade Assistant Telemetry SavedObject UIReindex', () => { - describe('Upsert UIReindex Option', () => { - it('call saved objects internal repository with the correct info', async () => { - const internalRepo = savedObjectsRepositoryMock.create(); - await upsertUIReindexOption({ - close: true, - open: true, - start: true, - stop: true, - savedObjects: { createInternalRepository: () => internalRepo } as any, - }); - - expect(internalRepo.incrementCounter).toHaveBeenCalledTimes(4); - expect(internalRepo.incrementCounter).toHaveBeenCalledWith( - UPGRADE_ASSISTANT_TYPE, - UPGRADE_ASSISTANT_DOC_ID, - [`ui_reindex.close`] - ); - expect(internalRepo.incrementCounter).toHaveBeenCalledWith( - UPGRADE_ASSISTANT_TYPE, - UPGRADE_ASSISTANT_DOC_ID, - [`ui_reindex.open`] - ); - expect(internalRepo.incrementCounter).toHaveBeenCalledWith( - UPGRADE_ASSISTANT_TYPE, - UPGRADE_ASSISTANT_DOC_ID, - [`ui_reindex.start`] - ); - expect(internalRepo.incrementCounter).toHaveBeenCalledWith( - UPGRADE_ASSISTANT_TYPE, - UPGRADE_ASSISTANT_DOC_ID, - [`ui_reindex.stop`] - ); - }); - }); -}); diff --git a/x-pack/plugins/upgrade_assistant/server/lib/telemetry/es_ui_reindex_apis.ts b/x-pack/plugins/upgrade_assistant/server/lib/telemetry/es_ui_reindex_apis.ts deleted file mode 100644 index caee1a58a4006..0000000000000 --- a/x-pack/plugins/upgrade_assistant/server/lib/telemetry/es_ui_reindex_apis.ts +++ /dev/null @@ -1,63 +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 { SavedObjectsServiceStart } from 'src/core/server'; -import { - UIReindex, - UIReindexOption, - UPGRADE_ASSISTANT_DOC_ID, - UPGRADE_ASSISTANT_TYPE, -} from '../../../common/types'; - -interface IncrementUIReindexOptionDependencies { - uiReindexOptionCounter: UIReindexOption; - savedObjects: SavedObjectsServiceStart; -} - -async function incrementUIReindexOptionCounter({ - savedObjects, - uiReindexOptionCounter, -}: IncrementUIReindexOptionDependencies) { - const internalRepository = savedObjects.createInternalRepository(); - - await internalRepository.incrementCounter(UPGRADE_ASSISTANT_TYPE, UPGRADE_ASSISTANT_DOC_ID, [ - `ui_reindex.${uiReindexOptionCounter}`, - ]); -} - -type UpsertUIReindexOptionDepencies = UIReindex & { savedObjects: SavedObjectsServiceStart }; - -export async function upsertUIReindexOption({ - start, - close, - open, - stop, - savedObjects, -}: UpsertUIReindexOptionDepencies): Promise { - if (close) { - await incrementUIReindexOptionCounter({ savedObjects, uiReindexOptionCounter: 'close' }); - } - - if (open) { - await incrementUIReindexOptionCounter({ savedObjects, uiReindexOptionCounter: 'open' }); - } - - if (start) { - await incrementUIReindexOptionCounter({ savedObjects, uiReindexOptionCounter: 'start' }); - } - - if (stop) { - await incrementUIReindexOptionCounter({ savedObjects, uiReindexOptionCounter: 'stop' }); - } - - return { - close, - open, - start, - stop, - }; -} diff --git a/x-pack/plugins/upgrade_assistant/server/lib/telemetry/usage_collector.test.ts b/x-pack/plugins/upgrade_assistant/server/lib/telemetry/usage_collector.test.ts index 50c5b358aa5cb..34d329557f11e 100644 --- a/x-pack/plugins/upgrade_assistant/server/lib/telemetry/usage_collector.test.ts +++ b/x-pack/plugins/upgrade_assistant/server/lib/telemetry/usage_collector.test.ts @@ -47,26 +47,6 @@ describe('Upgrade Assistant Usage Collector', () => { }; dependencies = { usageCollection, - savedObjects: { - createInternalRepository: jest.fn().mockImplementation(() => { - return { - get: () => { - return { - attributes: { - 'ui_open.overview': 10, - 'ui_open.elasticsearch': 20, - 'ui_open.kibana': 15, - 'ui_reindex.close': 1, - 'ui_reindex.open': 4, - 'ui_reindex.start': 2, - 'ui_reindex.stop': 1, - 'ui_reindex.not_defined': 1, - }, - }; - }, - }; - }), - }, elasticsearch: { client: clusterClient, }, @@ -91,17 +71,6 @@ describe('Upgrade Assistant Usage Collector', () => { callClusterStub ); expect(upgradeAssistantStats).toEqual({ - ui_open: { - overview: 10, - elasticsearch: 20, - kibana: 15, - }, - ui_reindex: { - close: 1, - open: 4, - start: 2, - stop: 1, - }, features: { deprecation_logging: { enabled: true, diff --git a/x-pack/plugins/upgrade_assistant/server/lib/telemetry/usage_collector.ts b/x-pack/plugins/upgrade_assistant/server/lib/telemetry/usage_collector.ts index 56932f5e54b06..c535cd14f104d 100644 --- a/x-pack/plugins/upgrade_assistant/server/lib/telemetry/usage_collector.ts +++ b/x-pack/plugins/upgrade_assistant/server/lib/telemetry/usage_collector.ts @@ -5,43 +5,14 @@ * 2.0. */ -import { get } from 'lodash'; -import { - ElasticsearchClient, - ElasticsearchServiceStart, - ISavedObjectsRepository, - SavedObjectsServiceStart, -} from 'src/core/server'; +import { ElasticsearchClient, ElasticsearchServiceStart } from 'src/core/server'; import { UsageCollectionSetup } from 'src/plugins/usage_collection/server'; -import { - UPGRADE_ASSISTANT_DOC_ID, - UPGRADE_ASSISTANT_TYPE, - UpgradeAssistantTelemetry, - UpgradeAssistantTelemetrySavedObject, - UpgradeAssistantTelemetrySavedObjectAttributes, -} from '../../../common/types'; +import { UpgradeAssistantTelemetry } from '../../../common/types'; import { isDeprecationLogIndexingEnabled, isDeprecationLoggingEnabled, } from '../es_deprecation_logging_apis'; -async function getSavedObjectAttributesFromRepo( - savedObjectsRepository: ISavedObjectsRepository, - docType: string, - docID: string -) { - try { - return ( - await savedObjectsRepository.get( - docType, - docID - ) - ).attributes; - } catch (e) { - return null; - } -} - async function getDeprecationLoggingStatusValue(esClient: ElasticsearchClient): Promise { try { const { body: loggerDeprecationCallResult } = await esClient.cluster.getSettings({ @@ -57,58 +28,14 @@ async function getDeprecationLoggingStatusValue(esClient: ElasticsearchClient): } } -export async function fetchUpgradeAssistantMetrics( - { client: esClient }: ElasticsearchServiceStart, - savedObjects: SavedObjectsServiceStart -): Promise { - const savedObjectsRepository = savedObjects.createInternalRepository(); - const upgradeAssistantSOAttributes = await getSavedObjectAttributesFromRepo( - savedObjectsRepository, - UPGRADE_ASSISTANT_TYPE, - UPGRADE_ASSISTANT_DOC_ID - ); +export async function fetchUpgradeAssistantMetrics({ + client: esClient, +}: ElasticsearchServiceStart): Promise { const deprecationLoggingStatusValue = await getDeprecationLoggingStatusValue( esClient.asInternalUser ); - const getTelemetrySavedObject = ( - upgradeAssistantTelemetrySavedObjectAttrs: UpgradeAssistantTelemetrySavedObjectAttributes | null - ): UpgradeAssistantTelemetrySavedObject => { - const defaultTelemetrySavedObject = { - ui_open: { - overview: 0, - elasticsearch: 0, - kibana: 0, - }, - ui_reindex: { - close: 0, - open: 0, - start: 0, - stop: 0, - }, - }; - - if (!upgradeAssistantTelemetrySavedObjectAttrs) { - return defaultTelemetrySavedObject; - } - - return { - ui_open: { - overview: get(upgradeAssistantTelemetrySavedObjectAttrs, 'ui_open.overview', 0), - elasticsearch: get(upgradeAssistantTelemetrySavedObjectAttrs, 'ui_open.elasticsearch', 0), - kibana: get(upgradeAssistantTelemetrySavedObjectAttrs, 'ui_open.kibana', 0), - }, - ui_reindex: { - close: get(upgradeAssistantTelemetrySavedObjectAttrs, 'ui_reindex.close', 0), - open: get(upgradeAssistantTelemetrySavedObjectAttrs, 'ui_reindex.open', 0), - start: get(upgradeAssistantTelemetrySavedObjectAttrs, 'ui_reindex.start', 0), - stop: get(upgradeAssistantTelemetrySavedObjectAttrs, 'ui_reindex.stop', 0), - }, - } as UpgradeAssistantTelemetrySavedObject; - }; - return { - ...getTelemetrySavedObject(upgradeAssistantSOAttributes), features: { deprecation_logging: { enabled: deprecationLoggingStatusValue, @@ -119,14 +46,12 @@ export async function fetchUpgradeAssistantMetrics( interface Dependencies { elasticsearch: ElasticsearchServiceStart; - savedObjects: SavedObjectsServiceStart; usageCollection: UsageCollectionSetup; } export function registerUpgradeAssistantUsageCollector({ elasticsearch, usageCollection, - savedObjects, }: Dependencies) { const upgradeAssistantUsageCollector = usageCollection.makeUsageCollector({ @@ -143,34 +68,8 @@ export function registerUpgradeAssistantUsageCollector({ }, }, }, - ui_open: { - elasticsearch: { - type: 'long', - _meta: { - description: 'Number of times a user viewed the list of Elasticsearch deprecations.', - }, - }, - overview: { - type: 'long', - _meta: { - description: 'Number of times a user viewed the Overview page.', - }, - }, - kibana: { - type: 'long', - _meta: { - description: 'Number of times a user viewed the list of Kibana deprecations', - }, - }, - }, - ui_reindex: { - close: { type: 'long' }, - open: { type: 'long' }, - start: { type: 'long' }, - stop: { type: 'long' }, - }, }, - fetch: async () => fetchUpgradeAssistantMetrics(elasticsearch, savedObjects), + fetch: async () => fetchUpgradeAssistantMetrics(elasticsearch), }); usageCollection.registerCollector(upgradeAssistantUsageCollector); diff --git a/x-pack/plugins/upgrade_assistant/server/plugin.ts b/x-pack/plugins/upgrade_assistant/server/plugin.ts index 800aeecc57d55..717f03758f825 100644 --- a/x-pack/plugins/upgrade_assistant/server/plugin.ts +++ b/x-pack/plugins/upgrade_assistant/server/plugin.ts @@ -19,6 +19,7 @@ import { SecurityPluginStart } from '../../security/server'; import { InfraPluginSetup } from '../../infra/server'; import { PluginSetupContract as FeaturesPluginSetup } from '../../features/server'; +import { SecurityPluginSetup } from '../../security/server'; import { LicensingPluginSetup } from '../../licensing/server'; import { DEPRECATION_LOGS_SOURCE_ID, DEPRECATION_LOGS_INDEX } from '../common/constants'; @@ -42,6 +43,7 @@ interface PluginsSetup { licensing: LicensingPluginSetup; features: FeaturesPluginSetup; infra: InfraPluginSetup; + security?: SecurityPluginSetup; } interface PluginsStart { @@ -76,7 +78,7 @@ export class UpgradeAssistantServerPlugin implements Plugin { setup( { http, getStartServices, savedObjects }: CoreSetup, - { usageCollection, features, licensing, infra }: PluginsSetup + { usageCollection, features, licensing, infra, security }: PluginsSetup ) { this.licensing = licensing; @@ -129,6 +131,9 @@ export class UpgradeAssistantServerPlugin implements Plugin { lib: { handleEsError, }, + config: { + isSecurityEnabled: () => security !== undefined && security.license.isEnabled(), + }, }; // Initialize version service with current kibana version @@ -137,11 +142,10 @@ export class UpgradeAssistantServerPlugin implements Plugin { registerRoutes(dependencies, this.getWorker.bind(this)); if (usageCollection) { - getStartServices().then(([{ savedObjects: savedObjectsService, elasticsearch }]) => { + getStartServices().then(([{ elasticsearch }]) => { registerUpgradeAssistantUsageCollector({ elasticsearch, usageCollection, - savedObjects: savedObjectsService, }); }); } diff --git a/x-pack/plugins/upgrade_assistant/server/routes/app.ts b/x-pack/plugins/upgrade_assistant/server/routes/app.ts new file mode 100644 index 0000000000000..682dc83410f81 --- /dev/null +++ b/x-pack/plugins/upgrade_assistant/server/routes/app.ts @@ -0,0 +1,80 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { API_BASE_PATH, DEPRECATION_LOGS_INDEX } from '../../common/constants'; +import { versionCheckHandlerWrapper } from '../lib/es_version_precheck'; +import { Privileges } from '../shared_imports'; +import { RouteDependencies } from '../types'; + +const extractMissingPrivileges = ( + privilegesObject: { [key: string]: Record } = {} +): string[] => + Object.keys(privilegesObject).reduce((privileges: string[], privilegeName: string): string[] => { + if (Object.values(privilegesObject[privilegeName]).some((e) => !e)) { + privileges.push(privilegeName); + } + return privileges; + }, []); + +export function registerAppRoutes({ + router, + lib: { handleEsError }, + config: { isSecurityEnabled }, +}: RouteDependencies) { + router.get( + { + path: `${API_BASE_PATH}/privileges`, + validate: false, + }, + versionCheckHandlerWrapper( + async ( + { + core: { + elasticsearch: { client }, + }, + }, + request, + response + ) => { + const privilegesResult: Privileges = { + hasAllPrivileges: true, + missingPrivileges: { + index: [], + }, + }; + + if (!isSecurityEnabled()) { + return response.ok({ body: privilegesResult }); + } + + try { + const { + body: { has_all_requested: hasAllPrivileges, index }, + } = await client.asCurrentUser.security.hasPrivileges({ + body: { + index: [ + { + names: [DEPRECATION_LOGS_INDEX], + privileges: ['read'], + }, + ], + }, + }); + + if (!hasAllPrivileges) { + privilegesResult.missingPrivileges.index = extractMissingPrivileges(index); + } + + privilegesResult.hasAllPrivileges = hasAllPrivileges; + return response.ok({ body: privilegesResult }); + } catch (error) { + return handleEsError({ error, response }); + } + } + ) + ); +} diff --git a/x-pack/plugins/upgrade_assistant/server/routes/register_routes.ts b/x-pack/plugins/upgrade_assistant/server/routes/register_routes.ts index 813b25c4a79d0..002f34a489cff 100644 --- a/x-pack/plugins/upgrade_assistant/server/routes/register_routes.ts +++ b/x-pack/plugins/upgrade_assistant/server/routes/register_routes.ts @@ -7,22 +7,22 @@ import { RouteDependencies } from '../types'; +import { registerAppRoutes } from './app'; import { registerCloudBackupStatusRoutes } from './cloud_backup_status'; import { registerESDeprecationRoutes } from './es_deprecations'; import { registerDeprecationLoggingRoutes } from './deprecation_logging'; import { registerReindexIndicesRoutes } from './reindex_indices'; -import { registerTelemetryRoutes } from './telemetry'; import { registerUpdateSettingsRoute } from './update_index_settings'; import { registerMlSnapshotRoutes } from './ml_snapshots'; import { ReindexWorker } from '../lib/reindexing'; import { registerUpgradeStatusRoute } from './status'; export function registerRoutes(dependencies: RouteDependencies, getWorker: () => ReindexWorker) { + registerAppRoutes(dependencies); registerCloudBackupStatusRoutes(dependencies); registerESDeprecationRoutes(dependencies); registerDeprecationLoggingRoutes(dependencies); registerReindexIndicesRoutes(dependencies, getWorker); - registerTelemetryRoutes(dependencies); registerUpdateSettingsRoute(dependencies); registerMlSnapshotRoutes(dependencies); // Route for cloud to retrieve the upgrade status for ES and Kibana diff --git a/x-pack/plugins/upgrade_assistant/server/routes/telemetry.test.ts b/x-pack/plugins/upgrade_assistant/server/routes/telemetry.test.ts deleted file mode 100644 index 578cceb702751..0000000000000 --- a/x-pack/plugins/upgrade_assistant/server/routes/telemetry.test.ts +++ /dev/null @@ -1,187 +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 { kibanaResponseFactory } from 'src/core/server'; -import { savedObjectsServiceMock } from 'src/core/server/mocks'; -import { createMockRouter, MockRouter, routeHandlerContextMock } from './__mocks__/routes.mock'; -import { createRequestMock } from './__mocks__/request.mock'; - -jest.mock('../lib/telemetry/es_ui_open_apis', () => ({ - upsertUIOpenOption: jest.fn(), -})); - -jest.mock('../lib/telemetry/es_ui_reindex_apis', () => ({ - upsertUIReindexOption: jest.fn(), -})); - -import { upsertUIOpenOption } from '../lib/telemetry/es_ui_open_apis'; -import { upsertUIReindexOption } from '../lib/telemetry/es_ui_reindex_apis'; -import { registerTelemetryRoutes } from './telemetry'; - -/** - * Since these route callbacks are so thin, these serve simply as integration tests - * to ensure they're wired up to the lib functions correctly. Business logic is tested - * more thoroughly in the lib/telemetry tests. - */ -describe('Upgrade Assistant Telemetry API', () => { - let routeDependencies: any; - let mockRouter: MockRouter; - beforeEach(() => { - mockRouter = createMockRouter(); - routeDependencies = { - getSavedObjectsService: () => savedObjectsServiceMock.create(), - router: mockRouter, - }; - registerTelemetryRoutes(routeDependencies); - }); - afterEach(() => jest.clearAllMocks()); - - describe('PUT /api/upgrade_assistant/stats/ui_open', () => { - it('returns correct payload with single option', async () => { - const returnPayload = { - overview: true, - elasticsearch: false, - kibana: false, - }; - - (upsertUIOpenOption as jest.Mock).mockResolvedValue(returnPayload); - - const resp = await routeDependencies.router.getHandler({ - method: 'put', - pathPattern: '/api/upgrade_assistant/stats/ui_open', - })( - routeHandlerContextMock, - createRequestMock({ body: returnPayload }), - kibanaResponseFactory - ); - - expect(resp.payload).toEqual(returnPayload); - }); - - it('returns correct payload with multiple option', async () => { - const returnPayload = { - overview: true, - elasticsearch: true, - kibana: true, - }; - - (upsertUIOpenOption as jest.Mock).mockResolvedValue(returnPayload); - - const resp = await routeDependencies.router.getHandler({ - method: 'put', - pathPattern: '/api/upgrade_assistant/stats/ui_open', - })( - routeHandlerContextMock, - createRequestMock({ - body: { - overview: true, - elasticsearch: true, - kibana: true, - }, - }), - kibanaResponseFactory - ); - - expect(resp.payload).toEqual(returnPayload); - }); - - it('returns an error if it throws', async () => { - (upsertUIOpenOption as jest.Mock).mockRejectedValue(new Error(`scary error!`)); - - await expect( - routeDependencies.router.getHandler({ - method: 'put', - pathPattern: '/api/upgrade_assistant/stats/ui_open', - })( - routeHandlerContextMock, - createRequestMock({ - body: { - overview: false, - }, - }), - kibanaResponseFactory - ) - ).rejects.toThrowError('scary error!'); - }); - }); - - describe('PUT /api/upgrade_assistant/stats/ui_reindex', () => { - it('returns correct payload with single option', async () => { - const returnPayload = { - close: false, - open: false, - start: true, - stop: false, - }; - - (upsertUIReindexOption as jest.Mock).mockResolvedValue(returnPayload); - - const resp = await routeDependencies.router.getHandler({ - method: 'put', - pathPattern: '/api/upgrade_assistant/stats/ui_reindex', - })( - routeHandlerContextMock, - createRequestMock({ - body: { - overview: false, - }, - }), - kibanaResponseFactory - ); - - expect(resp.payload).toEqual(returnPayload); - }); - - it('returns correct payload with multiple option', async () => { - const returnPayload = { - close: true, - open: true, - start: true, - stop: true, - }; - - (upsertUIReindexOption as jest.Mock).mockResolvedValue(returnPayload); - - const resp = await routeDependencies.router.getHandler({ - method: 'put', - pathPattern: '/api/upgrade_assistant/stats/ui_reindex', - })( - routeHandlerContextMock, - createRequestMock({ - body: { - close: true, - open: true, - start: true, - stop: true, - }, - }), - kibanaResponseFactory - ); - - expect(resp.payload).toEqual(returnPayload); - }); - - it('returns an error if it throws', async () => { - (upsertUIReindexOption as jest.Mock).mockRejectedValue(new Error(`scary error!`)); - - await expect( - routeDependencies.router.getHandler({ - method: 'put', - pathPattern: '/api/upgrade_assistant/stats/ui_reindex', - })( - routeHandlerContextMock, - createRequestMock({ - body: { - start: false, - }, - }), - kibanaResponseFactory - ) - ).rejects.toThrowError('scary error!'); - }); - }); -}); diff --git a/x-pack/plugins/upgrade_assistant/server/routes/telemetry.ts b/x-pack/plugins/upgrade_assistant/server/routes/telemetry.ts deleted file mode 100644 index d083b38c7c240..0000000000000 --- a/x-pack/plugins/upgrade_assistant/server/routes/telemetry.ts +++ /dev/null @@ -1,64 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { schema } from '@kbn/config-schema'; -import { API_BASE_PATH } from '../../common/constants'; -import { upsertUIOpenOption } from '../lib/telemetry/es_ui_open_apis'; -import { upsertUIReindexOption } from '../lib/telemetry/es_ui_reindex_apis'; -import { RouteDependencies } from '../types'; - -export function registerTelemetryRoutes({ router, getSavedObjectsService }: RouteDependencies) { - router.put( - { - path: `${API_BASE_PATH}/stats/ui_open`, - validate: { - body: schema.object({ - overview: schema.boolean({ defaultValue: false }), - elasticsearch: schema.boolean({ defaultValue: false }), - kibana: schema.boolean({ defaultValue: false }), - }), - }, - }, - async (ctx, request, response) => { - const { elasticsearch, overview, kibana } = request.body; - return response.ok({ - body: await upsertUIOpenOption({ - savedObjects: getSavedObjectsService(), - elasticsearch, - overview, - kibana, - }), - }); - } - ); - - router.put( - { - path: `${API_BASE_PATH}/stats/ui_reindex`, - validate: { - body: schema.object({ - close: schema.boolean({ defaultValue: false }), - open: schema.boolean({ defaultValue: false }), - start: schema.boolean({ defaultValue: false }), - stop: schema.boolean({ defaultValue: false }), - }), - }, - }, - async (ctx, request, response) => { - const { close, open, start, stop } = request.body; - return response.ok({ - body: await upsertUIReindexOption({ - savedObjects: getSavedObjectsService(), - close, - open, - start, - stop, - }), - }); - } - ); -} diff --git a/x-pack/plugins/upgrade_assistant/server/saved_object_types/telemetry_saved_object_type.ts b/x-pack/plugins/upgrade_assistant/server/saved_object_types/telemetry_saved_object_type.ts index 42d5d339dd050..cb3fbcaef59b7 100644 --- a/x-pack/plugins/upgrade_assistant/server/saved_object_types/telemetry_saved_object_type.ts +++ b/x-pack/plugins/upgrade_assistant/server/saved_object_types/telemetry_saved_object_type.ts @@ -15,42 +15,6 @@ export const telemetrySavedObjectType: SavedObjectsType = { namespaceType: 'agnostic', mappings: { properties: { - ui_open: { - properties: { - overview: { - type: 'long', - null_value: 0, - }, - elasticsearch: { - type: 'long', - null_value: 0, - }, - kibana: { - type: 'long', - null_value: 0, - }, - }, - }, - ui_reindex: { - properties: { - close: { - type: 'long', - null_value: 0, - }, - open: { - type: 'long', - null_value: 0, - }, - start: { - type: 'long', - null_value: 0, - }, - stop: { - type: 'long', - null_value: 0, - }, - }, - }, features: { properties: { deprecation_logging: { diff --git a/x-pack/plugins/upgrade_assistant/server/shared_imports.ts b/x-pack/plugins/upgrade_assistant/server/shared_imports.ts index 7f55d189457c7..3193c0b4b54a0 100644 --- a/x-pack/plugins/upgrade_assistant/server/shared_imports.ts +++ b/x-pack/plugins/upgrade_assistant/server/shared_imports.ts @@ -6,3 +6,4 @@ */ export { handleEsError } from '../../../../src/plugins/es_ui_shared/server'; +export { Privileges } from '../../../../src/plugins/es_ui_shared/common'; diff --git a/x-pack/plugins/upgrade_assistant/server/types.ts b/x-pack/plugins/upgrade_assistant/server/types.ts index 09272d270333e..376514c59d494 100644 --- a/x-pack/plugins/upgrade_assistant/server/types.ts +++ b/x-pack/plugins/upgrade_assistant/server/types.ts @@ -21,4 +21,7 @@ export interface RouteDependencies { lib: { handleEsError: typeof handleEsError; }; + config: { + isSecurityEnabled: () => boolean; + }; } diff --git a/x-pack/plugins/uptime/common/constants/ui.ts b/x-pack/plugins/uptime/common/constants/ui.ts index 29df2614d0617..659d5727abc0c 100644 --- a/x-pack/plugins/uptime/common/constants/ui.ts +++ b/x-pack/plugins/uptime/common/constants/ui.ts @@ -17,6 +17,8 @@ export const STEP_DETAIL_ROUTE = '/journey/:checkGroupId/step/:stepIndex'; export const SYNTHETIC_CHECK_STEPS_ROUTE = '/journey/:checkGroupId/steps'; +export const MAPPING_ERROR_ROUTE = '/mapping-error'; + export enum STATUS { UP = 'up', DOWN = 'down', diff --git a/x-pack/plugins/uptime/public/apps/uptime_page_template.tsx b/x-pack/plugins/uptime/public/apps/uptime_page_template.tsx new file mode 100644 index 0000000000000..829f587e248e7 --- /dev/null +++ b/x-pack/plugins/uptime/public/apps/uptime_page_template.tsx @@ -0,0 +1,64 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { useMemo } from 'react'; +import styled from 'styled-components'; +import { EuiPageHeaderProps } from '@elastic/eui'; +import { OVERVIEW_ROUTE } from '../../common/constants'; +import { useKibana } from '../../../../../src/plugins/kibana_react/public'; +import { ClientPluginsStart } from './plugin'; +import { useNoDataConfig } from './use_no_data_config'; +import { EmptyStateLoading } from '../components/overview/empty_state/empty_state_loading'; +import { EmptyStateError } from '../components/overview/empty_state/empty_state_error'; +import { useHasData } from '../components/overview/empty_state/use_has_data'; + +interface Props { + path: string; + pageHeader?: EuiPageHeaderProps; +} + +export const UptimePageTemplateComponent: React.FC = ({ path, pageHeader, children }) => { + const { + services: { observability }, + } = useKibana(); + + const PageTemplateComponent = observability.navigation.PageTemplate; + + const StyledPageTemplateComponent = useMemo(() => { + return styled(PageTemplateComponent)` + .euiPageHeaderContent > .euiFlexGroup { + flex-wrap: wrap; + } + `; + }, [PageTemplateComponent]); + + const noDataConfig = useNoDataConfig(); + + const { loading, error } = useHasData(); + + if (error) { + return ; + } + + return ( + <> +
    + + {loading && path === OVERVIEW_ROUTE && } +
    + {children} +
    +
    + + ); +}; diff --git a/x-pack/plugins/uptime/public/apps/use_no_data_config.ts b/x-pack/plugins/uptime/public/apps/use_no_data_config.ts new file mode 100644 index 0000000000000..dc00a25e3a111 --- /dev/null +++ b/x-pack/plugins/uptime/public/apps/use_no_data_config.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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { i18n } from '@kbn/i18n'; +import { useContext } from 'react'; +import { useSelector } from 'react-redux'; +import { KibanaPageTemplateProps, useKibana } from '../../../../../src/plugins/kibana_react/public'; +import { UptimeSettingsContext } from '../contexts'; +import { ClientPluginsStart } from './plugin'; +import { indexStatusSelector } from '../state/selectors'; + +export function useNoDataConfig(): KibanaPageTemplateProps['noDataConfig'] { + const { basePath } = useContext(UptimeSettingsContext); + + const { + services: { docLinks }, + } = useKibana(); + + const { data } = useSelector(indexStatusSelector); + + // Returns no data config when there is no historical data + if (data && !data.indexExists) { + return { + solution: i18n.translate('xpack.uptime.noDataConfig.solutionName', { + defaultMessage: 'Observability', + }), + actions: { + beats: { + title: i18n.translate('xpack.uptime.noDataConfig.beatsCard.title', { + defaultMessage: 'Add monitors with Heartbeat', + }), + description: i18n.translate('xpack.uptime.noDataConfig.beatsCard.description', { + defaultMessage: + 'Proactively monitor the availability of your sites and services. Receive alerts and resolve issues faster to optimize your users experience.', + }), + href: basePath + `/app/home#/tutorial/uptimeMonitors`, + }, + }, + docsLink: docLinks!.links.observability.guide, + }; + } +} diff --git a/x-pack/plugins/uptime/public/components/overview/empty_state/data_or_index_missing.test.tsx b/x-pack/plugins/uptime/public/components/overview/empty_state/data_or_index_missing.test.tsx deleted file mode 100644 index caff055ce987c..0000000000000 --- a/x-pack/plugins/uptime/public/components/overview/empty_state/data_or_index_missing.test.tsx +++ /dev/null @@ -1,26 +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 { screen } from '@testing-library/react'; -import { FormattedMessage } from '@kbn/i18n/react'; -import { render } from '../../../lib/helper/rtl_helpers'; -import { DataOrIndexMissing } from './data_or_index_missing'; - -describe('DataOrIndexMissing component', () => { - it('renders headingMessage', () => { - const headingMessage = ( - heartbeat-* }} - /> - ); - render(); - expect(screen.getByText(/heartbeat-*/)).toBeInTheDocument(); - }); -}); diff --git a/x-pack/plugins/uptime/public/components/overview/empty_state/data_or_index_missing.tsx b/x-pack/plugins/uptime/public/components/overview/empty_state/data_or_index_missing.tsx deleted file mode 100644 index 44e55de990bbf..0000000000000 --- a/x-pack/plugins/uptime/public/components/overview/empty_state/data_or_index_missing.tsx +++ /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 { - EuiFlexGroup, - EuiEmptyPrompt, - EuiFlexItem, - EuiSpacer, - EuiPanel, - EuiTitle, - EuiButton, -} from '@elastic/eui'; -import { FormattedMessage } from '@kbn/i18n/react'; -import React, { useContext } from 'react'; -import { UptimeSettingsContext } from '../../../contexts'; -import { DynamicSettings } from '../../../../common/runtime_types'; - -interface DataMissingProps { - headingMessage: JSX.Element; - settings?: DynamicSettings; -} - -export const DataOrIndexMissing = ({ headingMessage, settings }: DataMissingProps) => { - const { basePath } = useContext(UptimeSettingsContext); - return ( - - - - - -

    {headingMessage}

    - - } - body={ - <> -

    - -

    -

    - -

    - - } - actions={ - - - - - - - - - - - - - } - /> -
    -
    -
    - ); -}; diff --git a/x-pack/plugins/uptime/public/components/overview/empty_state/empty_state.test.tsx b/x-pack/plugins/uptime/public/components/overview/empty_state/empty_state.test.tsx deleted file mode 100644 index 45b107928d79a..0000000000000 --- a/x-pack/plugins/uptime/public/components/overview/empty_state/empty_state.test.tsx +++ /dev/null @@ -1,99 +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 { screen } from '@testing-library/react'; -import { EmptyStateComponent } from './empty_state'; -import { StatesIndexStatus } from '../../../../common/runtime_types'; -import { HttpFetchError, IHttpFetchError } from 'src/core/public'; -import { render } from '../../../lib/helper/rtl_helpers'; - -describe('EmptyState component', () => { - let statesIndexStatus: StatesIndexStatus; - - beforeEach(() => { - statesIndexStatus = { - indexExists: true, - docCount: 1, - indices: 'heartbeat-*,synthetics-*', - }; - }); - - it('renders child components when count is truthy', () => { - render( - -
    Foo
    -
    Bar
    -
    Baz
    -
    - ); - - expect(screen.getByText('Foo')).toBeInTheDocument(); - expect(screen.getByText('Bar')).toBeInTheDocument(); - expect(screen.getByText('Baz')).toBeInTheDocument(); - }); - - it(`doesn't render child components when count is falsy`, () => { - render( - -
    Should not be rendered
    -
    - ); - expect(screen.queryByText('Should not be rendered')).toBeNull(); - }); - - it(`renders error message when an error occurs`, () => { - const errors: IHttpFetchError[] = [ - new HttpFetchError('There was an error fetching your data.', 'error', {} as any, {} as any, { - body: { message: 'There was an error fetching your data.' }, - }), - ]; - render( - -
    Should not appear...
    -
    - ); - expect(screen.queryByText('Should not appear...')).toBeNull(); - }); - - it('renders loading state if no errors or doc count', () => { - render( - -
    Should appear even while loading...
    -
    - ); - expect(screen.queryByText('Should appear even while loading...')).toBeInTheDocument(); - }); - - it('does not render empty state with appropriate base path and no docs', () => { - statesIndexStatus = { - docCount: 0, - indexExists: true, - indices: 'heartbeat-*,synthetics-*', - }; - const text = 'If this is in the snapshot the test should fail'; - render( - -
    {text}
    -
    - ); - expect(screen.queryByText(text)).toBeNull(); - }); - - it('notifies when index does not exist', () => { - statesIndexStatus.indexExists = false; - - const text = 'This text should not render'; - - render( - -
    {text}
    -
    - ); - expect(screen.queryByText(text)).toBeNull(); - }); -}); diff --git a/x-pack/plugins/uptime/public/components/overview/empty_state/empty_state.tsx b/x-pack/plugins/uptime/public/components/overview/empty_state/empty_state.tsx deleted file mode 100644 index a6fd6579c49fa..0000000000000 --- a/x-pack/plugins/uptime/public/components/overview/empty_state/empty_state.tsx +++ /dev/null @@ -1,73 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React, { Fragment } from 'react'; -import { FormattedMessage } from '@kbn/i18n/react'; -import { IHttpFetchError } from 'src/core/public'; -import { EmptyStateError } from './empty_state_error'; -import { EmptyStateLoading } from './empty_state_loading'; -import { DataOrIndexMissing } from './data_or_index_missing'; -import { DynamicSettings, StatesIndexStatus } from '../../../../common/runtime_types'; - -interface EmptyStateProps { - children: JSX.Element[] | JSX.Element; - statesIndexStatus: StatesIndexStatus | null; - loading: boolean; - errors?: IHttpFetchError[]; - settings?: DynamicSettings; -} - -export const EmptyStateComponent = ({ - children, - statesIndexStatus, - loading, - errors, - settings, -}: EmptyStateProps) => { - if (errors?.length) { - return ; - } - const { indexExists, docCount } = statesIndexStatus ?? {}; - - const isLoading = loading && (!indexExists || docCount === 0 || !statesIndexStatus); - - const noIndicesMessage = ( - {settings?.heartbeatIndices} }} - /> - ); - - const noUptimeDataMessage = ( - {settings?.heartbeatIndices} }} - /> - ); - - if (!indexExists && !isLoading) { - return ; - } else if (indexExists && docCount === 0 && !isLoading) { - return ; - } - /** - * We choose to render the children any time the count > 0, even if - * the component is loading. If we render the loading state for this component, - * it will blow away the state of child components and trigger an ugly - * jittery UX any time the components refresh. This way we'll keep the stale - * state displayed during the fetching process. - */ - return ( - - {isLoading && } -
    {children}
    -
    - ); - // } -}; diff --git a/x-pack/plugins/uptime/public/components/overview/empty_state/empty_state_container.tsx b/x-pack/plugins/uptime/public/components/overview/empty_state/empty_state_container.tsx deleted file mode 100644 index 562e45727dda7..0000000000000 --- a/x-pack/plugins/uptime/public/components/overview/empty_state/empty_state_container.tsx +++ /dev/null @@ -1,54 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React, { useContext, useEffect } from 'react'; -import { useDispatch, useSelector } from 'react-redux'; -import { indexStatusAction } from '../../../state/actions'; -import { indexStatusSelector, selectDynamicSettings } from '../../../state/selectors'; -import { EmptyStateComponent } from './index'; -import { UptimeRefreshContext } from '../../../contexts'; -import { getDynamicSettings } from '../../../state/actions/dynamic_settings'; - -export const EmptyState: React.FC = ({ children }) => { - const { data, loading, error } = useSelector(indexStatusSelector); - const { lastRefresh } = useContext(UptimeRefreshContext); - - const { settings } = useSelector(selectDynamicSettings); - - const heartbeatIndices = settings?.heartbeatIndices || ''; - - const dispatch = useDispatch(); - - const noDataInfo = !data || data?.docCount === 0 || data?.indexExists === false; - - useEffect(() => { - if (noDataInfo) { - // only call when we haven't fetched it already - dispatch(indexStatusAction.get()); - } - }, [dispatch, lastRefresh, noDataInfo]); - - useEffect(() => { - // using separate side effect, we want to call index status, - // every statue indices setting changes - dispatch(indexStatusAction.get()); - }, [dispatch, heartbeatIndices]); - - useEffect(() => { - dispatch(getDynamicSettings()); - }, [dispatch]); - - return ( - - ); -}; diff --git a/x-pack/plugins/uptime/public/components/overview/empty_state/use_has_data.tsx b/x-pack/plugins/uptime/public/components/overview/empty_state/use_has_data.tsx new file mode 100644 index 0000000000000..66c68834f285f --- /dev/null +++ b/x-pack/plugins/uptime/public/components/overview/empty_state/use_has_data.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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { useContext, useEffect } from 'react'; +import { useDispatch, useSelector } from 'react-redux'; +import { indexStatusAction } from '../../../state/actions'; +import { indexStatusSelector, selectDynamicSettings } from '../../../state/selectors'; +import { UptimeRefreshContext } from '../../../contexts'; +import { getDynamicSettings } from '../../../state/actions/dynamic_settings'; + +export const useHasData = () => { + const { loading, error } = useSelector(indexStatusSelector); + const { lastRefresh } = useContext(UptimeRefreshContext); + + const { settings } = useSelector(selectDynamicSettings); + + const dispatch = useDispatch(); + + useEffect(() => { + dispatch(indexStatusAction.get()); + }, [dispatch, lastRefresh]); + + useEffect(() => { + dispatch(getDynamicSettings()); + }, [dispatch]); + + return { + error, + loading, + settings, + }; +}; diff --git a/x-pack/plugins/uptime/public/components/overview/index.ts b/x-pack/plugins/uptime/public/components/overview/index.ts index d647c38cee1ca..6ff4524df5277 100644 --- a/x-pack/plugins/uptime/public/components/overview/index.ts +++ b/x-pack/plugins/uptime/public/components/overview/index.ts @@ -6,6 +6,5 @@ */ export * from './monitor_list'; -export * from './empty_state'; export * from './alerts'; export * from './snapshot'; diff --git a/x-pack/plugins/uptime/public/components/overview/monitor_list/monitor_list_container.tsx b/x-pack/plugins/uptime/public/components/overview/monitor_list/monitor_list_container.tsx index 835a89e8f7272..726ef59827f9e 100644 --- a/x-pack/plugins/uptime/public/components/overview/monitor_list/monitor_list_container.tsx +++ b/x-pack/plugins/uptime/public/components/overview/monitor_list/monitor_list_container.tsx @@ -13,6 +13,7 @@ import { MonitorListComponent } from './monitor_list'; import { useUrlParams } from '../../../hooks'; import { UptimeRefreshContext } from '../../../contexts'; import { getConnectorsAction, getMonitorAlertsAction } from '../../../state/alerts/alerts'; +import { useMappingCheck } from '../../../hooks/use_mapping_check'; export interface MonitorListProps { filters?: string; @@ -41,6 +42,7 @@ export const MonitorList: React.FC = (props) => { const { lastRefresh } = useContext(UptimeRefreshContext); const monitorList = useSelector(monitorListSelector); + useMappingCheck(monitorList.error); useEffect(() => { dispatch( diff --git a/x-pack/plugins/uptime/public/hooks/use_mapping_check.test.ts b/x-pack/plugins/uptime/public/hooks/use_mapping_check.test.ts new file mode 100644 index 0000000000000..5f17e65d102b4 --- /dev/null +++ b/x-pack/plugins/uptime/public/hooks/use_mapping_check.test.ts @@ -0,0 +1,53 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { shouldRedirect } from './use_mapping_check'; + +describe('useMappingCheck', () => { + describe('should redirect', () => { + it('returns true for appropriate error', () => { + const error = { + request: {}, + response: {}, + body: { + statusCode: 400, + error: 'Bad Request', + message: + '[search_phase_execution_exception: [illegal_argument_exception] Reason: Text fields are not optimised for operations that require per-document field data like aggregations and sorting, so these operations are disabled by default. Please use a keyword field instead. Alternatively, set fielddata=true on [monitor.id] in order to load field data by uninverting the inverted index. Note that this can use significant memory.]: all shards failed', + }, + name: 'Error', + req: {}, + res: {}, + }; + expect(shouldRedirect(error)).toBe(true); + }); + + it('returns false for undefined', () => { + expect(shouldRedirect(undefined)).toBe(false); + }); + + it('returns false for missing body', () => { + expect(shouldRedirect({})).toBe(false); + }); + + it('returns false for incorrect error string', () => { + expect(shouldRedirect({ body: { error: 'not the right type' } })).toBe(false); + }); + + it('returns false for missing body message', () => { + expect(shouldRedirect({ body: { error: 'Bad Request' } })).toBe(false); + }); + + it('returns false for incorrect error message', () => { + expect( + shouldRedirect({ + body: { error: 'Bad Request', message: 'Not the correct kind of error message' }, + }) + ); + }); + }); +}); diff --git a/x-pack/plugins/uptime/public/hooks/use_mapping_check.ts b/x-pack/plugins/uptime/public/hooks/use_mapping_check.ts new file mode 100644 index 0000000000000..d8a7e0fac4065 --- /dev/null +++ b/x-pack/plugins/uptime/public/hooks/use_mapping_check.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 { useEffect } from 'react'; +import { useHistory } from 'react-router-dom'; +import { MAPPING_ERROR_ROUTE } from '../../common/constants'; + +interface EsBadRequestError { + body?: { + error?: string; + message?: string; + }; +} + +function contains(message: string, phrase: string) { + return message.indexOf(phrase) !== -1; +} + +export function shouldRedirect(error?: EsBadRequestError) { + if (!error || !error.body || error.body.error !== 'Bad Request' || !error.body.message) { + return false; + } + const { message } = error.body; + return ( + contains(message, 'search_phase_execution_exception') || + contains(message, 'Please use a keyword field instead.') || + contains(message, 'set fielddata=true') + ); +} + +export function useMappingCheck(error?: EsBadRequestError) { + const history = useHistory(); + + useEffect(() => { + if (shouldRedirect(error)) { + history.push(MAPPING_ERROR_ROUTE); + } + }, [error, history]); +} diff --git a/x-pack/plugins/uptime/public/hooks/use_telemetry.ts b/x-pack/plugins/uptime/public/hooks/use_telemetry.ts index 05fbd349b8f0f..f5abdb473fb0d 100644 --- a/x-pack/plugins/uptime/public/hooks/use_telemetry.ts +++ b/x-pack/plugins/uptime/public/hooks/use_telemetry.ts @@ -12,6 +12,7 @@ import { API_URLS } from '../../common/constants'; export enum UptimePage { Overview = 'Overview', + MappingError = 'MappingError', Monitor = 'Monitor', Settings = 'Settings', Certificates = 'Certificates', diff --git a/x-pack/plugins/uptime/public/pages/index.ts b/x-pack/plugins/uptime/public/pages/index.ts index 5624f61c3abb5..352ceb39123e8 100644 --- a/x-pack/plugins/uptime/public/pages/index.ts +++ b/x-pack/plugins/uptime/public/pages/index.ts @@ -5,6 +5,7 @@ * 2.0. */ +export { MappingErrorPage } from './mapping_error'; export { MonitorPage } from './monitor'; export { StepDetailPage } from './synthetics/step_detail_page'; export { SettingsPage } from './settings'; diff --git a/x-pack/plugins/uptime/public/pages/mapping_error.tsx b/x-pack/plugins/uptime/public/pages/mapping_error.tsx new file mode 100644 index 0000000000000..9c234700136b0 --- /dev/null +++ b/x-pack/plugins/uptime/public/pages/mapping_error.tsx @@ -0,0 +1,78 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { EuiCode, EuiEmptyPrompt, EuiLink, EuiTitle } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; + +import React from 'react'; + +import { useKibana } from '../../../../../src/plugins/kibana_react/public'; +import { useBreadcrumbs } from '../hooks/use_breadcrumbs'; +import { useTrackPageview } from '../../../observability/public'; + +export const MappingErrorPage = () => { + useTrackPageview({ app: 'uptime', path: 'mapping-error' }); + useTrackPageview({ app: 'uptime', path: 'mapping-error', delay: 15000 }); + + const docLinks = useKibana().services.docLinks; + + useBreadcrumbs([ + { + text: i18n.translate('xpack.uptime.mappingErrorRoute.breadcrumb', { + defaultMessage: 'Mapping error', + }), + }, + ]); + + return ( + +

    + +

    + + } + body={ +
    +

    + setup }} + /> +

    + {docLinks && ( +

    + + docs + + ), + }} + /> +

    + )} +
    + } + /> + ); +}; diff --git a/x-pack/plugins/uptime/public/pages/overview.tsx b/x-pack/plugins/uptime/public/pages/overview.tsx index c350c8e1c962f..5199eab00f09f 100644 --- a/x-pack/plugins/uptime/public/pages/overview.tsx +++ b/x-pack/plugins/uptime/public/pages/overview.tsx @@ -12,7 +12,6 @@ import styled from 'styled-components'; import { useBreadcrumbs } from '../hooks/use_breadcrumbs'; import { useTrackPageview } from '../../../observability/public'; import { MonitorList } from '../components/overview/monitor_list/monitor_list_container'; -import { EmptyState } from '../components/overview'; import { StatusPanel } from '../components/overview/status_panel'; import { QueryBar } from '../components/overview/query_bar/query_bar'; import { MONITORING_OVERVIEW_LABEL } from '../routes'; @@ -37,7 +36,7 @@ export const OverviewPageComponent = () => { useBreadcrumbs([{ text: MONITORING_OVERVIEW_LABEL }]); // No extra breadcrumbs on overview return ( - + <> @@ -48,6 +47,6 @@ export const OverviewPageComponent = () => { - + ); }; diff --git a/x-pack/plugins/uptime/public/routes.tsx b/x-pack/plugins/uptime/public/routes.tsx index d111d44f08c2d..9f7310b43e556 100644 --- a/x-pack/plugins/uptime/public/routes.tsx +++ b/x-pack/plugins/uptime/public/routes.tsx @@ -6,19 +6,19 @@ */ import React, { FC, useEffect } from 'react'; -import styled from 'styled-components'; import { Route, Switch } from 'react-router-dom'; import { FormattedMessage } from '@kbn/i18n/react'; import { i18n } from '@kbn/i18n'; import { CERTIFICATES_ROUTE, + MAPPING_ERROR_ROUTE, MONITOR_ROUTE, OVERVIEW_ROUTE, SETTINGS_ROUTE, STEP_DETAIL_ROUTE, SYNTHETIC_CHECK_STEPS_ROUTE, } from '../common/constants'; -import { MonitorPage, StepDetailPage, NotFoundPage, SettingsPage } from './pages'; +import { MappingErrorPage, MonitorPage, StepDetailPage, NotFoundPage, SettingsPage } from './pages'; import { CertificatesPage } from './pages/certificates'; import { UptimePage, useUptimeTelemetry } from './hooks'; import { OverviewPageComponent } from './pages/overview'; @@ -27,10 +27,8 @@ import { SyntheticsCheckStepsPageHeader, SyntheticsCheckStepsPageRightSideItem, } from './pages/synthetics/synthetics_checks'; -import { ClientPluginsStart } from './apps/plugin'; import { MonitorPageTitle, MonitorPageTitleContent } from './components/monitor/monitor_title'; import { UptimeDatePicker } from './components/common/uptime_date_picker'; -import { useKibana } from '../../../../src/plugins/kibana_react/public'; import { CertRefreshBtn } from './components/certificates/cert_refresh_btn'; import { CertificateTitle } from './components/certificates/certificate_title'; import { SyntheticsCallout } from './components/overview/synthetics_callout'; @@ -40,6 +38,7 @@ import { StepDetailPageHeader, StepDetailPageRightSideItem, } from './pages/synthetics/step_detail_page'; +import { UptimePageTemplateComponent } from './apps/uptime_page_template'; interface RouteProps { path: string; @@ -144,6 +143,26 @@ const Routes: RouteProps[] = [ rightSideItems: [], }, }, + { + title: i18n.translate('xpack.uptime.mappingErrorRoute.title', { + defaultMessage: 'Synthetics | mapping error', + }), + path: MAPPING_ERROR_ROUTE, + component: MappingErrorPage, + dataTestSubj: 'uptimeMappingErrorPage', + telemetryId: UptimePage.MappingError, + pageHeader: { + pageTitle: ( +
    + +
    + ), + rightSideItems: [], + }, + }, ]; const RouteInit: React.FC> = ({ @@ -159,17 +178,6 @@ const RouteInit: React.FC> = }; export const PageRouter: FC = () => { - const { - services: { observability }, - } = useKibana(); - const PageTemplateComponent = observability.navigation.PageTemplate; - - const StyledPageTemplateComponent = styled(PageTemplateComponent)` - .euiPageHeaderContent > .euiFlexGroup { - flex-wrap: wrap; - } - `; - return ( {Routes.map( @@ -178,9 +186,9 @@ export const PageRouter: FC = () => {
    - + - +
    ) diff --git a/x-pack/plugins/uptime/server/rest_api/monitors/monitor_list.ts b/x-pack/plugins/uptime/server/rest_api/monitors/monitor_list.ts index d28645bcb21a1..36bc5a80ef47a 100644 --- a/x-pack/plugins/uptime/server/rest_api/monitors/monitor_list.ts +++ b/x-pack/plugins/uptime/server/rest_api/monitors/monitor_list.ts @@ -27,7 +27,7 @@ export const createMonitorListRoute: UMRestApiRouteFactory = (libs) => ({ options: { tags: ['access:uptime-read'], }, - handler: async ({ uptimeEsClient, request }): Promise => { + handler: async ({ uptimeEsClient, request, response }): Promise => { const { dateRangeStart, dateRangeEnd, filters, pagination, statusFilter, pageSize, query } = request.query; @@ -35,20 +35,29 @@ export const createMonitorListRoute: UMRestApiRouteFactory = (libs) => ({ ? JSON.parse(decodeURIComponent(pagination)) : CONTEXT_DEFAULTS.CURSOR_PAGINATION; - const result = await libs.requests.getMonitorStates({ - uptimeEsClient, - dateRangeStart, - dateRangeEnd, - pagination: decodedPagination, - pageSize, - filters, - query, - // this is added to make typescript happy, - // this sort of reassignment used to be further downstream but I've moved it here - // because this code is going to be decomissioned soon - statusFilter: statusFilter || undefined, - }); + try { + const result = await libs.requests.getMonitorStates({ + uptimeEsClient, + dateRangeStart, + dateRangeEnd, + pagination: decodedPagination, + pageSize, + filters, + query, + statusFilter, + }); - return result; + return result; + } catch (e) { + /** + * This particular error is usually indicative of a mapping problem within the user's + * indices. It's relevant for the UI because we will be able to provide the user with a + * tailored message to help them remediate this problem on their own with minimal effort. + */ + if (e.name === 'ResponseError') { + return response.badRequest({ body: e }); + } + throw e; + } }, }); diff --git a/x-pack/test/api_integration/apis/metrics_ui/constants.ts b/x-pack/test/api_integration/apis/metrics_ui/constants.ts index f0ba9b4c368d5..2ca89f2f9ab87 100644 --- a/x-pack/test/api_integration/apis/metrics_ui/constants.ts +++ b/x-pack/test/api_integration/apis/metrics_ui/constants.ts @@ -31,7 +31,8 @@ export const DATES = { 'alert-test-data': { gauge: { min: 1609459200000, // '2022-01-01T00:00:00Z' - max: 1609462800000, // '2021-01-01T01:00:00Z' + max: 1609462800000, // '2021-01-01T01:00:00Z', + midpoint: 1609461000000, // '2021-01-01T00:30:00Z' }, rate: { min: 1609545600000, // '2021-01-02T00:00:00Z' diff --git a/x-pack/test/api_integration/apis/metrics_ui/metric_threshold_alert.ts b/x-pack/test/api_integration/apis/metrics_ui/metric_threshold_alert.ts index 28910bbc6b0c8..66c40e2e6e92d 100644 --- a/x-pack/test/api_integration/apis/metrics_ui/metric_threshold_alert.ts +++ b/x-pack/test/api_integration/apis/metrics_ui/metric_threshold_alert.ts @@ -100,7 +100,7 @@ export default function ({ getService }: FtrProviderContext) { ], }; const timeFrame = { end: gauge.max }; - const results = await evaluateAlert(esClient, params, configuration, timeFrame); + const results = await evaluateAlert(esClient, params, configuration, [], timeFrame); expect(results).to.eql([ { '*': { @@ -123,7 +123,7 @@ export default function ({ getService }: FtrProviderContext) { it('should alert on the last value when the end date is the same as the last event', async () => { const params = { ...baseParams }; const timeFrame = { end: gauge.max }; - const results = await evaluateAlert(esClient, params, configuration, timeFrame); + const results = await evaluateAlert(esClient, params, configuration, [], timeFrame); expect(results).to.eql([ { '*': { @@ -160,7 +160,7 @@ export default function ({ getService }: FtrProviderContext) { ], }; const timeFrame = { end: gauge.max }; - const results = await evaluateAlert(esClient, params, configuration, timeFrame); + const results = await evaluateAlert(esClient, params, configuration, [], timeFrame); expect(results).to.eql([ { dev: { @@ -200,7 +200,7 @@ export default function ({ getService }: FtrProviderContext) { groupBy: ['env'], }; const timeFrame = { end: gauge.max }; - const results = await evaluateAlert(esClient, params, configuration, timeFrame); + const results = await evaluateAlert(esClient, params, configuration, [], timeFrame); expect(results).to.eql([ { dev: { @@ -234,6 +234,53 @@ export default function ({ getService }: FtrProviderContext) { }, ]); }); + + it('should report no data when one of the groups has a data gap', async () => { + const params = { + ...baseParams, + groupBy: ['env'], + }; + const timeFrame = { end: gauge.midpoint }; + const results = await evaluateAlert( + esClient, + params, + configuration, + ['dev', 'prod'], + timeFrame + ); + expect(results).to.eql([ + { + dev: { + timeSize: 5, + timeUnit: 'm', + threshold: [1], + comparator: '>=', + aggType: 'sum', + metric: 'value', + currentValue: null, + timestamp: '2021-01-01T00:25:00.000Z', + shouldFire: [false], + shouldWarn: [false], + isNoData: [true], + isError: false, + }, + prod: { + timeSize: 5, + timeUnit: 'm', + threshold: [1], + comparator: '>=', + aggType: 'sum', + metric: 'value', + currentValue: 0, + timestamp: '2021-01-01T00:25:00.000Z', + shouldFire: [false], + shouldWarn: [false], + isNoData: [false], + isError: false, + }, + }, + ]); + }); }); }); @@ -254,7 +301,7 @@ export default function ({ getService }: FtrProviderContext) { ], }; const timeFrame = { end: rate.max }; - const results = await evaluateAlert(esClient, params, configuration, timeFrame); + const results = await evaluateAlert(esClient, params, configuration, [], timeFrame); expect(results).to.eql([ { '*': { @@ -294,7 +341,7 @@ export default function ({ getService }: FtrProviderContext) { ], }; const timeFrame = { end: rate.max }; - const results = await evaluateAlert(esClient, params, configuration, timeFrame); + const results = await evaluateAlert(esClient, params, configuration, [], timeFrame); expect(results).to.eql([ { dev: { diff --git a/x-pack/test/api_integration/apis/search/search.ts b/x-pack/test/api_integration/apis/search/search.ts index 82b62a61a932d..45e8933bf715f 100644 --- a/x-pack/test/api_integration/apis/search/search.ts +++ b/x-pack/test/api_integration/apis/search/search.ts @@ -14,7 +14,8 @@ export default function ({ getService }: FtrProviderContext) { const esArchiver = getService('esArchiver'); describe('search', () => { - describe('post', () => { + // https://github.com/elastic/kibana/issues/113082 + describe.skip('post', () => { it('should return 200 with final response if wait_for_completion_timeout is long enough', async () => { const resp = await supertest .post(`/internal/search/ese`) diff --git a/x-pack/test/api_integration/apis/security_solution/timeline_migrations.ts b/x-pack/test/api_integration/apis/security_solution/timeline_migrations.ts index 9863ebb7ba646..1bfefe04239e2 100644 --- a/x-pack/test/api_integration/apis/security_solution/timeline_migrations.ts +++ b/x-pack/test/api_integration/apis/security_solution/timeline_migrations.ts @@ -6,18 +6,28 @@ */ import expect from '@kbn/expect'; -import { SavedTimeline } from '../../../../plugins/security_solution/common/types/timeline'; -import { SavedNote } from '../../../../plugins/security_solution/common/types/timeline/note'; +import { + noteSavedObjectType, + pinnedEventSavedObjectType, + timelineSavedObjectType, +} from '../../../../plugins/security_solution/server/lib/timeline/saved_object_mappings'; +import { TimelineWithoutExternalRefs } from '../../../../plugins/security_solution/common/types/timeline'; +import { NoteWithoutExternalRefs } from '../../../../plugins/security_solution/common/types/timeline/note'; import { FtrProviderContext } from '../../ftr_provider_context'; import { getSavedObjectFromES } from './utils'; +import { PinnedEventWithoutExternalRefs } from '../../../../plugins/security_solution/common/types/timeline/pinned_event'; interface TimelineWithoutSavedQueryId { - 'siem-ui-timeline': Omit; + [timelineSavedObjectType]: TimelineWithoutExternalRefs; } interface NoteWithoutTimelineId { - 'siem-ui-timeline-note': Omit; + [noteSavedObjectType]: NoteWithoutExternalRefs; +} + +interface PinnedEventWithoutTimelineId { + [pinnedEventSavedObjectType]: PinnedEventWithoutExternalRefs; } export default function ({ getService }: FtrProviderContext) { @@ -28,23 +38,22 @@ export default function ({ getService }: FtrProviderContext) { const es = getService('es'); describe('7.16.0', () => { - describe('notes timelineId', () => { - before(async () => { - await esArchiver.load( - 'x-pack/test/functional/es_archives/security_solution/timelines/7.15.0' - ); - }); - - after(async () => { - await esArchiver.unload( - 'x-pack/test/functional/es_archives/security_solution/timelines/7.15.0' - ); - }); + before(async () => { + await esArchiver.load( + 'x-pack/test/functional/es_archives/security_solution/timelines/7.15.0' + ); + }); + after(async () => { + await esArchiver.unload( + 'x-pack/test/functional/es_archives/security_solution/timelines/7.15.0' + ); + }); + describe('notes timelineId', () => { it('removes the timelineId in the saved object', async () => { const timelines = await getSavedObjectFromES( es, - 'siem-ui-timeline-note', + noteSavedObjectType, { ids: { values: [ @@ -55,13 +64,13 @@ export default function ({ getService }: FtrProviderContext) { } ); - expect( - timelines.body.hits.hits[0]._source?.['siem-ui-timeline-note'] - ).to.not.have.property('timelineId'); + expect(timelines.body.hits.hits[0]._source?.[noteSavedObjectType]).to.not.have.property( + 'timelineId' + ); - expect( - timelines.body.hits.hits[1]._source?.['siem-ui-timeline-note'] - ).to.not.have.property('timelineId'); + expect(timelines.body.hits.hits[1]._source?.[noteSavedObjectType]).to.not.have.property( + 'timelineId' + ); }); it('preserves the eventId in the saved object after migration', async () => { @@ -87,30 +96,18 @@ export default function ({ getService }: FtrProviderContext) { }); describe('savedQueryId', () => { - before(async () => { - await esArchiver.load( - 'x-pack/test/functional/es_archives/security_solution/timelines/7.15.0' - ); - }); - - after(async () => { - await esArchiver.unload( - 'x-pack/test/functional/es_archives/security_solution/timelines/7.15.0' - ); - }); - it('removes the savedQueryId', async () => { const timelines = await getSavedObjectFromES( es, - 'siem-ui-timeline', + timelineSavedObjectType, { ids: { values: ['siem-ui-timeline:8dc70950-1012-11ec-9ad3-2d7c6600c0f7'] }, } ); - expect(timelines.body.hits.hits[0]._source?.['siem-ui-timeline']).to.not.have.property( - 'savedQueryId' - ); + expect( + timelines.body.hits.hits[0]._source?.[timelineSavedObjectType] + ).to.not.have.property('savedQueryId'); }); it('preserves the title in the saved object after migration', async () => { @@ -129,6 +126,57 @@ export default function ({ getService }: FtrProviderContext) { expect(resp.body.data.getOneTimeline.savedQueryId).to.be("It's me"); }); }); + + describe('pinned events timelineId', () => { + it('removes the timelineId in the saved object', async () => { + const timelines = await getSavedObjectFromES( + es, + pinnedEventSavedObjectType, + { + ids: { + values: [ + 'siem-ui-timeline-pinned-event:7a9a5540-126e-11ec-83d2-db1096c73738', + 'siem-ui-timeline-pinned-event:98d919b0-126e-11ec-83d2-db1096c73738', + ], + }, + } + ); + + expect( + timelines.body.hits.hits[0]._source?.[pinnedEventSavedObjectType] + ).to.not.have.property('timelineId'); + + expect( + timelines.body.hits.hits[1]._source?.[pinnedEventSavedObjectType] + ).to.not.have.property('timelineId'); + }); + + it('preserves the eventId in the saved object after migration', async () => { + const resp = await supertest + .get('/api/timeline') + .query({ id: '6484cc90-126e-11ec-83d2-db1096c73738' }); + + expect(resp.body.data.getOneTimeline.pinnedEventsSaveObject[0].eventId).to.be( + 'DNo00XsBEVtyvU-8LGNe' + ); + expect(resp.body.data.getOneTimeline.pinnedEventsSaveObject[1].eventId).to.be( + 'Edo00XsBEVtyvU-8LGNe' + ); + }); + + it('returns the timelineId in the response', async () => { + const resp = await supertest + .get('/api/timeline') + .query({ id: '6484cc90-126e-11ec-83d2-db1096c73738' }); + + expect(resp.body.data.getOneTimeline.pinnedEventsSaveObject[0].timelineId).to.be( + '6484cc90-126e-11ec-83d2-db1096c73738' + ); + expect(resp.body.data.getOneTimeline.pinnedEventsSaveObject[1].timelineId).to.be( + '6484cc90-126e-11ec-83d2-db1096c73738' + ); + }); + }); }); }); } diff --git a/x-pack/test/api_integration/apis/security_solution/tls.ts b/x-pack/test/api_integration/apis/security_solution/tls.ts index 2308ad7a0bf34..9fa251ded4e6b 100644 --- a/x-pack/test/api_integration/apis/security_solution/tls.ts +++ b/x-pack/test/api_integration/apis/security_solution/tls.ts @@ -24,7 +24,7 @@ const expectedResult = { _id: '16989191B1A93ECECD5FE9E63EBD4B5C3B606D26', subjects: ['CN=edgecert.googleapis.com,O=Google LLC,L=Mountain View,ST=California,C=US'], issuers: ['CN=GTS CA 1O1,O=Google Trust Services,C=US'], - ja3: [], + ja3: ['bd12d76eb0b6787e6a78a14d2ff96c2b'], notAfter: ['2020-05-06T11:52:15.000Z'], }; @@ -41,7 +41,7 @@ const expectedOverviewDestinationResult = { 'CN=*.cdn.mozilla.net,OU=Cloud Services,O=Mozilla Corporation,L=Mountain View,ST=California,C=US', ], issuers: ['CN=DigiCert SHA2 Secure Server CA,O=DigiCert Inc,C=US'], - ja3: [], + ja3: ['b20b44b18b853ef29ab773e921b03422'], notAfter: ['2020-12-09T12:00:00.000Z'], }, }, @@ -67,7 +67,7 @@ const expectedOverviewSourceResult = { 'CN=*.cdn.mozilla.net,OU=Cloud Services,O=Mozilla Corporation,L=Mountain View,ST=California,C=US', ], issuers: ['CN=DigiCert SHA2 Secure Server CA,O=DigiCert Inc,C=US'], - ja3: [], + ja3: ['b20b44b18b853ef29ab773e921b03422'], notAfter: ['2020-12-09T12:00:00.000Z'], }, }, diff --git a/x-pack/test/api_integration/apis/upgrade_assistant/es_deprecations.ts b/x-pack/test/api_integration/apis/upgrade_assistant/es_deprecations.ts new file mode 100644 index 0000000000000..aea003a317963 --- /dev/null +++ b/x-pack/test/api_integration/apis/upgrade_assistant/es_deprecations.ts @@ -0,0 +1,41 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { FtrProviderContext } from '../../ftr_provider_context'; + +export default function ({ getService }: FtrProviderContext) { + const supertestWithoutAuth = getService('supertestWithoutAuth'); + const security = getService('security'); + + describe('Elasticsearch deprecations', () => { + describe('GET /api/upgrade_assistant/es_deprecations', () => { + it('handles auth error', async () => { + const ROLE_NAME = 'authErrorRole'; + const USER_NAME = 'authErrorUser'; + const USER_PASSWORD = 'password'; + + try { + await security.role.create(ROLE_NAME, {}); + await security.user.create(USER_NAME, { + password: USER_PASSWORD, + roles: [ROLE_NAME], + }); + + await supertestWithoutAuth + .get('/api/upgrade_assistant/es_deprecations') + .auth(USER_NAME, USER_PASSWORD) + .set('kbn-xsrf', 'kibana') + .send() + .expect(403); + } finally { + await security.role.delete(ROLE_NAME); + await security.user.delete(USER_NAME); + } + }); + }); + }); +} diff --git a/x-pack/test/api_integration/apis/upgrade_assistant/index.ts b/x-pack/test/api_integration/apis/upgrade_assistant/index.ts index ddb71c59423ba..f6b231f038817 100644 --- a/x-pack/test/api_integration/apis/upgrade_assistant/index.ts +++ b/x-pack/test/api_integration/apis/upgrade_assistant/index.ts @@ -11,5 +11,7 @@ export default function ({ loadTestFile }: FtrProviderContext) { describe('Upgrade Assistant', () => { loadTestFile(require.resolve('./upgrade_assistant')); loadTestFile(require.resolve('./cloud_backup_status')); + loadTestFile(require.resolve('./privileges')); + loadTestFile(require.resolve('./es_deprecations')); }); } diff --git a/x-pack/test/api_integration/apis/upgrade_assistant/privileges.ts b/x-pack/test/api_integration/apis/upgrade_assistant/privileges.ts new file mode 100644 index 0000000000000..c5c00c9a33685 --- /dev/null +++ b/x-pack/test/api_integration/apis/upgrade_assistant/privileges.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 expect from '@kbn/expect'; + +import { FtrProviderContext } from '../../ftr_provider_context'; +import { DEPRECATION_LOGS_INDEX } from '../../../../plugins/upgrade_assistant/common/constants'; + +export default function ({ getService }: FtrProviderContext) { + const security = getService('security'); + const supertest = getService('supertest'); + const supertestWithoutAuth = getService('supertestWithoutAuth'); + + describe('Privileges', () => { + describe('GET /api/upgrade_assistant/privileges', () => { + it('User with with index privileges', async () => { + const { body } = await supertest + .get('/api/upgrade_assistant/privileges') + .set('kbn-xsrf', 'kibana') + .expect(200); + + expect(body.hasAllPrivileges).to.be(true); + expect(body.missingPrivileges.index.length).to.be(0); + }); + + it('User without index privileges', async () => { + const ROLE_NAME = 'test_role'; + const USER_NAME = 'test_user'; + const USER_PASSWORD = 'test_user'; + + try { + await security.role.create(ROLE_NAME, {}); + await security.user.create(USER_NAME, { + password: USER_PASSWORD, + roles: [ROLE_NAME], + }); + + const { body } = await supertestWithoutAuth + .get('/api/upgrade_assistant/privileges') + .auth(USER_NAME, USER_PASSWORD) + .set('kbn-xsrf', 'kibana') + .send() + .expect(200); + + expect(body.hasAllPrivileges).to.be(false); + expect(body.missingPrivileges.index[0]).to.be(DEPRECATION_LOGS_INDEX); + } finally { + await security.role.delete(ROLE_NAME); + await security.user.delete(USER_NAME); + } + }); + }); + }); +} diff --git a/x-pack/test/apm_api_integration/tests/correlations/errors_failed_transactions.ts b/x-pack/test/apm_api_integration/tests/correlations/errors_failed_transactions.ts deleted file mode 100644 index b3c5302ee2c6b..0000000000000 --- a/x-pack/test/apm_api_integration/tests/correlations/errors_failed_transactions.ts +++ /dev/null @@ -1,83 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import expect from '@kbn/expect'; -import { format } from 'url'; -import { APIReturnType } from '../../../../plugins/apm/public/services/rest/createCallApmApi'; -import archives_metadata from '../../common/fixtures/es_archiver/archives_metadata'; -import { FtrProviderContext } from '../../common/ftr_provider_context'; -import { registry } from '../../common/registry'; - -export default function ApiTest({ getService }: FtrProviderContext) { - const supertest = getService('legacySupertestAsApmReadUser'); - const archiveName = 'apm_8.0.0'; - const range = archives_metadata[archiveName]; - - const url = format({ - pathname: `/api/apm/correlations/errors/failed_transactions`, - query: { - start: range.start, - end: range.end, - fieldNames: 'http.response.status_code,user_agent.name,user_agent.os.name,url.original', - environment: 'ENVIRONMENT_ALL', - kuery: '', - }, - }); - registry.when( - 'correlations errors failed transactions without data', - { config: 'trial', archives: [] }, - () => { - it('handles the empty state', async () => { - const response = await supertest.get(url); - - expect(response.status).to.be(200); - expect(response.body.response).to.be(undefined); - }); - } - ); - - registry.when( - 'correlations errors failed transactions with data and default args', - { config: 'trial', archives: ['apm_8.0.0'] }, - () => { - type ResponseBody = APIReturnType<'GET /api/apm/correlations/errors/failed_transactions'>; - let response: { - status: number; - body: NonNullable; - }; - - before(async () => { - response = await supertest.get(url); - }); - - it('returns successfully', () => { - expect(response.status).to.eql(200); - }); - - it('returns significant terms', () => { - const { significantTerms } = response.body; - expect(significantTerms.length).to.be.greaterThan(0); - - const sortedFieldNames = significantTerms.map(({ fieldName }) => fieldName).sort(); - expectSnapshot(sortedFieldNames).toMatchInline(` - Array [ - "http.response.status_code", - ] - `); - }); - - it('returns a distribution per term', () => { - const { significantTerms } = response.body; - expectSnapshot(significantTerms.map((term) => term.timeseries.length)).toMatchInline(` - Array [ - 31, - ] - `); - }); - } - ); -} diff --git a/x-pack/test/apm_api_integration/tests/correlations/errors_overall.ts b/x-pack/test/apm_api_integration/tests/correlations/errors_overall.ts deleted file mode 100644 index f4e95816a3996..0000000000000 --- a/x-pack/test/apm_api_integration/tests/correlations/errors_overall.ts +++ /dev/null @@ -1,63 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import expect from '@kbn/expect'; -import { SupertestReturnType } from '../../common/apm_api_supertest'; -import archives_metadata from '../../common/fixtures/es_archiver/archives_metadata'; -import { FtrProviderContext } from '../../common/ftr_provider_context'; -import { registry } from '../../common/registry'; - -export default function ApiTest({ getService }: FtrProviderContext) { - const apmApiClient = getService('apmApiClient'); - const archiveName = 'apm_8.0.0'; - const range = archives_metadata[archiveName]; - - const urlConfig = { - endpoint: `GET /api/apm/correlations/errors/overall_timeseries` as const, - params: { - query: { - start: range.start, - end: range.end, - environment: 'ENVIRONMENT_ALL', - kuery: '', - }, - }, - }; - - registry.when( - 'correlations errors overall without data', - { config: 'trial', archives: [] }, - () => { - it('handles the empty state', async () => { - const response = await apmApiClient.readUser(urlConfig); - - expect(response.status).to.be(200); - expect(response.body.overall).to.be(null); - }); - } - ); - - registry.when( - 'correlations errors overall with data and default args', - { config: 'trial', archives: ['apm_8.0.0'] }, - () => { - let response: SupertestReturnType<'GET /api/apm/correlations/errors/overall_timeseries'>; - - before(async () => { - response = await apmApiClient.readUser(urlConfig); - }); - - it('returns successfully', () => { - expect(response.status).to.eql(200); - }); - - it('returns overall distribution', () => { - expectSnapshot(response.body?.overall?.timeseries.length).toMatchInline(`31`); - }); - } - ); -} diff --git a/x-pack/test/apm_api_integration/tests/correlations/latency_overall.ts b/x-pack/test/apm_api_integration/tests/correlations/latency_overall.ts deleted file mode 100644 index 722a9a2bc4fb7..0000000000000 --- a/x-pack/test/apm_api_integration/tests/correlations/latency_overall.ts +++ /dev/null @@ -1,71 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import expect from '@kbn/expect'; -import { format } from 'url'; -import { APIReturnType } from '../../../../plugins/apm/public/services/rest/createCallApmApi'; -import archives_metadata from '../../common/fixtures/es_archiver/archives_metadata'; -import { FtrProviderContext } from '../../common/ftr_provider_context'; -import { registry } from '../../common/registry'; - -export default function ApiTest({ getService }: FtrProviderContext) { - const supertest = getService('legacySupertestAsApmReadUser'); - const archiveName = 'apm_8.0.0'; - const range = archives_metadata[archiveName]; - - const url = format({ - pathname: `/api/apm/correlations/latency/overall_distribution`, - query: { - start: range.start, - end: range.end, - environment: 'ENVIRONMENT_ALL', - kuery: '', - }, - }); - - registry.when( - 'correlations latency overall without data', - { config: 'trial', archives: [] }, - () => { - it('handles the empty state', async () => { - const response = await supertest.get(url); - - expect(response.status).to.be(200); - expect(response.body.response).to.be(undefined); - }); - } - ); - - registry.when( - 'correlations latency overall with data and default args', - { config: 'trial', archives: ['apm_8.0.0'] }, - () => { - type ResponseBody = APIReturnType<'GET /api/apm/correlations/latency/overall_distribution'>; - let response: { - status: number; - body: NonNullable; - }; - - before(async () => { - response = await supertest.get(url); - }); - - it('returns successfully', () => { - expect(response.status).to.eql(200); - }); - - it('returns overall distribution', () => { - // less precision for distributionInterval as it is not exact - expectSnapshot(response.body?.distributionInterval?.toPrecision(2)).toMatchInline( - `"3.8e+5"` - ); - expectSnapshot(response.body?.maxLatency?.toPrecision(2)).toMatchInline(`"5.8e+6"`); - expectSnapshot(response.body?.overallDistribution?.length).toMatchInline(`15`); - }); - } - ); -} diff --git a/x-pack/test/apm_api_integration/tests/correlations/latency_slow_transactions.ts b/x-pack/test/apm_api_integration/tests/correlations/latency_slow_transactions.ts deleted file mode 100644 index c72753a86f6a6..0000000000000 --- a/x-pack/test/apm_api_integration/tests/correlations/latency_slow_transactions.ts +++ /dev/null @@ -1,96 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import expect from '@kbn/expect'; -import { format } from 'url'; -import { APIReturnType } from '../../../../plugins/apm/public/services/rest/createCallApmApi'; -import archives_metadata from '../../common/fixtures/es_archiver/archives_metadata'; -import { FtrProviderContext } from '../../common/ftr_provider_context'; -import { registry } from '../../common/registry'; - -export default function ApiTest({ getService }: FtrProviderContext) { - const supertest = getService('legacySupertestAsApmReadUser'); - const archiveName = 'apm_8.0.0'; - const range = archives_metadata[archiveName]; - - const url = format({ - pathname: `/api/apm/correlations/latency/slow_transactions`, - query: { - start: range.start, - end: range.end, - durationPercentile: 95, - fieldNames: 'user_agent.name,user_agent.os.name,url.original', - maxLatency: 3581640.00000003, - distributionInterval: 238776, - environment: 'ENVIRONMENT_ALL', - kuery: '', - }, - }); - registry.when( - 'correlations latency slow transactions without data', - { config: 'trial', archives: [] }, - () => { - it('handles the empty state', async () => { - const response = await supertest.get(url); - - expect(response.status).to.be(200); - expect(response.body.response).to.be(undefined); - }); - } - ); - - registry.when( - 'correlations latency slow transactions with data and default args', - { config: 'trial', archives: ['apm_8.0.0'] }, - () => { - type ResponseBody = APIReturnType<'GET /api/apm/correlations/latency/slow_transactions'>; - let response: { - status: number; - body: NonNullable; - }; - - before(async () => { - response = await supertest.get(url); - }); - - it('returns successfully', () => { - expect(response.status).to.eql(200); - }); - - it('returns significant terms', () => { - const { significantTerms } = response.body; - expect(significantTerms.length).to.be.greaterThan(0); - - const sortedFieldNames = significantTerms.map(({ fieldName }) => fieldName).sort(); - expectSnapshot(sortedFieldNames).toMatchInline(` - Array [ - "url.original", - "url.original", - "url.original", - "user_agent.name", - "user_agent.name", - "user_agent.os.name", - ] - `); - }); - - it('returns a distribution per term', () => { - const { significantTerms } = response.body; - expectSnapshot(significantTerms.map((term) => term.distribution.length)).toMatchInline(` - Array [ - 15, - 15, - 15, - 15, - 15, - 15, - ] - `); - }); - } - ); -} diff --git a/x-pack/test/apm_api_integration/tests/index.ts b/x-pack/test/apm_api_integration/tests/index.ts index 6c989e61bc6bf..5ea5ad78d9479 100644 --- a/x-pack/test/apm_api_integration/tests/index.ts +++ b/x-pack/test/apm_api_integration/tests/index.ts @@ -29,10 +29,6 @@ export default function apmApiIntegrationTests(providerContext: FtrProviderConte }); // correlations - describe('correlations/latency_slow_transactions', function () { - loadTestFile(require.resolve('./correlations/latency_slow_transactions')); - }); - describe('correlations/failed_transactions', function () { loadTestFile(require.resolve('./correlations/failed_transactions')); }); @@ -41,18 +37,6 @@ export default function apmApiIntegrationTests(providerContext: FtrProviderConte loadTestFile(require.resolve('./correlations/latency')); }); - describe('correlations/latency_overall', function () { - loadTestFile(require.resolve('./correlations/latency_overall')); - }); - - describe('correlations/errors_overall', function () { - loadTestFile(require.resolve('./correlations/errors_overall')); - }); - - describe('correlations/errors_failed_transactions', function () { - loadTestFile(require.resolve('./correlations/errors_failed_transactions')); - }); - describe('metrics_charts/metrics_charts', function () { loadTestFile(require.resolve('./metrics_charts/metrics_charts')); }); diff --git a/x-pack/test/case_api_integration/common/config.ts b/x-pack/test/case_api_integration/common/config.ts index 887e6e7894f98..514b54982ee42 100644 --- a/x-pack/test/case_api_integration/common/config.ts +++ b/x-pack/test/case_api_integration/common/config.ts @@ -93,6 +93,8 @@ export function createTestConfig(name: string, options: CreateTestConfigOptions) .isDirectory() ); + const casesConfig = ['--xpack.cases.enabled=true']; + return { testFiles: testFiles ? testFiles : [require.resolve('../tests/common')], servers, @@ -115,6 +117,7 @@ export function createTestConfig(name: string, options: CreateTestConfigOptions) ...xPackApiIntegrationTestsConfig.get('kbnTestServer'), serverArgs: [ ...xPackApiIntegrationTestsConfig.get('kbnTestServer.serverArgs'), + ...casesConfig, `--xpack.actions.allowedHosts=${JSON.stringify(['localhost', 'some.non.existent.com'])}`, `--xpack.actions.enabledActionTypes=${JSON.stringify(enabledActionTypes)}`, '--xpack.eventLog.logEntries=true', diff --git a/x-pack/test/case_api_integration/common/lib/utils.ts b/x-pack/test/case_api_integration/common/lib/utils.ts index 0021a228ee98b..7367641d71585 100644 --- a/x-pack/test/case_api_integration/common/lib/utils.ts +++ b/x-pack/test/case_api_integration/common/lib/utils.ts @@ -47,6 +47,7 @@ import { AlertResponse, ConnectorMappings, CasesByAlertId, + CaseResolveResponse, } from '../../../../plugins/cases/common/api'; import { getPostCaseRequest, postCollectionReq, postCommentGenAlertReq } from './mock'; import { getCaseUserActionUrl, getSubCasesUrl } from '../../../../plugins/cases/common/api/helpers'; @@ -1066,6 +1067,32 @@ export const getCase = async ({ return theCase; }; +export const resolveCase = async ({ + supertest, + caseId, + includeComments = false, + expectedHttpCode = 200, + auth = { user: superUser, space: null }, +}: { + supertest: SuperTest.SuperTest; + caseId: string; + includeComments?: boolean; + expectedHttpCode?: number; + auth?: { user: User; space: string | null }; +}): Promise => { + const { body: theResolvedCase } = await supertest + .get( + `${getSpaceUrlPrefix( + auth?.space + )}${CASES_URL}/${caseId}/resolve?includeComments=${includeComments}` + ) + .set('kbn-xsrf', 'true') + .auth(auth.user.username, auth.user.password) + .expect(expectedHttpCode); + + return theResolvedCase; +}; + export const findCases = async ({ supertest, query = {}, diff --git a/x-pack/test/case_api_integration/security_and_spaces/tests/common/cases/delete_cases.ts b/x-pack/test/case_api_integration/security_and_spaces/tests/common/cases/delete_cases.ts index 964e9135aba7b..68f0ba43d889b 100644 --- a/x-pack/test/case_api_integration/security_and_spaces/tests/common/cases/delete_cases.ts +++ b/x-pack/test/case_api_integration/security_and_spaces/tests/common/cases/delete_cases.ts @@ -85,7 +85,7 @@ export default ({ getService }: FtrProviderContext): void => { }); }); - it('should create a user action when creating a case', async () => { + it('should create a user action when deleting a case', async () => { const postedCase = await createCase(supertest, getPostCaseRequest()); await deleteCases({ supertest, caseIDs: [postedCase.id] }); const userActions = await getCaseUserActions({ supertest, caseID: postedCase.id }); @@ -106,6 +106,8 @@ export default ({ getService }: FtrProviderContext): void => { action_by: defaultUser, old_value: null, new_value: null, + new_val_connector_id: null, + old_val_connector_id: null, case_id: `${postedCase.id}`, comment_id: null, sub_case_id: '', diff --git a/x-pack/test/case_api_integration/security_and_spaces/tests/common/cases/import_export.ts b/x-pack/test/case_api_integration/security_and_spaces/tests/common/cases/import_export.ts index 4a0979c44359f..dd1c2e810f150 100644 --- a/x-pack/test/case_api_integration/security_and_spaces/tests/common/cases/import_export.ts +++ b/x-pack/test/case_api_integration/security_and_spaces/tests/common/cases/import_export.ts @@ -7,6 +7,8 @@ import expect from '@kbn/expect'; import { join } from 'path'; +import { SavedObject } from 'kibana/server'; +import supertest from 'supertest'; import { ObjectRemover as ActionsRemover } from '../../../../../alerting_api_integration/common/lib'; import { deleteAllCaseItems, @@ -22,16 +24,22 @@ import { CommentsResponse, CASES_URL, CaseType, + CASE_SAVED_OBJECT, + CaseAttributes, + CASE_USER_ACTION_SAVED_OBJECT, + CaseUserActionAttributes, + CASE_COMMENT_SAVED_OBJECT, + CasePostRequest, + CaseUserActionResponse, } from '../../../../../../plugins/cases/common'; // eslint-disable-next-line import/no-default-export export default ({ getService }: FtrProviderContext): void => { - const supertest = getService('supertest'); + const supertestService = getService('supertest'); const es = getService('es'); - // FLAKY: https://github.com/elastic/kibana/issues/112353 - describe.skip('import and export cases', () => { - const actionsRemover = new ActionsRemover(supertest); + describe('import and export cases', () => { + const actionsRemover = new ActionsRemover(supertestService); afterEach(async () => { await deleteAllCaseItems(es); @@ -40,14 +48,14 @@ export default ({ getService }: FtrProviderContext): void => { it('exports a case with its associated user actions and comments', async () => { const caseRequest = getPostCaseRequest(); - const postedCase = await createCase(supertest, caseRequest); + const postedCase = await createCase(supertestService, caseRequest); await createComment({ - supertest, + supertest: supertestService, caseId: postedCase.id, params: postCommentUserReq, }); - const { text } = await supertest + const { text } = await supertestService .post(`/api/saved_objects/_export`) .send({ type: ['cases'], @@ -60,46 +68,13 @@ export default ({ getService }: FtrProviderContext): void => { expect(objects).to.have.length(4); - // should be the case - expect(objects[0].attributes.title).to.eql(caseRequest.title); - expect(objects[0].attributes.description).to.eql(caseRequest.description); - expect(objects[0].attributes.connector.type).to.eql(caseRequest.connector.type); - expect(objects[0].attributes.connector.name).to.eql(caseRequest.connector.name); - expect(objects[0].attributes.connector.fields).to.eql([]); - expect(objects[0].attributes.settings).to.eql(caseRequest.settings); - - // should be two user actions - expect(objects[1].attributes.action).to.eql('create'); - - const parsedCaseNewValue = JSON.parse(objects[1].attributes.new_value); - const { - connector: { id: ignoreParsedId, ...restParsedConnector }, - ...restParsedCreateCase - } = parsedCaseNewValue; - - const { - connector: { id: ignoreConnectorId, ...restConnector }, - ...restCreateCase - } = caseRequest; - - expect(restParsedCreateCase).to.eql({ ...restCreateCase, type: CaseType.individual }); - expect(restParsedConnector).to.eql(restConnector); - - expect(objects[1].attributes.old_value).to.eql(null); - expect(includesAllRequiredFields(objects[1].attributes.action_field)).to.eql(true); - - // should be the comment - expect(objects[2].attributes.comment).to.eql(postCommentUserReq.comment); - expect(objects[2].attributes.type).to.eql(postCommentUserReq.type); - - expect(objects[3].attributes.action).to.eql('create'); - expect(JSON.parse(objects[3].attributes.new_value)).to.eql(postCommentUserReq); - expect(objects[3].attributes.old_value).to.eql(null); - expect(objects[3].attributes.action_field).to.eql(['comment']); + expectExportToHaveCaseSavedObject(objects, caseRequest); + expectExportToHaveUserActions(objects, caseRequest); + expectExportToHaveAComment(objects); }); it('imports a case with a comment and user actions', async () => { - await supertest + await supertestService .post('/api/saved_objects/_import') .query({ overwrite: true }) .attach( @@ -112,12 +87,12 @@ export default ({ getService }: FtrProviderContext): void => { .set('kbn-xsrf', 'true') .expect(200); - const findResponse = await findCases({ supertest, query: {} }); + const findResponse = await findCases({ supertest: supertestService, query: {} }); expect(findResponse.total).to.eql(1); expect(findResponse.cases[0].title).to.eql('A case to export'); expect(findResponse.cases[0].description).to.eql('a description'); - const { body: commentsResponse }: { body: CommentsResponse } = await supertest + const { body: commentsResponse }: { body: CommentsResponse } = await supertestService .get(`${CASES_URL}/${findResponse.cases[0].id}/comments/_find`) .send() .expect(200); @@ -126,13 +101,13 @@ export default ({ getService }: FtrProviderContext): void => { expect(comment.comment).to.eql('A comment for my case'); const userActions = await getCaseUserActions({ - supertest, + supertest: supertestService, caseID: findResponse.cases[0].id, }); expect(userActions).to.have.length(2); expect(userActions[0].action).to.eql('create'); - expect(includesAllRequiredFields(userActions[0].action_field)).to.eql(true); + expect(includesAllCreateCaseActionFields(userActions[0].action_field)).to.eql(true); expect(userActions[1].action).to.eql('create'); expect(userActions[1].action_field).to.eql(['comment']); @@ -145,7 +120,7 @@ export default ({ getService }: FtrProviderContext): void => { }); it('imports a case with a connector', async () => { - await supertest + await supertestService .post('/api/saved_objects/_import') .query({ overwrite: true }) .attach( @@ -160,51 +135,179 @@ export default ({ getService }: FtrProviderContext): void => { actionsRemover.add('default', '1cd34740-06ad-11ec-babc-0b08808e8e01', 'action', 'actions'); - const findResponse = await findCases({ supertest, query: {} }); - expect(findResponse.total).to.eql(1); - expect(findResponse.cases[0].title).to.eql('A case with a connector'); - expect(findResponse.cases[0].description).to.eql('super description'); + await expectImportToHaveOneCase(supertestService); const userActions = await getCaseUserActions({ - supertest, - caseID: findResponse.cases[0].id, + supertest: supertestService, + caseID: '2e85c3f0-06ad-11ec-babc-0b08808e8e01', }); - expect(userActions).to.have.length(3); - expect(userActions[0].action).to.eql('create'); - expect(includesAllRequiredFields(userActions[0].action_field)).to.eql(true); - expect(userActions[1].action).to.eql('push-to-service'); - expect(userActions[1].action_field).to.eql(['pushed']); - expect(userActions[1].old_value).to.eql(null); + expectImportToHaveCreateCaseUserAction(userActions[0]); + expectImportToHavePushUserAction(userActions[1]); + expectImportToHaveUpdateConnector(userActions[2]); + }); + }); +}; - const parsedPushNewValue = JSON.parse(userActions[1].new_value!); - expect(parsedPushNewValue.connector_name).to.eql('A jira connector'); - expect(parsedPushNewValue.connector_id).to.eql('1cd34740-06ad-11ec-babc-0b08808e8e01'); +const expectImportToHaveOneCase = async (supertestService: supertest.SuperTest) => { + const findResponse = await findCases({ supertest: supertestService, query: {} }); + expect(findResponse.total).to.eql(1); + expect(findResponse.cases[0].title).to.eql('A case with a connector'); + expect(findResponse.cases[0].description).to.eql('super description'); +}; - expect(userActions[2].action).to.eql('update'); - expect(userActions[2].action_field).to.eql(['connector']); +const expectImportToHaveCreateCaseUserAction = (userAction: CaseUserActionResponse) => { + expect(userAction.action).to.eql('create'); + expect(includesAllCreateCaseActionFields(userAction.action_field)).to.eql(true); +}; - const parsedUpdateNewValue = JSON.parse(userActions[2].new_value!); - expect(parsedUpdateNewValue.id).to.eql('none'); - }); - }); +const expectImportToHavePushUserAction = (userAction: CaseUserActionResponse) => { + expect(userAction.action).to.eql('push-to-service'); + expect(userAction.action_field).to.eql(['pushed']); + expect(userAction.old_value).to.eql(null); + + const parsedPushNewValue = JSON.parse(userAction.new_value!); + expect(parsedPushNewValue.connector_name).to.eql('A jira connector'); + expect(parsedPushNewValue).to.not.have.property('connector_id'); + expect(userAction.new_val_connector_id).to.eql('1cd34740-06ad-11ec-babc-0b08808e8e01'); +}; + +const expectImportToHaveUpdateConnector = (userAction: CaseUserActionResponse) => { + expect(userAction.action).to.eql('update'); + expect(userAction.action_field).to.eql(['connector']); + + const parsedUpdateNewValue = JSON.parse(userAction.new_value!); + expect(parsedUpdateNewValue).to.not.have.property('id'); + // the new val connector id is null because it is the none connector + expect(userAction.new_val_connector_id).to.eql(null); + + const parsedUpdateOldValue = JSON.parse(userAction.old_value!); + expect(parsedUpdateOldValue).to.not.have.property('id'); + expect(userAction.old_val_connector_id).to.eql('1cd34740-06ad-11ec-babc-0b08808e8e01'); }; const ndjsonToObject = (input: string) => { return input.split('\n').map((str) => JSON.parse(str)); }; -const includesAllRequiredFields = (actionFields: string[]): boolean => { - const requiredFields = [ - 'description', - 'status', - 'tags', - 'title', - 'connector', - 'settings', - 'owner', - ]; - - return requiredFields.every((field) => actionFields.includes(field)); +const expectExportToHaveCaseSavedObject = ( + objects: SavedObject[], + caseRequest: CasePostRequest +) => { + const caseSOs = findSavedObjectsByType(objects, CASE_SAVED_OBJECT); + expect(caseSOs.length).to.eql(1); + + const createdCaseSO = caseSOs[0]; + + // should be the case + expect(createdCaseSO.attributes.title).to.eql(caseRequest.title); + expect(createdCaseSO.attributes.description).to.eql(caseRequest.description); + expect(createdCaseSO.attributes.connector.type).to.eql(caseRequest.connector.type); + expect(createdCaseSO.attributes.connector.name).to.eql(caseRequest.connector.name); + expect(createdCaseSO.attributes.connector.fields).to.eql([]); + expect(createdCaseSO.attributes.settings).to.eql(caseRequest.settings); +}; + +const expectExportToHaveUserActions = (objects: SavedObject[], caseRequest: CasePostRequest) => { + const userActionSOs = findSavedObjectsByType( + objects, + CASE_USER_ACTION_SAVED_OBJECT + ); + + expect(userActionSOs.length).to.eql(2); + + expectCaseCreateUserAction(userActionSOs, caseRequest); + expectCreateCommentUserAction(userActionSOs); +}; + +const expectCaseCreateUserAction = ( + userActions: Array>, + caseRequest: CasePostRequest +) => { + const userActionForCaseCreate = findUserActionSavedObject( + userActions, + 'create', + createCaseActionFields + ); + + expect(userActionForCaseCreate?.attributes.action).to.eql('create'); + + const parsedCaseNewValue = JSON.parse(userActionForCaseCreate?.attributes.new_value as string); + const { + connector: { id: ignoreParsedId, ...restParsedConnector }, + ...restParsedCreateCase + } = parsedCaseNewValue; + + const { + connector: { id: ignoreConnectorId, ...restConnector }, + ...restCreateCase + } = caseRequest; + + expect(restParsedCreateCase).to.eql({ ...restCreateCase, type: CaseType.individual }); + expect(restParsedConnector).to.eql(restConnector); + + expect(userActionForCaseCreate?.attributes.old_value).to.eql(null); + expect( + includesAllCreateCaseActionFields(userActionForCaseCreate?.attributes.action_field) + ).to.eql(true); +}; + +const expectCreateCommentUserAction = ( + userActions: Array> +) => { + const userActionForComment = findUserActionSavedObject(userActions, 'create', ['comment']); + + expect(userActionForComment?.attributes.action).to.eql('create'); + expect(JSON.parse(userActionForComment!.attributes.new_value!)).to.eql(postCommentUserReq); + expect(userActionForComment?.attributes.old_value).to.eql(null); + expect(userActionForComment?.attributes.action_field).to.eql(['comment']); +}; + +const expectExportToHaveAComment = (objects: SavedObject[]) => { + const commentSOs = findSavedObjectsByType(objects, CASE_COMMENT_SAVED_OBJECT); + + expect(commentSOs.length).to.eql(1); + + const commentSO = commentSOs[0]; + expect(commentSO.attributes.comment).to.eql(postCommentUserReq.comment); + expect(commentSO.attributes.type).to.eql(postCommentUserReq.type); +}; + +const createCaseActionFields = [ + 'description', + 'status', + 'tags', + 'title', + 'connector', + 'settings', + 'owner', +]; + +const includesAllCreateCaseActionFields = (actionFields?: string[]): boolean => { + return createCaseActionFields.every( + (field) => actionFields != null && actionFields.includes(field) + ); +}; + +const findSavedObjectsByType = ( + savedObjects: SavedObject[], + type: string +): Array> => { + return (savedObjects.filter((so) => so.type === type) ?? []) as Array>; +}; + +const findUserActionSavedObject = ( + savedObjects: Array>, + action: string, + actionFields: string[] +): SavedObject | undefined => { + return savedObjects.find( + (so) => + so.attributes.action === action && hasAllStrings(so.attributes.action_field, actionFields) + ); +}; + +const hasAllStrings = (collection: string[], stringsToFind: string[]): boolean => { + return stringsToFind.every((str) => collection.includes(str)); }; diff --git a/x-pack/test/case_api_integration/security_and_spaces/tests/common/cases/migrations.ts b/x-pack/test/case_api_integration/security_and_spaces/tests/common/cases/migrations.ts index f21a0ab460424..af8bf464c198d 100644 --- a/x-pack/test/case_api_integration/security_and_spaces/tests/common/cases/migrations.ts +++ b/x-pack/test/case_api_integration/security_and_spaces/tests/common/cases/migrations.ts @@ -11,7 +11,7 @@ import { CASES_URL, SECURITY_SOLUTION_OWNER, } from '../../../../../../plugins/cases/common/constants'; -import { getCase, getCaseSavedObjectsFromES } from '../../../../common/lib/utils'; +import { getCase, getCaseSavedObjectsFromES, resolveCase } from '../../../../common/lib/utils'; // eslint-disable-next-line import/no-default-export export default function createGetTests({ getService }: FtrProviderContext) { @@ -207,5 +207,76 @@ export default function createGetTests({ getService }: FtrProviderContext) { }); }); }); + + describe('7.16.0', () => { + before(async () => { + await esArchiver.load('x-pack/test/functional/es_archives/cases/migrations/7.13.2'); + }); + + after(async () => { + await esArchiver.unload('x-pack/test/functional/es_archives/cases/migrations/7.13.2'); + }); + + describe('resolve', () => { + it('should return exactMatch outcome', async () => { + const { outcome } = await resolveCase({ + supertest, + caseId: 'e49ad6e0-cf9d-11eb-a603-13e7747d215c', + }); + + expect(outcome).to.be('exactMatch'); + }); + + it('should preserve the same case info', async () => { + const { case: theCase } = await resolveCase({ + supertest, + caseId: 'e49ad6e0-cf9d-11eb-a603-13e7747d215c', + }); + + expect(theCase.title).to.be('A case'); + expect(theCase.description).to.be('asdf'); + expect(theCase.owner).to.be(SECURITY_SOLUTION_OWNER); + }); + + it('should preserve the same connector', async () => { + const { case: theCase } = await resolveCase({ + supertest, + caseId: 'e49ad6e0-cf9d-11eb-a603-13e7747d215c', + }); + + expect(theCase.connector).to.eql({ + fields: { + issueType: '10002', + parent: null, + priority: null, + }, + id: 'd68508f0-cf9d-11eb-a603-13e7747d215c', + name: 'Test Jira', + type: '.jira', + }); + }); + + it('should preserve the same external service', async () => { + const { case: theCase } = await resolveCase({ + supertest, + caseId: 'e49ad6e0-cf9d-11eb-a603-13e7747d215c', + }); + + expect(theCase.external_service).to.eql({ + connector_id: 'd68508f0-cf9d-11eb-a603-13e7747d215c', + connector_name: 'Test Jira', + external_id: '10106', + external_title: 'TPN-99', + external_url: 'https://cases-testing.atlassian.net/browse/TPN-99', + pushed_at: '2021-06-17T18:57:45.524Z', + pushed_by: { + email: null, + full_name: 'j@j.com', + username: '711621466', + }, + }); + }); + }); + }); }); } diff --git a/x-pack/test/case_api_integration/security_and_spaces/tests/common/cases/patch_cases.ts b/x-pack/test/case_api_integration/security_and_spaces/tests/common/cases/patch_cases.ts index 63b2f2e9b90ed..d7c506a6b69d2 100644 --- a/x-pack/test/case_api_integration/security_and_spaces/tests/common/cases/patch_cases.ts +++ b/x-pack/test/case_api_integration/security_and_spaces/tests/common/cases/patch_cases.ts @@ -126,6 +126,8 @@ export default ({ getService }: FtrProviderContext): void => { action: 'update', action_by: defaultUser, new_value: CaseStatuses.closed, + new_val_connector_id: null, + old_val_connector_id: null, old_value: CaseStatuses.open, case_id: `${postedCase.id}`, comment_id: null, @@ -165,6 +167,8 @@ export default ({ getService }: FtrProviderContext): void => { action_by: defaultUser, new_value: CaseStatuses['in-progress'], old_value: CaseStatuses.open, + old_val_connector_id: null, + new_val_connector_id: null, case_id: `${postedCase.id}`, comment_id: null, sub_case_id: '', diff --git a/x-pack/test/case_api_integration/security_and_spaces/tests/common/cases/post_case.ts b/x-pack/test/case_api_integration/security_and_spaces/tests/common/cases/post_case.ts index 96709ee7c309d..13408c5d309d9 100644 --- a/x-pack/test/case_api_integration/security_and_spaces/tests/common/cases/post_case.ts +++ b/x-pack/test/case_api_integration/security_and_spaces/tests/common/cases/post_case.ts @@ -114,6 +114,8 @@ export default ({ getService }: FtrProviderContext): void => { const { new_value, ...rest } = creationUserAction as CaseUserActionResponse; const parsedNewValue = JSON.parse(new_value!); + const { id: connectorId, ...restCaseConnector } = postedCase.connector; + expect(rest).to.eql({ action_field: [ 'description', @@ -127,6 +129,9 @@ export default ({ getService }: FtrProviderContext): void => { action: 'create', action_by: defaultUser, old_value: null, + old_val_connector_id: null, + // the connector id will be null here because it the connector is none + new_val_connector_id: null, case_id: `${postedCase.id}`, comment_id: null, sub_case_id: '', @@ -138,7 +143,7 @@ export default ({ getService }: FtrProviderContext): void => { description: postedCase.description, title: postedCase.title, tags: postedCase.tags, - connector: postedCase.connector, + connector: restCaseConnector, settings: postedCase.settings, owner: postedCase.owner, }); diff --git a/x-pack/test/case_api_integration/security_and_spaces/tests/common/cases/resolve_case.ts b/x-pack/test/case_api_integration/security_and_spaces/tests/common/cases/resolve_case.ts new file mode 100644 index 0000000000000..27eae507b9a84 --- /dev/null +++ b/x-pack/test/case_api_integration/security_and_spaces/tests/common/cases/resolve_case.ts @@ -0,0 +1,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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import expect from '@kbn/expect'; +import { FtrProviderContext } from '../../../../common/ftr_provider_context'; + +import { AttributesTypeUser } from '../../../../../../plugins/cases/common/api'; +import { CASES_URL } from '../../../../../../plugins/cases/common/constants'; +import { + defaultUser, + postCaseReq, + postCaseResp, + postCommentUserReq, + getPostCaseRequest, +} from '../../../../common/lib/mock'; +import { + deleteCasesByESQuery, + createCase, + resolveCase, + createComment, + removeServerGeneratedPropertiesFromCase, + removeServerGeneratedPropertiesFromSavedObject, +} from '../../../../common/lib/utils'; +import { + secOnly, + obsOnly, + globalRead, + superUser, + secOnlyRead, + obsOnlyRead, + obsSecRead, + noKibanaPrivileges, + obsSec, +} from '../../../../common/lib/authentication/users'; +import { getUserInfo } from '../../../../common/lib/authentication'; + +// eslint-disable-next-line import/no-default-export +export default ({ getService }: FtrProviderContext): void => { + const supertest = getService('supertest'); + const supertestWithoutAuth = getService('supertestWithoutAuth'); + const es = getService('es'); + + describe('resolve_case', () => { + afterEach(async () => { + await deleteCasesByESQuery(es); + }); + + it('should resolve a case with no comments', async () => { + const postedCase = await createCase(supertest, getPostCaseRequest()); + const resolvedCase = await resolveCase({ + supertest, + caseId: postedCase.id, + includeComments: true, + }); + + const data = removeServerGeneratedPropertiesFromCase(resolvedCase.case); + expect(data).to.eql(postCaseResp()); + expect(data.comments?.length).to.eql(0); + }); + + it('should resolve a case with comments', async () => { + const postedCase = await createCase(supertest, postCaseReq); + await createComment({ supertest, caseId: postedCase.id, params: postCommentUserReq }); + const resolvedCase = await resolveCase({ + supertest, + caseId: postedCase.id, + includeComments: true, + }); + + const comment = removeServerGeneratedPropertiesFromSavedObject( + resolvedCase.case.comments![0] as AttributesTypeUser + ); + + expect(resolvedCase.case.comments?.length).to.eql(1); + expect(comment).to.eql({ + type: postCommentUserReq.type, + comment: postCommentUserReq.comment, + associationType: 'case', + created_by: defaultUser, + pushed_at: null, + pushed_by: null, + updated_by: null, + owner: 'securitySolutionFixture', + }); + }); + + it('should return a 400 when passing the includeSubCaseComments', async () => { + const { body } = await supertest + .get(`${CASES_URL}/case-id/resolve?includeSubCaseComments=true`) + .set('kbn-xsrf', 'true') + .send() + .expect(400); + + expect(body.message).to.contain('disabled'); + }); + + it('unhappy path - 404s when case is not there', async () => { + await supertest + .get(`${CASES_URL}/fake-id/resolve`) + .set('kbn-xsrf', 'true') + .send() + .expect(404); + }); + + describe('rbac', () => { + it('should resolve a case', async () => { + const newCase = await createCase( + supertestWithoutAuth, + getPostCaseRequest({ owner: 'securitySolutionFixture' }), + 200, + { + user: superUser, + space: 'space1', + } + ); + + for (const user of [globalRead, superUser, secOnly, secOnlyRead, obsSec, obsSecRead]) { + const resolvedCase = await resolveCase({ + supertest: supertestWithoutAuth, + caseId: newCase.id, + auth: { user, space: 'space1' }, + }); + + expect(resolvedCase.case.owner).to.eql('securitySolutionFixture'); + expect(resolvedCase.outcome).to.eql('exactMatch'); + expect(resolvedCase.alias_target_id).to.eql(undefined); + } + }); + + it('should resolve a case with comments', async () => { + const postedCase = await createCase( + supertestWithoutAuth, + getPostCaseRequest({ owner: 'securitySolutionFixture' }), + 200, + { + user: secOnly, + space: 'space1', + } + ); + + await createComment({ + supertest: supertestWithoutAuth, + caseId: postedCase.id, + params: postCommentUserReq, + expectedHttpCode: 200, + auth: { + user: secOnly, + space: 'space1', + }, + }); + + const resolvedCase = await resolveCase({ + supertest: supertestWithoutAuth, + caseId: postedCase.id, + includeComments: true, + auth: { user: secOnly, space: 'space1' }, + }); + + const comment = removeServerGeneratedPropertiesFromSavedObject( + resolvedCase.case.comments![0] as AttributesTypeUser + ); + + expect(resolvedCase.case.comments?.length).to.eql(1); + expect(comment).to.eql({ + type: postCommentUserReq.type, + comment: postCommentUserReq.comment, + associationType: 'case', + created_by: getUserInfo(secOnly), + pushed_at: null, + pushed_by: null, + updated_by: null, + owner: 'securitySolutionFixture', + }); + }); + + it('should not resolve a case when the user does not have access to owner', async () => { + const newCase = await createCase( + supertestWithoutAuth, + getPostCaseRequest({ owner: 'securitySolutionFixture' }), + 200, + { + user: superUser, + space: 'space1', + } + ); + + for (const user of [noKibanaPrivileges, obsOnly, obsOnlyRead]) { + await resolveCase({ + supertest: supertestWithoutAuth, + caseId: newCase.id, + expectedHttpCode: 403, + auth: { user, space: 'space1' }, + }); + } + }); + + it('should NOT resolve a case in a space with no permissions', async () => { + const newCase = await createCase( + supertestWithoutAuth, + getPostCaseRequest({ owner: 'securitySolutionFixture' }), + 200, + { + user: superUser, + space: 'space2', + } + ); + + await resolveCase({ + supertest: supertestWithoutAuth, + caseId: newCase.id, + expectedHttpCode: 403, + auth: { user: secOnly, space: 'space2' }, + }); + }); + }); + }); +}; diff --git a/x-pack/test/case_api_integration/security_and_spaces/tests/common/comments/post_comment.ts b/x-pack/test/case_api_integration/security_and_spaces/tests/common/comments/post_comment.ts index f4c31c052cddd..942293437b03f 100644 --- a/x-pack/test/case_api_integration/security_and_spaces/tests/common/comments/post_comment.ts +++ b/x-pack/test/case_api_integration/security_and_spaces/tests/common/comments/post_comment.ts @@ -148,7 +148,9 @@ export default ({ getService }: FtrProviderContext): void => { action: 'create', action_by: defaultUser, new_value: `{"comment":"${postCommentUserReq.comment}","type":"${postCommentUserReq.type}","owner":"securitySolutionFixture"}`, + new_val_connector_id: null, old_value: null, + old_val_connector_id: null, case_id: `${postedCase.id}`, comment_id: `${patchedCase.comments![0].id}`, sub_case_id: '', diff --git a/x-pack/test/case_api_integration/security_and_spaces/tests/common/index.ts b/x-pack/test/case_api_integration/security_and_spaces/tests/common/index.ts index fba60634cc3d7..0b933582d84a5 100644 --- a/x-pack/test/case_api_integration/security_and_spaces/tests/common/index.ts +++ b/x-pack/test/case_api_integration/security_and_spaces/tests/common/index.ts @@ -25,6 +25,7 @@ export default ({ loadTestFile }: FtrProviderContext): void => { loadTestFile(require.resolve('./cases/get_case')); loadTestFile(require.resolve('./cases/patch_cases')); loadTestFile(require.resolve('./cases/post_case')); + loadTestFile(require.resolve('./cases/resolve_case')); loadTestFile(require.resolve('./cases/reporters/get_reporters')); loadTestFile(require.resolve('./cases/status/get_status')); loadTestFile(require.resolve('./cases/tags/get_tags')); diff --git a/x-pack/test/case_api_integration/security_and_spaces/tests/common/user_actions/get_all_user_actions.ts b/x-pack/test/case_api_integration/security_and_spaces/tests/common/user_actions/get_all_user_actions.ts index 35ebb1a4bf7b1..4cae10510d28e 100644 --- a/x-pack/test/case_api_integration/security_and_spaces/tests/common/user_actions/get_all_user_actions.ts +++ b/x-pack/test/case_api_integration/security_and_spaces/tests/common/user_actions/get_all_user_actions.ts @@ -48,6 +48,15 @@ export default ({ getService }: FtrProviderContext): void => { }); it(`on new case, user action: 'create' should be called with actionFields: ['description', 'status', 'tags', 'title', 'connector', 'settings, owner]`, async () => { + const { id: connectorId, ...restConnector } = userActionPostResp.connector; + + const userActionNewValueNoId = { + ...userActionPostResp, + connector: { + ...restConnector, + }, + }; + const { body: postedCase } = await supertest .post(CASES_URL) .set('kbn-xsrf', 'true') @@ -73,7 +82,10 @@ export default ({ getService }: FtrProviderContext): void => { ]); expect(body[0].action).to.eql('create'); expect(body[0].old_value).to.eql(null); - expect(JSON.parse(body[0].new_value)).to.eql(userActionPostResp); + expect(body[0].old_val_connector_id).to.eql(null); + // this will be null because it is for the none connector + expect(body[0].new_val_connector_id).to.eql(null); + expect(JSON.parse(body[0].new_value)).to.eql(userActionNewValueNoId); }); it(`on close case, user action: 'update' should be called with actionFields: ['status']`, async () => { @@ -147,18 +159,19 @@ export default ({ getService }: FtrProviderContext): void => { expect(body.length).to.eql(2); expect(body[1].action_field).to.eql(['connector']); expect(body[1].action).to.eql('update'); + // this is null because it is the none connector + expect(body[1].old_val_connector_id).to.eql(null); expect(JSON.parse(body[1].old_value)).to.eql({ - id: 'none', name: 'none', type: '.none', fields: null, }); expect(JSON.parse(body[1].new_value)).to.eql({ - id: '123', name: 'Connector', type: '.jira', fields: { issueType: 'Task', priority: 'High', parent: null }, }); + expect(body[1].new_val_connector_id).to.eql('123'); }); it(`on update tags, user action: 'add' and 'delete' should be called with actionFields: ['tags']`, async () => { diff --git a/x-pack/test/case_api_integration/security_and_spaces/tests/common/user_actions/migrations.ts b/x-pack/test/case_api_integration/security_and_spaces/tests/common/user_actions/migrations.ts index b4c2dca47bf5f..f9e66880c5230 100644 --- a/x-pack/test/case_api_integration/security_and_spaces/tests/common/user_actions/migrations.ts +++ b/x-pack/test/case_api_integration/security_and_spaces/tests/common/user_actions/migrations.ts @@ -12,6 +12,10 @@ import { SECURITY_SOLUTION_OWNER, } from '../../../../../../plugins/cases/common/constants'; import { getCaseUserActions } from '../../../../common/lib/utils'; +import { + CaseUserActionResponse, + CaseUserActionsResponse, +} from '../../../../../../plugins/cases/common'; // eslint-disable-next-line import/no-default-export export default function createGetTests({ getService }: FtrProviderContext) { @@ -41,14 +45,18 @@ export default function createGetTests({ getService }: FtrProviderContext) { expect(connectorUserAction.action_field.length).eql(1); expect(connectorUserAction.action_field[0]).eql('connector'); + expect(connectorUserAction.old_val_connector_id).to.eql( + 'c1900ac0-017f-11eb-93f8-d161651bf509' + ); expect(oldValue).to.eql({ - id: 'c1900ac0-017f-11eb-93f8-d161651bf509', name: 'none', type: '.none', fields: null, }); + expect(connectorUserAction.new_val_connector_id).to.eql( + 'b1900ac0-017f-11eb-93f8-d161651bf509' + ); expect(newValue).to.eql({ - id: 'b1900ac0-017f-11eb-93f8-d161651bf509', name: 'none', type: '.none', fields: null, @@ -77,5 +85,142 @@ export default function createGetTests({ getService }: FtrProviderContext) { } }); }); + + describe('7.13 connector id extraction', () => { + let userActions: CaseUserActionsResponse; + + before(async () => { + await esArchiver.load( + 'x-pack/test/functional/es_archives/cases/migrations/7.13_user_actions' + ); + }); + + after(async () => { + await esArchiver.unload( + 'x-pack/test/functional/es_archives/cases/migrations/7.13_user_actions' + ); + }); + + describe('none connector case', () => { + it('removes the connector id from the case create user action and sets the ids to null', async () => { + userActions = await getCaseUserActions({ + supertest, + caseID: 'aa8ac630-005e-11ec-91f1-6daf2ab59fb5', + }); + + const userAction = getUserActionById( + userActions, + 'ab43b5f0-005e-11ec-91f1-6daf2ab59fb5' + )!; + + const newValDecoded = JSON.parse(userAction.new_value!); + expect(newValDecoded.description).to.be('a description'); + expect(newValDecoded.title).to.be('a case'); + expect(newValDecoded.connector).not.have.property('id'); + // the connector id should be none so it should be removed + expect(userAction.new_val_connector_id).to.be(null); + expect(userAction.old_val_connector_id).to.be(null); + }); + + it('sets the connector ids to null for a create user action with null new and old values', async () => { + const userAction = getUserActionById( + userActions, + 'b3094de0-005e-11ec-91f1-6daf2ab59fb5' + )!; + + expect(userAction.new_val_connector_id).to.be(null); + expect(userAction.old_val_connector_id).to.be(null); + }); + }); + + describe('case with many user actions', () => { + before(async () => { + userActions = await getCaseUserActions({ + supertest, + caseID: 'e6fa9370-005e-11ec-91f1-6daf2ab59fb5', + }); + }); + + it('removes the connector id field for a created case user action', async () => { + const userAction = getUserActionById( + userActions, + 'e7882d70-005e-11ec-91f1-6daf2ab59fb5' + )!; + + const newValDecoded = JSON.parse(userAction.new_value!); + expect(newValDecoded.description).to.be('a description'); + expect(newValDecoded.title).to.be('a case'); + + expect(newValDecoded.connector).to.not.have.property('id'); + expect(userAction.new_val_connector_id).to.be('d92243b0-005e-11ec-91f1-6daf2ab59fb5'); + expect(userAction.old_val_connector_id).to.be(null); + }); + + it('removes the connector id from the external service new value', async () => { + const userAction = getUserActionById( + userActions, + 'e9471b80-005e-11ec-91f1-6daf2ab59fb5' + )!; + + const newValDecoded = JSON.parse(userAction.new_value!); + expect(newValDecoded.connector_name).to.be('a jira connector'); + expect(newValDecoded).to.not.have.property('connector_id'); + expect(userAction.new_val_connector_id).to.be('d92243b0-005e-11ec-91f1-6daf2ab59fb5'); + expect(userAction.old_val_connector_id).to.be(null); + }); + + it('sets the connector ids to null for a comment user action', async () => { + const userAction = getUserActionById( + userActions, + 'efe9de50-005e-11ec-91f1-6daf2ab59fb5' + )!; + + const newValDecoded = JSON.parse(userAction.new_value!); + expect(newValDecoded.comment).to.be('a comment'); + expect(userAction.new_val_connector_id).to.be(null); + expect(userAction.old_val_connector_id).to.be(null); + }); + + it('removes the connector id for an update connector action', async () => { + const userAction = getUserActionById( + userActions, + '16cd9e30-005f-11ec-91f1-6daf2ab59fb5' + )!; + + const newValDecoded = JSON.parse(userAction.new_value!); + const oldValDecoded = JSON.parse(userAction.old_value!); + + expect(newValDecoded.name).to.be('a different jira connector'); + expect(oldValDecoded.name).to.be('a jira connector'); + + expect(newValDecoded).to.not.have.property('id'); + expect(oldValDecoded).to.not.have.property('id'); + expect(userAction.new_val_connector_id).to.be('0a572860-005f-11ec-91f1-6daf2ab59fb5'); + expect(userAction.old_val_connector_id).to.be('d92243b0-005e-11ec-91f1-6daf2ab59fb5'); + }); + + it('removes the connector id from the external service new value for second push', async () => { + const userAction = getUserActionById( + userActions, + '1ea33bb0-005f-11ec-91f1-6daf2ab59fb5' + )!; + + const newValDecoded = JSON.parse(userAction.new_value!); + + expect(newValDecoded.connector_name).to.be('a different jira connector'); + + expect(newValDecoded).to.not.have.property('connector_id'); + expect(userAction.new_val_connector_id).to.be('0a572860-005f-11ec-91f1-6daf2ab59fb5'); + expect(userAction.old_val_connector_id).to.be(null); + }); + }); + }); }); } + +function getUserActionById( + userActions: CaseUserActionsResponse, + id: string +): CaseUserActionResponse | undefined { + return userActions.find((userAction) => userAction.action_id === id); +} diff --git a/x-pack/test/case_api_integration/security_and_spaces/tests/trial/cases/push_case.ts b/x-pack/test/case_api_integration/security_and_spaces/tests/trial/cases/push_case.ts index 94fe494fc7cc4..0ea66d35b63b8 100644 --- a/x-pack/test/case_api_integration/security_and_spaces/tests/trial/cases/push_case.ts +++ b/x-pack/test/case_api_integration/security_and_spaces/tests/trial/cases/push_case.ts @@ -275,6 +275,8 @@ export default ({ getService }: FtrProviderContext): void => { action: 'push-to-service', action_by: defaultUser, old_value: null, + old_val_connector_id: null, + new_val_connector_id: connector.id, case_id: `${postedCase.id}`, comment_id: null, sub_case_id: '', @@ -284,7 +286,6 @@ export default ({ getService }: FtrProviderContext): void => { expect(parsedNewValue).to.eql({ pushed_at: pushedCase.external_service!.pushed_at, pushed_by: defaultUser, - connector_id: connector.id, connector_name: connector.name, external_id: '123', external_title: 'INC01', diff --git a/x-pack/test/case_api_integration/security_and_spaces/tests/trial/cases/user_actions/get_all_user_actions.ts b/x-pack/test/case_api_integration/security_and_spaces/tests/trial/cases/user_actions/get_all_user_actions.ts index 79af6bb279a3d..255a2a4ce28b5 100644 --- a/x-pack/test/case_api_integration/security_and_spaces/tests/trial/cases/user_actions/get_all_user_actions.ts +++ b/x-pack/test/case_api_integration/security_and_spaces/tests/trial/cases/user_actions/get_all_user_actions.ts @@ -108,8 +108,10 @@ export default ({ getService }: FtrProviderContext): void => { expect(body[1].action_field).to.eql(['pushed']); expect(body[1].action).to.eql('push-to-service'); expect(body[1].old_value).to.eql(null); + expect(body[1].old_val_connector_id).to.eql(null); + expect(body[1].new_val_connector_id).to.eql(configure.connector.id); const newValue = JSON.parse(body[1].new_value); - expect(newValue.connector_id).to.eql(configure.connector.id); + expect(newValue).to.not.have.property('connector_id'); expect(newValue.pushed_by).to.eql(defaultUser); }); }); diff --git a/x-pack/test/case_api_integration/security_only/tests/common/cases/reporters/get_reporters.ts b/x-pack/test/case_api_integration/security_only/tests/common/cases/reporters/get_reporters.ts index f72db1ac1b27e..8266b456ea1f2 100644 --- a/x-pack/test/case_api_integration/security_only/tests/common/cases/reporters/get_reporters.ts +++ b/x-pack/test/case_api_integration/security_only/tests/common/cases/reporters/get_reporters.ts @@ -28,14 +28,17 @@ import { superUserDefaultSpaceAuth, obsSecDefaultSpaceAuth, } from '../../../../utils'; +import { UserInfo } from '../../../../../common/lib/authentication/types'; + +const sortReporters = (reporters: UserInfo[]) => + reporters.sort((a, b) => a.username.localeCompare(b.username)); // eslint-disable-next-line import/no-default-export export default ({ getService }: FtrProviderContext): void => { const supertestWithoutAuth = getService('supertestWithoutAuth'); const es = getService('es'); - // Failing: See https://github.com/elastic/kibana/issues/106658 - describe.skip('get_reporters', () => { + describe('get_reporters', () => { afterEach(async () => { await deleteCasesByESQuery(es); }); @@ -80,7 +83,10 @@ export default ({ getService }: FtrProviderContext): void => { }, }); - expect(reporters).to.eql(scenario.expectedReporters); + // sort reporters to prevent order failure + expect(sortReporters(reporters as unknown as UserInfo[])).to.eql( + sortReporters(scenario.expectedReporters) + ); } }); diff --git a/x-pack/test/functional/apps/canvas/reports.ts b/x-pack/test/functional/apps/canvas/reports.ts index 468430c390030..e21ec5b404d1f 100644 --- a/x-pack/test/functional/apps/canvas/reports.ts +++ b/x-pack/test/functional/apps/canvas/reports.ts @@ -198,7 +198,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { " `); - expect(res.get('content-length')).to.be('20725'); + const contentLength = parseInt(res.get('content-length'), 10); + expect(contentLength >= 20725 && contentLength <= 20726).to.be(true); // contentLength can be between 20725 and 20726 }); it('downloaded PDF base64 string is correct without borders and logo', async function () { diff --git a/x-pack/test/functional/apps/discover/feature_controls/discover_spaces.ts b/x-pack/test/functional/apps/discover/feature_controls/discover_spaces.ts index c245b45917497..5e14cc6201ec2 100644 --- a/x-pack/test/functional/apps/discover/feature_controls/discover_spaces.ts +++ b/x-pack/test/functional/apps/discover/feature_controls/discover_spaces.ts @@ -28,6 +28,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { await PageObjects.timePicker.setDefaultAbsoluteRange(); } + // FLAKY https://github.com/elastic/kibana/issues/113067 describe('spaces', () => { before(async () => { await esArchiver.loadIfNeeded('x-pack/test/functional/es_archives/logstash_functional'); diff --git a/x-pack/test/functional/apps/discover/saved_searches.ts b/x-pack/test/functional/apps/discover/saved_searches.ts index 1d8de9fe9fb6d..ec649935adec2 100644 --- a/x-pack/test/functional/apps/discover/saved_searches.ts +++ b/x-pack/test/functional/apps/discover/saved_searches.ts @@ -18,7 +18,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const panelActionsTimeRange = getService('dashboardPanelTimeRange'); const ecommerceSOPath = 'x-pack/test/functional/fixtures/kbn_archiver/reporting/ecommerce.json'; - describe('Discover Saved Searches', () => { + // FLAKY https://github.com/elastic/kibana/issues/104578 + describe.skip('Discover Saved Searches', () => { before('initialize tests', async () => { await esArchiver.load('x-pack/test/functional/es_archives/reporting/ecommerce'); await kibanaServer.importExport.load(ecommerceSOPath); diff --git a/x-pack/test/functional/apps/lens/formula.ts b/x-pack/test/functional/apps/lens/formula.ts index 20ab5e165fda0..a8d074ad0631b 100644 --- a/x-pack/test/functional/apps/lens/formula.ts +++ b/x-pack/test/functional/apps/lens/formula.ts @@ -87,6 +87,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { input = await find.activeElement(); await input.type(`Men\'s Clothing`); + await PageObjects.common.sleep(100); + await PageObjects.lens.expectFormulaText(`count(kql='Men\\'s Clothing')`); }); @@ -234,6 +236,38 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { ); }); + it('should keep the formula if the user does not fully transition to a static value', async () => { + await PageObjects.visualize.navigateToNewVisualization(); + await PageObjects.visualize.clickVisType('lens'); + await PageObjects.lens.goToTimeRange(); + + await PageObjects.lens.configureDimension({ + dimension: 'lnsXY_yDimensionPanel > lns-empty-dimension', + operation: 'average', + field: 'bytes', + }); + + await PageObjects.lens.createLayer('threshold'); + + await PageObjects.lens.configureDimension( + { + dimension: 'lnsXY_yThresholdLeftPanel > lns-dimensionTrigger', + operation: 'formula', + formula: `count()`, + keepOpen: true, + }, + 1 + ); + + await PageObjects.lens.switchToStaticValue(); + await PageObjects.lens.closeDimensionEditor(); + await PageObjects.common.sleep(1000); + + expect(await PageObjects.lens.getDimensionTriggerText('lnsXY_yThresholdLeftPanel', 0)).to.eql( + 'count()' + ); + }); + it('should allow numeric only formulas', async () => { await PageObjects.visualize.navigateToNewVisualization(); await PageObjects.visualize.clickVisType('lens'); diff --git a/x-pack/test/functional/apps/lens/index.ts b/x-pack/test/functional/apps/lens/index.ts index 09bbda595d55c..a6f579a9a4890 100644 --- a/x-pack/test/functional/apps/lens/index.ts +++ b/x-pack/test/functional/apps/lens/index.ts @@ -43,6 +43,7 @@ export default function ({ getService, loadTestFile }: FtrProviderContext) { loadTestFile(require.resolve('./lens_tagging')); loadTestFile(require.resolve('./formula')); loadTestFile(require.resolve('./heatmap')); + loadTestFile(require.resolve('./thresholds')); loadTestFile(require.resolve('./inspector')); // has to be last one in the suite because it overrides saved objects diff --git a/x-pack/test/functional/apps/lens/smokescreen.ts b/x-pack/test/functional/apps/lens/smokescreen.ts index 6d9360ac32b4b..ff5bae8aa7e61 100644 --- a/x-pack/test/functional/apps/lens/smokescreen.ts +++ b/x-pack/test/functional/apps/lens/smokescreen.ts @@ -180,6 +180,17 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { ); }); + it('should not show static value tab for data layers', async () => { + await PageObjects.lens.openDimensionEditor('lnsXY_yDimensionPanel > lns-dimensionTrigger'); + // Quick functions and Formula tabs should be visible + expect(await testSubjects.exists('lens-dimensionTabs-quickFunctions')).to.eql(true); + expect(await testSubjects.exists('lens-dimensionTabs-formula')).to.eql(true); + // Static value tab should not be visible + expect(await testSubjects.exists('lens-dimensionTabs-static_value')).to.eql(false); + + await PageObjects.lens.closeDimensionEditor(); + }); + it('should be able to add very long labels and still be able to remove a dimension', async () => { await PageObjects.lens.openDimensionEditor('lnsXY_yDimensionPanel > lns-dimensionTrigger'); const longLabel = diff --git a/x-pack/test/functional/apps/lens/thresholds.ts b/x-pack/test/functional/apps/lens/thresholds.ts new file mode 100644 index 0000000000000..bf6535acc7c8e --- /dev/null +++ b/x-pack/test/functional/apps/lens/thresholds.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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import expect from '@kbn/expect'; +import { FtrProviderContext } from '../../ftr_provider_context'; + +export default function ({ getService, getPageObjects }: FtrProviderContext) { + const PageObjects = getPageObjects(['visualize', 'lens', 'common', 'header']); + const find = getService('find'); + const retry = getService('retry'); + const testSubjects = getService('testSubjects'); + + describe('lens thresholds tests', () => { + it('should show a disabled threshold layer button if no data dimension is defined', async () => { + await PageObjects.visualize.navigateToNewVisualization(); + await PageObjects.visualize.clickVisType('lens'); + + await testSubjects.click('lnsLayerAddButton'); + await retry.waitFor('wait for layer popup to appear', async () => + testSubjects.exists(`lnsLayerAddButton-threshold`) + ); + expect( + await (await testSubjects.find(`lnsLayerAddButton-threshold`)).getAttribute('disabled') + ).to.be('true'); + }); + + it('should add a threshold layer with a static value in it', async () => { + await PageObjects.lens.goToTimeRange(); + + await PageObjects.lens.configureDimension({ + dimension: 'lnsXY_xDimensionPanel > lns-empty-dimension', + operation: 'date_histogram', + field: '@timestamp', + }); + + await PageObjects.lens.configureDimension({ + dimension: 'lnsXY_yDimensionPanel > lns-empty-dimension', + operation: 'average', + field: 'bytes', + }); + + await PageObjects.lens.createLayer('threshold'); + + expect((await find.allByCssSelector(`[data-test-subj^="lns-layerPanel-"]`)).length).to.eql(2); + expect( + await ( + await testSubjects.find('lnsXY_yThresholdLeftPanel > lns-dimensionTrigger') + ).getVisibleText() + ).to.eql('Static value: 4992.44'); + }); + + it('should create a dynamic threshold when dragging a field to a threshold dimension group', async () => { + await PageObjects.lens.dragFieldToDimensionTrigger( + 'bytes', + 'lnsXY_yThresholdLeftPanel > lns-empty-dimension' + ); + + expect(await PageObjects.lens.getDimensionTriggersTexts('lnsXY_yThresholdLeftPanel')).to.eql([ + 'Static value: 4992.44', + 'Median of bytes', + ]); + }); + }); +} diff --git a/x-pack/test/functional/apps/uptime/certificates.ts b/x-pack/test/functional/apps/uptime/certificates.ts index 70affdf836072..610f07c183782 100644 --- a/x-pack/test/functional/apps/uptime/certificates.ts +++ b/x-pack/test/functional/apps/uptime/certificates.ts @@ -9,19 +9,27 @@ import { FtrProviderContext } from '../../ftr_provider_context'; import { makeCheck } from '../../../api_integration/apis/uptime/rest/helper/make_checks'; import { getSha256 } from '../../../api_integration/apis/uptime/rest/helper/make_tls'; +const BLANK_INDEX_PATH = 'x-pack/test/functional/es_archives/uptime/blank'; + export default ({ getPageObjects, getService }: FtrProviderContext) => { const { uptime } = getPageObjects(['uptime']); const uptimeService = getService('uptime'); + const esArchiver = getService('esArchiver'); const es = getService('es'); describe('certificates', function () { describe('empty certificates', function () { before(async () => { + await esArchiver.load(BLANK_INDEX_PATH); await makeCheck({ es }); await uptime.goToRoot(true); }); + after(async () => { + await esArchiver.unload(BLANK_INDEX_PATH); + }); + it('go to certs page', async () => { await uptimeService.common.waitUntilDataIsLoaded(); await uptimeService.cert.hasViewCertButton(); @@ -34,10 +42,15 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { describe('with certs', function () { before(async () => { + await esArchiver.load(BLANK_INDEX_PATH); await makeCheck({ es, tls: true }); await uptime.goToRoot(true); }); + after(async () => { + await esArchiver.unload(BLANK_INDEX_PATH); + }); + beforeEach(async () => { await makeCheck({ es, tls: true }); }); diff --git a/x-pack/test/functional/apps/uptime/index.ts b/x-pack/test/functional/apps/uptime/index.ts index 501fec5002666..294ea9b393878 100644 --- a/x-pack/test/functional/apps/uptime/index.ts +++ b/x-pack/test/functional/apps/uptime/index.ts @@ -80,5 +80,9 @@ export default ({ loadTestFile, getService }: FtrProviderContext) => { loadTestFile(require.resolve('./ml_anomaly')); loadTestFile(require.resolve('./feature_controls')); }); + + describe('mappings error state', () => { + loadTestFile(require.resolve('./missing_mappings')); + }); }); }; diff --git a/x-pack/test/functional/apps/uptime/missing_mappings.ts b/x-pack/test/functional/apps/uptime/missing_mappings.ts new file mode 100644 index 0000000000000..2483aa45ecef9 --- /dev/null +++ b/x-pack/test/functional/apps/uptime/missing_mappings.ts @@ -0,0 +1,26 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { FtrProviderContext } from '../../ftr_provider_context'; +import { makeCheck } from '../../../api_integration/apis/uptime/rest/helper/make_checks'; + +export default ({ getPageObjects, getService }: FtrProviderContext) => { + const { common } = getPageObjects(['common']); + const uptimeService = getService('uptime'); + + const es = getService('es'); + describe('missing mappings', function () { + before(async () => { + await makeCheck({ es }); + await common.navigateToApp('uptime'); + }); + + it('redirects to mappings error page', async () => { + await uptimeService.common.hasMappingsError(); + }); + }); +}; diff --git a/x-pack/test/functional/es_archives/cases/migrations/7.13_user_actions/data.json.gz b/x-pack/test/functional/es_archives/cases/migrations/7.13_user_actions/data.json.gz new file mode 100644 index 0000000000000..5f73dfd89d166 Binary files /dev/null and b/x-pack/test/functional/es_archives/cases/migrations/7.13_user_actions/data.json.gz differ diff --git a/x-pack/test/functional/es_archives/cases/migrations/7.13_user_actions/mappings.json b/x-pack/test/functional/es_archives/cases/migrations/7.13_user_actions/mappings.json new file mode 100644 index 0000000000000..c6b71a2613859 --- /dev/null +++ b/x-pack/test/functional/es_archives/cases/migrations/7.13_user_actions/mappings.json @@ -0,0 +1,2954 @@ +{ + "type": "index", + "value": { + "aliases": { + ".kibana": { + }, + ".kibana_7.13.4": { + } + }, + "index": ".kibana_1", + "mappings": { + "_meta": { + "migrationMappingPropertyHashes": { + "action": "6e96ac5e648f57523879661ea72525b7", + "action_task_params": "a9d49f184ee89641044be0ca2950fa3a", + "alert": "d75d3b0e95fe394753d73d8f7952cd7d", + "api_key_pending_invalidation": "16f515278a295f6245149ad7c5ddedb7", + "apm-indices": "9bb9b2bf1fa636ed8619cbab5ce6a1dd", + "apm-telemetry": "3d1b76c39bfb2cc8296b024d73854724", + "app_search_telemetry": "3d1b76c39bfb2cc8296b024d73854724", + "application_usage_daily": "43b8830d5d0df85a6823d290885fc9fd", + "application_usage_totals": "3d1b76c39bfb2cc8296b024d73854724", + "application_usage_transactional": "3d1b76c39bfb2cc8296b024d73854724", + "canvas-element": "7390014e1091044523666d97247392fc", + "canvas-workpad": "b0a1706d356228dbdcb4a17e6b9eb231", + "canvas-workpad-template": "ae2673f678281e2c055d764b153e9715", + "cases": "7c28a18fbac7c2a4e79449e9802ef476", + "cases-comments": "112cefc2b6737e613a8ef033234755e6", + "cases-configure": "387c5f3a3bda7e0ae0dd4e106f914a69", + "cases-connector-mappings": "6bc7e49411d38be4969dc6aa8bd43776", + "cases-user-actions": "32277330ec6b721abe3b846cfd939a71", + "config": "c63748b75f39d0c54de12d12c1ccbc20", + "core-usage-stats": "3d1b76c39bfb2cc8296b024d73854724", + "coreMigrationVersion": "2f4316de49999235636386fe51dc06c1", + "dashboard": "40554caf09725935e2c02e02563a2d07", + "endpoint:user-artifact": "4a11183eee21e6fbad864f7a30b39ad0", + "endpoint:user-artifact-manifest": "a0d7b04ad405eed54d76e279c3727862", + "enterprise_search_telemetry": "3d1b76c39bfb2cc8296b024d73854724", + "epm-packages": "0cbbb16506734d341a96aaed65ec6413", + "epm-packages-assets": "44621b2f6052ef966da47b7c3a00f33b", + "exception-list": "baf108c9934dda844921f692a513adae", + "exception-list-agnostic": "baf108c9934dda844921f692a513adae", + "file-upload-usage-collection-telemetry": "a34fbb8e3263d105044869264860c697", + "fleet-agent-actions": "9511b565b1cc6441a42033db3d5de8e9", + "fleet-agents": "59fd74f819f028f8555776db198d2562", + "fleet-enrollment-api-keys": "a69ef7ae661dab31561d6c6f052ef2a7", + "fleet-preconfiguration-deletion-record": "4c36f199189a367e43541f236141204c", + "graph-workspace": "27a94b2edcb0610c6aea54a7c56d7752", + "index-pattern": "45915a1ad866812242df474eb0479052", + "infrastructure-ui-source": "3d1b76c39bfb2cc8296b024d73854724", + "ingest-agent-policies": "cb4dbcc5a695e53f40a359303cb6286f", + "ingest-outputs": "1acb789ca37cbee70259ca79e124d9ad", + "ingest-package-policies": "c91ca97b1ff700f0fc64dc6b13d65a85", + "ingest_manager_settings": "f159646d76ab261bfbf8ef504d9631e4", + "inventory-view": "3d1b76c39bfb2cc8296b024d73854724", + "kql-telemetry": "d12a98a6f19a2d273696597547e064ee", + "legacy-url-alias": "3d1b76c39bfb2cc8296b024d73854724", + "lens": "52346cfec69ff7b47d5f0c12361a2797", + "lens-ui-telemetry": "509bfa5978586998e05f9e303c07a327", + "map": "9134b47593116d7953f6adba096fc463", + "maps-telemetry": "5ef305b18111b77789afefbd36b66171", + "metrics-explorer-view": "3d1b76c39bfb2cc8296b024d73854724", + "migrationVersion": "4a1746014a75ade3a714e1db5763276f", + "ml-job": "3bb64c31915acf93fc724af137a0891b", + "ml-module": "46ef4f0d6682636f0fff9799d6a2d7ac", + "monitoring-telemetry": "2669d5ec15e82391cf58df4294ee9c68", + "namespace": "2f4316de49999235636386fe51dc06c1", + "namespaces": "2f4316de49999235636386fe51dc06c1", + "originId": "2f4316de49999235636386fe51dc06c1", + "query": "11aaeb7f5f7fa5bb43f25e18ce26e7d9", + "references": "7997cf5a56cc02bdc9c93361bde732b0", + "sample-data-telemetry": "7d3cfeb915303c9641c59681967ffeb4", + "search": "db2c00e39b36f40930a3b9fc71c823e1", + "search-session": "4e238afeeaa2550adef326e140454265", + "search-telemetry": "3d1b76c39bfb2cc8296b024d73854724", + "security-rule": "8ae39a88fc70af3375b7050e8d8d5cc7", + "security-solution-signals-migration": "72761fd374ca11122ac8025a92b84fca", + "siem-detection-engine-rule-actions": "6569b288c169539db10cb262bf79de18", + "siem-detection-engine-rule-status": "ae783f41c6937db6b7a2ef5c93a9e9b0", + "siem-ui-timeline": "3e97beae13cdfc6d62bc1846119f7276", + "siem-ui-timeline-note": "8874706eedc49059d4cf0f5094559084", + "siem-ui-timeline-pinned-event": "20638091112f0e14f0e443d512301c29", + "space": "c5ca8acafa0beaa4d08d014a97b6bc6b", + "spaces-usage-stats": "3d1b76c39bfb2cc8296b024d73854724", + "tag": "83d55da58f6530f7055415717ec06474", + "telemetry": "36a616f7026dfa617d6655df850fe16d", + "timelion-sheet": "9a2a2748877c7a7b582fef201ab1d4cf", + "type": "2f4316de49999235636386fe51dc06c1", + "ui-counter": "0d409297dc5ebe1e3a1da691c6ee32e3", + "ui-metric": "0d409297dc5ebe1e3a1da691c6ee32e3", + "updated_at": "00da57df13e94e9d98437d13ace4bfe0", + "upgrade-assistant-reindex-operation": "215107c281839ea9b3ad5f6419819763", + "upgrade-assistant-telemetry": "56702cec857e0a9dacfb696655b4ff7b", + "uptime-dynamic-settings": "3d1b76c39bfb2cc8296b024d73854724", + "url": "c7f66a0df8b1b52f17c28c4adb111105", + "usage-counters": "8cc260bdceffec4ffc3ad165c97dc1b4", + "visualization": "f819cf6636b75c9e76ba733a0c6ef355", + "workplace_search_telemetry": "3d1b76c39bfb2cc8296b024d73854724" + } + }, + "dynamic": "strict", + "properties": { + "action": { + "properties": { + "actionTypeId": { + "type": "keyword" + }, + "config": { + "enabled": false, + "type": "object" + }, + "name": { + "fields": { + "keyword": { + "type": "keyword" + } + }, + "type": "text" + }, + "secrets": { + "type": "binary" + } + } + }, + "action_task_params": { + "properties": { + "actionId": { + "type": "keyword" + }, + "apiKey": { + "type": "binary" + }, + "params": { + "enabled": false, + "type": "object" + } + } + }, + "alert": { + "properties": { + "actions": { + "properties": { + "actionRef": { + "type": "keyword" + }, + "actionTypeId": { + "type": "keyword" + }, + "group": { + "type": "keyword" + }, + "params": { + "enabled": false, + "type": "object" + } + }, + "type": "nested" + }, + "alertTypeId": { + "type": "keyword" + }, + "apiKey": { + "type": "binary" + }, + "apiKeyOwner": { + "type": "keyword" + }, + "consumer": { + "type": "keyword" + }, + "createdAt": { + "type": "date" + }, + "createdBy": { + "type": "keyword" + }, + "enabled": { + "type": "boolean" + }, + "executionStatus": { + "properties": { + "error": { + "properties": { + "message": { + "type": "keyword" + }, + "reason": { + "type": "keyword" + } + } + }, + "lastExecutionDate": { + "type": "date" + }, + "status": { + "type": "keyword" + } + } + }, + "meta": { + "properties": { + "versionApiKeyLastmodified": { + "type": "keyword" + } + } + }, + "muteAll": { + "type": "boolean" + }, + "mutedInstanceIds": { + "type": "keyword" + }, + "name": { + "fields": { + "keyword": { + "type": "keyword" + } + }, + "type": "text" + }, + "notifyWhen": { + "type": "keyword" + }, + "params": { + "ignore_above": 4096, + "type": "flattened" + }, + "schedule": { + "properties": { + "interval": { + "type": "keyword" + } + } + }, + "scheduledTaskId": { + "type": "keyword" + }, + "tags": { + "type": "keyword" + }, + "throttle": { + "type": "keyword" + }, + "updatedAt": { + "type": "date" + }, + "updatedBy": { + "type": "keyword" + } + } + }, + "api_key_pending_invalidation": { + "properties": { + "apiKeyId": { + "type": "keyword" + }, + "createdAt": { + "type": "date" + } + } + }, + "apm-indices": { + "properties": { + "apm_oss": { + "properties": { + "errorIndices": { + "type": "keyword" + }, + "metricsIndices": { + "type": "keyword" + }, + "onboardingIndices": { + "type": "keyword" + }, + "sourcemapIndices": { + "type": "keyword" + }, + "spanIndices": { + "type": "keyword" + }, + "transactionIndices": { + "type": "keyword" + } + } + } + } + }, + "apm-telemetry": { + "dynamic": "false", + "type": "object" + }, + "app_search_telemetry": { + "dynamic": "false", + "type": "object" + }, + "application_usage_daily": { + "dynamic": "false", + "properties": { + "timestamp": { + "type": "date" + } + } + }, + "application_usage_totals": { + "dynamic": "false", + "type": "object" + }, + "application_usage_transactional": { + "dynamic": "false", + "type": "object" + }, + "canvas-element": { + "dynamic": "false", + "properties": { + "@created": { + "type": "date" + }, + "@timestamp": { + "type": "date" + }, + "content": { + "type": "text" + }, + "help": { + "type": "text" + }, + "image": { + "type": "text" + }, + "name": { + "fields": { + "keyword": { + "type": "keyword" + } + }, + "type": "text" + } + } + }, + "canvas-workpad": { + "dynamic": "false", + "properties": { + "@created": { + "type": "date" + }, + "@timestamp": { + "type": "date" + }, + "name": { + "fields": { + "keyword": { + "type": "keyword" + } + }, + "type": "text" + } + } + }, + "canvas-workpad-template": { + "dynamic": "false", + "properties": { + "help": { + "fields": { + "keyword": { + "type": "keyword" + } + }, + "type": "text" + }, + "name": { + "fields": { + "keyword": { + "type": "keyword" + } + }, + "type": "text" + }, + "tags": { + "fields": { + "keyword": { + "type": "keyword" + } + }, + "type": "text" + }, + "template_key": { + "type": "keyword" + } + } + }, + "cases": { + "properties": { + "closed_at": { + "type": "date" + }, + "closed_by": { + "properties": { + "email": { + "type": "keyword" + }, + "full_name": { + "type": "keyword" + }, + "username": { + "type": "keyword" + } + } + }, + "connector": { + "properties": { + "fields": { + "properties": { + "key": { + "type": "text" + }, + "value": { + "type": "text" + } + } + }, + "id": { + "type": "keyword" + }, + "name": { + "type": "text" + }, + "type": { + "type": "keyword" + } + } + }, + "created_at": { + "type": "date" + }, + "created_by": { + "properties": { + "email": { + "type": "keyword" + }, + "full_name": { + "type": "keyword" + }, + "username": { + "type": "keyword" + } + } + }, + "description": { + "type": "text" + }, + "external_service": { + "properties": { + "connector_id": { + "type": "keyword" + }, + "connector_name": { + "type": "keyword" + }, + "external_id": { + "type": "keyword" + }, + "external_title": { + "type": "text" + }, + "external_url": { + "type": "text" + }, + "pushed_at": { + "type": "date" + }, + "pushed_by": { + "properties": { + "email": { + "type": "keyword" + }, + "full_name": { + "type": "keyword" + }, + "username": { + "type": "keyword" + } + } + } + } + }, + "settings": { + "properties": { + "syncAlerts": { + "type": "boolean" + } + } + }, + "status": { + "type": "keyword" + }, + "tags": { + "type": "keyword" + }, + "title": { + "type": "keyword" + }, + "type": { + "type": "keyword" + }, + "updated_at": { + "type": "date" + }, + "updated_by": { + "properties": { + "email": { + "type": "keyword" + }, + "full_name": { + "type": "keyword" + }, + "username": { + "type": "keyword" + } + } + } + } + }, + "cases-comments": { + "properties": { + "alertId": { + "type": "keyword" + }, + "associationType": { + "type": "keyword" + }, + "comment": { + "type": "text" + }, + "created_at": { + "type": "date" + }, + "created_by": { + "properties": { + "email": { + "type": "keyword" + }, + "full_name": { + "type": "keyword" + }, + "username": { + "type": "keyword" + } + } + }, + "index": { + "type": "keyword" + }, + "pushed_at": { + "type": "date" + }, + "pushed_by": { + "properties": { + "email": { + "type": "keyword" + }, + "full_name": { + "type": "keyword" + }, + "username": { + "type": "keyword" + } + } + }, + "rule": { + "properties": { + "id": { + "type": "keyword" + }, + "name": { + "type": "keyword" + } + } + }, + "type": { + "type": "keyword" + }, + "updated_at": { + "type": "date" + }, + "updated_by": { + "properties": { + "email": { + "type": "keyword" + }, + "full_name": { + "type": "keyword" + }, + "username": { + "type": "keyword" + } + } + } + } + }, + "cases-configure": { + "properties": { + "closure_type": { + "type": "keyword" + }, + "connector": { + "properties": { + "fields": { + "properties": { + "key": { + "type": "text" + }, + "value": { + "type": "text" + } + } + }, + "id": { + "type": "keyword" + }, + "name": { + "type": "text" + }, + "type": { + "type": "keyword" + } + } + }, + "created_at": { + "type": "date" + }, + "created_by": { + "properties": { + "email": { + "type": "keyword" + }, + "full_name": { + "type": "keyword" + }, + "username": { + "type": "keyword" + } + } + }, + "updated_at": { + "type": "date" + }, + "updated_by": { + "properties": { + "email": { + "type": "keyword" + }, + "full_name": { + "type": "keyword" + }, + "username": { + "type": "keyword" + } + } + } + } + }, + "cases-connector-mappings": { + "properties": { + "mappings": { + "properties": { + "action_type": { + "type": "keyword" + }, + "source": { + "type": "keyword" + }, + "target": { + "type": "keyword" + } + } + } + } + }, + "cases-user-actions": { + "properties": { + "action": { + "type": "keyword" + }, + "action_at": { + "type": "date" + }, + "action_by": { + "properties": { + "email": { + "type": "keyword" + }, + "full_name": { + "type": "keyword" + }, + "username": { + "type": "keyword" + } + } + }, + "action_field": { + "type": "keyword" + }, + "new_value": { + "type": "text" + }, + "old_value": { + "type": "text" + } + } + }, + "config": { + "dynamic": "false", + "properties": { + "buildNum": { + "type": "keyword" + } + } + }, + "core-usage-stats": { + "dynamic": "false", + "type": "object" + }, + "coreMigrationVersion": { + "type": "keyword" + }, + "dashboard": { + "properties": { + "description": { + "type": "text" + }, + "hits": { + "doc_values": false, + "index": false, + "type": "integer" + }, + "kibanaSavedObjectMeta": { + "properties": { + "searchSourceJSON": { + "index": false, + "type": "text" + } + } + }, + "optionsJSON": { + "index": false, + "type": "text" + }, + "panelsJSON": { + "index": false, + "type": "text" + }, + "refreshInterval": { + "properties": { + "display": { + "doc_values": false, + "index": false, + "type": "keyword" + }, + "pause": { + "doc_values": false, + "index": false, + "type": "boolean" + }, + "section": { + "doc_values": false, + "index": false, + "type": "integer" + }, + "value": { + "doc_values": false, + "index": false, + "type": "integer" + } + } + }, + "timeFrom": { + "doc_values": false, + "index": false, + "type": "keyword" + }, + "timeRestore": { + "doc_values": false, + "index": false, + "type": "boolean" + }, + "timeTo": { + "doc_values": false, + "index": false, + "type": "keyword" + }, + "title": { + "type": "text" + }, + "version": { + "type": "integer" + } + } + }, + "endpoint:user-artifact": { + "properties": { + "body": { + "type": "binary" + }, + "compressionAlgorithm": { + "index": false, + "type": "keyword" + }, + "created": { + "index": false, + "type": "date" + }, + "decodedSha256": { + "index": false, + "type": "keyword" + }, + "decodedSize": { + "index": false, + "type": "long" + }, + "encodedSha256": { + "type": "keyword" + }, + "encodedSize": { + "index": false, + "type": "long" + }, + "encryptionAlgorithm": { + "index": false, + "type": "keyword" + }, + "identifier": { + "type": "keyword" + } + } + }, + "endpoint:user-artifact-manifest": { + "properties": { + "artifacts": { + "properties": { + "artifactId": { + "index": false, + "type": "keyword" + }, + "policyId": { + "index": false, + "type": "keyword" + } + }, + "type": "nested" + }, + "created": { + "index": false, + "type": "date" + }, + "schemaVersion": { + "type": "keyword" + }, + "semanticVersion": { + "index": false, + "type": "keyword" + } + } + }, + "enterprise_search_telemetry": { + "dynamic": "false", + "type": "object" + }, + "epm-packages": { + "properties": { + "es_index_patterns": { + "enabled": false, + "type": "object" + }, + "install_source": { + "type": "keyword" + }, + "install_started_at": { + "type": "date" + }, + "install_status": { + "type": "keyword" + }, + "install_version": { + "type": "keyword" + }, + "installed_es": { + "properties": { + "id": { + "type": "keyword" + }, + "type": { + "type": "keyword" + } + }, + "type": "nested" + }, + "installed_kibana": { + "properties": { + "id": { + "type": "keyword" + }, + "type": { + "type": "keyword" + } + }, + "type": "nested" + }, + "internal": { + "type": "boolean" + }, + "name": { + "type": "keyword" + }, + "package_assets": { + "properties": { + "id": { + "type": "keyword" + }, + "type": { + "type": "keyword" + } + }, + "type": "nested" + }, + "removable": { + "type": "boolean" + }, + "version": { + "type": "keyword" + } + } + }, + "epm-packages-assets": { + "properties": { + "asset_path": { + "type": "keyword" + }, + "data_base64": { + "type": "binary" + }, + "data_utf8": { + "index": false, + "type": "text" + }, + "install_source": { + "type": "keyword" + }, + "media_type": { + "type": "keyword" + }, + "package_name": { + "type": "keyword" + }, + "package_version": { + "type": "keyword" + } + } + }, + "exception-list": { + "properties": { + "_tags": { + "type": "keyword" + }, + "comments": { + "properties": { + "comment": { + "type": "keyword" + }, + "created_at": { + "type": "keyword" + }, + "created_by": { + "type": "keyword" + }, + "id": { + "type": "keyword" + }, + "updated_at": { + "type": "keyword" + }, + "updated_by": { + "type": "keyword" + } + } + }, + "created_at": { + "type": "keyword" + }, + "created_by": { + "type": "keyword" + }, + "description": { + "type": "keyword" + }, + "entries": { + "properties": { + "entries": { + "properties": { + "field": { + "type": "keyword" + }, + "operator": { + "type": "keyword" + }, + "type": { + "type": "keyword" + }, + "value": { + "fields": { + "text": { + "type": "text" + } + }, + "type": "keyword" + } + } + }, + "field": { + "type": "keyword" + }, + "list": { + "properties": { + "id": { + "type": "keyword" + }, + "type": { + "type": "keyword" + } + } + }, + "operator": { + "type": "keyword" + }, + "type": { + "type": "keyword" + }, + "value": { + "fields": { + "text": { + "type": "text" + } + }, + "type": "keyword" + } + } + }, + "immutable": { + "type": "boolean" + }, + "item_id": { + "type": "keyword" + }, + "list_id": { + "type": "keyword" + }, + "list_type": { + "type": "keyword" + }, + "meta": { + "type": "keyword" + }, + "name": { + "fields": { + "text": { + "type": "text" + } + }, + "type": "keyword" + }, + "os_types": { + "type": "keyword" + }, + "tags": { + "fields": { + "text": { + "type": "text" + } + }, + "type": "keyword" + }, + "tie_breaker_id": { + "type": "keyword" + }, + "type": { + "type": "keyword" + }, + "updated_by": { + "type": "keyword" + }, + "version": { + "type": "keyword" + } + } + }, + "exception-list-agnostic": { + "properties": { + "_tags": { + "type": "keyword" + }, + "comments": { + "properties": { + "comment": { + "type": "keyword" + }, + "created_at": { + "type": "keyword" + }, + "created_by": { + "type": "keyword" + }, + "id": { + "type": "keyword" + }, + "updated_at": { + "type": "keyword" + }, + "updated_by": { + "type": "keyword" + } + } + }, + "created_at": { + "type": "keyword" + }, + "created_by": { + "type": "keyword" + }, + "description": { + "type": "keyword" + }, + "entries": { + "properties": { + "entries": { + "properties": { + "field": { + "type": "keyword" + }, + "operator": { + "type": "keyword" + }, + "type": { + "type": "keyword" + }, + "value": { + "fields": { + "text": { + "type": "text" + } + }, + "type": "keyword" + } + } + }, + "field": { + "type": "keyword" + }, + "list": { + "properties": { + "id": { + "type": "keyword" + }, + "type": { + "type": "keyword" + } + } + }, + "operator": { + "type": "keyword" + }, + "type": { + "type": "keyword" + }, + "value": { + "fields": { + "text": { + "type": "text" + } + }, + "type": "keyword" + } + } + }, + "immutable": { + "type": "boolean" + }, + "item_id": { + "type": "keyword" + }, + "list_id": { + "type": "keyword" + }, + "list_type": { + "type": "keyword" + }, + "meta": { + "type": "keyword" + }, + "name": { + "fields": { + "text": { + "type": "text" + } + }, + "type": "keyword" + }, + "os_types": { + "type": "keyword" + }, + "tags": { + "fields": { + "text": { + "type": "text" + } + }, + "type": "keyword" + }, + "tie_breaker_id": { + "type": "keyword" + }, + "type": { + "type": "keyword" + }, + "updated_by": { + "type": "keyword" + }, + "version": { + "type": "keyword" + } + } + }, + "file-upload-usage-collection-telemetry": { + "properties": { + "file_upload": { + "properties": { + "index_creation_count": { + "type": "long" + } + } + } + } + }, + "fleet-agent-actions": { + "properties": { + "ack_data": { + "type": "text" + }, + "agent_id": { + "type": "keyword" + }, + "created_at": { + "type": "date" + }, + "data": { + "type": "binary" + }, + "policy_id": { + "type": "keyword" + }, + "policy_revision": { + "type": "integer" + }, + "sent_at": { + "type": "date" + }, + "type": { + "type": "keyword" + } + } + }, + "fleet-agents": { + "properties": { + "access_api_key_id": { + "type": "keyword" + }, + "active": { + "type": "boolean" + }, + "current_error_events": { + "index": false, + "type": "text" + }, + "default_api_key": { + "type": "binary" + }, + "default_api_key_id": { + "type": "keyword" + }, + "enrolled_at": { + "type": "date" + }, + "last_checkin": { + "type": "date" + }, + "last_checkin_status": { + "type": "keyword" + }, + "last_updated": { + "type": "date" + }, + "local_metadata": { + "type": "flattened" + }, + "packages": { + "type": "keyword" + }, + "policy_id": { + "type": "keyword" + }, + "policy_revision": { + "type": "integer" + }, + "type": { + "type": "keyword" + }, + "unenrolled_at": { + "type": "date" + }, + "unenrollment_started_at": { + "type": "date" + }, + "updated_at": { + "type": "date" + }, + "upgrade_started_at": { + "type": "date" + }, + "upgraded_at": { + "type": "date" + }, + "user_provided_metadata": { + "type": "flattened" + }, + "version": { + "type": "keyword" + } + } + }, + "fleet-enrollment-api-keys": { + "properties": { + "active": { + "type": "boolean" + }, + "api_key": { + "type": "binary" + }, + "api_key_id": { + "type": "keyword" + }, + "created_at": { + "type": "date" + }, + "expire_at": { + "type": "date" + }, + "name": { + "type": "keyword" + }, + "policy_id": { + "type": "keyword" + }, + "type": { + "type": "keyword" + }, + "updated_at": { + "type": "date" + } + } + }, + "fleet-preconfiguration-deletion-record": { + "properties": { + "id": { + "type": "keyword" + } + } + }, + "graph-workspace": { + "properties": { + "description": { + "type": "text" + }, + "kibanaSavedObjectMeta": { + "properties": { + "searchSourceJSON": { + "type": "text" + } + } + }, + "legacyIndexPatternRef": { + "index": false, + "type": "text" + }, + "numLinks": { + "type": "integer" + }, + "numVertices": { + "type": "integer" + }, + "title": { + "type": "text" + }, + "version": { + "type": "integer" + }, + "wsState": { + "type": "text" + } + } + }, + "index-pattern": { + "dynamic": "false", + "properties": { + "title": { + "type": "text" + }, + "type": { + "type": "keyword" + } + } + }, + "infrastructure-ui-source": { + "dynamic": "false", + "type": "object" + }, + "ingest-agent-policies": { + "properties": { + "description": { + "type": "text" + }, + "is_default": { + "type": "boolean" + }, + "is_default_fleet_server": { + "type": "boolean" + }, + "is_managed": { + "type": "boolean" + }, + "is_preconfigured": { + "type": "keyword" + }, + "monitoring_enabled": { + "index": false, + "type": "keyword" + }, + "name": { + "type": "keyword" + }, + "namespace": { + "type": "keyword" + }, + "package_policies": { + "type": "keyword" + }, + "revision": { + "type": "integer" + }, + "status": { + "type": "keyword" + }, + "updated_at": { + "type": "date" + }, + "updated_by": { + "type": "keyword" + } + } + }, + "ingest-outputs": { + "properties": { + "ca_sha256": { + "index": false, + "type": "keyword" + }, + "config": { + "type": "flattened" + }, + "config_yaml": { + "type": "text" + }, + "hosts": { + "type": "keyword" + }, + "is_default": { + "type": "boolean" + }, + "name": { + "type": "keyword" + }, + "type": { + "type": "keyword" + } + } + }, + "ingest-package-policies": { + "properties": { + "created_at": { + "type": "date" + }, + "created_by": { + "type": "keyword" + }, + "description": { + "type": "text" + }, + "enabled": { + "type": "boolean" + }, + "inputs": { + "enabled": false, + "properties": { + "compiled_input": { + "type": "flattened" + }, + "config": { + "type": "flattened" + }, + "enabled": { + "type": "boolean" + }, + "streams": { + "properties": { + "compiled_stream": { + "type": "flattened" + }, + "config": { + "type": "flattened" + }, + "data_stream": { + "properties": { + "dataset": { + "type": "keyword" + }, + "type": { + "type": "keyword" + } + } + }, + "enabled": { + "type": "boolean" + }, + "id": { + "type": "keyword" + }, + "vars": { + "type": "flattened" + } + }, + "type": "nested" + }, + "type": { + "type": "keyword" + }, + "vars": { + "type": "flattened" + } + }, + "type": "nested" + }, + "name": { + "type": "keyword" + }, + "namespace": { + "type": "keyword" + }, + "output_id": { + "type": "keyword" + }, + "package": { + "properties": { + "name": { + "type": "keyword" + }, + "title": { + "type": "keyword" + }, + "version": { + "type": "keyword" + } + } + }, + "policy_id": { + "type": "keyword" + }, + "revision": { + "type": "integer" + }, + "updated_at": { + "type": "date" + }, + "updated_by": { + "type": "keyword" + } + } + }, + "ingest_manager_settings": { + "properties": { + "fleet_server_hosts": { + "type": "keyword" + }, + "has_seen_add_data_notice": { + "index": false, + "type": "boolean" + }, + "has_seen_fleet_migration_notice": { + "index": false, + "type": "boolean" + } + } + }, + "inventory-view": { + "dynamic": "false", + "type": "object" + }, + "kql-telemetry": { + "properties": { + "optInCount": { + "type": "long" + }, + "optOutCount": { + "type": "long" + } + } + }, + "legacy-url-alias": { + "dynamic": "false", + "type": "object" + }, + "lens": { + "properties": { + "description": { + "type": "text" + }, + "expression": { + "doc_values": false, + "index": false, + "type": "keyword" + }, + "state": { + "type": "flattened" + }, + "title": { + "type": "text" + }, + "visualizationType": { + "type": "keyword" + } + } + }, + "lens-ui-telemetry": { + "properties": { + "count": { + "type": "integer" + }, + "date": { + "type": "date" + }, + "name": { + "type": "keyword" + }, + "type": { + "type": "keyword" + } + } + }, + "map": { + "properties": { + "bounds": { + "dynamic": "false", + "type": "object" + }, + "description": { + "type": "text" + }, + "layerListJSON": { + "type": "text" + }, + "mapStateJSON": { + "type": "text" + }, + "title": { + "type": "text" + }, + "uiStateJSON": { + "type": "text" + }, + "version": { + "type": "integer" + } + } + }, + "maps-telemetry": { + "enabled": false, + "type": "object" + }, + "metrics-explorer-view": { + "dynamic": "false", + "type": "object" + }, + "migrationVersion": { + "dynamic": "true", + "properties": { + "action": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + }, + "alert": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + }, + "cases": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + }, + "cases-comments": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + }, + "cases-configure": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + }, + "cases-user-actions": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + }, + "config": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + }, + "dashboard": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + }, + "exception-list-agnostic": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + }, + "index-pattern": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + }, + "ingest-agent-policies": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + }, + "ingest-outputs": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + }, + "ingest-package-policies": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + }, + "ingest_manager_settings": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + }, + "search": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + }, + "search-session": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + }, + "search-telemetry": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + }, + "siem-detection-engine-rule-actions": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + }, + "space": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + }, + "visualization": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + } + } + }, + "ml-job": { + "properties": { + "datafeed_id": { + "fields": { + "keyword": { + "type": "keyword" + } + }, + "type": "text" + }, + "job_id": { + "fields": { + "keyword": { + "type": "keyword" + } + }, + "type": "text" + }, + "type": { + "type": "keyword" + } + } + }, + "ml-module": { + "dynamic": "false", + "properties": { + "datafeeds": { + "type": "object" + }, + "defaultIndexPattern": { + "fields": { + "keyword": { + "type": "keyword" + } + }, + "type": "text" + }, + "description": { + "fields": { + "keyword": { + "type": "keyword" + } + }, + "type": "text" + }, + "id": { + "fields": { + "keyword": { + "type": "keyword" + } + }, + "type": "text" + }, + "jobs": { + "type": "object" + }, + "logo": { + "type": "object" + }, + "query": { + "type": "object" + }, + "title": { + "fields": { + "keyword": { + "type": "keyword" + } + }, + "type": "text" + }, + "type": { + "fields": { + "keyword": { + "type": "keyword" + } + }, + "type": "text" + } + } + }, + "monitoring-telemetry": { + "properties": { + "reportedClusterUuids": { + "type": "keyword" + } + } + }, + "namespace": { + "type": "keyword" + }, + "namespaces": { + "type": "keyword" + }, + "originId": { + "type": "keyword" + }, + "query": { + "properties": { + "description": { + "type": "text" + }, + "filters": { + "enabled": false, + "type": "object" + }, + "query": { + "properties": { + "language": { + "type": "keyword" + }, + "query": { + "index": false, + "type": "keyword" + } + } + }, + "timefilter": { + "enabled": false, + "type": "object" + }, + "title": { + "type": "text" + } + } + }, + "references": { + "properties": { + "id": { + "type": "keyword" + }, + "name": { + "type": "keyword" + }, + "type": { + "type": "keyword" + } + }, + "type": "nested" + }, + "sample-data-telemetry": { + "properties": { + "installCount": { + "type": "long" + }, + "unInstallCount": { + "type": "long" + } + } + }, + "search": { + "properties": { + "columns": { + "doc_values": false, + "index": false, + "type": "keyword" + }, + "description": { + "type": "text" + }, + "grid": { + "enabled": false, + "type": "object" + }, + "hideChart": { + "doc_values": false, + "index": false, + "type": "boolean" + }, + "hits": { + "doc_values": false, + "index": false, + "type": "integer" + }, + "kibanaSavedObjectMeta": { + "properties": { + "searchSourceJSON": { + "index": false, + "type": "text" + } + } + }, + "sort": { + "doc_values": false, + "index": false, + "type": "keyword" + }, + "title": { + "type": "text" + }, + "version": { + "type": "integer" + } + } + }, + "search-session": { + "properties": { + "appId": { + "type": "keyword" + }, + "completed": { + "type": "date" + }, + "created": { + "type": "date" + }, + "expires": { + "type": "date" + }, + "idMapping": { + "enabled": false, + "type": "object" + }, + "initialState": { + "enabled": false, + "type": "object" + }, + "name": { + "type": "keyword" + }, + "persisted": { + "type": "boolean" + }, + "realmName": { + "type": "keyword" + }, + "realmType": { + "type": "keyword" + }, + "restoreState": { + "enabled": false, + "type": "object" + }, + "sessionId": { + "type": "keyword" + }, + "status": { + "type": "keyword" + }, + "touched": { + "type": "date" + }, + "urlGeneratorId": { + "type": "keyword" + }, + "username": { + "type": "keyword" + } + } + }, + "search-telemetry": { + "dynamic": "false", + "type": "object" + }, + "security-rule": { + "dynamic": "false", + "properties": { + "name": { + "type": "keyword" + }, + "rule_id": { + "type": "keyword" + }, + "version": { + "type": "long" + } + } + }, + "security-solution-signals-migration": { + "properties": { + "created": { + "index": false, + "type": "date" + }, + "createdBy": { + "index": false, + "type": "text" + }, + "destinationIndex": { + "index": false, + "type": "keyword" + }, + "error": { + "index": false, + "type": "text" + }, + "sourceIndex": { + "type": "keyword" + }, + "status": { + "index": false, + "type": "keyword" + }, + "taskId": { + "index": false, + "type": "keyword" + }, + "updated": { + "index": false, + "type": "date" + }, + "updatedBy": { + "index": false, + "type": "text" + }, + "version": { + "type": "long" + } + } + }, + "siem-detection-engine-rule-actions": { + "properties": { + "actions": { + "properties": { + "action_type_id": { + "type": "keyword" + }, + "group": { + "type": "keyword" + }, + "id": { + "type": "keyword" + }, + "params": { + "enabled": false, + "type": "object" + } + } + }, + "alertThrottle": { + "type": "keyword" + }, + "ruleAlertId": { + "type": "keyword" + }, + "ruleThrottle": { + "type": "keyword" + } + } + }, + "siem-detection-engine-rule-status": { + "properties": { + "alertId": { + "type": "keyword" + }, + "bulkCreateTimeDurations": { + "type": "float" + }, + "gap": { + "type": "text" + }, + "lastFailureAt": { + "type": "date" + }, + "lastFailureMessage": { + "type": "text" + }, + "lastLookBackDate": { + "type": "date" + }, + "lastSuccessAt": { + "type": "date" + }, + "lastSuccessMessage": { + "type": "text" + }, + "searchAfterTimeDurations": { + "type": "float" + }, + "status": { + "type": "keyword" + }, + "statusDate": { + "type": "date" + } + } + }, + "siem-ui-timeline": { + "properties": { + "columns": { + "properties": { + "aggregatable": { + "type": "boolean" + }, + "category": { + "type": "keyword" + }, + "columnHeaderType": { + "type": "keyword" + }, + "description": { + "type": "text" + }, + "example": { + "type": "text" + }, + "id": { + "type": "keyword" + }, + "indexes": { + "type": "keyword" + }, + "name": { + "type": "text" + }, + "placeholder": { + "type": "text" + }, + "searchable": { + "type": "boolean" + }, + "type": { + "type": "keyword" + } + } + }, + "created": { + "type": "date" + }, + "createdBy": { + "type": "text" + }, + "dataProviders": { + "properties": { + "and": { + "properties": { + "enabled": { + "type": "boolean" + }, + "excluded": { + "type": "boolean" + }, + "id": { + "type": "keyword" + }, + "kqlQuery": { + "type": "text" + }, + "name": { + "type": "text" + }, + "queryMatch": { + "properties": { + "displayField": { + "type": "text" + }, + "displayValue": { + "type": "text" + }, + "field": { + "type": "text" + }, + "operator": { + "type": "text" + }, + "value": { + "type": "text" + } + } + }, + "type": { + "type": "text" + } + } + }, + "enabled": { + "type": "boolean" + }, + "excluded": { + "type": "boolean" + }, + "id": { + "type": "keyword" + }, + "kqlQuery": { + "type": "text" + }, + "name": { + "type": "text" + }, + "queryMatch": { + "properties": { + "displayField": { + "type": "text" + }, + "displayValue": { + "type": "text" + }, + "field": { + "type": "text" + }, + "operator": { + "type": "text" + }, + "value": { + "type": "text" + } + } + }, + "type": { + "type": "text" + } + } + }, + "dateRange": { + "properties": { + "end": { + "type": "date" + }, + "start": { + "type": "date" + } + } + }, + "description": { + "type": "text" + }, + "eqlOptions": { + "properties": { + "eventCategoryField": { + "type": "text" + }, + "query": { + "type": "text" + }, + "size": { + "type": "text" + }, + "tiebreakerField": { + "type": "text" + }, + "timestampField": { + "type": "text" + } + } + }, + "eventType": { + "type": "keyword" + }, + "excludedRowRendererIds": { + "type": "text" + }, + "favorite": { + "properties": { + "favoriteDate": { + "type": "date" + }, + "fullName": { + "type": "text" + }, + "keySearch": { + "type": "text" + }, + "userName": { + "type": "text" + } + } + }, + "filters": { + "properties": { + "exists": { + "type": "text" + }, + "match_all": { + "type": "text" + }, + "meta": { + "properties": { + "alias": { + "type": "text" + }, + "controlledBy": { + "type": "text" + }, + "disabled": { + "type": "boolean" + }, + "field": { + "type": "text" + }, + "formattedValue": { + "type": "text" + }, + "index": { + "type": "keyword" + }, + "key": { + "type": "keyword" + }, + "negate": { + "type": "boolean" + }, + "params": { + "type": "text" + }, + "type": { + "type": "keyword" + }, + "value": { + "type": "text" + } + } + }, + "missing": { + "type": "text" + }, + "query": { + "type": "text" + }, + "range": { + "type": "text" + }, + "script": { + "type": "text" + } + } + }, + "indexNames": { + "type": "text" + }, + "kqlMode": { + "type": "keyword" + }, + "kqlQuery": { + "properties": { + "filterQuery": { + "properties": { + "kuery": { + "properties": { + "expression": { + "type": "text" + }, + "kind": { + "type": "keyword" + } + } + }, + "serializedQuery": { + "type": "text" + } + } + } + } + }, + "savedQueryId": { + "type": "keyword" + }, + "sort": { + "dynamic": "false", + "properties": { + "columnId": { + "type": "keyword" + }, + "columnType": { + "type": "keyword" + }, + "sortDirection": { + "type": "keyword" + } + } + }, + "status": { + "type": "keyword" + }, + "templateTimelineId": { + "type": "text" + }, + "templateTimelineVersion": { + "type": "integer" + }, + "timelineType": { + "type": "keyword" + }, + "title": { + "type": "text" + }, + "updated": { + "type": "date" + }, + "updatedBy": { + "type": "text" + } + } + }, + "siem-ui-timeline-note": { + "properties": { + "created": { + "type": "date" + }, + "createdBy": { + "type": "text" + }, + "eventId": { + "type": "keyword" + }, + "note": { + "type": "text" + }, + "timelineId": { + "type": "keyword" + }, + "updated": { + "type": "date" + }, + "updatedBy": { + "type": "text" + } + } + }, + "siem-ui-timeline-pinned-event": { + "properties": { + "created": { + "type": "date" + }, + "createdBy": { + "type": "text" + }, + "eventId": { + "type": "keyword" + }, + "timelineId": { + "type": "keyword" + }, + "updated": { + "type": "date" + }, + "updatedBy": { + "type": "text" + } + } + }, + "space": { + "properties": { + "_reserved": { + "type": "boolean" + }, + "color": { + "type": "keyword" + }, + "description": { + "type": "text" + }, + "disabledFeatures": { + "type": "keyword" + }, + "imageUrl": { + "index": false, + "type": "text" + }, + "initials": { + "type": "keyword" + }, + "name": { + "fields": { + "keyword": { + "ignore_above": 2048, + "type": "keyword" + } + }, + "type": "text" + } + } + }, + "spaces-usage-stats": { + "dynamic": "false", + "type": "object" + }, + "tag": { + "properties": { + "color": { + "type": "text" + }, + "description": { + "type": "text" + }, + "name": { + "type": "text" + } + } + }, + "telemetry": { + "properties": { + "allowChangingOptInStatus": { + "type": "boolean" + }, + "enabled": { + "type": "boolean" + }, + "lastReported": { + "type": "date" + }, + "lastVersionChecked": { + "type": "keyword" + }, + "reportFailureCount": { + "type": "integer" + }, + "reportFailureVersion": { + "type": "keyword" + }, + "sendUsageFrom": { + "type": "keyword" + }, + "userHasSeenNotice": { + "type": "boolean" + } + } + }, + "timelion-sheet": { + "properties": { + "description": { + "type": "text" + }, + "hits": { + "type": "integer" + }, + "kibanaSavedObjectMeta": { + "properties": { + "searchSourceJSON": { + "type": "text" + } + } + }, + "timelion_chart_height": { + "type": "integer" + }, + "timelion_columns": { + "type": "integer" + }, + "timelion_interval": { + "type": "keyword" + }, + "timelion_other_interval": { + "type": "keyword" + }, + "timelion_rows": { + "type": "integer" + }, + "timelion_sheet": { + "type": "text" + }, + "title": { + "type": "text" + }, + "version": { + "type": "integer" + } + } + }, + "type": { + "type": "keyword" + }, + "ui-counter": { + "properties": { + "count": { + "type": "integer" + } + } + }, + "ui-metric": { + "properties": { + "count": { + "type": "integer" + } + } + }, + "updated_at": { + "type": "date" + }, + "upgrade-assistant-reindex-operation": { + "properties": { + "errorMessage": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + }, + "indexName": { + "type": "keyword" + }, + "lastCompletedStep": { + "type": "long" + }, + "locked": { + "type": "date" + }, + "newIndexName": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + }, + "reindexOptions": { + "properties": { + "openAndClose": { + "type": "boolean" + }, + "queueSettings": { + "properties": { + "queuedAt": { + "type": "long" + }, + "startedAt": { + "type": "long" + } + } + } + } + }, + "reindexTaskId": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + }, + "reindexTaskPercComplete": { + "type": "float" + }, + "runningReindexCount": { + "type": "integer" + }, + "status": { + "type": "integer" + } + } + }, + "upgrade-assistant-telemetry": { + "properties": { + "features": { + "properties": { + "deprecation_logging": { + "properties": { + "enabled": { + "null_value": true, + "type": "boolean" + } + } + } + } + }, + "ui_open": { + "properties": { + "cluster": { + "null_value": 0, + "type": "long" + }, + "indices": { + "null_value": 0, + "type": "long" + }, + "overview": { + "null_value": 0, + "type": "long" + } + } + }, + "ui_reindex": { + "properties": { + "close": { + "null_value": 0, + "type": "long" + }, + "open": { + "null_value": 0, + "type": "long" + }, + "start": { + "null_value": 0, + "type": "long" + }, + "stop": { + "null_value": 0, + "type": "long" + } + } + } + } + }, + "uptime-dynamic-settings": { + "dynamic": "false", + "type": "object" + }, + "url": { + "properties": { + "accessCount": { + "type": "long" + }, + "accessDate": { + "type": "date" + }, + "createDate": { + "type": "date" + }, + "url": { + "fields": { + "keyword": { + "ignore_above": 2048, + "type": "keyword" + } + }, + "type": "text" + } + } + }, + "usage-counters": { + "dynamic": "false", + "properties": { + "domainId": { + "type": "keyword" + } + } + }, + "visualization": { + "properties": { + "description": { + "type": "text" + }, + "kibanaSavedObjectMeta": { + "properties": { + "searchSourceJSON": { + "index": false, + "type": "text" + } + } + }, + "savedSearchRefName": { + "doc_values": false, + "index": false, + "type": "keyword" + }, + "title": { + "type": "text" + }, + "uiStateJSON": { + "index": false, + "type": "text" + }, + "version": { + "type": "integer" + }, + "visState": { + "index": false, + "type": "text" + } + } + }, + "workplace_search_telemetry": { + "dynamic": "false", + "type": "object" + } + } + }, + "settings": { + "index": { + "auto_expand_replicas": "0-1", + "number_of_replicas": "1", + "number_of_shards": "1", + "priority": "10", + "refresh_interval": "1s" + } + } + } +} \ No newline at end of file diff --git a/x-pack/test/functional/es_archives/infra/alerts_test_data/data.json.gz b/x-pack/test/functional/es_archives/infra/alerts_test_data/data.json.gz index 1c76205f4caa2..92815ba80a3a5 100644 Binary files a/x-pack/test/functional/es_archives/infra/alerts_test_data/data.json.gz and b/x-pack/test/functional/es_archives/infra/alerts_test_data/data.json.gz differ diff --git a/x-pack/test/functional/es_archives/security_solution/timelines/7.15.0/data.json.gz b/x-pack/test/functional/es_archives/security_solution/timelines/7.15.0/data.json.gz index e942ef732b22a..91e3e459f826b 100644 Binary files a/x-pack/test/functional/es_archives/security_solution/timelines/7.15.0/data.json.gz and b/x-pack/test/functional/es_archives/security_solution/timelines/7.15.0/data.json.gz differ diff --git a/x-pack/test/functional/page_objects/lens_page.ts b/x-pack/test/functional/page_objects/lens_page.ts index 2e1151602f311..e26ea8f598c46 100644 --- a/x-pack/test/functional/page_objects/lens_page.ts +++ b/x-pack/test/functional/page_objects/lens_page.ts @@ -645,8 +645,21 @@ export function LensPageProvider({ getService, getPageObjects }: FtrProviderCont /** * Adds a new layer to the chart, fails if the chart does not support new layers */ - async createLayer() { + async createLayer(layerType: string = 'data') { await testSubjects.click('lnsLayerAddButton'); + const layerCount = (await find.allByCssSelector(`[data-test-subj^="lns-layerPanel-"]`)) + .length; + + await retry.waitFor('check for layer type support', async () => { + const fasterChecks = await Promise.all([ + (await find.allByCssSelector(`[data-test-subj^="lns-layerPanel-"]`)).length > layerCount, + testSubjects.exists(`lnsLayerAddButton-${layerType}`), + ]); + return fasterChecks.filter(Boolean).length > 0; + }); + if (await testSubjects.exists(`lnsLayerAddButton-${layerType}`)) { + await testSubjects.click(`lnsLayerAddButton-${layerType}`); + } }, /** @@ -1075,6 +1088,10 @@ export function LensPageProvider({ getService, getPageObjects }: FtrProviderCont await testSubjects.click('lens-dimensionTabs-formula'); }, + async switchToStaticValue() { + await testSubjects.click('lens-dimensionTabs-static_value'); + }, + async toggleFullscreen() { await testSubjects.click('lnsFormula-fullscreen'); }, diff --git a/x-pack/test/functional/page_objects/search_sessions_management_page.ts b/x-pack/test/functional/page_objects/search_sessions_management_page.ts index 86391b568fdf2..15c87ea450425 100644 --- a/x-pack/test/functional/page_objects/search_sessions_management_page.ts +++ b/x-pack/test/functional/page_objects/search_sessions_management_page.ts @@ -38,6 +38,7 @@ export function SearchSessionsPageProvider({ getService, getPageObjects }: FtrPr mainUrl: $.findTestSubject('sessionManagementNameCol').text(), created: $.findTestSubject('sessionManagementCreatedCol').text(), expires: $.findTestSubject('sessionManagementExpiresCol').text(), + searchesCount: Number($.findTestSubject('sessionManagementNumSearchesCol').text()), app: $.findTestSubject('sessionManagementAppIcon').attr('data-test-app-id'), view: async () => { log.debug('management ui: view the session'); diff --git a/x-pack/test/functional/services/observability/alerts.ts b/x-pack/test/functional/services/observability/alerts.ts index 8926d734d7313..435da8ad94037 100644 --- a/x-pack/test/functional/services/observability/alerts.ts +++ b/x-pack/test/functional/services/observability/alerts.ts @@ -17,6 +17,7 @@ const DATE_WITH_DATA = { }; const ALERTS_FLYOUT_SELECTOR = 'alertsFlyout'; +const COPY_TO_CLIPBOARD_BUTTON_SELECTOR = 'copy-to-clipboard'; const ALERTS_TABLE_CONTAINER_SELECTOR = 'events-viewer-panel'; const ACTION_COLUMN_INDEX = 1; @@ -77,7 +78,7 @@ export function ObservabilityAlertsProvider({ getPageObjects, getService }: FtrP }; const clearQueryBar = async () => { - return await (await getQueryBar()).clearValueWithKeyboard({ charByChar: true }); + return await (await getQueryBar()).clearValueWithKeyboard(); }; const typeInQueryBar = async (query: string) => { @@ -132,6 +133,20 @@ export function ObservabilityAlertsProvider({ getPageObjects, getService }: FtrP return await testSubjects.findAllDescendant('alertsFlyoutDescriptionListDescription', flyout); }; + // Cell actions + + const copyToClipboardButtonExists = async () => { + return await testSubjects.exists(COPY_TO_CLIPBOARD_BUTTON_SELECTOR); + }; + + const getCopyToClipboardButton = async () => { + return await testSubjects.find(COPY_TO_CLIPBOARD_BUTTON_SELECTOR); + }; + + const getFilterForValueButton = async () => { + return await testSubjects.find('filter-for-value'); + }; + const openActionsMenuForRow = async (rowIndex: number) => { const rows = await getTableCellsInRows(); const actionsOverflowButton = await testSubjects.findDescendant( @@ -163,6 +178,7 @@ export function ObservabilityAlertsProvider({ getPageObjects, getService }: FtrP }; return { + getQueryBar, clearQueryBar, closeAlertsFlyout, getAlertsFlyout, @@ -171,6 +187,9 @@ export function ObservabilityAlertsProvider({ getPageObjects, getService }: FtrP getAlertsFlyoutOrFail, getAlertsFlyoutTitle, getAlertsFlyoutViewInAppButtonOrFail, + getCopyToClipboardButton, + getFilterForValueButton, + copyToClipboardButtonExists, getNoDataStateOrFail, getTableCells, getTableCellsInRows, diff --git a/x-pack/test/functional/services/uptime/common.ts b/x-pack/test/functional/services/uptime/common.ts index 7d47bcf985943..e3c73a1e1ca97 100644 --- a/x-pack/test/functional/services/uptime/common.ts +++ b/x-pack/test/functional/services/uptime/common.ts @@ -115,5 +115,8 @@ export function UptimeCommonProvider({ getService, getPageObjects }: FtrProvider await testSubjects.missingOrFail('data-missing'); }); }, + async hasMappingsError() { + return testSubjects.exists('xpack.uptime.mappingsErrorPage'); + }, }; } diff --git a/x-pack/test/functional_cors/plugins/kibana_cors_test/server/plugin.ts b/x-pack/test/functional_cors/plugins/kibana_cors_test/server/plugin.ts index e6c3f4b05aabd..9ba373c02fafa 100644 --- a/x-pack/test/functional_cors/plugins/kibana_cors_test/server/plugin.ts +++ b/x-pack/test/functional_cors/plugins/kibana_cors_test/server/plugin.ts @@ -7,7 +7,6 @@ import Hapi from '@hapi/hapi'; import { kbnTestConfig } from '@kbn/test'; -import { take } from 'rxjs/operators'; import Url from 'url'; import abab from 'abab'; @@ -47,20 +46,18 @@ fetch('${url}', { export class CorsTestPlugin implements Plugin { private server?: Hapi.Server; + constructor(private readonly initializerContext: PluginInitializerContext) {} - async setup(core: CoreSetup) { + setup(core: CoreSetup) { const router = core.http.createRouter(); router.post({ path: '/cors-test', validate: false }, (context, req, res) => res.ok({ body: 'content from kibana' }) ); } - async start(core: CoreStart) { - const config = await this.initializerContext.config - .create() - .pipe(take(1)) - .toPromise(); + start(core: CoreStart) { + const config = this.initializerContext.config.get(); const server = new Hapi.Server({ port: config.port, @@ -78,8 +75,9 @@ export class CorsTestPlugin implements Plugin { return h.response(renderBody(kibanaUrl)); }, }); - await server.start(); + server.start(); } + public stop() { if (this.server) { this.server.stop(); diff --git a/x-pack/test/functional_embedded/tests/iframe_embedded.ts b/x-pack/test/functional_embedded/tests/iframe_embedded.ts index 21bc19424c893..0e2a461dd15f9 100644 --- a/x-pack/test/functional_embedded/tests/iframe_embedded.ts +++ b/x-pack/test/functional_embedded/tests/iframe_embedded.ts @@ -17,7 +17,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const testSubjects = getService('testSubjects'); const retry = getService('retry'); - describe('in iframe', () => { + // FLAKY https://github.com/elastic/kibana/issues/70928 + describe.skip('in iframe', () => { it('should open Kibana for logged-in user', async () => { const isChromeHiddenBefore = await PageObjects.common.isChromeHidden(); expect(isChromeHiddenBefore).to.be(true); diff --git a/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/alert_create_flyout.ts b/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/alert_create_flyout.ts index 2ce771f7b993f..88ba4c37559c5 100644 --- a/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/alert_create_flyout.ts +++ b/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/alert_create_flyout.ts @@ -103,7 +103,8 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { await testSubjects.click('test.always-firing-SelectOption'); } - describe('create alert', function () { + // FLAKY https://github.com/elastic/kibana/issues/112749 + describe.skip('create alert', function () { before(async () => { await pageObjects.common.navigateToApp('triggersActions'); await testSubjects.click('rulesTab'); diff --git a/x-pack/test/observability_functional/apps/observability/alerts/index.ts b/x-pack/test/observability_functional/apps/observability/alerts/index.ts index 6b130ae0f6264..856d7e60996ec 100644 --- a/x-pack/test/observability_functional/apps/observability/alerts/index.ts +++ b/x-pack/test/observability_functional/apps/observability/alerts/index.ts @@ -14,11 +14,14 @@ async function asyncForEach(array: T[], callback: (item: T, index: number) => } } +const ACTIVE_ALERTS_CELL_COUNT = 48; +const RECOVERED_ALERTS_CELL_COUNT = 24; +const TOTAL_ALERTS_CELL_COUNT = 72; + export default ({ getPageObjects, getService }: FtrProviderContext) => { const esArchiver = getService('esArchiver'); - // Failing: See https://github.com/elastic/kibana/issues/111907 - describe.skip('Observability alerts', function () { + describe('Observability alerts', function () { this.tags('includeFirefox'); const pageObjects = getPageObjects(['common']); @@ -41,9 +44,10 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { }); it('Renders the correct number of cells', async () => { - // NOTE: This isn't ideal, but EuiDataGrid doesn't really have the concept of "rows" - const cells = await observability.alerts.getTableCells(); - expect(cells.length).to.be(72); + await retry.try(async () => { + const cells = await observability.alerts.getTableCells(); + expect(cells.length).to.be(TOTAL_ALERTS_CELL_COUNT); + }); }); describe('Filtering', () => { @@ -67,7 +71,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { await observability.alerts.submitQuery('kibana.alert.status: recovered'); await retry.try(async () => { const cells = await observability.alerts.getTableCells(); - expect(cells.length).to.be(24); + expect(cells.length).to.be(RECOVERED_ALERTS_CELL_COUNT); }); }); @@ -87,9 +91,9 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { await (await testSubjects.find('superDatePickerToggleQuickMenuButton')).click(); // We shouldn't expect any data for the last 15 minutes await (await testSubjects.find('superDatePickerCommonlyUsed_Last_15 minutes')).click(); + await observability.alerts.getNoDataStateOrFail(); + await pageObjects.common.waitUntilUrlIncludes('rangeFrom=now-15m&rangeTo=now'); }); - await observability.alerts.getNoDataStateOrFail(); - await pageObjects.common.waitUntilUrlIncludes('rangeFrom=now-15m&rangeTo=now'); }); }); @@ -114,10 +118,12 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { }); it('Displays the correct title', async () => { - const titleText = await ( - await observability.alerts.getAlertsFlyoutTitle() - ).getVisibleText(); - expect(titleText).to.contain('Log threshold'); + await retry.try(async () => { + const titleText = await ( + await observability.alerts.getAlertsFlyoutTitle() + ).getVisibleText(); + expect(titleText).to.contain('Log threshold'); + }); }); it('Displays the correct content', async () => { @@ -156,6 +162,43 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { }); }); }); + + describe('Cell actions', () => { + beforeEach(async () => { + await retry.try(async () => { + const cells = await observability.alerts.getTableCells(); + const alertStatusCell = cells[2]; + await alertStatusCell.moveMouseTo(); + await retry.waitFor( + 'cell actions visible', + async () => await observability.alerts.copyToClipboardButtonExists() + ); + }); + }); + + afterEach(async () => { + await observability.alerts.clearQueryBar(); + }); + + it('Copy button works', async () => { + // NOTE: We don't have access to the clipboard in a headless environment, + // so we'll just check the button is clickable in the functional tests. + await (await observability.alerts.getCopyToClipboardButton()).click(); + }); + + it('Filter for value works', async () => { + await (await observability.alerts.getFilterForValueButton()).click(); + const queryBarValue = await ( + await observability.alerts.getQueryBar() + ).getAttribute('value'); + expect(queryBarValue).to.be('kibana.alert.status: "active"'); + // Wait for request + await retry.try(async () => { + const cells = await observability.alerts.getTableCells(); + expect(cells.length).to.be(ACTIVE_ALERTS_CELL_COUNT); + }); + }); + }); }); }); }; diff --git a/x-pack/test/reporting_api_integration/reporting_and_security/bwc_existing_indexes.ts b/x-pack/test/reporting_api_integration/reporting_and_security/bwc_existing_indexes.ts index 0da51901f9086..168390bc6fc28 100644 --- a/x-pack/test/reporting_api_integration/reporting_and_security/bwc_existing_indexes.ts +++ b/x-pack/test/reporting_api_integration/reporting_and_security/bwc_existing_indexes.ts @@ -19,6 +19,7 @@ import { FtrProviderContext } from '../ftr_provider_context'; export default function ({ getService }: FtrProviderContext) { const esArchiver = getService('esArchiver'); const reportingAPI = getService('reportingAPI'); + const kibanaServer = getService('kibanaServer'); describe('BWC report generation into existing indexes', () => { let cleanupIndexAlias: () => Promise; @@ -28,7 +29,8 @@ export default function ({ getService }: FtrProviderContext) { await reportingAPI.deleteAllReports(); // data to report on await esArchiver.load('test/functional/fixtures/es_archiver/logstash_functional'); - await esArchiver.load('test/functional/fixtures/es_archiver/discover'); // includes index pattern for logstash_functional + await kibanaServer.importExport.load('test/functional/fixtures/kbn_archiver/discover'); + // archive with reporting index mappings v6.2 await esArchiver.load('x-pack/test/functional/es_archives/reporting/bwc/6_2'); @@ -41,8 +43,8 @@ export default function ({ getService }: FtrProviderContext) { }); after('remove index alias', async () => { - await esArchiver.load('test/functional/fixtures/es_archiver/logstash_functional'); - await esArchiver.load('test/functional/fixtures/es_archiver/discover'); + await esArchiver.unload('test/functional/fixtures/es_archiver/logstash_functional'); + await kibanaServer.importExport.unload('test/functional/fixtures/kbn_archiver/discover'); await cleanupIndexAlias(); await esArchiver.unload('x-pack/test/functional/es_archives/reporting/bwc/6_2'); diff --git a/x-pack/test/search_sessions_integration/tests/apps/dashboard/async_search/sessions_in_space.ts b/x-pack/test/search_sessions_integration/tests/apps/dashboard/async_search/sessions_in_space.ts index d05ac46d5b88f..98eca99ff436c 100644 --- a/x-pack/test/search_sessions_integration/tests/apps/dashboard/async_search/sessions_in_space.ts +++ b/x-pack/test/search_sessions_integration/tests/apps/dashboard/async_search/sessions_in_space.ts @@ -23,7 +23,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const browser = getService('browser'); const searchSessions = getService('searchSessions'); - describe('dashboard in space', () => { + // Failing: See https://github.com/elastic/kibana/issues/112732 + describe.skip('dashboard in space', () => { describe('Storing search sessions in space', () => { before(async () => { await esArchiver.load('x-pack/test/functional/es_archives/dashboard/session_in_space'); diff --git a/x-pack/test/search_sessions_integration/tests/apps/discover/async_search.ts b/x-pack/test/search_sessions_integration/tests/apps/discover/async_search.ts index 3f02e64056325..cd13f71cf1bb7 100644 --- a/x-pack/test/search_sessions_integration/tests/apps/discover/async_search.ts +++ b/x-pack/test/search_sessions_integration/tests/apps/discover/async_search.ts @@ -26,6 +26,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { const searchSessions = getService('searchSessions'); const retry = getService('retry'); const kibanaServer = getService('kibanaServer'); + const toasts = getService('toasts'); describe('discover async search', () => { before(async () => { @@ -112,12 +113,20 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { // load URL to restore a saved session await PageObjects.searchSessionsManagement.goTo(); - const searchSessionList = await PageObjects.searchSessionsManagement.getList(); + const searchSessionListBeforeRestore = await PageObjects.searchSessionsManagement.getList(); + const searchesCountBeforeRestore = searchSessionListBeforeRestore[0].searchesCount; // navigate to Discover - await searchSessionList[0].view(); + await searchSessionListBeforeRestore[0].view(); await PageObjects.header.waitUntilLoadingHasFinished(); await searchSessions.expectState('restored'); expect(await PageObjects.discover.hasNoResults()).to.be(true); + expect(await toasts.getToastCount()).to.be(0); // no session restoration related warnings + + await PageObjects.searchSessionsManagement.goTo(); + const searchSessionListAfterRestore = await PageObjects.searchSessionsManagement.getList(); + const searchesCountAfterRestore = searchSessionListAfterRestore[0].searchesCount; + + expect(searchesCountBeforeRestore).to.be(searchesCountAfterRestore); // no new searches started during restore }); }); diff --git a/x-pack/test/search_sessions_integration/tests/apps/discover/sessions_in_space.ts b/x-pack/test/search_sessions_integration/tests/apps/discover/sessions_in_space.ts index 728ad056f4e6b..064c6bdc4495e 100644 --- a/x-pack/test/search_sessions_integration/tests/apps/discover/sessions_in_space.ts +++ b/x-pack/test/search_sessions_integration/tests/apps/discover/sessions_in_space.ts @@ -23,7 +23,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const browser = getService('browser'); const searchSessions = getService('searchSessions'); - describe('discover in space', () => { + // FLAKY https://github.com/elastic/kibana/issues/112913 + describe.skip('discover in space', () => { describe('Storing search sessions in space', () => { before(async () => { await esArchiver.load('x-pack/test/functional/es_archives/dashboard/session_in_space'); diff --git a/x-pack/test/security_solution_endpoint/apps/endpoint/policy_details.ts b/x-pack/test/security_solution_endpoint/apps/endpoint/policy_details.ts index a793582cb7295..95299d8a81f5c 100644 --- a/x-pack/test/security_solution_endpoint/apps/endpoint/policy_details.ts +++ b/x-pack/test/security_solution_endpoint/apps/endpoint/policy_details.ts @@ -21,7 +21,8 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { const testSubjects = getService('testSubjects'); const policyTestResources = getService('policyTestResources'); - describe('When on the Endpoint Policy Details Page', function () { + // FLAKY https://github.com/elastic/kibana/issues/100296 + describe.skip('When on the Endpoint Policy Details Page', function () { describe('with an invalid policy id', () => { it('should display an error', async () => { await pageObjects.policy.navigateToPolicyDetails('invalid-id'); @@ -879,6 +880,14 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { ); expect(await testSubjects.isSelected('policyWindowsEvent_dns')).to.be(wasSelected); }); + + it('should show trusted apps card and link should go back to policy', async () => { + await testSubjects.existOrFail('fleetTrustedAppsCard'); + await (await testSubjects.find('linkToTrustedApps')).click(); + await testSubjects.existOrFail('policyDetailsPage'); + await (await testSubjects.find('policyDetailsBackLink')).click(); + await testSubjects.existOrFail('endpointIntegrationPolicyForm'); + }); }); }); } diff --git a/x-pack/test/security_solution_endpoint/config.ts b/x-pack/test/security_solution_endpoint/config.ts index b00df7732ea4f..2bfb231887ac2 100644 --- a/x-pack/test/security_solution_endpoint/config.ts +++ b/x-pack/test/security_solution_endpoint/config.ts @@ -44,6 +44,8 @@ export default async function ({ readConfigFile }: FtrConfigProviderContext) { // always install Endpoint package by default when Fleet sets up `--xpack.fleet.packages.0.name=endpoint`, `--xpack.fleet.packages.0.version=latest`, + // TODO: Remove feature flags once we're good to go + '--xpack.securitySolution.enableExperimental=["trustedAppsByPolicyEnabled"]', ], }, layout: { diff --git a/x-pack/test/stack_functional_integration/apps/filebeat/filebeat.js b/x-pack/test/stack_functional_integration/apps/filebeat/filebeat.ts similarity index 83% rename from x-pack/test/stack_functional_integration/apps/filebeat/filebeat.js rename to x-pack/test/stack_functional_integration/apps/filebeat/filebeat.ts index 85570fc8b0158..4bcdc75d7fa50 100644 --- a/x-pack/test/stack_functional_integration/apps/filebeat/filebeat.js +++ b/x-pack/test/stack_functional_integration/apps/filebeat/filebeat.ts @@ -6,8 +6,9 @@ */ import expect from '@kbn/expect'; +import { FtrProviderContext } from '../../../functional/ftr_provider_context'; -export default function ({ getService, getPageObjects }) { +export default function ({ getService, getPageObjects }: FtrProviderContext) { describe('check filebeat', function () { const retry = getService('retry'); const PageObjects = getPageObjects(['common', 'discover', 'timePicker']); @@ -17,7 +18,7 @@ export default function ({ getService, getPageObjects }) { await PageObjects.discover.selectIndexPattern('filebeat-*'); await PageObjects.timePicker.setCommonlyUsedTime('Last_30 days'); await retry.try(async () => { - const hitCount = parseInt(await PageObjects.discover.getHitCount()); + const hitCount = parseInt(await PageObjects.discover.getHitCount(), 10); expect(hitCount).to.be.greaterThan(0); }); }); diff --git a/x-pack/test/stack_functional_integration/apps/filebeat/index.js b/x-pack/test/stack_functional_integration/apps/filebeat/index.ts similarity index 70% rename from x-pack/test/stack_functional_integration/apps/filebeat/index.js rename to x-pack/test/stack_functional_integration/apps/filebeat/index.ts index 478914d395c39..24077f40c9324 100644 --- a/x-pack/test/stack_functional_integration/apps/filebeat/index.js +++ b/x-pack/test/stack_functional_integration/apps/filebeat/index.ts @@ -4,8 +4,9 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ +import { FtrProviderContext } from '../../../functional/ftr_provider_context'; -export default function ({ loadTestFile }) { +export default function ({ loadTestFile }: FtrProviderContext) { describe('filebeat app', function () { loadTestFile(require.resolve('./filebeat')); }); diff --git a/x-pack/test/stack_functional_integration/apps/heartbeat/_heartbeat.js b/x-pack/test/stack_functional_integration/apps/heartbeat/_heartbeat.ts similarity index 54% rename from x-pack/test/stack_functional_integration/apps/heartbeat/_heartbeat.js rename to x-pack/test/stack_functional_integration/apps/heartbeat/_heartbeat.ts index 05f03a115f616..801e651d8b92e 100644 --- a/x-pack/test/stack_functional_integration/apps/heartbeat/_heartbeat.js +++ b/x-pack/test/stack_functional_integration/apps/heartbeat/_heartbeat.ts @@ -6,19 +6,23 @@ */ import expect from '@kbn/expect'; +import { FtrProviderContext } from '../../../functional/ftr_provider_context'; -export default function ({ getService, getPageObjects }) { +export default function ({ getService, getPageObjects }: FtrProviderContext) { const retry = getService('retry'); const PageObjects = getPageObjects(['common', 'uptime']); - describe('check heartbeat', function () { - it('Uptime app should show snapshot count greater than zero', async function () { + describe('check heartbeat overview page', function () { + it('Uptime app should show 1 UP monitor', async function () { await PageObjects.common.navigateToApp('uptime', { insertTimestamp: false }); await retry.try(async function () { - const upCount = parseInt((await PageObjects.uptime.getSnapshotCount()).up); - expect(upCount).to.be.greaterThan(0); + const upCount = parseInt((await PageObjects.uptime.getSnapshotCount()).up, 10); + expect(upCount).to.eql(1); }); }); + it('Uptime app should show Kibana QA Monitor present', async function () { + await PageObjects.uptime.pageHasExpectedIds(['kibana-qa-monitor']); + }); }); } diff --git a/x-pack/test/stack_functional_integration/apps/heartbeat/index.js b/x-pack/test/stack_functional_integration/apps/heartbeat/index.ts similarity index 72% rename from x-pack/test/stack_functional_integration/apps/heartbeat/index.js rename to x-pack/test/stack_functional_integration/apps/heartbeat/index.ts index 226350a74afc0..85c195a9ceff4 100644 --- a/x-pack/test/stack_functional_integration/apps/heartbeat/index.js +++ b/x-pack/test/stack_functional_integration/apps/heartbeat/index.ts @@ -4,8 +4,9 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ +import { FtrProviderContext } from '../../../functional/ftr_provider_context'; -export default function ({ loadTestFile }) { +export default function ({ loadTestFile }: FtrProviderContext) { describe('heartbeat app', function () { require('./_heartbeat'); loadTestFile(require.resolve('./_heartbeat')); diff --git a/x-pack/test/stack_functional_integration/apps/metricbeat/_metricbeat.js b/x-pack/test/stack_functional_integration/apps/metricbeat/_metricbeat.ts similarity index 87% rename from x-pack/test/stack_functional_integration/apps/metricbeat/_metricbeat.js rename to x-pack/test/stack_functional_integration/apps/metricbeat/_metricbeat.ts index 34f7c924f5ddb..79dc213e5485a 100644 --- a/x-pack/test/stack_functional_integration/apps/metricbeat/_metricbeat.js +++ b/x-pack/test/stack_functional_integration/apps/metricbeat/_metricbeat.ts @@ -6,8 +6,9 @@ */ import expect from '@kbn/expect'; +import { FtrProviderContext } from '../../../functional/ftr_provider_context'; -export default function ({ getService, getPageObjects }) { +export default function ({ getService, getPageObjects }: FtrProviderContext) { const log = getService('log'); const retry = getService('retry'); const browser = getService('browser'); @@ -27,7 +28,7 @@ export default function ({ getService, getPageObjects }) { await PageObjects.discover.selectIndexPattern('metricbeat-*'); await PageObjects.timePicker.setCommonlyUsedTime('Today'); await retry.try(async function () { - const hitCount = parseInt(await PageObjects.discover.getHitCount()); + const hitCount = parseInt(await PageObjects.discover.getHitCount(), 10); expect(hitCount).to.be.greaterThan(0); }); }); diff --git a/x-pack/test/stack_functional_integration/apps/metricbeat/_metricbeat_dashboard.js b/x-pack/test/stack_functional_integration/apps/metricbeat/_metricbeat_dashboard.ts similarity index 93% rename from x-pack/test/stack_functional_integration/apps/metricbeat/_metricbeat_dashboard.js rename to x-pack/test/stack_functional_integration/apps/metricbeat/_metricbeat_dashboard.ts index ac911a941c146..d2e9adbfd2683 100644 --- a/x-pack/test/stack_functional_integration/apps/metricbeat/_metricbeat_dashboard.js +++ b/x-pack/test/stack_functional_integration/apps/metricbeat/_metricbeat_dashboard.ts @@ -8,11 +8,16 @@ import expect from '@kbn/expect'; import { resolve } from 'path'; import { REPO_ROOT } from '@kbn/dev-utils'; +import { FtrProviderContext } from '../../../functional/ftr_provider_context'; const INTEGRATION_TEST_ROOT = process.env.WORKSPACE || resolve(REPO_ROOT, '../integration-test'); const ARCHIVE = resolve(INTEGRATION_TEST_ROOT, 'test/es_archives/metricbeat'); -export default function ({ getService, getPageObjects, updateBaselines }) { +export default function ({ + getService, + getPageObjects, + updateBaselines, +}: FtrProviderContext & { updateBaselines: boolean }) { const screenshot = getService('screenshots'); const browser = getService('browser'); const log = getService('log'); diff --git a/x-pack/test/stack_functional_integration/apps/metricbeat/index.js b/x-pack/test/stack_functional_integration/apps/metricbeat/index.ts similarity index 74% rename from x-pack/test/stack_functional_integration/apps/metricbeat/index.js rename to x-pack/test/stack_functional_integration/apps/metricbeat/index.ts index 9ee04df965dcc..c4e0db2797b94 100644 --- a/x-pack/test/stack_functional_integration/apps/metricbeat/index.js +++ b/x-pack/test/stack_functional_integration/apps/metricbeat/index.ts @@ -4,8 +4,9 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ +import { FtrProviderContext } from '../../../functional/ftr_provider_context'; -export default function ({ loadTestFile }) { +export default function ({ loadTestFile }: FtrProviderContext) { describe('metricbeat app', function () { loadTestFile(require.resolve('./_metricbeat')); loadTestFile(require.resolve('./_metricbeat_dashboard')); diff --git a/x-pack/test/stack_functional_integration/apps/packetbeat/_packetbeat.js b/x-pack/test/stack_functional_integration/apps/packetbeat/_packetbeat.ts similarity index 88% rename from x-pack/test/stack_functional_integration/apps/packetbeat/_packetbeat.js rename to x-pack/test/stack_functional_integration/apps/packetbeat/_packetbeat.ts index a5d6e6e924667..d0d7e326441a0 100644 --- a/x-pack/test/stack_functional_integration/apps/packetbeat/_packetbeat.js +++ b/x-pack/test/stack_functional_integration/apps/packetbeat/_packetbeat.ts @@ -6,8 +6,9 @@ */ import expect from '@kbn/expect'; +import { FtrProviderContext } from '../../../functional/ftr_provider_context'; -export default function ({ getService, getPageObjects }) { +export default function ({ getService, getPageObjects }: FtrProviderContext) { const log = getService('log'); const retry = getService('retry'); const browser = getService('browser'); @@ -31,7 +32,7 @@ export default function ({ getService, getPageObjects }) { await PageObjects.discover.selectIndexPattern('packetbeat-*'); await PageObjects.timePicker.setCommonlyUsedTime('Today'); await retry.try(async function () { - const hitCount = parseInt(await PageObjects.discover.getHitCount()); + const hitCount = parseInt(await PageObjects.discover.getHitCount(), 10); expect(hitCount).to.be.greaterThan(0); }); }); diff --git a/x-pack/test/stack_functional_integration/apps/packetbeat/index.js b/x-pack/test/stack_functional_integration/apps/packetbeat/index.ts similarity index 71% rename from x-pack/test/stack_functional_integration/apps/packetbeat/index.js rename to x-pack/test/stack_functional_integration/apps/packetbeat/index.ts index ba0af98d21f6b..70e38b6284fbe 100644 --- a/x-pack/test/stack_functional_integration/apps/packetbeat/index.js +++ b/x-pack/test/stack_functional_integration/apps/packetbeat/index.ts @@ -4,8 +4,9 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ +import { FtrProviderContext } from '../../../functional/ftr_provider_context'; -export default function ({ loadTestFile }) { +export default function ({ loadTestFile }: FtrProviderContext) { describe('packetbeat app', function () { loadTestFile(require.resolve('./_packetbeat')); }); diff --git a/x-pack/test/stack_functional_integration/apps/winlogbeat/_winlogbeat.js b/x-pack/test/stack_functional_integration/apps/winlogbeat/_winlogbeat.ts similarity index 87% rename from x-pack/test/stack_functional_integration/apps/winlogbeat/_winlogbeat.js rename to x-pack/test/stack_functional_integration/apps/winlogbeat/_winlogbeat.ts index c983a9155ae6a..9ef8b85c0ec09 100644 --- a/x-pack/test/stack_functional_integration/apps/winlogbeat/_winlogbeat.js +++ b/x-pack/test/stack_functional_integration/apps/winlogbeat/_winlogbeat.ts @@ -6,8 +6,9 @@ */ import expect from '@kbn/expect'; +import { FtrProviderContext } from '../../../functional/ftr_provider_context'; -export default function ({ getService, getPageObjects }) { +export default function ({ getService, getPageObjects }: FtrProviderContext) { const log = getService('log'); const browser = getService('browser'); const PageObjects = getPageObjects(['common', 'discover', 'timePicker']); @@ -26,7 +27,7 @@ export default function ({ getService, getPageObjects }) { await PageObjects.discover.selectIndexPattern('winlogbeat-*'); await PageObjects.timePicker.setCommonlyUsedTime('Today'); await retry.try(async function () { - const hitCount = parseInt(await PageObjects.discover.getHitCount()); + const hitCount = parseInt(await PageObjects.discover.getHitCount(), 10); expect(hitCount).to.be.greaterThan(0); }); }); diff --git a/x-pack/test/stack_functional_integration/apps/winlogbeat/index.js b/x-pack/test/stack_functional_integration/apps/winlogbeat/index.ts similarity index 71% rename from x-pack/test/stack_functional_integration/apps/winlogbeat/index.js rename to x-pack/test/stack_functional_integration/apps/winlogbeat/index.ts index bb883ee498181..826a292de5659 100644 --- a/x-pack/test/stack_functional_integration/apps/winlogbeat/index.js +++ b/x-pack/test/stack_functional_integration/apps/winlogbeat/index.ts @@ -4,8 +4,9 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ +import { FtrProviderContext } from '../../../functional/ftr_provider_context'; -export default function ({ loadTestFile }) { +export default function ({ loadTestFile }: FtrProviderContext) { describe('winlogbeat app', function () { loadTestFile(require.resolve('./_winlogbeat')); }); diff --git a/x-pack/test/usage_collection/plugins/application_usage_test/public/plugin.ts b/x-pack/test/usage_collection/plugins/application_usage_test/public/plugin.ts index b0c777593b021..ff1e89b58c7e8 100644 --- a/x-pack/test/usage_collection/plugins/application_usage_test/public/plugin.ts +++ b/x-pack/test/usage_collection/plugins/application_usage_test/public/plugin.ts @@ -12,8 +12,12 @@ import './types'; export class ApplicationUsageTest implements Plugin { public setup(core: CoreSetup) {} - public async start(core: CoreStart) { - const applications = await core.application.applications$.pipe(first()).toPromise(); - window.__applicationIds__ = [...applications.keys()]; + public start(core: CoreStart) { + core.application.applications$ + .pipe(first()) + .toPromise() + .then((applications) => { + window.__applicationIds__ = [...applications.keys()]; + }); } } diff --git a/yarn.lock b/yarn.lock index dea325c9cb030..2a8dff971791f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1886,14 +1886,14 @@ dependencies: "@hapi/hoek" "9.x.x" -"@hapi/boom@9.x.x", "@hapi/boom@^9.0.0", "@hapi/boom@^9.1.1": - version "9.1.1" - resolved "https://registry.yarnpkg.com/@hapi/boom/-/boom-9.1.1.tgz#89e6f0e01637c2a4228da0d113e8157c93677b04" - integrity sha512-VNR8eDbBrOxBgbkddRYIe7+8DZ+vSbV6qlmaN2x7eWjsUjy2VmQgChkOKcVZIeupEZYj+I0dqNg430OhwzagjA== +"@hapi/boom@9.x.x", "@hapi/boom@^9.0.0", "@hapi/boom@^9.1.0", "@hapi/boom@^9.1.4": + version "9.1.4" + resolved "https://registry.yarnpkg.com/@hapi/boom/-/boom-9.1.4.tgz#1f9dad367c6a7da9f8def24b4a986fc5a7bd9db6" + integrity sha512-Ls1oH8jaN1vNsqcaHVYJrKmgMcKsC1wcp8bujvXrHaAqD2iDYq3HoOwsxwo09Cuda5R5nC0o0IxlrlTuvPuzSw== dependencies: "@hapi/hoek" "9.x.x" -"@hapi/bounce@2.x.x": +"@hapi/bounce@2.x.x", "@hapi/bounce@^2.0.0": version "2.0.0" resolved "https://registry.yarnpkg.com/@hapi/bounce/-/bounce-2.0.0.tgz#e6ef56991c366b1e2738b2cd83b01354d938cf3d" integrity sha512-JesW92uyzOOyuzJKjoLHM1ThiOvHPOLDHw01YV8yh5nCso7sDwJho1h0Ad2N+E62bZyz46TG3xhAi/78Gsct6A== @@ -1906,7 +1906,7 @@ resolved "https://registry.yarnpkg.com/@hapi/bourne/-/bourne-2.0.0.tgz#5bb2193eb685c0007540ca61d166d4e1edaf918d" integrity sha512-WEezM1FWztfbzqIUbsDzFRVMxSoLy3HugVcux6KDDtTqzPsLE8NDRHfXvev66aH1i2oOKKar3/XDjbvh/OUBdg== -"@hapi/call@8.x.x": +"@hapi/call@^8.0.0": version "8.0.1" resolved "https://registry.yarnpkg.com/@hapi/call/-/call-8.0.1.tgz#9e64cd8ba6128eb5be6e432caaa572b1ed8cd7c0" integrity sha512-bOff6GTdOnoe5b8oXRV3lwkQSb/LAWylvDMae6RgEWWntd0SHtkYbQukDHKlfaYtVnSAgIavJ0kqszF/AIBb6g== @@ -1914,7 +1914,7 @@ "@hapi/boom" "9.x.x" "@hapi/hoek" "9.x.x" -"@hapi/catbox-memory@5.x.x": +"@hapi/catbox-memory@^5.0.0": version "5.0.0" resolved "https://registry.yarnpkg.com/@hapi/catbox-memory/-/catbox-memory-5.0.0.tgz#6c18dad1a80737480d1c33bfbefd5d028deec86d" integrity sha512-ByuxVJPHNaXwLzbBv4GdTr6ccpe1nG+AfYt+8ftDWEJY7EWBWzD+Klhy5oPTDGzU26pNUh1e7fcYI1ILZRxAXQ== @@ -1979,29 +1979,29 @@ "@hapi/validate" "1.x.x" "@hapi/wreck" "17.x.x" -"@hapi/hapi@^20.0.3": - version "20.0.3" - resolved "https://registry.yarnpkg.com/@hapi/hapi/-/hapi-20.0.3.tgz#e72cad460394e6d2c15f9c57abb5d3332dea27e3" - integrity sha512-aqJVHVjoY3phiZsgsGjDRG15CoUNIs1azScqLZDOCZUSKYGTbzPi+K0QP+RUjUJ0m8L9dRuTZ27c8HKxG3wEhA== +"@hapi/hapi@^20.2.0": + version "20.2.0" + resolved "https://registry.yarnpkg.com/@hapi/hapi/-/hapi-20.2.0.tgz#bf0eca9cc591e83f3d72d06a998d31be35d044a1" + integrity sha512-yPH/z8KvlSLV8lI4EuId9z595fKKk5n6YA7H9UddWYWsBXMcnCyoFmHtYq0PCV4sNgKLD6QW9e27R9V9Z9aqqw== dependencies: "@hapi/accept" "^5.0.1" "@hapi/ammo" "^5.0.1" - "@hapi/boom" "9.x.x" - "@hapi/bounce" "2.x.x" - "@hapi/call" "8.x.x" + "@hapi/boom" "^9.1.0" + "@hapi/bounce" "^2.0.0" + "@hapi/call" "^8.0.0" "@hapi/catbox" "^11.1.1" - "@hapi/catbox-memory" "5.x.x" + "@hapi/catbox-memory" "^5.0.0" "@hapi/heavy" "^7.0.1" - "@hapi/hoek" "9.x.x" - "@hapi/mimos" "5.x.x" + "@hapi/hoek" "^9.0.4" + "@hapi/mimos" "^6.0.0" "@hapi/podium" "^4.1.1" - "@hapi/shot" "^5.0.1" - "@hapi/somever" "3.x.x" + "@hapi/shot" "^5.0.5" + "@hapi/somever" "^3.0.0" "@hapi/statehood" "^7.0.3" "@hapi/subtext" "^7.0.3" - "@hapi/teamwork" "5.x.x" - "@hapi/topo" "5.x.x" - "@hapi/validate" "^1.1.0" + "@hapi/teamwork" "^5.1.0" + "@hapi/topo" "^5.0.0" + "@hapi/validate" "^1.1.1" "@hapi/heavy@^7.0.1": version "7.0.1" @@ -2012,15 +2012,15 @@ "@hapi/hoek" "9.x.x" "@hapi/validate" "1.x.x" -"@hapi/hoek@9.x.x", "@hapi/hoek@^9.0.0", "@hapi/hoek@^9.0.4", "@hapi/hoek@^9.1.1": - version "9.1.1" - resolved "https://registry.yarnpkg.com/@hapi/hoek/-/hoek-9.1.1.tgz#9daf5745156fd84b8e9889a2dc721f0c58e894aa" - integrity sha512-CAEbWH7OIur6jEOzaai83jq3FmKmv4PmX1JYfs9IrYcGEVI/lyL1EXJGCj7eFVJ0bg5QR8LMxBlEtA+xKiLpFw== +"@hapi/hoek@9.x.x", "@hapi/hoek@^9.0.0", "@hapi/hoek@^9.0.4", "@hapi/hoek@^9.2.0": + version "9.2.0" + resolved "https://registry.yarnpkg.com/@hapi/hoek/-/hoek-9.2.0.tgz#f3933a44e365864f4dad5db94158106d511e8131" + integrity sha512-sqKVVVOe5ivCaXDWivIJYVSaEgdQK9ul7a4Kity5Iw7u9+wBAPbX1RMSnLLmp7O4Vzj0WOWwMAJsTL00xwaNug== -"@hapi/inert@^6.0.3": - version "6.0.3" - resolved "https://registry.yarnpkg.com/@hapi/inert/-/inert-6.0.3.tgz#57af5d912893fabcb57eb4b956f84f6cd8020fe1" - integrity sha512-Z6Pi0Wsn2pJex5CmBaq+Dky9q40LGzXLUIUFrYpDtReuMkmfy9UuUeYc4064jQ1Xe9uuw7kbwE6Fq6rqKAdjAg== +"@hapi/inert@^6.0.4": + version "6.0.4" + resolved "https://registry.yarnpkg.com/@hapi/inert/-/inert-6.0.4.tgz#0544221eabc457110a426818358d006e70ff1f41" + integrity sha512-tpmNqtCCAd+5Ts07bJmMaA79+ZUIf0zSWnQMaWtbcO4nGrO/yXB2AzoslfzFX2JEV9vGeF3FfL8mYw0pHl8VGg== dependencies: "@hapi/ammo" "5.x.x" "@hapi/boom" "9.x.x" @@ -2040,10 +2040,10 @@ "@hapi/cryptiles" "5.x.x" "@hapi/hoek" "9.x.x" -"@hapi/mimos@5.x.x": - version "5.0.0" - resolved "https://registry.yarnpkg.com/@hapi/mimos/-/mimos-5.0.0.tgz#245c6c98b1cc2c13395755c730321b913de074eb" - integrity sha512-EVS6wJYeE73InTlPWt+2e3Izn319iIvffDreci3qDNT+t3lA5ylJ0/SoTaID8e0TPNUkHUSsgJZXEmLHvoYzrA== +"@hapi/mimos@^6.0.0": + version "6.0.0" + resolved "https://registry.yarnpkg.com/@hapi/mimos/-/mimos-6.0.0.tgz#daa523d9c07222c7e8860cb7c9c5501fd6506484" + integrity sha512-Op/67tr1I+JafN3R3XN5DucVSxKRT/Tc+tUszDwENoNpolxeXkhrJ2Czt6B6AAqrespHoivhgZBWYSuANN9QXg== dependencies: "@hapi/hoek" "9.x.x" mime-db "1.x.x" @@ -2074,24 +2074,24 @@ "@hapi/hoek" "9.x.x" "@hapi/nigel" "4.x.x" -"@hapi/podium@4.x.x", "@hapi/podium@^4.1.1": - version "4.1.1" - resolved "https://registry.yarnpkg.com/@hapi/podium/-/podium-4.1.1.tgz#106e5849f2cb19b8767cc16007e0107f27c3c791" - integrity sha512-jh7a6+5Z4FUWzx8fgmxjaAa1DTBu+Qfg+NbVdo0f++rE5DgsVidUYrLDp3db65+QjDLleA2MfKQXkpT8ylBDXA== +"@hapi/podium@4.x.x", "@hapi/podium@^4.1.1", "@hapi/podium@^4.1.3": + version "4.1.3" + resolved "https://registry.yarnpkg.com/@hapi/podium/-/podium-4.1.3.tgz#91e20838fc2b5437f511d664aabebbb393578a26" + integrity sha512-ljsKGQzLkFqnQxE7qeanvgGj4dejnciErYd30dbrYzUOF/FyS/DOF97qcrT3bhoVwCYmxa6PEMhxfCPlnUcD2g== dependencies: "@hapi/hoek" "9.x.x" "@hapi/teamwork" "5.x.x" "@hapi/validate" "1.x.x" -"@hapi/shot@^5.0.1": - version "5.0.4" - resolved "https://registry.yarnpkg.com/@hapi/shot/-/shot-5.0.4.tgz#6c978314f21a054c041f4becc50095dd78d3d775" - integrity sha512-PcEz0WJgFDA3xNSMeONgQmothFr7jhbbRRSAKaDh7chN7zOXBlhl13bvKZW6CMb2xVfJUmt34CW3e/oExMgBhQ== +"@hapi/shot@^5.0.5": + version "5.0.5" + resolved "https://registry.yarnpkg.com/@hapi/shot/-/shot-5.0.5.tgz#a25c23d18973bec93c7969c51bf9579632a5bebd" + integrity sha512-x5AMSZ5+j+Paa8KdfCoKh+klB78otxF+vcJR/IoN91Vo2e5ulXIW6HUsFTCU+4W6P/Etaip9nmdAx2zWDimB2A== dependencies: "@hapi/hoek" "9.x.x" "@hapi/validate" "1.x.x" -"@hapi/somever@3.x.x": +"@hapi/somever@^3.0.0": version "3.0.0" resolved "https://registry.yarnpkg.com/@hapi/somever/-/somever-3.0.0.tgz#f4e9b16a948415b926b4dd898013602b0cb45758" integrity sha512-Upw/kmKotC9iEmK4y047HMYe4LDKsE5NWfjgX41XNKmFvxsQL7OiaCWVhuyyhU0ShDGBfIAnCH8jZr49z/JzZA== @@ -2125,19 +2125,19 @@ "@hapi/pez" "^5.0.1" "@hapi/wreck" "17.x.x" -"@hapi/teamwork@5.x.x": +"@hapi/teamwork@5.x.x", "@hapi/teamwork@^5.1.0": version "5.1.0" resolved "https://registry.yarnpkg.com/@hapi/teamwork/-/teamwork-5.1.0.tgz#7801a61fc727f702fd2196ef7625eb4e389f4124" integrity sha512-llqoQTrAJDTXxG3c4Kz/uzhBS1TsmSBa/XG5SPcVXgmffHE1nFtyLIK0hNJHCB3EuBKT84adzd1hZNY9GJLWtg== -"@hapi/topo@5.x.x", "@hapi/topo@^5.0.0": +"@hapi/topo@^5.0.0": version "5.0.0" resolved "https://registry.yarnpkg.com/@hapi/topo/-/topo-5.0.0.tgz#c19af8577fa393a06e9c77b60995af959be721e7" integrity sha512-tFJlT47db0kMqVm3H4nQYgn6Pwg10GTZHb1pwmSiv1K4ks6drQOtfEF5ZnPjkvC+y4/bUPHK+bc87QvLcL+WMw== dependencies: "@hapi/hoek" "^9.0.0" -"@hapi/validate@1.x.x", "@hapi/validate@^1.1.0": +"@hapi/validate@1.x.x", "@hapi/validate@^1.1.1": version "1.1.3" resolved "https://registry.yarnpkg.com/@hapi/validate/-/validate-1.1.3.tgz#f750a07283929e09b51aa16be34affb44e1931ad" integrity sha512-/XMR0N0wjw0Twzq2pQOzPBZlDzkekGcoCtzO314BpIEsbXdYGthQUbxgkGDf4nhk1+IPDAsXqWjMohRQYO06UA== @@ -5170,41 +5170,42 @@ resolved "https://registry.yarnpkg.com/@types/hapi__catbox/-/hapi__catbox-10.2.3.tgz#c9279c16d709bf2987491c332e11d18124ae018f" integrity sha512-gs6MKMKXzWpSqeYsPaDIDAxD8jLNg7aFxgAJE6Jnc+ns072Z9fuh39/NF5gSk1KNoGCLnIpeZ0etT9gY9QDCKg== -"@types/hapi__cookie@^10.1.1": - version "10.1.1" - resolved "https://registry.yarnpkg.com/@types/hapi__cookie/-/hapi__cookie-10.1.1.tgz#4420c7f89ef466aa8c1f4d9975c62e6b5b066b1c" - integrity sha512-sWVS20wvqbYSjpjpfOwsD/gtDBba3mi+Y4Yg2qZMBs0/VAgvhOOmpBXzFf2rE8rrEuR44n7tzmEgPWRw5q7kaw== +"@types/hapi__cookie@^10.1.3": + version "10.1.3" + resolved "https://registry.yarnpkg.com/@types/hapi__cookie/-/hapi__cookie-10.1.3.tgz#b0ab2be28669e083c63253927262c43f24395c2c" + integrity sha512-v/hPXxOVfBdkTa+S4cGec88vZjvEbLaZp8xjg2MtjDhykx1/mLtY4EJHk6fI1cW5WGgFV9pgMjz5mOktjNwILw== dependencies: "@types/hapi__hapi" "*" + joi "^17.3.0" -"@types/hapi__h2o2@^8.3.2": - version "8.3.2" - resolved "https://registry.yarnpkg.com/@types/hapi__h2o2/-/hapi__h2o2-8.3.2.tgz#43cce95972c3097a2ca3efe6b7054a0c95fbf291" - integrity sha512-l36uuLHTwUQNbNUIkT14Z4WbJl1CIWpBZu7ZCBemGBypiNnbJxN3o0YyQ6QAid3YYa2C7LVDIdyY4MhpX8q9ZA== +"@types/hapi__h2o2@^8.3.3": + version "8.3.3" + resolved "https://registry.yarnpkg.com/@types/hapi__h2o2/-/hapi__h2o2-8.3.3.tgz#f6c5ac480a6fd421025f7d0f78dfa916703511b7" + integrity sha512-+qWZVFVGc5Y0wuNZvVe876VJjUBCJ8eQdXovg4Rg9laHpeERQejluI7aw31xXWfLojTuHz3ThZzC6Orqras05Q== dependencies: "@hapi/boom" "^9.0.0" "@hapi/wreck" "^17.0.0" "@types/hapi__hapi" "*" "@types/node" "*" -"@types/hapi__hapi@*", "@types/hapi__hapi@^20.0.2": - version "20.0.2" - resolved "https://registry.yarnpkg.com/@types/hapi__hapi/-/hapi__hapi-20.0.2.tgz#e7571451f7fb75e87ab3873ec91b92f92cd55fff" - integrity sha512-7FwFoaxSCtHXbHbDdArSeVABFOfMLgVkOvOUtWrqUBzw639B2rq9OHv3kOVDHY0bOao0f6ubMzUxio8WQ9QZfQ== +"@types/hapi__hapi@*", "@types/hapi__hapi@^20.0.9": + version "20.0.9" + resolved "https://registry.yarnpkg.com/@types/hapi__hapi/-/hapi__hapi-20.0.9.tgz#9d570846c96268266a14c970c13aeeaccfc8e172" + integrity sha512-fGpKScknCKZityRXdZgpCLGbm41R1ppFgnKHerfZlqOOlCX/jI129S6ghgBqkqCE8m9A0CIu1h7Ch04lD9KOoA== dependencies: "@hapi/boom" "^9.0.0" "@hapi/iron" "^6.0.0" + "@hapi/podium" "^4.1.3" "@types/hapi__catbox" "*" "@types/hapi__mimos" "*" - "@types/hapi__podium" "*" "@types/hapi__shot" "*" - "@types/joi" "*" "@types/node" "*" + joi "^17.3.0" -"@types/hapi__inert@^5.2.2": - version "5.2.2" - resolved "https://registry.yarnpkg.com/@types/hapi__inert/-/hapi__inert-5.2.2.tgz#6513c487d216ed9377c2c0efceb178fda0928bfa" - integrity sha512-Vp9HS2wi3Qbm1oUlcTvzA2Zd+f3Dwg+tgLqWA6KTCgKbQX4LCPKIvVssbaQAVncmcpH0aPrtkAfftJlS/sMsGg== +"@types/hapi__inert@^5.2.3": + version "5.2.3" + resolved "https://registry.yarnpkg.com/@types/hapi__inert/-/hapi__inert-5.2.3.tgz#f586eb240d5997c9968d1b4e8b37679517045ca1" + integrity sha512-I1mWQrEc7oMqGtofT0rwBgRBCBurz0wNzbq8QZsHWR+aXM0bk1j9GA6zwyGIeO53PNl2C1c2kpXlc084xCV+Tg== dependencies: "@types/hapi__hapi" "*" @@ -5215,11 +5216,6 @@ dependencies: "@types/mime-db" "*" -"@types/hapi__podium@*", "@types/hapi__podium@^3.4.1": - version "3.4.1" - resolved "https://registry.yarnpkg.com/@types/hapi__podium/-/hapi__podium-3.4.1.tgz#826ffed038979c844410e576b574f8237afd59bc" - integrity sha512-qgMyeXTZhGWvvUnXFavW2Pksf07IV1haBM/Fdq6cFi1lCIXhUHsaTrr2w651q+rhHZf+1dgP1vltJ0/quLxYYw== - "@types/hapi__shot@*": version "4.1.1" resolved "https://registry.yarnpkg.com/@types/hapi__shot/-/hapi__shot-4.1.1.tgz#c760322b90eb77f36a3003a442e8dc69e6ae3922" @@ -5361,11 +5357,6 @@ jest-diff "^25.2.1" pretty-format "^25.2.1" -"@types/joi@*": - version "14.3.4" - resolved "https://registry.yarnpkg.com/@types/joi/-/joi-14.3.4.tgz#eed1e14cbb07716079c814138831a520a725a1e0" - integrity sha512-1TQNDJvIKlgYXGNIABfgFp9y0FziDpuGrd799Q5RcnsDu+krD+eeW/0Fs5PHARvWWFelOhIG2OPCo6KbadBM4A== - "@types/joi@^17.2.3": version "17.2.3" resolved "https://registry.yarnpkg.com/@types/joi/-/joi-17.2.3.tgz#b7768ed9d84f1ebd393328b9f97c1cf3d2b94798" @@ -5631,10 +5622,10 @@ "@types/node" "*" form-data "^3.0.0" -"@types/node-forge@^0.10.4": - version "0.10.4" - resolved "https://registry.yarnpkg.com/@types/node-forge/-/node-forge-0.10.4.tgz#f30025cc2da0177393b9deaefbf3b9edd55b807b" - integrity sha512-RpP7JCxlPA32n8FE0kjOpCsCrsX6VjiD0fjOCo4NwIn8IdcicHi4B2e+votWuOpOmwzUjMwRLqVIF95epGd5nA== +"@types/node-forge@^0.10.5": + version "0.10.5" + resolved "https://registry.yarnpkg.com/@types/node-forge/-/node-forge-0.10.5.tgz#f79925c88202817a7ec0958c3a9d3a915d362b4f" + integrity sha512-P+Q+MPSDr0RgIzv5h0gJuJDCm1e4RaSu/EMJZTUS4ZzboWH2uX/T7TiqAAcEFTHzCKtgMRqCgTVTX9SD72fMTQ== dependencies: "@types/node" "*" @@ -7732,6 +7723,13 @@ axios@^0.21.1: dependencies: follow-redirects "^1.10.0" +axios@^0.21.2: + version "0.21.4" + resolved "https://registry.yarnpkg.com/axios/-/axios-0.21.4.tgz#c67b90dc0568e5c1cf2b0b858c43ba28e2eda575" + integrity sha512-ut5vewkiu8jjGBdqpM44XxjuCjq9LAKeHVmoVfHVzy8eHgxxq8SbAVQNovDA8mVi05kP0Ea/n/UzcSHcTJQfNg== + dependencies: + follow-redirects "^1.14.0" + axobject-query@^2.0.2: version "2.0.2" resolved "https://registry.yarnpkg.com/axobject-query/-/axobject-query-2.0.2.tgz#ea187abe5b9002b377f925d8bf7d1c561adf38f9" @@ -9441,13 +9439,13 @@ chrome-trace-event@^1.0.2: dependencies: tslib "^1.9.0" -chromedriver@^92.0.1: - version "92.0.1" - resolved "https://registry.yarnpkg.com/chromedriver/-/chromedriver-92.0.1.tgz#3e28b7e0c9fb94d693cf74d51af0c29d57f18dca" - integrity sha512-LptlDVCs1GgyFNVbRoHzzy948JDVzTgGiVPXjNj385qXKQP3hjAVBIgyvb/Hco0xSEW8fjwJfsm1eQRmu6t4pQ== +chromedriver@^93.0.1: + version "93.0.1" + resolved "https://registry.yarnpkg.com/chromedriver/-/chromedriver-93.0.1.tgz#3ed1f7baa98a754fc1788c42ac8e4bb1ab27db32" + integrity sha512-KDzbW34CvQLF5aTkm3b5VdlTrvdIt4wEpCzT2p4XJIQWQZEPco5pNce7Lu9UqZQGkhQ4mpZt4Ky6NKVyIS2N8A== dependencies: "@testim/chrome-version" "^1.0.7" - axios "^0.21.1" + axios "^0.21.2" del "^6.0.0" extract-zip "^2.0.1" https-proxy-agent "^5.0.0" @@ -12215,10 +12213,10 @@ ejs@^3.1.2, ejs@^3.1.6: dependencies: jake "^10.6.1" -elastic-apm-http-client@^9.8.1: - version "9.8.1" - resolved "https://registry.yarnpkg.com/elastic-apm-http-client/-/elastic-apm-http-client-9.8.1.tgz#62a0352849e2d7a75696a1c777ad90ddb55083b0" - integrity sha512-tVU7+y4nSDUEZp/TXbXDxE+kXbWHsGVG1umk0OOV71UEPc/AqC7xSP5ACirOlDkewkfCOFXkvNThgu2zlx8PUw== +elastic-apm-http-client@^10.0.0: + version "10.0.0" + resolved "https://registry.yarnpkg.com/elastic-apm-http-client/-/elastic-apm-http-client-10.0.0.tgz#495651716c13a744544c4dc983107a948418d213" + integrity sha512-D0Frzaqo2h6RxrbxkwfTZSu7tKkmmP3UGYLCp2Fq25cGT/3px4hBWvTc+nV7iDwj2rwdQl7CNkcathYNkyHRWQ== dependencies: breadth-filter "^2.0.0" container-info "^1.0.1" @@ -12228,12 +12226,11 @@ elastic-apm-http-client@^9.8.1: object-filter-sequence "^1.0.0" readable-stream "^3.4.0" stream-chopper "^3.0.1" - unicode-byte-truncate "^1.0.0" -elastic-apm-node@^3.20.0: - version "3.20.0" - resolved "https://registry.yarnpkg.com/elastic-apm-node/-/elastic-apm-node-3.20.0.tgz#c2f3c90a779d8580cceba292c22dad17ff859749" - integrity sha512-oaUrj3IrtCUg3kzQnoFClw210OpXaCFzIdMO3EnY7z7+zHcjd5fLEMDHQ64qFzKeMt3aPrLBu6ou0HwuUe48Eg== +elastic-apm-node@^3.21.1: + version "3.21.1" + resolved "https://registry.yarnpkg.com/elastic-apm-node/-/elastic-apm-node-3.21.1.tgz#5f79cfc6ba60469e4ec83d176b3d28ddee78b530" + integrity sha512-qnYWvWXQx00pS98IFYxkRQ9+T+R8oh0KdsbCU8t1ouSozZI6l5frlwC9CVpsqakPnAuvWP/qIYJEKF3CkYPv0w== dependencies: "@elastic/ecs-pino-format" "^1.2.0" after-all-results "^2.0.0" @@ -12242,7 +12239,7 @@ elastic-apm-node@^3.20.0: basic-auth "^2.0.1" cookie "^0.4.0" core-util-is "^1.0.2" - elastic-apm-http-client "^9.8.1" + elastic-apm-http-client "^10.0.0" end-of-stream "^1.4.4" error-callsites "^2.0.4" error-stack-parser "^2.0.6" @@ -13928,6 +13925,11 @@ follow-redirects@^1.0.0, follow-redirects@^1.10.0: resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.13.0.tgz#b42e8d93a2a7eea5ed88633676d6597bc8e384db" integrity sha512-aq6gF1BEKje4a9i9+5jimNFIpq4Q1WiwBToeRK5NvZBd/TRsmW8BsJfOEGkr76TbOyPVD3OVDN910EcUNtRYEA== +follow-redirects@^1.14.0: + version "1.14.3" + resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.14.3.tgz#6ada78118d8d24caee595595accdc0ac6abd022e" + integrity sha512-3MkHxknWMUtb23apkgz/83fDoe+y+qr0TdgacGIA7bew+QLBo3vdgEN2xEsuXNivpFy4CyDhBBZnNZOtalmenw== + font-awesome@4.7.0: version "4.7.0" resolved "https://registry.yarnpkg.com/font-awesome/-/font-awesome-4.7.0.tgz#8fa8cf0411a1a31afd07b06d2902bb9fc815a133"